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'


Reply via email to