add APIs for management of backup repositories and backing up from local stores and stopped VMs
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 7a167a7..3037f0a 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1140,6 +1140,7 @@
public static final String WEBHOOK_NAME = "webhookname";
public static final String NFS_MOUNT_OPTIONS = "nfsmountopts";
+ public static final String MOUNT_OPTIONS = "mountopts";
/**
* This enum specifies IO Drivers, each option controls specific policies on I/O.
diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
index ef759aa..92032d2 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java
@@ -22,6 +22,8 @@
import java.util.Map;
import java.util.Set;
+import org.apache.cloudstack.api.response.BackupRepositoryResponse;
+import org.apache.cloudstack.backup.BackupRepository;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.affinity.AffinityGroup;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
@@ -549,4 +551,6 @@
ObjectStoreResponse createObjectStoreResponse(ObjectStore os);
BucketResponse createBucketResponse(Bucket bucket);
+
+ BackupRepositoryResponse createBackupRepositoryResponse(BackupRepository repository);
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java
new file mode 100644
index 0000000..6ad3894
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java
@@ -0,0 +1,117 @@
+package org.apache.cloudstack.api.command.user.backup.repository;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.BackupRepositoryResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.backup.BackupRepository;
+import org.apache.cloudstack.backup.BackupRepositoryService;
+import org.apache.cloudstack.context.CallContext;
+
+import javax.inject.Inject;
+
+@APICommand(name = "addBackupRepository",
+ description = "Adds a backup repository to store NAS backups",
+ responseObject = BackupRepositoryResponse.class, since = "4.20.0",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class AddBackupRepositoryCmd extends BaseCmd {
+
+ @Inject
+ BackupRepositoryService backupRepositoryService;
+
+ /////////////////////////////////////////////////////
+ //////////////// API parameters /////////////////////
+ /////////////////////////////////////////////////////
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "name of the backup repository")
+ private String name;
+
+ @Parameter(name = ApiConstants.ADDRESS, type = CommandType.STRING, required = true, description = "address of the backup repository")
+ private String address;
+
+ @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "type of the backup repository. Supported values: NFS" )
+ private String type;
+
+ @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "backup repository provider")
+ private String provider;
+
+ @Parameter(name = ApiConstants.MOUNT_OPTIONS, type = CommandType.STRING, description = "mount options")
+ private String mountOptions;
+
+ @Parameter(name = ApiConstants.ZONE_ID,
+ type = CommandType.UUID,
+ entityType = ZoneResponse.class,
+ required = true,
+ description = "ID of the zone where the backup repository is to be added")
+ private Long zoneId;
+
+ @Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, description = "capacity of this backup repository")
+ private Long capacityBytes;
+
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public BackupRepositoryService getBackupRepositoryService() {
+ return backupRepositoryService;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public String getMountOptions() {
+ return mountOptions;
+ }
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public Long getCapacityBytes() {
+ return capacityBytes;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ try {
+ BackupRepository result = backupRepositoryService.addBackupRepository(this);
+ if (result != null) {
+ BackupRepositoryResponse response = _responseGenerator.createBackupRepositoryResponse(result);
+ response.setResponseName(getCommandName());
+ this.setResponseObject(response);
+ } else {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add backup repository");
+ }
+ } catch (Exception ex4) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex4.getMessage());
+ }
+
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/DeleteBackupRepositoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/DeleteBackupRepositoryCmd.java
new file mode 100644
index 0000000..70f37c3
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/DeleteBackupRepositoryCmd.java
@@ -0,0 +1,59 @@
+package org.apache.cloudstack.api.command.user.backup.repository;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.BackupRepositoryResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.backup.BackupRepositoryService;
+
+import javax.inject.Inject;
+
+@APICommand(name = "deleteBackupRepository",
+ description = "delete a backup repository",
+ responseObject = SuccessResponse.class, since = "4.20.0",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class DeleteBackupRepositoryCmd extends BaseCmd {
+
+ @Inject
+ BackupRepositoryService backupRepositoryService;
+
+ /////////////////////////////////////////////////////
+ //////////////// API parameters /////////////////////
+ /////////////////////////////////////////////////////
+ @Parameter(name = ApiConstants.ID,
+ type = CommandType.UUID,
+ entityType = BackupRepositoryResponse.class,
+ required = true,
+ description = "ID of the backup repository to be deleted")
+ private Long id;
+
+
+ /////////////////////////////////////////////////////
+ //////////////// Accessors //////////////////////////
+ /////////////////////////////////////////////////////
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public void execute() {
+ boolean result = backupRepositoryService.deleteBackupRepository(this);
+ if (result) {
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ this.setResponseObject(response);
+ } else {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete backup repository");
+ }
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/ListBackupRepositoriesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/ListBackupRepositoriesCmd.java
new file mode 100644
index 0000000..2e47192
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/ListBackupRepositoriesCmd.java
@@ -0,0 +1,93 @@
+package org.apache.cloudstack.api.command.user.backup.repository;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.utils.Pair;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.BackupRepositoryResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.backup.BackupRepository;
+import org.apache.cloudstack.backup.BackupRepositoryService;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+@APICommand(name = "listBackupRepositories",
+ description = "Lists all backup repositories",
+ responseObject = BackupRepositoryResponse.class, since = "4.20.0",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class ListBackupRepositoriesCmd extends BaseListCmd {
+
+ @Inject
+ BackupRepositoryService backupRepositoryService;
+
+ /////////////////////////////////////////////////////
+ //////////////// API parameters /////////////////////
+ /////////////////////////////////////////////////////
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "name of the backup repository")
+ private String name;
+
+ @Parameter(name = ApiConstants.ZONE_ID,
+ type = CommandType.UUID,
+ entityType = ZoneResponse.class,
+ description = "ID of the zone where the backup repository is to be added")
+ private Long zoneId;
+
+ @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "the backup repository provider")
+ private String provider;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = BackupRepositoryResponse.class, description = "ID of the backup repository")
+ private Long id;
+
+ /////////////////////////////////////////////////////
+ //////////////// Accessors //////////////////////////
+ /////////////////////////////////////////////////////
+
+
+ public String getName() {
+ return name;
+ }
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ try {
+ Pair<List<BackupRepository>, Integer> repositoriesPair = backupRepositoryService.listBackupRepositories(this);
+ List<BackupRepository> backupRepositories = repositoriesPair.first();
+ ListResponse<BackupRepositoryResponse> response = new ListResponse<>();
+ List<BackupRepositoryResponse> responses = new ArrayList<>();
+ for (BackupRepository repository : backupRepositories) {
+ responses.add(_responseGenerator.createBackupRepositoryResponse(repository));
+ }
+ response.setResponses(responses, repositoriesPair.second());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ } catch (Exception e) {
+ String msg = String.format("Error listing backup repositories, due to: %s", e.getMessage());
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, msg);
+ }
+
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java
new file mode 100644
index 0000000..d68b483
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java
@@ -0,0 +1,134 @@
+package org.apache.cloudstack.api.response;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import java.util.Date;
+
+public class BackupRepositoryResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the backup repository")
+ private String id;
+
+ @SerializedName(ApiConstants.ZONE_ID)
+ @Param(description = "the Zone ID of the backup repository")
+ private String zoneId;
+
+ @SerializedName(ApiConstants.ZONE_NAME)
+ @Param(description = "the Zone name of the backup repository")
+ private String zoneName;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "the name of the backup repository")
+ private String name;
+
+ @SerializedName(ApiConstants.ADDRESS)
+ @Param(description = "the address / url of the backup repository")
+ private String address;
+
+ @SerializedName(ApiConstants.PROVIDER)
+ @Param(description = "name of the provider")
+ private String providerName;
+
+ @SerializedName(ApiConstants.TYPE)
+ @Param(description = "backup type")
+ private String type;
+
+ @SerializedName(ApiConstants.MOUNT_OPTIONS)
+ @Param(description = "mount options for the backup repository")
+ private String mountOptions;
+
+ @SerializedName(ApiConstants.CAPACITY_BYTES)
+ @Param(description = "capacity of the backup repository")
+ private Long capacityBytes;
+
+ @SerializedName("created")
+ @Param(description = "the date and time the backup repository was added")
+ private Date created;
+
+ public BackupRepositoryResponse() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getZoneId() {
+ return zoneId;
+ }
+
+ public void setZoneId(String zoneId) {
+ this.zoneId = zoneId;
+ }
+
+ public String getZoneName() {
+ return zoneName;
+ }
+
+ public void setZoneName(String zoneName) {
+ this.zoneName = zoneName;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getMountOptions() {
+ return mountOptions;
+ }
+
+ public void setMountOptions(String mountOptions) {
+ this.mountOptions = mountOptions;
+ }
+
+ public String getProviderName() {
+ return providerName;
+ }
+
+ public void setProviderName(String providerName) {
+ this.providerName = providerName;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public Long getCapacityBytes() {
+ return capacityBytes;
+ }
+
+ public void setCapacityBytes(Long capacityBytes) {
+ this.capacityBytes = capacityBytes;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java
new file mode 100644
index 0000000..8468c2b
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java
@@ -0,0 +1,15 @@
+package org.apache.cloudstack.backup;
+
+import com.cloud.utils.Pair;
+import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
+
+import java.util.List;
+
+public interface BackupRepositoryService {
+ BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd);
+ boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd);
+ Pair<List<BackupRepository>, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd);
+
+}
diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
index 3deb3eb..93855ea 100644
--- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
+++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
@@ -22,11 +22,14 @@
import com.cloud.agent.api.Command;
import com.cloud.agent.api.LogLevel;
+import java.util.List;
+
public class TakeBackupCommand extends Command {
private String vmName;
private String backupPath;
private String backupRepoType;
private String backupRepoAddress;
+ private List<String> volumePaths;
@LogLevel(LogLevel.Log4jLevel.Off)
private String mountOptions;
@@ -76,6 +79,14 @@
this.mountOptions = mountOptions;
}
+ public List<String> getVolumePaths() {
+ return volumePaths;
+ }
+
+ public void setVolumePaths(List<String> volumePaths) {
+ this.volumePaths = volumePaths;
+ }
+
@Override
public boolean executeInSequence() {
return true;
diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
index 33a648a..ceb0223 100644
--- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
+++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
@@ -25,6 +25,7 @@
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.ScopeType;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
@@ -40,6 +41,8 @@
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
+import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
@@ -51,9 +54,11 @@
import java.util.List;
import java.util.Map;
import java.util.HashMap;
+import java.util.Objects;
public class NASBackupProvider extends AdapterBase implements BackupProvider, Configurable {
private static final Logger LOG = LogManager.getLogger(NASBackupProvider.class);
+ private static final String SHARED_VOLUME_PATH_PREFIX = "/mnt";
@Inject
private BackupDao backupDao;
@@ -80,6 +85,9 @@
private VMInstanceDao vmInstanceDao;
@Inject
+ private PrimaryDataStoreDao primaryDataStoreDao;
+
+ @Inject
private AgentManager agentManager;
protected Host getLastVMHypervisorHost(VirtualMachine vm) {
@@ -111,10 +119,16 @@
return null;
}
- protected Host getRunningVMHypervisorHost(VirtualMachine vm) {
+ protected Host getVMHypervisorHost(VirtualMachine vm) {
Long hostId = vm.getHostId();
+ if (hostId == null && VirtualMachine.State.Running.equals(vm.getState())) {
+ throw new CloudRuntimeException(String.format("Unable to find the hypervisor host for %s. Make sure the virtual machine is running", vm.getName()));
+ }
+ if (VirtualMachine.State.Stopped.equals(vm.getState())) {
+ hostId = vm.getLastHostId();
+ }
if (hostId == null) {
- throw new CloudRuntimeException("Unable to find the HYPERVISOR for " + vm.getName() + ". Make sure the virtual machine is running");
+ throw new CloudRuntimeException(String.format("Unable to find the hypervisor host for stopped VM: %s."));
}
final Host host = hostDao.findById(hostId);
if (host == null || !Status.Up.equals(host.getStatus()) || !Hypervisor.HypervisorType.KVM.equals(host.getHypervisorType())) {
@@ -125,9 +139,7 @@
@Override
public boolean takeBackup(final VirtualMachine vm) {
- // TODO: currently works for only running VMs
- // TODO: add support for backup of stopped VMs
- final Host host = getRunningVMHypervisorHost(vm);
+ final Host host = getVMHypervisorHost(vm);
final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId());
if (backupRepository == null) {
@@ -143,6 +155,23 @@
command.setBackupRepoAddress(backupRepository.getAddress());
command.setMountOptions(backupRepository.getMountOptions());
+ if (VirtualMachine.State.Shutdown.equals(vm.getState())) {
+ List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());
+ List<String> volumePaths = new ArrayList<>();
+ for (VolumeVO volume : vmVolumes) {
+ StoragePoolVO storagePool = primaryDataStoreDao.findById(volume.getPoolId());
+ if (Objects.isNull(storagePool)) {
+ throw new CloudRuntimeException("Unable to find storage pool associated to the volume");
+ }
+ String volumePathPrefix = String.format("/mnt/%s", storagePool.getPath());
+ if (ScopeType.HOST.equals(storagePool.getScope())) {
+ volumePathPrefix = storagePool.getPath();
+ }
+ volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath()));
+ }
+ command.setVolumePaths(volumePaths);
+ }
+
BackupAnswer answer = null;
try {
answer = (BackupAnswer) agentManager.send(host.getId(), command);
@@ -192,6 +221,7 @@
// TODO: get KVM agent to restore VM backup
+
return true;
}
@@ -216,9 +246,7 @@
} catch (Exception e) {
throw new CloudRuntimeException("Unable to craft restored volume due to: "+e);
}
-
- // TODO: get KVM agent to copy/restore the specific volume to datastore
-
+ // TODO: get KVM agent to copy/restore the specific volume to
return null;
}
@@ -231,7 +259,7 @@
// TODO: this can be any host in the cluster or last host
final VirtualMachine vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId());
- final Host host = getRunningVMHypervisorHost(vm);
+ final Host host = getVMHypervisorHost(vm);
DeleteBackupCommand command = new DeleteBackupCommand(backup.getExternalId(), backupRepository.getType(),
backupRepository.getAddress(), backupRepository.getMountOptions());
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java
index af02f2a..2a33218 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java
@@ -41,6 +41,7 @@
final String backupRepoType = command.getBackupRepoType();
final String backupRepoAddress = command.getBackupRepoAddress();
final String mountOptions = command.getMountOptions();
+ final List<String> diskPaths = command.getVolumePaths();
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{
@@ -50,7 +51,8 @@
"-t", backupRepoType,
"-s", backupRepoAddress,
"-m", mountOptions,
- "-p", backupPath
+ "-p", backupPath,
+ "-d", String.join(",", diskPaths)
});
Pair<Integer, String> result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout());
diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh
index 4caae9c..9a73bd7 100755
--- a/scripts/vm/hypervisor/kvm/nasbackup.sh
+++ b/scripts/vm/hypervisor/kvm/nasbackup.sh
@@ -30,14 +30,12 @@
NAS_ADDRESS=""
MOUNT_OPTS=""
BACKUP_DIR=""
+DISK_PATHS=""
### Operation methods ###
-backup_vm() {
- mount_point=$(mktemp -d -t csbackup.XXXXX)
- dest="$mount_point/${BACKUP_DIR}"
-
- mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS})
+backup_running_vm() {
+ mount_operation
mkdir -p $dest
deviceId=0
@@ -74,11 +72,19 @@
rmdir $mount_point
}
-delete_backup() {
- mount_point=$(mktemp -d -t csbackup.XXXXX)
- dest="$mount_point/${BACKUP_DIR}"
+backup_stopped_vm() {
+ mount_operation
+ mkdir -p $dest
- mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS})
+ IFS=","
+
+ for disk in $DISK_PATHS; do
+ rsync -az $disk $dest
+ done
+}
+
+delete_backup() {
+ mount_operation
rm -frv $dest
sync
@@ -87,6 +93,13 @@
rmdir $mount_point
}
+mount_operation() {
+ mount_point=$(mktemp -d -t csbackup.XXXXX)
+ dest="$mount_point/${BACKUP_DIR}"
+
+ mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS})
+}
+
function usage {
echo ""
echo "Usage: $0 -b <domain> -s <NAS storage mount path> -p <backup dest path>"
@@ -126,6 +139,11 @@
shift
shift
;;
+ -d|--diskpaths)
+ DISK_PATHS="$2"
+ shift
+ shift
+ ;;
-h|--help)
usage
shift
@@ -138,7 +156,12 @@
done
if [ "$OP" = "backup" ]; then
- backup_vm
+ STATE=$(virsh -c qemu:///system list | grep $VM | awk '{print $3}')
+ if [ "$STATE" = "running" ]; then
+ backup_running_vm
+ else
+ backup_stopped_vm
+ fi
elif [ "$OP" = "delete" ]; then
delete_backup
fi
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index cd1d653..86a5e8a 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -66,6 +66,7 @@
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
+import org.apache.cloudstack.api.response.BackupRepositoryResponse;
import org.apache.cloudstack.api.response.BackupResponse;
import org.apache.cloudstack.api.response.BackupScheduleResponse;
import org.apache.cloudstack.api.response.BucketResponse;
@@ -184,8 +185,10 @@
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.backup.BackupOffering;
+import org.apache.cloudstack.backup.BackupRepository;
import org.apache.cloudstack.backup.BackupSchedule;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
+import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.config.ConfigurationGroup;
import org.apache.cloudstack.config.ConfigurationSubGroup;
@@ -487,6 +490,8 @@
UserDataDao userDataDao;
@Inject
VlanDetailsDao vlanDetailsDao;
+ @Inject
+ BackupRepositoryDao backupRepositoryDao;
@Inject
ObjectStoreDao _objectStoreDao;
@@ -5281,4 +5286,23 @@
populateAccount(bucketResponse, bucket.getAccountId());
return bucketResponse;
}
+
+ @Override
+ public BackupRepositoryResponse createBackupRepositoryResponse(BackupRepository backupRepository) {
+ BackupRepositoryResponse response = new BackupRepositoryResponse();
+ response.setName(backupRepository.getName());
+ response.setId(backupRepository.getUuid());
+ response.setCreated(backupRepository.getCreated());
+ response.setAddress(backupRepository.getAddress());
+ response.setProviderName(backupRepository.getProvider());
+ response.setType(backupRepository.getType());
+ response.setMountOptions(backupRepository.getMountOptions());
+ response.setCapacityBytes(backupRepository.getCapacityBytes());
+ DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId());
+ if (zone != null) {
+ response.setZoneId(zone.getUuid());
+ response.setZoneName(zone.getName());
+ }
+ return response;
+ }
}
diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
index 8753597..90e85c6 100644
--- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
@@ -52,6 +52,9 @@
import org.apache.cloudstack.api.command.user.backup.RestoreBackupCmd;
import org.apache.cloudstack.api.command.user.backup.RestoreVolumeFromBackupAndAttachToVMCmd;
import org.apache.cloudstack.api.command.user.backup.UpdateBackupScheduleCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
@@ -945,6 +948,9 @@
cmdList.add(RestoreBackupCmd.class);
cmdList.add(DeleteBackupCmd.class);
cmdList.add(RestoreVolumeFromBackupAndAttachToVMCmd.class);
+ cmdList.add(AddBackupRepositoryCmd.class);
+ cmdList.add(DeleteBackupRepositoryCmd.class);
+ cmdList.add(ListBackupRepositoriesCmd.class);
return cmdList;
}
diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java
new file mode 100644
index 0000000..7088547
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/backup/BackupRepositoryServiceImpl.java
@@ -0,0 +1,84 @@
+package org.apache.cloudstack.backup;
+
+import com.cloud.user.AccountManager;
+import com.cloud.utils.Pair;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import org.apache.cloudstack.api.command.user.backup.repository.AddBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.DeleteBackupRepositoryCmd;
+import org.apache.cloudstack.api.command.user.backup.repository.ListBackupRepositoriesCmd;
+import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
+import org.apache.cloudstack.context.CallContext;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class BackupRepositoryServiceImpl extends ManagerBase implements BackupRepositoryService {
+
+ @Inject
+ private BackupRepositoryDao repositoryDao;
+ @Inject
+ private AccountManager accountManager;
+
+ @Override
+ public BackupRepository addBackupRepository(AddBackupRepositoryCmd cmd) {
+ BackupRepositoryVO repository = new BackupRepositoryVO(cmd.getZoneId(), cmd.getProvider(), cmd.getName(),
+ cmd.getType(), cmd.getAddress(), cmd.getMountOptions(), cmd.getCapacityBytes());
+ repositoryDao.persist(repository);
+
+ return repository;
+ }
+
+ @Override
+ public boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd) {
+ BackupRepositoryVO backupRepositoryVO = repositoryDao.findById(cmd.getId());
+ if (Objects.isNull(backupRepositoryVO)) {
+ logger.debug("Backup repository appears to already be deleted");
+ return true;
+ }
+ repositoryDao.remove(backupRepositoryVO.getId());
+ return true;
+ }
+
+ @Override
+ public Pair<List<BackupRepository>, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd) {
+ Long zoneId = accountManager.checkAccessAndSpecifyAuthority(CallContext.current().getCallingAccount(), cmd.getZoneId());
+ Long id = cmd.getId();
+ String name = cmd.getName();
+ String provider = cmd.getProvider();
+ String keyword = cmd.getKeyword();
+
+ SearchBuilder<BackupRepositoryVO> sb = repositoryDao.createSearchBuilder();
+ sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
+ sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
+ sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ);
+ sb.and("provider", sb.entity().getProvider(), SearchCriteria.Op.EQ);
+
+ SearchCriteria<BackupRepositoryVO> sc = sb.create();
+ if (keyword != null) {
+ SearchCriteria<BackupRepositoryVO> ssc = repositoryDao.createSearchCriteria();
+ ssc.addOr("name", SearchCriteria.Op.LIKE, "%" + keyword + "%");
+ ssc.addOr("provider", SearchCriteria.Op.LIKE, "%" + keyword + "%");
+ sc.addAnd("name", SearchCriteria.Op.SC, ssc);
+ }
+ if (Objects.nonNull(id)) {
+ sc.setParameters("id", id);
+ }
+ if (Objects.nonNull(name)) {
+ sc.setParameters("name", name);
+ }
+ if (Objects.nonNull(zoneId)) {
+ sc.setParameters("zoneId", zoneId);
+ }
+ if (Objects.nonNull(provider)) {
+ sc.setParameters("provider", provider);
+ }
+
+ // search Store details by ids
+ Pair<List<BackupRepositoryVO>, Integer> repositoryVOPair = repositoryDao.searchAndCount(sc, null);
+ return new Pair<>(new ArrayList<>(repositoryVOPair.first()), repositoryVOPair.second());
+ }
+}
diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
index 1ca630c..226e51e 100644
--- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
+++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
@@ -338,6 +338,8 @@
<property name="asyncJobDispatcher" ref="ApiAsyncJobDispatcher" />
</bean>
+ <bean id="backupRepositoryService" class="org.apache.cloudstack.backup.BackupRepositoryServiceImpl" />
+
<bean id="storageLayer" class="com.cloud.storage.JavaStorageLayer" />
<bean id="nfsMountManager" class="org.apache.cloudstack.storage.NfsMountManagerImpl" >
diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py
index 0618d84..679b1d5 100644
--- a/tools/marvin/setup.py
+++ b/tools/marvin/setup.py
@@ -27,7 +27,7 @@
raise RuntimeError("python setuptools is required to build Marvin")
-VERSION = "4.20.0.0-SNAPSHOT"
+VERSION = "4.20.0.0"
setup(name="Marvin",
version=VERSION,
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 267a376..a212d36 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -402,6 +402,7 @@
"label.backup.restore": "Restore Instance backup",
"label.backupofferingid": "Backup offering",
"label.backupofferingname": "Backup offering",
+"label.backup.repository.add": "Add backup repository",
"label.balance": "Balance",
"label.bandwidth": "Bandwidth",
"label.baremetal.dhcp.devices": "Bare metal DHCP devices",
diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js
index 9c21f62..6a3ea3b 100644
--- a/ui/src/config/section/config.js
+++ b/ui/src/config/section/config.js
@@ -147,8 +147,16 @@
label: 'label.backup.repository.add',
listView: true,
args: [
- 'name', 'provider', 'address', 'opts', 'zoneid'
- ]
+ 'name', 'provider', 'address', 'type', 'mountopts', 'zoneid'
+ ],
+ mapping: {
+ type: {
+ value: (record) => { return 'nfs' }
+ },
+ provider: {
+ value: (record) => { return 'nas' }
+ }
+ }
}
]
},
diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue
index 36eb6d4..29898fe 100644
--- a/ui/src/views/AutogenView.vue
+++ b/ui/src/views/AutogenView.vue
@@ -1169,6 +1169,7 @@
this.showAction = true
const listIconForFillValues = ['copy-outlined', 'CopyOutlined', 'edit-outlined', 'EditOutlined', 'share-alt-outlined', 'ShareAltOutlined']
+ console.log(this.currentAction.paramFields)
for (const param of this.currentAction.paramFields) {
if (param.type === 'list' && ['tags', 'hosttags', 'storagetags', 'files'].includes(param.name)) {
param.type = 'string'