This is an automated email from the ASF dual-hosted git repository. pearl11594 pushed a commit to branch kvm-backup-plugin-shared-storage in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit fe5c9d72dea9926bd70b47c31b0fe39b4b910d96 Author: Pearl Dsilva <pearl1...@gmail.com> AuthorDate: Mon Aug 12 12:59:10 2024 -0400 add APIs for management of backup repositories and backing up from local stores and stopped VMs --- .../org/apache/cloudstack/api/ApiConstants.java | 1 + .../apache/cloudstack/api/ResponseGenerator.java | 4 + .../backup/repository/AddBackupRepositoryCmd.java | 117 ++++++++++++++++++ .../repository/DeleteBackupRepositoryCmd.java | 59 +++++++++ .../repository/ListBackupRepositoriesCmd.java | 93 ++++++++++++++ .../api/response/BackupRepositoryResponse.java | 134 +++++++++++++++++++++ .../cloudstack/backup/BackupRepositoryService.java | 15 +++ .../cloudstack/backup/TakeBackupCommand.java | 11 ++ .../cloudstack/backup/NASBackupProvider.java | 46 +++++-- .../wrapper/LibvirtTakeBackupCommandWrapper.java | 4 +- scripts/vm/hypervisor/kvm/nasbackup.sh | 43 +++++-- .../main/java/com/cloud/api/ApiResponseHelper.java | 24 ++++ .../cloudstack/backup/BackupManagerImpl.java | 6 + .../backup/BackupRepositoryServiceImpl.java | 84 +++++++++++++ .../core/spring-server-core-managers-context.xml | 2 + tools/marvin/setup.py | 2 +- ui/public/locales/en.json | 1 + ui/src/config/section/config.js | 12 +- ui/src/views/AutogenView.vue | 1 + 19 files changed, 636 insertions(+), 23 deletions(-) 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 7a167a7aeb6..3037f0aa9a2 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 class ApiConstants { 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 ef759aaf9c3..92032d2d979 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.List; 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 @@ public interface ResponseGenerator { 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 00000000000..6ad38949f6f --- /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 00000000000..70f37c34cb6 --- /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 00000000000..2e47192b072 --- /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 00000000000..d68b48313bf --- /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 00000000000..8468c2bf1f2 --- /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 3deb3eb47b0..93855ea1721 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 @@ package org.apache.cloudstack.backup; 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 @@ public class TakeBackupCommand extends Command { 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 33a648ac8fb..ceb0223e1bf 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.HostVO; 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.BackupOfferingDao; 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.Date; 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; @@ -79,6 +84,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private VMInstanceDao vmInstanceDao; + @Inject + private PrimaryDataStoreDao primaryDataStoreDao; + @Inject private AgentManager agentManager; @@ -111,10 +119,16 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co 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 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @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 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co 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 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co // TODO: get KVM agent to restore VM backup + return true; } @@ -216,9 +246,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co } 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 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co // 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 af02f2aa06a..2a3321870b9 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 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper<TakeBackupCo 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 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper<TakeBackupCo "-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 4caae9c4da7..9a73bd71b60 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -30,14 +30,12 @@ NAS_TYPE="" 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 @@ backup_vm() { 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 @@ delete_backup() { 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 @@ while [[ $# -gt 0 ]]; do shift shift ;; + -d|--diskpaths) + DISK_PATHS="$2" + shift + shift + ;; -h|--help) usage shift @@ -138,7 +156,12 @@ while [[ $# -gt 0 ]]; do 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 cd1d65366ad..86a5e8a5648 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.AutoScalePolicyResponse; 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.VpnUsersResponse; 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 @@ public class ApiResponseHelper implements ResponseGenerator { UserDataDao userDataDao; @Inject VlanDetailsDao vlanDetailsDao; + @Inject + BackupRepositoryDao backupRepositoryDao; @Inject ObjectStoreDao _objectStoreDao; @@ -5281,4 +5286,23 @@ public class ApiResponseHelper implements ResponseGenerator { 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 87535978e55..90e85c6a8a0 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.RemoveVirtualMachineFromBac 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 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { 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 00000000000..7088547f09e --- /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 1ca630cfa8a..226e51e86fb 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 0618d84370a..679b1d5920d 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ except ImportError: 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 267a376e8d1..a212d365e96 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 9c21f62027a..6a3ea3b8805 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -147,8 +147,16 @@ export default { 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 36eb6d4de1d..29898fec159 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -1169,6 +1169,7 @@ export default { 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'