Veeam: Support Veeam 11 and 12 (#8241)

This PR fixes several issues in the testing of Veeam 11 and Veeam12
- Import Veeam.Backup.PowerShell and silently ignore the warning messages
- Fix issue when assign vm to backup offerings, which caused by separator (\r\n)
- Fix authorization failure in veeam 12a, which is because v1_4 is not supported in veeam 12a any more
- Fix exception if backup name has space
- Fix backup metrics in veeam12, which is because powershell command does not return the values needed
- Fix Incorrect datetime value, which is because powershell command returns a datetime which is not supported in Java
- Fix issue during backup restoration if VM has both ROOT and DATA disks.

This PR also has the following update
- Add integration test test/integration/smoke/test_backup_recovery_veeam.py
- Make some UI changes
- Add zone setting backup.plugin.veeam.version. If it is not set, CloudStack will get veeam version via powershell commands.
- Add zone setting backup.plugin.veeam.task.poll.interval and backup.plugin.veeam.task.poll.max.retry
This commit is contained in:
Wei Zhou 2024-01-19 18:42:01 +01:00 committed by GitHub
parent 76aff0f422
commit 33bb92acce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1654 additions and 62 deletions

View File

@ -95,8 +95,8 @@ public class ListVMsCmd extends BaseListTaggedResourcesCmd implements UserCmd {
@Parameter(name = ApiConstants.DETAILS,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "comma separated list of host details requested, "
+ "value can be a list of [all, group, nics, stats, secgrp, tmpl, servoff, diskoff, iso, volume, min, affgrp]."
description = "comma separated list of vm details requested, "
+ "value can be a list of [all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]."
+ " If no parameter is passed in, the details will be defaulted to all")
private List<String> viewDetails;

View File

@ -0,0 +1,43 @@
//
// 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.backup;
import com.cloud.agent.api.Command;
public class PrepareForBackupRestorationCommand extends Command {
private String vmName;
protected PrepareForBackupRestorationCommand() {
}
public PrepareForBackupRestorationCommand(String vmName) {
this.vmName = vmName;
}
public String getVmName() {
return vmName;
}
@Override
public boolean executeInSequence() {
return true;
}
}

View File

@ -30,6 +30,9 @@ import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import com.cloud.network.Networks.AddressFormat;
import com.cloud.network.Networks.Mode;
import com.cloud.utils.db.GenericDao;
@ -399,6 +402,15 @@ public class NicVO implements Nic {
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 31).append(id).toHashCode();
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
public Integer getMtu() {
return mtu;
}

View File

@ -91,6 +91,8 @@ public interface NicDao extends GenericDao<NicVO, Long> {
NicVO findByMacAddress(String macAddress);
NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac);
List<NicVO> findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName);
NicVO findByIpAddressAndVmType(String ip, VirtualMachine.Type vmType);

View File

@ -219,6 +219,14 @@ public class NicDaoImpl extends GenericDaoBase<NicVO, Long> implements NicDao {
return findOneBy(sc);
}
@Override
public NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac) {
SearchCriteria<NicVO> sc = AllFieldsSearch.create();
sc.setParameters("network", networkId);
sc.setParameters("macAddress", mac);
return findOneIncludingRemovedBy(sc);
}
@Override
public NicVO findDefaultNicForVM(long instanceId) {
SearchCriteria<NicVO> sc = AllFieldsSearch.create();

View File

@ -17,6 +17,9 @@
package org.apache.cloudstack.backup;
import com.cloud.utils.db.GenericDao;
import java.util.Date;
import java.util.UUID;
import javax.persistence.Column;
@ -51,6 +54,9 @@ public class BackupVO implements Backup {
@Column(name = "date")
private String date;
@Column(name = GenericDao.REMOVED_COLUMN)
private Date removed;
@Column(name = "size")
private Long size;
@ -192,4 +198,12 @@ public class BackupVO implements Backup {
public String getName() {
return null;
}
public Date getRemoved() {
return removed;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
}

View File

@ -28,6 +28,21 @@
</parent>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-components-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-hypervisor-vmware</artifactId>

View File

@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.backup.Backup.Metric;
import org.apache.cloudstack.backup.dao.BackupDao;
@ -40,11 +41,17 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.log4j.Logger;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.event.EventVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.vmware.VmwareDatacenter;
import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMap;
import com.cloud.hypervisor.vmware.dao.VmwareDatacenterDao;
import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao;
import com.cloud.user.User;
import com.cloud.utils.Pair;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.db.Transaction;
@ -53,6 +60,7 @@ import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.VMInstanceDao;
public class VeeamBackupProvider extends AdapterBase implements BackupProvider, Configurable {
@ -64,6 +72,10 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
"backup.plugin.veeam.url", "https://localhost:9398/api/",
"The Veeam backup and recovery URL.", true, ConfigKey.Scope.Zone);
public ConfigKey<Integer> VeeamVersion = new ConfigKey<>("Advanced", Integer.class,
"backup.plugin.veeam.version", "0",
"The version of Veeam backup and recovery. CloudStack will get Veeam server version via PowerShell commands if it is 0 or not set", true, ConfigKey.Scope.Zone);
private ConfigKey<String> VeeamUsername = new ConfigKey<>("Advanced", String.class,
"backup.plugin.veeam.username", "administrator",
"The Veeam backup and recovery username.", true, ConfigKey.Scope.Zone);
@ -81,6 +93,12 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
private static ConfigKey<Integer> VeeamRestoreTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.plugin.veeam.restore.timeout", "600",
"The Veeam B&R API restore backup timeout in seconds.", true, ConfigKey.Scope.Zone);
private static ConfigKey<Integer> VeeamTaskPollInterval = new ConfigKey<>("Advanced", Integer.class, "backup.plugin.veeam.task.poll.interval", "5",
"The time interval in seconds when the management server polls for Veeam task status.", true, ConfigKey.Scope.Zone);
private static ConfigKey<Integer> VeeamTaskPollMaxRetry = new ConfigKey<>("Advanced", Integer.class, "backup.plugin.veeam.task.poll.max.retry", "120",
"The max number of retrying times when the management server polls for Veeam task status.", true, ConfigKey.Scope.Zone);
@Inject
private VmwareDatacenterZoneMapDao vmwareDatacenterZoneMapDao;
@Inject
@ -89,11 +107,16 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
private BackupDao backupDao;
@Inject
private VMInstanceDao vmInstanceDao;
@Inject
private AgentManager agentMgr;
@Inject
private VirtualMachineManager virtualMachineManager;
protected VeeamClient getClient(final Long zoneId) {
try {
return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId),
VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId), VeeamRestoreTimeout.valueIn(zoneId));
return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamVersion.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId),
VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId), VeeamRestoreTimeout.valueIn(zoneId),
VeeamTaskPollInterval.valueIn(zoneId), VeeamTaskPollMaxRetry.valueIn(zoneId));
} catch (URISyntaxException e) {
throw new CloudRuntimeException("Failed to parse Veeam API URL: " + e.getMessage());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
@ -234,7 +257,36 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
@Override
public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) {
final String restorePointId = backup.getExternalId();
return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId);
try {
return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId);
} catch (Exception ex) {
LOG.error(String.format("Failed to restore Full VM due to: %s. Retrying after some preparation", ex.getMessage()));
prepareForBackupRestoration(vm);
return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId);
}
}
private void prepareForBackupRestoration(VirtualMachine vm) {
if (!Hypervisor.HypervisorType.VMware.equals(vm.getHypervisorType())) {
return;
}
LOG.info("Preparing for restoring VM " + vm);
PrepareForBackupRestorationCommand command = new PrepareForBackupRestorationCommand(vm.getInstanceName());
Long hostId = virtualMachineManager.findClusterAndHostIdForVm(vm.getId()).second();
if (hostId == null) {
throw new CloudRuntimeException("Cannot find a host to prepare for restoring VM " + vm);
}
try {
Answer answer = agentMgr.easySend(hostId, command);
if (answer != null && answer.getResult()) {
LOG.info("Succeeded to prepare for restoring VM " + vm);
} else {
throw new CloudRuntimeException(String.format("Failed to prepare for restoring VM %s. details: %s", vm,
(answer != null ? answer.getDetails() : null)));
}
} catch (Exception e) {
throw new CloudRuntimeException(String.format("Failed to prepare for restoring VM %s due to exception %s", vm, e));
}
}
@Override
@ -330,6 +382,10 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
+ "domain_id: %s, zone_id: %s].", backup.getUuid(), backup.getVmId(), backup.getExternalId(), backup.getType(), backup.getDate(),
backup.getBackupOfferingId(), backup.getAccountId(), backup.getDomainId(), backup.getZoneId()));
backupDao.persist(backup);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VM_BACKUP_CREATE,
String.format("Created backup %s for VM ID: %s", backup.getUuid(), vm.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0);
}
}
for (final Long backupIdToRemove : removeList) {
@ -349,11 +405,14 @@ public class VeeamBackupProvider extends AdapterBase implements BackupProvider,
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[]{
VeeamUrl,
VeeamVersion,
VeeamUsername,
VeeamPassword,
VeeamValidateSSLSecurity,
VeeamApiRequestTimeout,
VeeamRestoreTimeout
VeeamRestoreTimeout,
VeeamTaskPollInterval,
VeeamTaskPollMaxRetry
};
}

View File

@ -20,12 +20,15 @@ package org.apache.cloudstack.backup.veeam;
import static org.apache.cloudstack.backup.VeeamBackupProvider.BACKUP_IDENTIFIER;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@ -42,6 +45,8 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.backup.veeam.api.BackupFile;
import org.apache.cloudstack.backup.veeam.api.BackupFiles;
import org.apache.cloudstack.backup.veeam.api.BackupJobCloneInfo;
import org.apache.cloudstack.backup.veeam.api.CreateObjectInJobSpec;
import org.apache.cloudstack.backup.veeam.api.EntityReferences;
@ -55,7 +60,10 @@ import org.apache.cloudstack.backup.veeam.api.ObjectsInJob;
import org.apache.cloudstack.backup.veeam.api.Ref;
import org.apache.cloudstack.backup.veeam.api.RestoreSession;
import org.apache.cloudstack.backup.veeam.api.Task;
import org.apache.cloudstack.backup.veeam.api.VmRestorePoint;
import org.apache.cloudstack.backup.veeam.api.VmRestorePoints;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
@ -71,6 +79,7 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.log4j.Logger;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.nio.TrustAllManager;
@ -90,18 +99,31 @@ public class VeeamClient {
private final HttpClient httpClient;
private static final String RESTORE_VM_SUFFIX = "CS-RSTR-";
private static final String SESSION_HEADER = "X-RestSvcSessionId";
private static final String BACKUP_REFERENCE = "BackupReference";
private static final String HIERARCHY_ROOT_REFERENCE = "HierarchyRootReference";
private static final String REPOSITORY_REFERENCE = "RepositoryReference";
private static final String RESTORE_POINT_REFERENCE = "RestorePointReference";
private static final String BACKUP_FILE_REFERENCE = "BackupFileReference";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private static final SimpleDateFormat newDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private String veeamServerIp;
private final Integer veeamServerVersion;
private String veeamServerUsername;
private String veeamServerPassword;
private String veeamSessionId = null;
private int restoreTimeout;
private final int restoreTimeout;
private final int veeamServerPort = 22;
private final int taskPollInterval;
private final int taskPollMaxRetry;
public VeeamClient(final String url, final String username, final String password, final boolean validateCertificate, final int timeout,
final int restoreTimeout) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException {
public VeeamClient(final String url, final Integer version, final String username, final String password, final boolean validateCertificate, final int timeout,
final int restoreTimeout, final int taskPollInterval, final int taskPollMaxRetry) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException {
this.apiURI = new URI(url);
this.restoreTimeout = restoreTimeout;
this.taskPollInterval = taskPollInterval;
this.taskPollMaxRetry = taskPollMaxRetry;
final RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeout * 1000)
@ -125,6 +147,7 @@ public class VeeamClient {
authenticate(username, password);
setVeeamSshCredentials(this.apiURI.getHost(), username, password);
this.veeamServerVersion = (version != null && version != 0) ? version : getVeeamServerVersion();
}
protected void setVeeamSshCredentials(String hostIp, String username, String password) {
@ -135,7 +158,7 @@ public class VeeamClient {
private void authenticate(final String username, final String password) {
// https://helpcenter.veeam.com/docs/backup/rest/http_authentication.html?ver=95u4
final HttpPost request = new HttpPost(apiURI.toString() + "/sessionMngr/?v=v1_4");
final HttpPost request = new HttpPost(apiURI.toString() + "/sessionMngr/?v=latest");
request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()));
try {
final HttpResponse response = httpClient.execute(request);
@ -158,6 +181,26 @@ public class VeeamClient {
}
}
protected Integer getVeeamServerVersion() {
final List<String> cmds = Arrays.asList(
"$InstallPath = Get-ItemProperty -Path 'HKLM:\\Software\\Veeam\\Veeam Backup and Replication\\' ^| Select -ExpandProperty CorePath",
"Add-Type -LiteralPath \\\"$InstallPath\\Veeam.Backup.Configuration.dll\\\"",
"$ProductData = [Veeam.Backup.Configuration.BackupProduct]::Create()",
"$Version = $ProductData.ProductVersion.ToString()",
"if ($ProductData.MarketName -ne '') {$Version += \\\" $($ProductData.MarketName)\\\"}",
"$Version"
);
Pair<Boolean, String> response = executePowerShellCommands(cmds);
if (response == null || !response.first() || response.second() == null || StringUtils.isBlank(response.second().trim())) {
LOG.error("Failed to get veeam server version, using default version");
return 0;
} else {
Integer majorVersion = NumbersUtil.parseInt(response.second().trim().split("\\.")[0], 0);
LOG.info(String.format("Veeam server full version is %s, major version is %s", response.second().trim(), majorVersion));
return majorVersion;
}
}
private void checkResponseOK(final HttpResponse response) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
LOG.debug("Requested Veeam resource does not exist");
@ -238,7 +281,7 @@ public class VeeamClient {
final ObjectMapper objectMapper = new XmlMapper();
final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class);
for (final Ref ref : references.getRefs()) {
if (ref.getName().equals(vmwareDcName) && ref.getType().equals("HierarchyRootReference")) {
if (ref.getName().equals(vmwareDcName) && ref.getType().equals(HIERARCHY_ROOT_REFERENCE)) {
return ref.getUid();
}
}
@ -286,7 +329,7 @@ public class VeeamClient {
private boolean checkTaskStatus(final HttpResponse response) throws IOException {
final Task task = parseTaskResponse(response);
for (int i = 0; i < 120; i++) {
for (int i = 0; i < this.taskPollMaxRetry; i++) {
final HttpResponse taskResponse = get("/tasks/" + task.getTaskId());
final Task polledTask = parseTaskResponse(taskResponse);
if (polledTask.getState().equals("Finished")) {
@ -309,7 +352,7 @@ public class VeeamClient {
throw new CloudRuntimeException("Failed to assign VM to backup offering due to: " + polledTask.getResult().getMessage());
}
try {
Thread.sleep(5000);
Thread.sleep(this.taskPollInterval * 1000);
} catch (InterruptedException e) {
LOG.debug("Failed to sleep while polling for Veeam task status due to: ", e);
}
@ -324,6 +367,10 @@ public class VeeamClient {
if (session.getResult().equals("Success")) {
return true;
}
if (session.getResult().equalsIgnoreCase("Failed")) {
String sessionUid = session.getUid();
throw new CloudRuntimeException(String.format("Restore job [%s] failed.", sessionUid));
}
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
@ -355,7 +402,7 @@ public class VeeamClient {
final ObjectMapper objectMapper = new XmlMapper();
final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class);
for (final Ref ref : references.getRefs()) {
if (ref.getType().equals("RepositoryReference") && ref.getName().equals(repositoryName)) {
if (ref.getType().equals(REPOSITORY_REFERENCE) && ref.getName().equals(repositoryName)) {
return ref;
}
}
@ -368,7 +415,7 @@ public class VeeamClient {
protected String getRepositoryNameFromJob(String backupName) {
final List<String> cmds = Arrays.asList(
String.format("$Job = Get-VBRJob -name \"%s\"", backupName),
String.format("$Job = Get-VBRJob -name '%s'", backupName),
"$Job.GetBackupTargetRepository() ^| select Name ^| Format-List"
);
Pair<Boolean, String> result = executePowerShellCommands(cmds);
@ -376,7 +423,7 @@ public class VeeamClient {
throw new CloudRuntimeException(String.format("Failed to get Repository Name from Job [name: %s].", backupName));
}
for (String block : result.second().split("\n\n")) {
for (String block : result.second().split("\r\n")) {
if (block.matches("Name(\\s)+:(.)*")) {
return block.split(":")[1].trim();
}
@ -553,7 +600,11 @@ public class VeeamClient {
*/
protected String transformPowerShellCommandList(List<String> cmds) {
StringJoiner joiner = new StringJoiner(";");
joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin");
if (isLegacyServer()) {
joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin");
} else {
joiner.add("PowerShell Import-Module Veeam.Backup.PowerShell -WarningAction SilentlyContinue");
}
for (String cmd : cmds) {
joiner.add(cmd);
}
@ -584,22 +635,22 @@ public class VeeamClient {
public boolean setJobSchedule(final String jobName) {
Pair<Boolean, String> result = executePowerShellCommands(Arrays.asList(
String.format("$job = Get-VBRJob -Name \"%s\"", jobName),
String.format("$job = Get-VBRJob -Name '%s'", jobName),
"if ($job) { Set-VBRJobSchedule -Job $job -Daily -At \"11:00\" -DailyKind Weekdays }"
));
return result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE);
return result != null && result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE);
}
public boolean deleteJobAndBackup(final String jobName) {
Pair<Boolean, String> result = executePowerShellCommands(Arrays.asList(
String.format("$job = Get-VBRJob -Name \"%s\"", jobName),
String.format("$job = Get-VBRJob -Name '%s'", jobName),
"if ($job) { Remove-VBRJob -Job $job -Confirm:$false }",
String.format("$backup = Get-VBRBackup -Name \"%s\"", jobName),
String.format("$backup = Get-VBRBackup -Name '%s'", jobName),
"if ($backup) { Remove-VBRBackup -Backup $backup -FromDisk -Confirm:$false }",
"$repo = Get-VBRBackupRepository",
"Sync-VBRBackupRepository -Repository $repo"
));
return result.first() && !result.second().contains(FAILED_TO_DELETE);
return result != null && result.first() && !result.second().contains(FAILED_TO_DELETE);
}
public boolean deleteBackup(final String restorePointId) {
@ -610,40 +661,123 @@ public class VeeamClient {
"$repo = Get-VBRBackupRepository",
"Sync-VBRBackupRepository -Repository $repo",
"} else { ",
" Write-Output \"Failed to delete\"",
" Write-Output 'Failed to delete'",
" Exit 1",
"}"
));
return result.first() && !result.second().contains(FAILED_TO_DELETE);
return result != null && result.first() && !result.second().contains(FAILED_TO_DELETE);
}
public Map<String, Backup.Metric> getBackupMetrics() {
if (isLegacyServer()) {
return getBackupMetricsLegacy();
} else {
return getBackupMetricsViaVeeamAPI();
}
}
public Map<String, Backup.Metric> getBackupMetricsViaVeeamAPI() {
LOG.debug("Trying to get backup metrics via Veeam B&R API");
try {
final HttpResponse response = get(String.format("/backupFiles?format=Entity"));
checkResponseOK(response);
return processHttpResponseForBackupMetrics(response.getEntity().getContent());
} catch (final IOException e) {
LOG.error("Failed to get backup metrics via Veeam B&R API due to:", e);
checkResponseTimeOut(e);
}
return new HashMap<>();
}
protected Map<String, Backup.Metric> processHttpResponseForBackupMetrics(final InputStream content) {
Map<String, Backup.Metric> metrics = new HashMap<>();
try {
final ObjectMapper objectMapper = new XmlMapper();
final BackupFiles backupFiles = objectMapper.readValue(content, BackupFiles.class);
if (backupFiles == null || CollectionUtils.isEmpty(backupFiles.getBackupFiles())) {
throw new CloudRuntimeException("Could not get backup metrics via Veeam B&R API");
}
for (final BackupFile backupFile : backupFiles.getBackupFiles()) {
String vmUuid = null;
String backupName = null;
List<Link> links = backupFile.getLink();
for (Link link : links) {
if (BACKUP_REFERENCE.equals(link.getType())) {
backupName = link.getName();
break;
}
}
if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) {
final String[] names = backupName.split(BACKUP_IDENTIFIER);
if (names.length > 1) {
vmUuid = names[1];
}
}
if (vmUuid == null) {
continue;
}
if (vmUuid.contains(" - ")) {
vmUuid = vmUuid.split(" - ")[0];
}
Long usedSize = 0L;
Long dataSize = 0L;
if (metrics.containsKey(vmUuid)) {
usedSize = metrics.get(vmUuid).getBackupSize();
dataSize = metrics.get(vmUuid).getDataSize();
}
if (backupFile.getBackupSize() != null) {
usedSize += Long.valueOf(backupFile.getBackupSize());
}
if (backupFile.getDataSize() != null) {
dataSize += Long.valueOf(backupFile.getDataSize());
}
metrics.put(vmUuid, new Backup.Metric(usedSize, dataSize));
}
} catch (final IOException e) {
LOG.error("Failed to process response to get backup metrics via Veeam B&R API due to:", e);
checkResponseTimeOut(e);
}
return metrics;
}
public Map<String, Backup.Metric> getBackupMetricsLegacy() {
final String separator = "=====";
final List<String> cmds = Arrays.asList(
"$backups = Get-VBRBackup",
"foreach ($backup in $backups) {" +
"$backup.JobName;" +
"$storageGroups = $backup.GetStorageGroups();" +
"foreach ($group in $storageGroups) {" +
"$usedSize = 0;" +
"$dataSize = 0;" +
"$sizePerStorage = $group.GetStorages().Stats.BackupSize;" +
"$dataPerStorage = $group.GetStorages().Stats.DataSize;" +
"foreach ($size in $sizePerStorage) {" +
"$usedSize += $size;" +
"}" +
"foreach ($size in $dataPerStorage) {" +
"$dataSize += $size;" +
"}" +
"$usedSize;" +
"$dataSize;" +
"}" +
"echo \"" + separator + "\"" +
"}"
"$backups = Get-VBRBackup",
"foreach ($backup in $backups) {" +
" $backup.JobName;" +
" $storageGroups = $backup.GetStorageGroups();" +
" foreach ($group in $storageGroups) {" +
" $usedSize = 0;" +
" $dataSize = 0;" +
" $sizePerStorage = $group.GetStorages().Stats.BackupSize;" +
" $dataPerStorage = $group.GetStorages().Stats.DataSize;" +
" foreach ($size in $sizePerStorage) {" +
" $usedSize += $size;" +
" }" +
" foreach ($size in $dataPerStorage) {" +
" $dataSize += $size;" +
" }" +
" $usedSize;" +
" $dataSize;" +
" }" +
" echo \"" + separator + "\"" +
"}"
);
Pair<Boolean, String> response = executePowerShellCommands(cmds);
if (response == null || !response.first()) {
throw new CloudRuntimeException("Failed to get backup metrics via PowerShell command");
}
return processPowerShellResultForBackupMetrics(response.second());
}
protected Map<String, Backup.Metric> processPowerShellResultForBackupMetrics(final String result) {
LOG.debug("Processing powershell result: " + result);
final String separator = "=====";
final Map<String, Backup.Metric> sizes = new HashMap<>();
for (final String block : response.second().split(separator + "\r\n")) {
for (final String block : result.split(separator + "\r\n")) {
final String[] parts = block.split("\r\n");
if (parts.length != 3) {
continue;
@ -677,9 +811,9 @@ public class VeeamClient {
return new Backup.RestorePoint(id, created, type);
}
public List<Backup.RestorePoint> listRestorePoints(String backupName, String vmInternalName) {
public List<Backup.RestorePoint> listRestorePointsLegacy(String backupName, String vmInternalName) {
final List<String> cmds = Arrays.asList(
String.format("$backup = Get-VBRBackup -Name \"%s\"", backupName),
String.format("$backup = Get-VBRBackup -Name '%s'", backupName),
String.format("if ($backup) { $restore = (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true})", vmInternalName),
"if ($restore) { $restore ^| Format-List } }"
);
@ -700,6 +834,71 @@ public class VeeamClient {
return restorePoints;
}
public List<Backup.RestorePoint> listRestorePoints(String backupName, String vmInternalName) {
if (isLegacyServer()) {
return listRestorePointsLegacy(backupName, vmInternalName);
} else {
return listVmRestorePointsViaVeeamAPI(vmInternalName);
}
}
public List<Backup.RestorePoint> listVmRestorePointsViaVeeamAPI(String vmInternalName) {
LOG.debug(String.format("Trying to list VM restore points via Veeam B&R API for VM %s: ", vmInternalName));
try {
final HttpResponse response = get(String.format("/vmRestorePoints?format=Entity"));
checkResponseOK(response);
return processHttpResponseForVmRestorePoints(response.getEntity().getContent(), vmInternalName);
} catch (final IOException e) {
LOG.error("Failed to list VM restore points via Veeam B&R API due to:", e);
checkResponseTimeOut(e);
}
return new ArrayList<>();
}
public List<Backup.RestorePoint> processHttpResponseForVmRestorePoints(InputStream content, String vmInternalName) {
List<Backup.RestorePoint> vmRestorePointList = new ArrayList<>();
try {
final ObjectMapper objectMapper = new XmlMapper();
final VmRestorePoints vmRestorePoints = objectMapper.readValue(content, VmRestorePoints.class);
if (vmRestorePoints == null) {
throw new CloudRuntimeException("Could not get VM restore points via Veeam B&R API");
}
for (final VmRestorePoint vmRestorePoint : vmRestorePoints.getVmRestorePoints()) {
LOG.debug(String.format("Processing VM restore point Name=%s, VmDisplayName=%s for vm name=%s",
vmRestorePoint.getName(), vmRestorePoint.getVmDisplayName(), vmInternalName));
if (!vmInternalName.equals(vmRestorePoint.getVmDisplayName())) {
continue;
}
boolean isReady = true;
List<Link> links = vmRestorePoint.getLink();
for (Link link : links) {
if (Arrays.asList(BACKUP_FILE_REFERENCE, RESTORE_POINT_REFERENCE).contains(link.getType()) && !link.getRel().equals("Up")) {
LOG.info(String.format("The VM restore point is not ready. Reference: %s, state: %s", link.getType(), link.getRel()));
isReady = false;
break;
}
}
if (!isReady) {
continue;
}
String vmRestorePointId = vmRestorePoint.getUid().substring(vmRestorePoint.getUid().lastIndexOf(':') + 1);
String created = formatDate(vmRestorePoint.getCreationTimeUtc());
String type = vmRestorePoint.getPointType();
LOG.debug(String.format("Adding restore point %s, %s, %s", vmRestorePointId, created, type));
vmRestorePointList.add(new Backup.RestorePoint(vmRestorePointId, created, type));
}
} catch (final IOException | ParseException e) {
LOG.error("Failed to process response to get VM restore points via Veeam B&R API due to:", e);
checkResponseTimeOut(e);
}
return vmRestorePointList;
}
private String formatDate(String date) throws ParseException {
return newDateFormat.format(dateFormat.parse(StringUtils.substring(date, 0, 19)));
}
public Pair<Boolean, String> restoreVMToDifferentLocation(String restorePointId, String hostIp, String dataStoreUuid) {
final String restoreLocation = RESTORE_VM_SUFFIX + UUID.randomUUID().toString();
final String datastoreId = dataStoreUuid.replace("-","");
@ -717,4 +916,8 @@ public class VeeamClient {
}
return new Pair<>(result.first(), restoreLocation);
}
private boolean isLegacyServer() {
return this.veeamServerVersion != null && (this.veeamServerVersion > 0 && this.veeamServerVersion < 11);
}
}

View File

@ -0,0 +1,160 @@
// 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.backup.veeam.api;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.List;
@JacksonXmlRootElement(localName = "BackupFile")
public class BackupFile {
@JacksonXmlProperty(localName = "Type", isAttribute = true)
private String type;
@JacksonXmlProperty(localName = "Href", isAttribute = true)
private String href;
@JacksonXmlProperty(localName = "Name", isAttribute = true)
private String name;
@JacksonXmlProperty(localName = "UID", isAttribute = true)
private String uid;
@JacksonXmlProperty(localName = "Link")
@JacksonXmlElementWrapper(localName = "Links")
private List<Link> link;
@JacksonXmlProperty(localName = "FilePath")
private String filePath;
@JacksonXmlProperty(localName = "BackupSize")
private String backupSize;
@JacksonXmlProperty(localName = "DataSize")
private String dataSize;
@JacksonXmlProperty(localName = "DeduplicationRatio")
private String deduplicationRatio;
@JacksonXmlProperty(localName = "CompressRatio")
private String compressRatio;
@JacksonXmlProperty(localName = "CreationTimeUtc")
private String creationTimeUtc;
@JacksonXmlProperty(localName = "FileType")
private String fileType;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public List<Link> getLink() {
return link;
}
public void setLink(List<Link> link) {
this.link = link;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getBackupSize() {
return backupSize;
}
public void setBackupSize(String backupSize) {
this.backupSize = backupSize;
}
public String getDataSize() {
return dataSize;
}
public void setDataSize(String dataSize) {
this.dataSize = dataSize;
}
public String getDeduplicationRatio() {
return deduplicationRatio;
}
public void setDeduplicationRatio(String deduplicationRatio) {
this.deduplicationRatio = deduplicationRatio;
}
public String getCompressRatio() {
return compressRatio;
}
public void setCompressRatio(String compressRatio) {
this.compressRatio = compressRatio;
}
public String getCreationTimeUtc() {
return creationTimeUtc;
}
public void setCreationTimeUtc(String creationTimeUtc) {
this.creationTimeUtc = creationTimeUtc;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
}

View File

@ -0,0 +1,39 @@
// 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.backup.veeam.api;
import java.util.List;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JacksonXmlRootElement(localName = "BackupFiles")
public class BackupFiles {
@JacksonXmlProperty(localName = "BackupFile")
@JacksonXmlElementWrapper(localName = "BackupFile", useWrapping = false)
private List<BackupFile> backupFiles;
public List<BackupFile> getBackupFiles() {
return backupFiles;
}
public void setBackupFiles(List<BackupFile> backupFiles) {
this.backupFiles = backupFiles;
}
}

View File

@ -0,0 +1,149 @@
// 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.backup.veeam.api;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.List;
@JacksonXmlRootElement(localName = "VmRestorePoint")
public class VmRestorePoint {
@JacksonXmlProperty(localName = "Type", isAttribute = true)
private String type;
@JacksonXmlProperty(localName = "Href", isAttribute = true)
private String href;
@JacksonXmlProperty(localName = "Name", isAttribute = true)
private String name;
@JacksonXmlProperty(localName = "UID", isAttribute = true)
private String uid;
@JacksonXmlProperty(localName = "VmDisplayName", isAttribute = true)
private String vmDisplayName;
@JacksonXmlProperty(localName = "Link")
@JacksonXmlElementWrapper(localName = "Links")
private List<Link> link;
@JacksonXmlProperty(localName = "CreationTimeUTC")
private String creationTimeUtc;
@JacksonXmlProperty(localName = "VmName")
private String vmName;
@JacksonXmlProperty(localName = "Algorithm")
private String algorithm;
@JacksonXmlProperty(localName = "PointType")
private String pointType;
@JacksonXmlProperty(localName = "HierarchyObjRef")
private String hierarchyObjRef;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getVmDisplayName() {
return vmDisplayName;
}
public void setVmDisplayName(String vmDisplayName) {
this.vmDisplayName = vmDisplayName;
}
public List<Link> getLink() {
return link;
}
public void setLink(List<Link> link) {
this.link = link;
}
public String getCreationTimeUtc() {
return creationTimeUtc;
}
public void setCreationTimeUtc(String creationTimeUtc) {
this.creationTimeUtc = creationTimeUtc;
}
public String getVmName() {
return vmName;
}
public void setVmName(String vmName) {
this.vmName = vmName;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getPointType() {
return pointType;
}
public void setPointType(String pointType) {
this.pointType = pointType;
}
public String getHierarchyObjRef() {
return hierarchyObjRef;
}
public void setHierarchyObjRef(String hierarchyObjRef) {
this.hierarchyObjRef = hierarchyObjRef;
}
}

View File

@ -0,0 +1,39 @@
// 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.backup.veeam.api;
import java.util.List;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JacksonXmlRootElement(localName = "VmRestorePoints")
public class VmRestorePoints {
@JacksonXmlProperty(localName = "VmRestorePoint")
@JacksonXmlElementWrapper(localName = "VmRestorePoint", useWrapping = false)
private List<VmRestorePoint> VmRestorePoints;
public List<VmRestorePoint> getVmRestorePoints() {
return VmRestorePoints;
}
public void setVmRestorePoints(List<VmRestorePoint> vmRestorePoints) {
VmRestorePoints = vmRestorePoints;
}
}

View File

@ -27,9 +27,13 @@ import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.times;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.backup.veeam.api.RestoreSession;
import org.apache.http.HttpResponse;
@ -62,9 +66,10 @@ public class VeeamClientTest {
.withStatus(201)
.withHeader("X-RestSvcSessionId", "some-session-auth-id")
.withBody("")));
client = new VeeamClient("http://localhost:9399/api/", adminUsername, adminPassword, true, 60, 600);
client = new VeeamClient("http://localhost:9399/api/", 12, adminUsername, adminPassword, true, 60, 600, 5, 120);
mockClient = Mockito.mock(VeeamClient.class);
Mockito.when(mockClient.getRepositoryNameFromJob(Mockito.anyString())).thenCallRealMethod();
Mockito.when(mockClient.getVeeamServerVersion()).thenCallRealMethod();
}
@Test
@ -139,7 +144,7 @@ public class VeeamClientTest {
@Test
public void getRepositoryNameFromJobTestSuccess() throws Exception {
String backupName = "TEST-BACKUP3";
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "\n\nName : test");
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "\r\nName : test");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
String repositoryNameFromJob = mockClient.getRepositoryNameFromJob(backupName);
Assert.assertEquals("test", repositoryNameFromJob);
@ -162,4 +167,324 @@ public class VeeamClientTest {
}
Mockito.verify(mockClient, times(10)).get(Mockito.anyString());
}
private void verifyBackupMetrics(Map<String, Backup.Metric> metrics) {
Assert.assertEquals(2, metrics.size());
Assert.assertTrue(metrics.containsKey("d1bd8abd-fc73-4b77-9047-7be98a2ecb72"));
Assert.assertEquals(537776128L, (long) metrics.get("d1bd8abd-fc73-4b77-9047-7be98a2ecb72").getBackupSize());
Assert.assertEquals(2147506644L, (long) metrics.get("d1bd8abd-fc73-4b77-9047-7be98a2ecb72").getDataSize());
Assert.assertTrue(metrics.containsKey("0d752ca6-d628-4d85-a739-75275e4661e6"));
Assert.assertEquals(1268682752L, (long) metrics.get("0d752ca6-d628-4d85-a739-75275e4661e6").getBackupSize());
Assert.assertEquals(15624049921L, (long) metrics.get("0d752ca6-d628-4d85-a739-75275e4661e6").getDataSize());
}
@Test
public void testProcessPowerShellResultForBackupMetrics() {
String result = "i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98a2ecb72\r\n" +
"537776128\r\n" +
"2147506644\r\n" +
"=====\r\n" +
"i-13-22-VM-CSBKP-b3b3cb75-cfbf-4496-9c63-a08a93347276\r\n" +
"=====\r\n" +
"backup-job-based-on-sla\r\n" +
"=====\r\n" +
"i-12-20-VM-CSBKP-9f292f11-00ec-4915-84f0-e3895828640e\r\n" +
"=====\r\n" +
"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\r\n" +
"1268682752\r\n" +
"15624049921\r\n" +
"=====\r\n";
Map<String, Backup.Metric> metrics = client.processPowerShellResultForBackupMetrics(result);
verifyBackupMetrics(metrics);
}
@Test
public void testProcessHttpResponseForBackupMetricsForV11() {
String xmlResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<BackupFiles xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.veeam.com/ent/v1.0\">\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/d2110f5f-aa22-4e67-8084-5d8597f26d63?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-28T000059_745D.vbk\" UID=\"urn:veeam:BackupFile:d2110f5f-aa22-4e67-8084-5d8597f26d63\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d2110f5f-aa22-4e67-8084-5d8597f26d63\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-28T000059_745D.vbk\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d2110f5f-aa22-4e67-8084-5d8597f26d63/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d2110f5f-aa22-4e67-8084-5d8597f26d63/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-28T000059_745D.vbk</FilePath>\n" +
" <BackupSize>579756032</BackupSize>\n" +
" <DataSize>7516219400</DataSize>\n" +
" <DeduplicationRatio>5.83</DeduplicationRatio>\n" +
" <CompressRatio>2.22</CompressRatio>\n" +
" <CreationTimeUtc>2023-10-27T23:00:13.74Z</CreationTimeUtc>\n" +
" <FileType>vbk</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/7c54d13d-7b9c-465a-8ec8-7a276bde57dd?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-05T000022_7987.vib\" UID=\"urn:veeam:BackupFile:7c54d13d-7b9c-465a-8ec8-7a276bde57dd\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/7c54d13d-7b9c-465a-8ec8-7a276bde57dd\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-05T000022_7987.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/7c54d13d-7b9c-465a-8ec8-7a276bde57dd/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/7c54d13d-7b9c-465a-8ec8-7a276bde57dd/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-05T000022_7987.vib</FilePath>\n" +
" <BackupSize>12083200</BackupSize>\n" +
" <DataSize>69232800</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.67</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-05T00:00:22.827Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/4b1181fd-7b1e-4af1-a76b-8284a8953b99?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-01T000035_BEBF.vib\" UID=\"urn:veeam:BackupFile:4b1181fd-7b1e-4af1-a76b-8284a8953b99\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/4b1181fd-7b1e-4af1-a76b-8284a8953b99\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-01T000035_BEBF.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/4b1181fd-7b1e-4af1-a76b-8284a8953b99/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/4b1181fd-7b1e-4af1-a76b-8284a8953b99/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-01T000035_BEBF.vib</FilePath>\n" +
" <BackupSize>12398592</BackupSize>\n" +
" <DataSize>71329948</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.67</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-01T00:00:35.163Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/66b39f48-af76-4373-b333-996fc04da894?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-04T000109_2AC1.vbk\" UID=\"urn:veeam:BackupFile:66b39f48-af76-4373-b333-996fc04da894\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/66b39f48-af76-4373-b333-996fc04da894\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-04T000109_2AC1.vbk\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/66b39f48-af76-4373-b333-996fc04da894/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/66b39f48-af76-4373-b333-996fc04da894/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-04T000109_2AC1.vbk</FilePath>\n" +
" <BackupSize>581083136</BackupSize>\n" +
" <DataSize>7516219404</DataSize>\n" +
" <DeduplicationRatio>5.82</DeduplicationRatio>\n" +
" <CompressRatio>2.22</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-04T00:00:24.973Z</CreationTimeUtc>\n" +
" <FileType>vbk</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/8e9a854e-9bb8-4a34-815c-a6ab17a1e72f?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-29T000033_F468.vib\" UID=\"urn:veeam:BackupFile:8e9a854e-9bb8-4a34-815c-a6ab17a1e72f\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/8e9a854e-9bb8-4a34-815c-a6ab17a1e72f\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-29T000033_F468.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/8e9a854e-9bb8-4a34-815c-a6ab17a1e72f/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/8e9a854e-9bb8-4a34-815c-a6ab17a1e72f/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-29T000033_F468.vib</FilePath>\n" +
" <BackupSize>11870208</BackupSize>\n" +
" <DataSize>72378524</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>7.14</CompressRatio>\n" +
" <CreationTimeUtc>2023-10-28T23:00:33.233Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/cf4536c0-d752-4ba5-ad7f-bbc17c7e107b?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-30T000022_0CE3.vib\" UID=\"urn:veeam:BackupFile:cf4536c0-d752-4ba5-ad7f-bbc17c7e107b\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/cf4536c0-d752-4ba5-ad7f-bbc17c7e107b\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-30T000022_0CE3.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/cf4536c0-d752-4ba5-ad7f-bbc17c7e107b/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/cf4536c0-d752-4ba5-ad7f-bbc17c7e107b/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-30T000022_0CE3.vib</FilePath>\n" +
" <BackupSize>14409728</BackupSize>\n" +
" <DataSize>76572828</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.25</CompressRatio>\n" +
" <CreationTimeUtc>2023-10-30T00:00:22.7Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/2dd7f5b6-8a10-406d-9c4f-c0dfa987e85c?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-06T000018_055B.vib\" UID=\"urn:veeam:BackupFile:2dd7f5b6-8a10-406d-9c4f-c0dfa987e85c\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/2dd7f5b6-8a10-406d-9c4f-c0dfa987e85c\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-06T000018_055B.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/2dd7f5b6-8a10-406d-9c4f-c0dfa987e85c/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/2dd7f5b6-8a10-406d-9c4f-c0dfa987e85c/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-06T000018_055B.vib</FilePath>\n" +
" <BackupSize>17883136</BackupSize>\n" +
" <DataSize>80767136</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>5</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-06T00:00:18.253Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/3fd6da3a-47bf-45fa-a4c8-c436e3cd34a7?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-02T000029_65BE.vib\" UID=\"urn:veeam:BackupFile:3fd6da3a-47bf-45fa-a4c8-c436e3cd34a7\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/3fd6da3a-47bf-45fa-a4c8-c436e3cd34a7\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-02T000029_65BE.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/3fd6da3a-47bf-45fa-a4c8-c436e3cd34a7/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/3fd6da3a-47bf-45fa-a4c8-c436e3cd34a7/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-02T000029_65BE.vib</FilePath>\n" +
" <BackupSize>12521472</BackupSize>\n" +
" <DataSize>72378525</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.67</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-02T00:00:29.05Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/d93d7c7d-068a-4e8f-ba54-e08cea3cb9d2?format=Entity\" Type=\"BackupFile\" Name=\"i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98aD2023-10-25T145951_8062.vbk\" UID=\"urn:veeam:BackupFile:d93d7c7d-068a-4e8f-ba54-e08cea3cb9d2\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/a34cae53-2d9e-454b-8d3e-0aaa7b34c228\" Name=\"i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98a2ecb72\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d93d7c7d-068a-4e8f-ba54-e08cea3cb9d2\" Name=\"i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98aD2023-10-25T145951_8062.vbk\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d93d7c7d-068a-4e8f-ba54-e08cea3cb9d2/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/d93d7c7d-068a-4e8f-ba54-e08cea3cb9d2/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98a2ecb72\\i-2-3-VM-CSBKP-d1bd8abd-fc73-4b77-9047-7be98aD2023-10-25T145951_8062.vbk</FilePath>\n" +
" <BackupSize>537776128</BackupSize>\n" +
" <DataSize>2147506644</DataSize>\n" +
" <DeduplicationRatio>1.68</DeduplicationRatio>\n" +
" <CompressRatio>2.38</CompressRatio>\n" +
" <CreationTimeUtc>2023-10-25T13:59:51.76Z</CreationTimeUtc>\n" +
" <FileType>vbk</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/094564ff-02a1-46c7-b9e5-e249b8b9acf6?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-03T000024_7ACF.vib\" UID=\"urn:veeam:BackupFile:094564ff-02a1-46c7-b9e5-e249b8b9acf6\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/094564ff-02a1-46c7-b9e5-e249b8b9acf6\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-03T000024_7ACF.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/094564ff-02a1-46c7-b9e5-e249b8b9acf6/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/094564ff-02a1-46c7-b9e5-e249b8b9acf6/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-11-03T000024_7ACF.vib</FilePath>\n" +
" <BackupSize>14217216</BackupSize>\n" +
" <DataSize>76572832</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.25</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-03T00:00:24.803Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
" <BackupFile Href=\"https://10.0.3.141:9398/api/backupFiles/1f6f5c49-92ef-4757-b327-e63ae9f1fdea?format=Entity\" Type=\"BackupFile\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-31T000015_4624.vib\" UID=\"urn:veeam:BackupFile:1f6f5c49-92ef-4757-b327-e63ae9f1fdea\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backups/e7484f82-b01b-47cf-92ad-ac5e8379a4fe\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\" Type=\"BackupReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupServers/bb188236-7b8b-4763-b35a-5d6645d3e95b\" Name=\"10.0.3.141\" Type=\"BackupServerReference\" Rel=\"Up\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/1f6f5c49-92ef-4757-b327-e63ae9f1fdea\" Name=\"i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-31T000015_4624.vib\" Type=\"BackupFileReference\" Rel=\"Alternate\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/1f6f5c49-92ef-4757-b327-e63ae9f1fdea/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\"/>\n" +
" <Link Href=\"https://10.0.3.141:9398/api/backupFiles/1f6f5c49-92ef-4757-b327-e63ae9f1fdea/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\"/>\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275e4661e6\\i-2-5-VM-CSBKP-0d752ca6-d628-4d85-a739-75275eD2023-10-31T000015_4624.vib</FilePath>\n" +
" <BackupSize>12460032</BackupSize>\n" +
" <DataSize>72378524</DataSize>\n" +
" <DeduplicationRatio>1</DeduplicationRatio>\n" +
" <CompressRatio>6.67</CompressRatio>\n" +
" <CreationTimeUtc>2023-10-31T00:00:15.853Z</CreationTimeUtc>\n" +
" <FileType>vib</FileType>\n" +
" </BackupFile>\n" +
"</BackupFiles>\n";
InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes());
Map<String, Backup.Metric> metrics = client.processHttpResponseForBackupMetrics(inputStream);
verifyBackupMetrics(metrics);
}
@Test
public void testGetBackupMetricsViaVeeamAPI() {
String xmlResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<BackupFiles\n" +
" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xmlns=\"http://www.veeam.com/ent/v1.0\">\n" +
" <BackupFile Href=\"https://10.0.3.142:9398/api/backupFiles/6bf10cad-9181-45d9-9cc5-dd669366a381?format=Entity\" Type=\"BackupFile\" Name=\"i-2-4-VM.vm-1036D2023-11-03T162535_89D6.vbk\" UID=\"urn:veeam:BackupFile:6bf10cad-9181-45d9-9cc5-dd669366a381\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backups/957d3817-2480-4c06-85f9-103e625c20e5\" Name=\"i-2-4-VM-CSBKP-506760dc-ed77-40d6-a91d-e0914e7a1ad8 - i-2-4-VM\" Type=\"BackupReference\" Rel=\"Up\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupServers/18cc2a81-1ff0-42cd-8389-62f2bbcc6b7f\" Name=\"10.0.3.142\" Type=\"BackupServerReference\" Rel=\"Up\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupFiles/6bf10cad-9181-45d9-9cc5-dd669366a381\" Name=\"i-2-4-VM.vm-1036D2023-11-03T162535_89D6.vbk\" Type=\"BackupFileReference\" Rel=\"Alternate\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupFiles/6bf10cad-9181-45d9-9cc5-dd669366a381/restorePoints\" Type=\"RestorePointReferenceList\" Rel=\"Related\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupFiles/6bf10cad-9181-45d9-9cc5-dd669366a381/vmRestorePoints\" Type=\"VmRestorePointReferenceList\" Rel=\"Down\" />\n" +
" </Links>\n" +
" <FilePath>V:\\Backup\\i-2-4-VM-CSBKP-506760dc-ed77-40d6-a91d-e0914e7a1ad8\\i-2-4-VM.vm-1036D2023-11-03T162535_89D6.vbk</FilePath>\n" +
" <BackupSize>535875584</BackupSize>\n" +
" <DataSize>2147507235</DataSize>\n" +
" <DeduplicationRatio>1.68</DeduplicationRatio>\n" +
" <CompressRatio>2.38</CompressRatio>\n" +
" <CreationTimeUtc>2023-11-03T16:25:35.920773Z</CreationTimeUtc>\n" +
" <FileType>vbk</FileType>\n" +
" </BackupFile>\n" +
"</BackupFiles>";
wireMockRule.stubFor(get(urlMatching(".*/backupFiles\\?format=Entity"))
.willReturn(aResponse()
.withHeader("content-type", "application/xml")
.withStatus(200)
.withBody(xmlResponse)));
Map<String, Backup.Metric> metrics = client.getBackupMetricsViaVeeamAPI();
Assert.assertEquals(1, metrics.size());
Assert.assertTrue(metrics.containsKey("506760dc-ed77-40d6-a91d-e0914e7a1ad8"));
Assert.assertEquals(535875584L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getBackupSize());
Assert.assertEquals(2147507235L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getDataSize());
}
@Test
public void testListVmRestorePointsViaVeeamAPI() {
String xmlResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<VmRestorePoints\n" +
" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xmlns=\"http://www.veeam.com/ent/v1.0\">\n" +
" <VmRestorePoint Href=\"https://10.0.3.142:9398/api/vmRestorePoints/f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977?format=Entity\" Type=\"VmRestorePoint\" Name=\"i-2-4-VM@2023-11-03 16:26:12.209913\" UID=\"urn:veeam:VmRestorePoint:f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977\" VmDisplayName=\"i-2-4-VM\">\n" +
" <Links>\n" +
" <Link Href=\"https://10.0.3.142:9398/api/vmRestorePoints/f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977?action=restore\" Rel=\"Restore\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupServers/18cc2a81-1ff0-42cd-8389-62f2bbcc6b7f\" Name=\"10.0.3.142\" Type=\"BackupServerReference\" Rel=\"Up\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/restorePoints/c030b23e-d7fa-45b6-a5a7-feb8525d2563\" Name=\"2023-11-03 16:25:35.920773\" Type=\"RestorePointReference\" Rel=\"Up\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/backupFiles/6bf10cad-9181-45d9-9cc5-dd669366a381\" Name=\"i-2-4-VM.vm-1036D2023-11-03T162535_89D6.vbk\" Type=\"BackupFileReference\" Rel=\"Up\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/vmRestorePoints/f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977\" Name=\"i-2-4-VM@2023-11-03 16:26:12.209913\" Type=\"VmRestorePointReference\" Rel=\"Alternate\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/vmRestorePoints/f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977/mounts\" Type=\"VmRestorePointMountList\" Rel=\"Down\" />\n" +
" <Link Href=\"https://10.0.3.142:9398/api/vmRestorePoints/f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977/mounts\" Type=\"VmRestorePointMount\" Rel=\"Create\" />\n" +
" </Links>\n" +
" <CreationTimeUTC>2023-11-03T16:26:12.209913Z</CreationTimeUTC>\n" +
" <VmName>i-2-4-VM</VmName>\n" +
" <Algorithm>Full</Algorithm>\n" +
" <PointType>Full</PointType>\n" +
" <HierarchyObjRef>urn:VMware:Vm:adb5423b-b578-4c26-8ab8-cde9c1faec55.vm-1036</HierarchyObjRef>\n" +
" </VmRestorePoint>\n" +
"</VmRestorePoints>\n";
String vmName = "i-2-4-VM";
wireMockRule.stubFor(get(urlMatching(".*/vmRestorePoints\\?format=Entity"))
.willReturn(aResponse()
.withHeader("content-type", "application/xml")
.withStatus(200)
.withBody(xmlResponse)));
List<Backup.RestorePoint> vmRestorePointList = client.listVmRestorePointsViaVeeamAPI(vmName);
Assert.assertEquals(1, vmRestorePointList.size());
Assert.assertEquals("f6d504cf-eafe-4cd2-8dfc-e9cfe2f1e977", vmRestorePointList.get(0).getId());
Assert.assertEquals("2023-11-03 16:26:12", vmRestorePointList.get(0).getCreated());
Assert.assertEquals("Full", vmRestorePointList.get(0).getType());
}
@Test
public void testGetVeeamServerVersionAllGood() {
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "12.0.0.1");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
Assert.assertEquals(12, (int) mockClient.getVeeamServerVersion());
}
@Test
public void testGetVeeamServerVersionWithError() {
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.FALSE, "");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
Assert.assertEquals(0, (int) mockClient.getVeeamServerVersion());
}
@Test
public void testGetVeeamServerVersionWithEmptyVersion() {
Pair<Boolean, String> response = new Pair<Boolean, String>(Boolean.TRUE, "");
Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList());
Assert.assertEquals(0, (int) mockClient.getVeeamServerVersion());
}
}

View File

@ -781,8 +781,7 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co
volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true);
operation = "created";
}
s_logger.debug(String.format("VM [id: %s, instanceName: %s] backup restore operation %s volume [id: %s].", instanceId, vmInstanceVO.getInstanceName(),
operation, volume.getUuid()));
s_logger.debug(String.format("Sync volumes to %s in backup restore operation: %s volume [id: %s].", vmInstanceVO, operation, volume.getUuid()));
}
}
@ -879,9 +878,13 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co
String tag = parts[parts.length - 1];
String[] tagSplit = tag.split("-");
tag = tagSplit[tagSplit.length - 1];
s_logger.debug(String.format("Trying to find network with vlan: [%s].", vlan));
NetworkVO networkVO = networkDao.findByVlan(vlan);
if (networkVO == null) {
networkVO = createNetworkRecord(zoneId, tag, vlan, accountId, domainId);
s_logger.debug(String.format("Created new network record [id: %s] with details [zoneId: %s, tag: %s, vlan: %s, accountId: %s and domainId: %s].",
networkVO.getUuid(), zoneId, tag, vlan, accountId, domainId));
}
return networkVO;
}
@ -893,6 +896,7 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co
Map<String, NetworkVO> mapping = new HashMap<>();
for (String networkName : vmNetworkNames) {
NetworkVO networkVO = getGuestNetworkFromNetworkMorName(networkName, accountId, zoneId, domainId);
s_logger.debug(String.format("Mapping network name [%s] to networkVO [id: %s].", networkName, networkVO.getUuid()));
mapping.put(networkName, networkVO);
}
return mapping;
@ -927,12 +931,19 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co
String macAddress = pair.first();
String networkName = pair.second();
NetworkVO networkVO = networksMapping.get(networkName);
NicVO nicVO = nicDao.findByNetworkIdAndMacAddress(networkVO.getId(), macAddress);
NicVO nicVO = nicDao.findByNetworkIdAndMacAddressIncludingRemoved(networkVO.getId(), macAddress);
if (nicVO != null) {
s_logger.warn(String.format("Find NIC in DB with networkId [%s] and MAC Address [%s], so this NIC will be removed from list of unmapped NICs of VM [id: %s, name: %s].",
networkVO.getId(), macAddress, vm.getUuid(), vm.getInstanceName()));
allNics.remove(nicVO);
if (nicVO.getRemoved() != null) {
nicDao.unremove(nicVO.getId());
}
}
}
for (final NicVO unMappedNic : allNics) {
s_logger.debug(String.format("Removing NIC [%s] from backup restored %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(unMappedNic, "uuid", "macAddress"), vm));
vmManager.removeNicFromVm(vm, unMappedNic);
}
}

View File

@ -49,6 +49,7 @@ import javax.naming.ConfigurationException;
import javax.xml.datatype.XMLGregorianCalendar;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.backup.PrepareForBackupRestorationCommand;
import org.apache.cloudstack.storage.command.CopyCommand;
import org.apache.cloudstack.storage.command.StorageSubSystemCommand;
import org.apache.cloudstack.storage.configdrive.ConfigDrive;
@ -606,6 +607,8 @@ public class VmwareResource extends ServerResourceBase implements StoragePoolRes
answer = execute((GetVmVncTicketCommand) cmd);
} else if (clz == GetAutoScaleMetricsCommand.class) {
answer = execute((GetAutoScaleMetricsCommand) cmd);
} else if (clz == PrepareForBackupRestorationCommand.class) {
answer = execute((PrepareForBackupRestorationCommand) cmd);
} else {
answer = Answer.createUnsupportedCommandAnswer(cmd);
}
@ -7751,6 +7754,35 @@ public class VmwareResource extends ServerResourceBase implements StoragePoolRes
}
}
private Answer execute(PrepareForBackupRestorationCommand command) {
try {
VmwareHypervisorHost hyperHost = getHyperHost(getServiceContext());
String vmName = command.getVmName();
VirtualMachineMO vmMo = hyperHost.findVmOnHyperHost(vmName);
if (vmMo == null) {
if (hyperHost instanceof HostMO) {
ClusterMO clusterMo = new ClusterMO(hyperHost.getContext(), ((HostMO) hyperHost).getParentMor());
vmMo = clusterMo.findVmOnHyperHost(vmName);
}
}
if (vmMo == null) {
String msg = "VM " + vmName + " no longer exists to execute PrepareForBackupRestorationCommand command";
s_logger.error(msg);
throw new Exception(msg);
}
vmMo.removeChangeTrackPathFromVmdkForDisks();
return new Answer(command, true, "success");
} catch (Exception e) {
s_logger.error("Unexpected exception: ", e);
return new Answer(command, false, "Unable to execute PrepareForBackupRestorationCommand due to " + e.toString());
}
}
private Integer getVmwareWindowTimeInterval() {
Integer windowInterval = VmwareManager.VMWARE_STATS_TIME_WINDOW.value();
if (windowInterval == null || windowInterval < 20) {

View File

@ -75,6 +75,7 @@ import com.cloud.dc.dao.DataCenterDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.event.EventVO;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
@ -477,6 +478,11 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("The assigned backup offering does not allow ad-hoc user backup");
}
ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, vm.getAccountId(),
EventTypes.EVENT_VM_BACKUP_CREATE, "creating backup for VM ID:" + vm.getUuid(),
vmId, ApiCommandResourceType.VirtualMachine.toString(),
true, 0);
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
if (backupProvider != null && backupProvider.takeBackup(vm)) {
return true;
@ -555,10 +561,21 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
} catch (final Exception e) {
LOG.error(String.format("Failed to import VM [vmInternalName: %s] from backup restoration [%s] with hypervisor [type: %s] due to: [%s].", vmInternalName,
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "id", "uuid", "vmId", "externalId", "backupType"), hypervisorType, e.getMessage()), e);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE,
String.format("Failed to import VM %s from backup %s with hypervisor [type: %s]", vmInternalName, backup.getUuid(), hypervisorType),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0);
throw new CloudRuntimeException("Error during vm backup restoration and import: " + e.getMessage());
}
if (vm == null) {
LOG.error("Failed to import restored VM " + vmInternalName + " with hypervisor type " + hypervisorType + " using backup of VM ID " + backup.getVmId());
String message = String.format("Failed to import restored VM %s with hypervisor type %s using backup of VM ID %s",
vmInternalName, hypervisorType, backup.getVmId());
LOG.error(message);
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE,
message, vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0);
} else {
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VM_BACKUP_RESTORE,
String.format("Restored VM %s from backup %s", vm.getUuid(), backup.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0);
}
return vm != null;
}
@ -588,9 +605,17 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("Failed to find backup offering of the VM backup");
}
ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventTypes.EVENT_VM_BACKUP_RESTORE,
String.format("Restoring VM %s from backup %s", vm.getUuid(), backup.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),
true, 0);
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
if (!backupProvider.restoreVMFromBackup(vm, backup)) {
throw new CloudRuntimeException("Error restoring VM from backup ID " + backup.getId());
ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE,
String.format("Failed to restore VM %s from backup %s", vm.getInstanceName(), backup.getUuid()),
vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0);
throw new CloudRuntimeException("Error restoring VM from backup with uuid " + backup.getUuid());
}
return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(),
vm.getInstanceName(), vm.getHypervisorType(), backup);

View File

@ -0,0 +1,302 @@
#!/usr/bin/env python
# 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 marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import wait_until
from marvin.lib.base import (Account, ServiceOffering, DiskOffering, Volume, VirtualMachine,
BackupOffering, Configurations, Backup, BackupSchedule)
from marvin.lib.common import (get_domain, get_zone, get_template)
from nose.plugins.attrib import attr
from marvin.codes import FAILED
import time
class TestVeeamBackupAndRecovery(cloudstackTestCase):
@classmethod
def setUpClass(cls):
# Setup
cls.testClient = super(TestVeeamBackupAndRecovery, cls).getClsTestClient()
cls.apiclient = cls.testClient.getApiClient()
cls.services = cls.testClient.getParsedTestDataConfig()
cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests())
cls.services["mode"] = cls.zone.networktype
cls.hypervisor = cls.testClient.getHypervisorInfo()
cls.domain = get_domain(cls.apiclient)
cls.template = get_template(cls.apiclient, cls.zone.id, cls.services["ostype"])
if cls.template == FAILED:
assert False, "get_template() failed to return template with description %s" % cls.services["ostype"]
cls.services["small"]["zoneid"] = cls.zone.id
cls.services["small"]["template"] = cls.template.id
cls._cleanup = []
# Check backup configuration values, set them to enable the veeam provider
backup_enabled_cfg = Configurations.list(cls.apiclient, name='backup.framework.enabled', zoneid=cls.zone.id)
backup_provider_cfg = Configurations.list(cls.apiclient, name='backup.framework.provider.plugin', zoneid=cls.zone.id)
cls.backup_enabled = backup_enabled_cfg[0].value
cls.backup_provider = backup_provider_cfg[0].value
if cls.backup_enabled == "false":
Configurations.update(cls.apiclient, 'backup.framework.enabled', value='true', zoneid=cls.zone.id)
if cls.backup_provider != "veeam":
return
if cls.hypervisor.lower() != 'vmware':
return
cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["small"])
cls._cleanup.append(cls.service_offering)
cls.disk_offering = DiskOffering.create(cls.apiclient, cls.services["disk_offering"])
cls._cleanup.append(cls.disk_offering)
@classmethod
def isBackupOfferingUsed(cls, existing_offerings, provider_offering):
if not existing_offerings:
return False
for existing_offering in existing_offerings:
if existing_offering.externalid == provider_offering.externalid:
return True
return False
def waitForBackUp(self, vm):
def checkBackUp():
backups = Backup.list(self.user_apiclient, vm.id)
if isinstance(backups, list) and len(backups) != 0:
return True, None
return False, None
res, _ = wait_until(10, 60, checkBackUp)
if not res:
self.fail("Failed to wait for backup of VM %s to be Up" % vm.id)
@classmethod
def tearDownClass(cls):
if cls.backup_enabled == "false":
Configurations.update(cls.apiclient, 'backup.framework.enabled', value=cls.backup_enabled, zoneid=cls.zone.id)
super(TestVeeamBackupAndRecovery, cls).tearDownClass()
def setUp(self):
if self.backup_provider != "veeam":
raise self.skipTest("Skipping test cases which must only run for veeam")
if self.hypervisor.lower() != 'vmware':
raise self.skipTest("Skipping test cases which must only run for VMware")
self.cleanup = []
# Import backup offering
self.offering = None
existing_offerings = BackupOffering.listByZone(self.apiclient, self.zone.id)
provider_offerings = BackupOffering.listExternal(self.apiclient, self.zone.id)
if not provider_offerings:
self.skipTest("Skipping test cases as the provider offering is None")
for provider_offering in provider_offerings:
if not self.isBackupOfferingUsed(existing_offerings, provider_offering):
self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name))
self.offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid,
provider_offering.name, provider_offering.description)
if not self.offering:
self.fail("Failed to import backup offering %s" % provider_offering.name)
break
if not self.offering:
self.skipTest("Skipping test cases as there is no available provider offerings to import")
# Create user account
self.account = Account.create(self.apiclient, self.services["account"], domainid=self.domain.id)
self.user_user = self.account.user[0]
self.user_apiclient = self.testClient.getUserApiClient(
self.user_user.username, self.domain.name
)
self.cleanup.append(self.account)
def tearDown(self):
super(TestVeeamBackupAndRecovery, self).tearDown()
@attr(tags=["advanced", "backup"], required_hardware="false")
def test_01_import_list_delete_backup_offering(self):
"""
Import provider backup offering from Veeam Backup and Recovery Provider
"""
# Verify offering is listed by user
imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id)
self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response")
self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response")
matching_offerings = [x for x in imported_offering if x.id == self.offering.id]
self.assertNotEqual(len(matching_offerings), 0, "Check if there is a matching offering")
# Delete backup offering
self.debug("Deleting backup offering %s" % self.offering.id)
self.offering.delete(self.apiclient)
# Verify offering is not listed by user
imported_offering = BackupOffering.listByZone(self.user_apiclient, self.zone.id)
if imported_offering:
self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response")
matching_offerings = [x for x in imported_offering if x.id == self.offering.id]
self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering")
@attr(tags=["advanced", "backup"], required_hardware="false")
def test_02_vm_backup_lifecycle(self):
"""
Test VM backup lifecycle
"""
if self.offering:
self.cleanup.insert(0, self.offering)
self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name,
domainid=self.account.domainid, serviceofferingid=self.service_offering.id,
diskofferingid=self.disk_offering.id)
# Verify there are no backups for the VM
backups = Backup.list(self.user_apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Assign VM to offering and create ad-hoc backup
self.offering.assignOffering(self.user_apiclient, self.vm.id)
vms = VirtualMachine.list(
self.user_apiclient,
id=self.vm.id,
listall=True
)
self.assertEqual(
isinstance(vms, list),
True,
"List virtual machines should return a valid list"
)
self.assertEqual(1, len(vms), "List of the virtual machines should have 1 vm")
self.assertEqual(self.offering.id, vms[0].backupofferingid, "The virtual machine should have backup offering %s" % self.offering.id)
# Create backup schedule on 01:00AM every Sunday
BackupSchedule.create(self.user_apiclient, self.vm.id, intervaltype="WEEKLY", timezone="CET", schedule="00:01:1")
backupSchedule = BackupSchedule.list(self.user_apiclient, self.vm.id)
self.assertIsNotNone(backupSchedule)
self.assertEqual("WEEKLY", backupSchedule.intervaltype)
self.assertEqual("00:01:1", backupSchedule.schedule)
self.assertEqual("CET", backupSchedule.timezone)
self.assertEqual(self.vm.id, backupSchedule.virtualmachineid)
self.assertEqual(self.vm.name, backupSchedule.virtualmachinename)
# Update backup schedule on 02:00AM every 20th
BackupSchedule.update(self.user_apiclient, self.vm.id, intervaltype="MONTHLY", timezone="CET", schedule="00:02:20")
backupSchedule = BackupSchedule.list(self.user_apiclient, self.vm.id)
self.assertIsNotNone(backupSchedule)
self.assertEqual("MONTHLY", backupSchedule.intervaltype)
self.assertEqual("00:02:20", backupSchedule.schedule)
# Delete backup schedule
BackupSchedule.delete(self.user_apiclient, self.vm.id)
# Create backup
Backup.create(self.user_apiclient, self.vm.id)
# Verify backup is created for the VM
self.waitForBackUp(self.vm)
backups = Backup.list(self.user_apiclient, self.vm.id)
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM")
backup = backups[0]
# Stop VM
self.vm.stop(self.user_apiclient, forced=True)
# Restore backup
Backup.restoreVM(self.user_apiclient, backup.id)
# Delete backup
Backup.delete(self.user_apiclient, backup.id, forced=True)
# Verify backup is deleted
backups = Backup.list(self.user_apiclient, self.vm.id)
self.assertEqual(backups, None, "There should not exist any backup for the VM")
# Remove VM from offering
self.offering.removeOffering(self.user_apiclient, self.vm.id)
@attr(tags=["advanced", "backup"], required_hardware="false")
def test_03_restore_volume_attach_vm(self):
"""
Test Volume Restore from Backup and Attach to VM
"""
if self.offering:
self.cleanup.insert(0, self.offering)
self.vm = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name,
domainid=self.account.domainid, serviceofferingid=self.service_offering.id)
self.vm_with_datadisk = VirtualMachine.create(self.user_apiclient, self.services["small"], accountid=self.account.name,
domainid=self.account.domainid, serviceofferingid=self.service_offering.id,
diskofferingid=self.disk_offering.id)
# Assign VM to offering and create ad-hoc backup
self.offering.assignOffering(self.user_apiclient, self.vm_with_datadisk.id)
# Create backup
Backup.create(self.user_apiclient, self.vm_with_datadisk.id)
# Verify backup is created for the VM with datadisk
self.waitForBackUp(self.vm_with_datadisk)
backups = Backup.list(self.user_apiclient, self.vm_with_datadisk.id)
self.assertEqual(len(backups), 1, "There should exist only one backup for the VM with datadisk")
backup = backups[0]
try:
volumes = Volume.list(
self.user_apiclient,
virtualmachineid=self.vm_with_datadisk.id,
listall=True
)
rootDiskId = None
dataDiskId = None
for volume in volumes:
if volume.type == 'ROOT':
rootDiskId = volume.id
elif volume.type == 'DATADISK':
dataDiskId = volume.id
if rootDiskId:
# Restore ROOT volume of vm_with_datadisk and attach to vm
Backup.restoreVolumeFromBackupAndAttachToVM(
self.user_apiclient,
backupid=backup.id,
volumeid=rootDiskId,
virtualmachineid=self.vm.id
)
vm_volumes = Volume.list(
self.user_apiclient,
virtualmachineid=self.vm.id,
listall=True
)
self.assertTrue(isinstance(vm_volumes, list), "List volumes should return a valid list")
self.assertEqual(2, len(vm_volumes), "The number of volumes should be 2")
if dataDiskId:
# Restore DATADISK volume of vm_with_datadisk and attach to vm
Backup.restoreVolumeFromBackupAndAttachToVM(
self.user_apiclient,
backupid=backup.id,
volumeid=dataDiskId,
virtualmachineid=self.vm.id
)
vm_volumes = Volume.list(
self.user_apiclient,
virtualmachineid=self.vm.id,
listall=True
)
self.assertTrue(isinstance(vm_volumes, list), "List volumes should return a valid list")
self.assertEqual(3, len(vm_volumes), "The number of volumes should be 2")
finally:
# Delete backup
Backup.delete(self.user_apiclient, backup.id, forced=True)

View File

@ -5916,8 +5916,10 @@ class ResourceDetails:
cmd.resourcetype = resourcetype
return (apiclient.removeResourceDetail(cmd))
# Backup and Recovery
class BackupOffering:
def __init__(self, items):
@ -5982,6 +5984,7 @@ class BackupOffering:
cmd.forced = forced
return (apiclient.removeVirtualMachineFromBackupOffering(cmd))
class Backup:
def __init__(self, items):
@ -5993,14 +5996,16 @@ class Backup:
cmd = createBackup.createBackupCmd()
cmd.virtualmachineid = vmid
return (apiclient.createBackup(cmd))
return Backup(apiclient.createBackup(cmd).__dict__)
@classmethod
def delete(self, apiclient, id):
def delete(self, apiclient, id, forced=None):
"""Delete VM backup"""
cmd = deleteBackup.deleteBackupCmd()
cmd.id = id
if forced:
cmd.forced = forced
return (apiclient.deleteBackup(cmd))
@classmethod
@ -6012,13 +6017,66 @@ class Backup:
cmd.listall = True
return (apiclient.listBackups(cmd))
def restoreVM(self, apiclient):
@classmethod
def restoreVM(self, apiclient, backupid):
"""Restore VM from backup"""
cmd = restoreBackup.restoreBackupCmd()
cmd.id = self.id
cmd.id = backupid
return (apiclient.restoreBackup(cmd))
@classmethod
def restoreVolumeFromBackupAndAttachToVM(self, apiclient, backupid, volumeid, virtualmachineid):
"""Restore VM from backup"""
cmd = restoreVolumeFromBackupAndAttachToVM.restoreVolumeFromBackupAndAttachToVMCmd()
cmd.backupid = backupid
cmd.volumeid = volumeid
cmd.virtualmachineid = virtualmachineid
return (apiclient.restoreVolumeFromBackupAndAttachToVM(cmd))
class BackupSchedule:
def __init__(self, items):
self.__dict__.update(items)
@classmethod
def create(self, apiclient, vmid, **kwargs):
"""Create VM backup schedule"""
cmd = createBackupSchedule.createBackupScheduleCmd()
cmd.virtualmachineid = vmid
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return BackupSchedule(apiclient.createBackupSchedule(cmd).__dict__)
@classmethod
def delete(self, apiclient, vmid):
"""Delete VM backup schedule"""
cmd = deleteBackupSchedule.deleteBackupScheduleCmd()
cmd.virtualmachineid = vmid
return (apiclient.deleteBackupSchedule(cmd))
@classmethod
def list(self, apiclient, vmid):
"""List VM backup schedule"""
cmd = listBackupSchedule.listBackupScheduleCmd()
cmd.virtualmachineid = vmid
cmd.listall = True
return (apiclient.listBackupSchedule(cmd))
@classmethod
def update(self, apiclient, vmid, **kwargs):
"""Update VM backup schedule"""
cmd = updateBackupSchedule.updateBackupScheduleCmd()
cmd.virtualmachineid = vmid
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return (apiclient.updateBackupSchedule(cmd))
class ProjectRole:
def __init__(self, items):

View File

@ -577,7 +577,7 @@ export default {
},
enableGroupAction () {
return ['vm', 'alert', 'vmgroup', 'ssh', 'userdata', 'affinitygroup', 'autoscalevmgroup', 'volume', 'snapshot',
'vmsnapshot', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway',
'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway',
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment'
].includes(this.$route.name)

View File

@ -32,9 +32,9 @@ export default {
permission: ['listVirtualMachinesMetrics'],
resourceType: 'UserVm',
params: () => {
var params = { details: 'servoff,tmpl,nics' }
var params = { details: 'servoff,tmpl,nics,backoff' }
if (store.getters.metrics) {
params = { details: 'servoff,tmpl,nics,stats' }
params = { details: 'servoff,tmpl,nics,backoff,stats' }
}
return params
},

View File

@ -488,7 +488,11 @@ export default {
label: 'label.delete.backup',
message: 'message.delete.backup',
dataView: true,
show: (record) => { return record.state !== 'Destroyed' }
show: (record) => { return record.state !== 'Destroyed' },
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) },
args: ['forced']
}
]
}

View File

@ -40,6 +40,9 @@
</span>
</label>
</template>
<template #intervaltype="{ text, record }" :name="text">
<label>{{ record.intervaltype }}</label>
</template>
<template #time="{ text, record }" :name="text">
<label class="interval-content">
<span v-if="record.intervaltype==='HOURLY'">{{ record.schedule + ' ' + $t('label.min.past.hour') }}</span>
@ -112,6 +115,11 @@ export default {
width: 30,
slots: { customRender: 'icon' }
},
{
title: this.$t('label.intervaltype'),
dataIndex: 'intervaltype',
slots: { customRender: 'intervaltype' }
},
{
title: this.$t('label.time'),
dataIndex: 'schedule',

View File

@ -3736,4 +3736,29 @@ public class VirtualMachineMO extends BaseMO {
String workerTag = String.format("%d-%s", System.currentTimeMillis(), getContext().getStockObject("noderuninfo"));
setCustomFieldValue(CustomFieldConstants.CLOUD_WORKER_TAG, workerTag);
}
public void removeChangeTrackPathFromVmdkForDisks() throws Exception {
VirtualDisk[] disks = getAllDiskDevice();
for (int i = 0; i < disks.length; i++) {
VirtualDisk disk = disks[i];
VirtualDeviceBackingInfo backingInfo = disk.getBacking();
if (!(backingInfo instanceof VirtualDiskFlatVer2BackingInfo)) {
throw new Exception("Unsupported VirtualDeviceBackingInfo");
}
VirtualDiskFlatVer2BackingInfo diskBackingInfo = (VirtualDiskFlatVer2BackingInfo)backingInfo;
s_logger.info("Removing property ChangeTrackPath from VMDK content file " + diskBackingInfo.getFileName());
Pair<VmdkFileDescriptor, byte[]> vmdkInfo = getVmdkFileInfo(diskBackingInfo.getFileName());
VmdkFileDescriptor vmdkFileDescriptor = vmdkInfo.first();
byte[] content = vmdkInfo.second();
if (content == null || content.length == 0) {
break;
}
byte[] newVmdkContent = vmdkFileDescriptor.removeChangeTrackPath(content);
Pair<DatacenterMO, String> dcPair = getOwnerDatacenter();
String vmdkUrl = getContext().composeDatastoreBrowseUrl(dcPair.second(), diskBackingInfo.getFileName());
getContext().uploadResourceContent(vmdkUrl, newVmdkContent);
s_logger.info("Removed property ChangeTrackPath from VMDK content file " + diskBackingInfo.getFileName());
}
}
}

View File

@ -33,6 +33,8 @@ public class VmdkFileDescriptor {
private static final String VMDK_CREATE_TYPE_VMFSSPARSE = "vmfsSparse";
private static final String VMDK_CREATE_TYPE_SESPARSE = "SEsparse";
private static final String VMDK_PROPERTY_ADAPTER_TYPE = "ddb.adapterType";
private static final String VMDK_PROPERTY_CHANGE_TRACK_PATH = "changeTrackPath";
private static final String VMDK_PROPERTY_CHANGE_TRACK_PATH_COMMENT = "# Change Tracking File";
private Properties _properties = new Properties();
private String _baseFileName;
@ -225,4 +227,61 @@ public class VmdkFileDescriptor {
return bos.toByteArray();
}
public static byte[] removeChangeTrackPath(byte[] vmdkContent) throws IOException {
assert (vmdkContent != null);
BufferedReader in = null;
BufferedWriter out = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vmdkContent)));
out = new BufferedWriter(new OutputStreamWriter(bos));
String line;
while ((line = in.readLine()) != null) {
// ignore empty and comment lines
line = line.trim();
if (line.isEmpty()) {
out.newLine();
continue;
}
if (line.equals(VMDK_PROPERTY_CHANGE_TRACK_PATH_COMMENT)) {
s_logger.debug("Removed line from vmdk: " + line);
continue;
}
if (line.charAt(0) == '#') {
out.write(line);
out.newLine();
continue;
}
String[] tokens = line.split("=");
if (tokens.length == 2) {
String name = tokens[0].trim();
String value = tokens[1].trim();
if (value.charAt(0) == '\"')
value = value.substring(1, value.length() - 1);
if (name.equals(VMDK_PROPERTY_CHANGE_TRACK_PATH)) {
s_logger.debug("Removed line from vmdk: " + line);
} else {
out.write(line);
out.newLine();
}
} else {
out.write(line);
out.newLine();
}
}
} finally {
if (in != null)
in.close();
if (out != null)
out.close();
}
return bos.toByteArray();
}
}