You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2023/11/16 15:43:49 UTC

(cloudstack) branch main updated: multi local storage handling for kvm (#6699)

This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 60017723357 multi local storage handling for kvm (#6699)
60017723357 is described below

commit 60017723357c5c5d3f81931d2b1be286aa986ad8
Author: DK101010 <57...@users.noreply.github.com>
AuthorDate: Thu Nov 16 16:43:42 2023 +0100

    multi local storage handling for kvm (#6699)
    
    Co-authored-by: DK101010 <di...@itelligence.de>
    Co-authored-by: João Jandre <48...@users.noreply.github.com>
---
 .../java/com/cloud/agent/api/StoragePoolInfo.java  |   7 ++
 .../api/command/admin/host/ListHostsCmd.java       |   4 +
 .../command/admin/storage/ListStoragePoolsCmd.java |  29 +++++-
 .../CloudStackPrimaryDataStoreLifeCycleImpl.java   |  83 +++++-----------
 .../java/com/cloud/api/query/QueryManagerImpl.java |  27 ++++-
 .../java/com/cloud/storage/StorageManagerImpl.java | 110 ++++++++++++++++++++-
 ui/src/views/infra/AddPrimaryStorage.vue           |  32 +++++-
 7 files changed, 220 insertions(+), 72 deletions(-)

diff --git a/api/src/main/java/com/cloud/agent/api/StoragePoolInfo.java b/api/src/main/java/com/cloud/agent/api/StoragePoolInfo.java
index d923694a854..eb7e8813ecd 100644
--- a/api/src/main/java/com/cloud/agent/api/StoragePoolInfo.java
+++ b/api/src/main/java/com/cloud/agent/api/StoragePoolInfo.java
@@ -52,6 +52,13 @@ public class StoragePoolInfo {
         this.details = details;
     }
 
+    public StoragePoolInfo(String uuid, String host, String hostPath, String localPath, StoragePoolType poolType, long capacityBytes, long availableBytes,
+            Map<String, String> details, String name) {
+        this(uuid, host, hostPath, localPath, poolType, capacityBytes, availableBytes);
+        this.details = details;
+        this.name = name;
+    }
+
     public long getCapacityBytes() {
         return capacityBytes;
     }
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java
index 75fe339b710..b8668f61ca4 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java
@@ -119,6 +119,10 @@ public class ListHostsCmd extends BaseListCmd {
         return id;
     }
 
+    public void setId(Long id) {
+        this.id = id;
+    }
+
     public String getHostName() {
         return hostName;
     }
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ListStoragePoolsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ListStoragePoolsCmd.java
index 209aaac279c..6923353b3bf 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ListStoragePoolsCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ListStoragePoolsCmd.java
@@ -24,6 +24,7 @@ import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseListCmd;
 import org.apache.cloudstack.api.Parameter;
 import org.apache.cloudstack.api.response.ClusterResponse;
+import org.apache.cloudstack.api.response.HostResponse;
 import org.apache.cloudstack.api.response.ListResponse;
 import org.apache.cloudstack.api.response.PodResponse;
 import org.apache.cloudstack.api.response.StoragePoolResponse;
@@ -63,16 +64,25 @@ public class ListStoragePoolsCmd extends BaseListCmd {
     @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = StoragePoolResponse.class, description = "the ID of the storage pool")
     private Long id;
 
-    @Parameter(name = ApiConstants.SCOPE, type = CommandType.STRING, entityType = StoragePoolResponse.class, description = "the ID of the storage pool")
+    @Parameter(name = ApiConstants.SCOPE, type = CommandType.STRING, entityType = StoragePoolResponse.class, description = "the scope of the storage pool")
     private String scope;
 
+
     @Parameter(name = ApiConstants.STATUS, type = CommandType.STRING, description = "the status of the storage pool")
     private String status;
 
+    @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "host ID of the storage pools")
+    private Long hostId;
+
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
 
+    public Long getHostId() {
+        return hostId;
+    }
+
     public Long getClusterId() {
         return clusterId;
     }
@@ -81,6 +91,10 @@ public class ListStoragePoolsCmd extends BaseListCmd {
         return ipAddress;
     }
 
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
     public String getStoragePoolName() {
         return storagePoolName;
     }
@@ -108,6 +122,15 @@ public class ListStoragePoolsCmd extends BaseListCmd {
     public void setId(Long id) {
         this.id = id;
     }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
@@ -123,8 +146,4 @@ public class ListStoragePoolsCmd extends BaseListCmd {
         response.setResponseName(getCommandName());
         this.setResponseObject(response);
     }
-
-    public String getScope() {
-        return scope;
-    }
 }
diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
index 213e5620553..6e178c119e3 100644
--- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
+++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
@@ -27,6 +27,7 @@ import com.cloud.agent.api.ValidateVcenterDetailsCommand;
 import com.cloud.alert.AlertManager;
 import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.exception.StorageConflictException;
+import com.cloud.exception.StorageUnavailableException;
 import com.cloud.host.Host;
 import com.cloud.host.HostVO;
 import com.cloud.host.dao.HostDao;
@@ -44,7 +45,6 @@ import com.cloud.storage.dao.StoragePoolWorkDao;
 import com.cloud.storage.dao.VolumeDao;
 import com.cloud.user.dao.UserDao;
 import com.cloud.utils.NumbersUtil;
-import com.cloud.utils.UriUtils;
 import com.cloud.utils.db.DB;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.vm.VirtualMachineManager;
@@ -67,10 +67,6 @@ import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
 import org.apache.log4j.Logger;
 
 import javax.inject.Inject;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -138,64 +134,26 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl implements PrimaryDataStore
 
         PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters();
 
-        UriUtils.UriInfo uriInfo = UriUtils.getUriInfo(url);
-
-        String scheme = uriInfo.getScheme();
-        String storageHost = uriInfo.getStorageHost();
-        String storagePath = uriInfo.getStoragePath();
-        try {
-            if (scheme == null) {
-                throw new InvalidParameterValueException("scheme is null " + url + ", add nfs:// (or cifs://) as a prefix");
-            } else if (scheme.equalsIgnoreCase("nfs")) {
-                if (storageHost == null || storagePath == null || storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
-                    throw new InvalidParameterValueException("host or path is null, should be nfs://hostname/path");
-                }
-            } else if (scheme.equalsIgnoreCase("cifs")) {
-                // Don't validate against a URI encoded URI.
-                URI cifsUri = new URI(url);
-                String warnMsg = UriUtils.getCifsUriParametersProblems(cifsUri);
-                if (warnMsg != null) {
-                    throw new InvalidParameterValueException(warnMsg);
-                }
-            } else if (scheme.equalsIgnoreCase("sharedMountPoint")) {
-                if (storagePath == null) {
-                    throw new InvalidParameterValueException("host or path is null, should be sharedmountpoint://localhost/path");
-                }
-            } else if (scheme.equalsIgnoreCase("rbd")) {
-                if (storagePath == null) {
-                    throw new InvalidParameterValueException("host or path is null, should be rbd://hostname/pool");
-                }
-            } else if (scheme.equalsIgnoreCase("gluster")) {
-                if (storageHost == null || storagePath == null || storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
-                    throw new InvalidParameterValueException("host or path is null, should be gluster://hostname/volume");
-                }
-            }
-        } catch (URISyntaxException e) {
-            throw new InvalidParameterValueException(url + " is not a valid uri");
-        }
-
         String tags = (String)dsInfos.get("tags");
         Map<String, String> details = (Map<String, String>)dsInfos.get("details");
 
         parameters.setTags(tags);
         parameters.setDetails(details);
 
-        String hostPath = null;
-        try {
-            hostPath = URLDecoder.decode(storagePath, "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            s_logger.error("[ignored] we are on a platform not supporting \"UTF-8\"!?!", e);
-        }
-        if (hostPath == null) { // if decoding fails, use getPath() anyway
-            hostPath = storagePath;
-        }
+        String scheme = dsInfos.get("scheme").toString();
+        String storageHost = dsInfos.get("host").toString();
+        String hostPath = dsInfos.get("hostPath").toString();
+        String uri = String.format("%s://%s%s", scheme, storageHost, hostPath);
+
         Object localStorage = dsInfos.get("localStorage");
-        if (localStorage != null) {
-            hostPath = hostPath.replaceFirst("/", "");
+           if (localStorage != null) {
+            hostPath = hostPath.contains("//") ? hostPath.replaceFirst("/", "") : hostPath;
             hostPath = hostPath.replace("+", " ");
         }
-        String userInfo = uriInfo.getUserInfo();
-        int port = uriInfo.getPort();
+
+        String userInfo = dsInfos.get("userInfo") != null ? dsInfos.get("userInfo").toString() : null;
+        int port = dsInfos.get("port") != null ? Integer.parseInt(dsInfos.get("port").toString()) : -1;
+
         if (s_logger.isDebugEnabled()) {
             s_logger.debug("createPool Params @ scheme - " + scheme + " storageHost - " + storageHost + " hostPath - " + hostPath + " port - " + port);
         }
@@ -312,8 +270,8 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl implements PrimaryDataStore
                 parameters.setPort(0);
                 parameters.setPath(hostPath);
             } else {
-                s_logger.warn("Unable to figure out the scheme for URI: " + uriInfo);
-                throw new IllegalArgumentException("Unable to figure out the scheme for URI: " + uriInfo);
+                s_logger.warn("Unable to figure out the scheme for URI: " + scheme);
+                throw new IllegalArgumentException("Unable to figure out the scheme for URI: " + scheme);
             }
         }
 
@@ -321,7 +279,7 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl implements PrimaryDataStore
             List<StoragePoolVO> pools = primaryDataStoreDao.listPoolByHostPath(storageHost, hostPath);
             if (!pools.isEmpty() && !scheme.equalsIgnoreCase("sharedmountpoint")) {
                 Long oldPodId = pools.get(0).getPodId();
-                throw new CloudRuntimeException("Storage pool " + uriInfo + " already in use by another pod (id=" + oldPodId + ")");
+                throw new CloudRuntimeException("Storage pool " + hostPath + " already in use by another pod (id=" + oldPodId + ")");
             }
         }
 
@@ -550,7 +508,16 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl implements PrimaryDataStore
 
     @Override
     public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) {
-        dataStoreHelper.attachHost(store, scope, existingInfo);
+        DataStore dataStore = dataStoreHelper.attachHost(store, scope, existingInfo);
+        if(existingInfo.getCapacityBytes() == 0){
+            try {
+                storageMgr.connectHostToSharedPool(scope.getScopeId(), dataStore.getId());
+            } catch (StorageUnavailableException ex) {
+                s_logger.error("Storage unavailable ",ex);
+            } catch (StorageConflictException ex) {
+                s_logger.error("Storage already exists ",ex);
+            }
+        }
         return true;
     }
 
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index 0056be5fce9..7a7e88ef449 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -2793,10 +2793,29 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
 
     @Override
     public ListResponse<StoragePoolResponse> searchForStoragePools(ListStoragePoolsCmd cmd) {
-        Pair<List<StoragePoolJoinVO>, Integer> result = searchForStoragePoolsInternal(cmd);
-        ListResponse<StoragePoolResponse> response = new ListResponse<StoragePoolResponse>();
+        Pair<List<StoragePoolJoinVO>, Integer> result = cmd.getHostId() != null ? searchForLocalStorages(cmd) : searchForStoragePoolsInternal(cmd);
+        return createStoragesPoolResponse(result);
+    }
+
+    private Pair<List<StoragePoolJoinVO>, Integer> searchForLocalStorages(ListStoragePoolsCmd cmd) {
+        long id = cmd.getHostId();
+        String scope = ScopeType.HOST.toString();
+        Pair<List<StoragePoolJoinVO>, Integer> localStorages;
+
+        ListHostsCmd listHostsCmd = new ListHostsCmd();
+        listHostsCmd.setId(id);
+        Pair<List<HostJoinVO>, Integer> hosts = searchForServersInternal(listHostsCmd);
+
+        cmd.setScope(scope);
+        localStorages = searchForStoragePoolsInternal(cmd);
 
-        List<StoragePoolResponse> poolResponses = ViewResponseHelper.createStoragePoolResponse(result.first().toArray(new StoragePoolJoinVO[result.first().size()]));
+        return localStorages;
+    }
+
+    private ListResponse<StoragePoolResponse> createStoragesPoolResponse(Pair<List<StoragePoolJoinVO>, Integer> storagePools) {
+        ListResponse<StoragePoolResponse> response = new ListResponse<>();
+
+        List<StoragePoolResponse> poolResponses = ViewResponseHelper.createStoragePoolResponse(storagePools.first().toArray(new StoragePoolJoinVO[storagePools.first().size()]));
         for (StoragePoolResponse poolResponse : poolResponses) {
             DataStore store = dataStoreManager.getPrimaryDataStore(poolResponse.getId());
             if (store != null) {
@@ -2816,7 +2835,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
             }
         }
 
-        response.setResponses(poolResponses, result.second());
+        response.setResponses(poolResponses, storagePools.second());
         return response;
     }
 
diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
index 82a6c159565..494855b253d 100644
--- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
+++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
@@ -237,6 +237,9 @@ import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.VirtualMachine.State;
 import com.cloud.vm.dao.VMInstanceDao;
 import com.google.common.collect.Sets;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
 
 @Component
 public class StorageManagerImpl extends ManagerBase implements StorageManager, ClusterManagerListener, Configurable {
@@ -653,6 +656,41 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
         return true;
     }
 
+    private DataStore createLocalStorage(Map<String, Object> poolInfos) throws ConnectionException{
+        Object existingUuid = poolInfos.get("uuid");
+        if( existingUuid == null ){
+            poolInfos.put("uuid", UUID.randomUUID().toString());
+        }
+        String hostAddress = poolInfos.get("host").toString();
+        Host host = _hostDao.findByName(hostAddress);
+
+        if( host == null ) {
+            host = _hostDao.findByIp(hostAddress);
+
+            if( host == null ) {
+                host = _hostDao.findByPublicIp(hostAddress);
+
+                if( host == null ) {
+                    throw new InvalidParameterValueException(String.format("host %s not found",hostAddress));
+                }
+             }
+         }
+
+        long capacityBytes = poolInfos.get("capacityBytes") != null ? Long.parseLong(poolInfos.get("capacityBytes").toString()) : 0;
+
+        StoragePoolInfo pInfo = new StoragePoolInfo(poolInfos.get("uuid").toString(),
+                                                    host.getPrivateIpAddress(),
+                                                    poolInfos.get("hostPath").toString(),
+                                                    poolInfos.get("hostPath").toString(),
+                                                    StoragePoolType.Filesystem,
+                                                    capacityBytes,
+                                                    0,
+                                                    (Map<String,String>)poolInfos.get("details"),
+                                                    poolInfos.get("name").toString());
+
+        return createLocalStorage(host, pInfo);
+    }
+
     @DB
     @Override
     public DataStore createLocalStorage(Host host, StoragePoolInfo pInfo) throws ConnectionException {
@@ -698,17 +736,19 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
             DataStoreLifeCycle lifeCycle = provider.getDataStoreLifeCycle();
             if (pool == null) {
                 Map<String, Object> params = new HashMap<String, Object>();
-                String name = createLocalStoragePoolName(host, pInfo);
+                String name = pInfo.getName() != null ? pInfo.getName() : createLocalStoragePoolName(host, pInfo);
                 params.put("zoneId", host.getDataCenterId());
                 params.put("clusterId", host.getClusterId());
                 params.put("podId", host.getPodId());
                 params.put("hypervisorType", host.getHypervisorType());
-                params.put("url", pInfo.getPoolType().toString() + "://" + pInfo.getHost() + "/" + pInfo.getHostPath());
                 params.put("name", name);
                 params.put("localStorage", true);
                 params.put("details", pInfo.getDetails());
                 params.put("uuid", pInfo.getUuid());
                 params.put("providerName", provider.getName());
+                params.put("scheme", pInfo.getPoolType().toString());
+                params.put("host", pInfo.getHost());
+                params.put("hostPath", pInfo.getHostPath());
 
                 store = lifeCycle.initialize(params);
             } else {
@@ -740,6 +780,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
     @Override
     public PrimaryDataStoreInfo createPool(CreateStoragePoolCmd cmd) throws ResourceInUseException, IllegalArgumentException, UnknownHostException, ResourceUnavailableException {
         String providerName = cmd.getStorageProviderName();
+        Map<String,String> uriParams = extractUriParamsAsMap(cmd.getUrl());
         DataStoreProvider storeProvider = _dataStoreProviderMgr.getDataStoreProvider(providerName);
 
         if (storeProvider == null) {
@@ -753,7 +794,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
         Long podId = cmd.getPodId();
         Long zoneId = cmd.getZoneId();
 
-        ScopeType scopeType = ScopeType.CLUSTER;
+        ScopeType scopeType = uriParams.get("scheme").toString().equals("file") ? ScopeType.HOST : ScopeType.CLUSTER;
         String scope = cmd.getScope();
         if (scope != null) {
             try {
@@ -819,11 +860,16 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
         params.put("managed", cmd.isManaged());
         params.put("capacityBytes", cmd.getCapacityBytes());
         params.put("capacityIops", cmd.getCapacityIops());
+        params.putAll(uriParams);
 
         DataStoreLifeCycle lifeCycle = storeProvider.getDataStoreLifeCycle();
         DataStore store = null;
         try {
-            store = lifeCycle.initialize(params);
+            if (params.get("scheme").toString().equals("file")) {
+                store = createLocalStorage(params);
+            } else {
+                store = lifeCycle.initialize(params);
+            }
             if (scopeType == ScopeType.CLUSTER) {
                 ClusterScope clusterScope = new ClusterScope(clusterId, podId, zoneId);
                 lifeCycle.attachCluster(store, clusterScope);
@@ -848,6 +894,62 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
         return (PrimaryDataStoreInfo)_dataStoreMgr.getDataStore(store.getId(), DataStoreRole.Primary);
     }
 
+    private Map<String,String> extractUriParamsAsMap(String url){
+        Map<String,String> uriParams = new HashMap<>();
+        UriUtils.UriInfo uriInfo = UriUtils.getUriInfo(url);
+
+        String scheme = uriInfo.getScheme();
+        String storageHost = uriInfo.getStorageHost();
+        String storagePath = uriInfo.getStoragePath();
+        try {
+            if (scheme == null) {
+                throw new InvalidParameterValueException("scheme is null " + url + ", add nfs:// (or cifs://) as a prefix");
+            } else if (scheme.equalsIgnoreCase("nfs")) {
+                if (storageHost == null || storagePath == null || storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
+                    throw new InvalidParameterValueException("host or path is null, should be nfs://hostname/path");
+                }
+            } else if (scheme.equalsIgnoreCase("cifs")) {
+                // Don't validate against a URI encoded URI.
+                URI cifsUri = new URI(url);
+                String warnMsg = UriUtils.getCifsUriParametersProblems(cifsUri);
+                if (warnMsg != null) {
+                    throw new InvalidParameterValueException(warnMsg);
+                }
+            } else if (scheme.equalsIgnoreCase("sharedMountPoint")) {
+                if (storagePath == null) {
+                    throw new InvalidParameterValueException("host or path is null, should be sharedmountpoint://localhost/path");
+                }
+            } else if (scheme.equalsIgnoreCase("rbd")) {
+                if (storagePath == null) {
+                    throw new InvalidParameterValueException("host or path is null, should be rbd://hostname/pool");
+                }
+            } else if (scheme.equalsIgnoreCase("gluster")) {
+                if (storageHost == null || storagePath == null || storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
+                    throw new InvalidParameterValueException("host or path is null, should be gluster://hostname/volume");
+                }
+            }
+        } catch (URISyntaxException e) {
+            throw new InvalidParameterValueException(url + " is not a valid uri");
+        }
+
+        String hostPath = null;
+        try {
+            hostPath = URLDecoder.decode(storagePath, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            s_logger.error("[ignored] we are on a platform not supporting \"UTF-8\"!?!", e);
+        }
+        if (hostPath == null) { // if decoding fails, use getPath() anyway
+            hostPath = storagePath;
+        }
+
+        uriParams.put("scheme", scheme);
+        uriParams.put("host", storageHost);
+        uriParams.put("hostPath", hostPath);
+        uriParams.put("userInfo", uriInfo.getUserInfo());
+        uriParams.put("port", uriInfo.getPort() + "");
+        return uriParams;
+    }
+
     private Map<String, String> extractApiParamAsMap(Map ds) {
         Map<String, String> details = new HashMap<String, String>();
         if (ds != null) {
diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue
index b30b0341ca4..91c0dcbf42e 100644
--- a/ui/src/views/infra/AddPrimaryStorage.vue
+++ b/ui/src/views/infra/AddPrimaryStorage.vue
@@ -32,6 +32,7 @@
           <a-select
             v-model:value="form.scope"
             v-focus="true"
+            @change="val => changeScope(val)"
             showSearch
             optionFilterProp="label"
             :filterOption="(input, option) => {
@@ -40,6 +41,7 @@
             :placeholder="apiParams.scope.description" >
             <a-select-option :value="'cluster'" :label="$t('label.clusterid')"> {{ $t('label.clusterid') }} </a-select-option>
             <a-select-option :value="'zone'" :label="$t('label.zoneid')"> {{ $t('label.zoneid') }} </a-select-option>
+            <a-select-option :value="'host'" :label="$t('label.hostid')"> {{ $t('label.hostid') }} </a-select-option>
           </a-select>
         </a-form-item>
         <div v-if="form.scope === 'zone'">
@@ -168,7 +170,7 @@
             <a-input v-model:value="form.server" :placeholder="$t('message.server.description')" />
           </a-form-item>
         </div>
-        <div v-if="form.protocol === 'nfs' || form.protocol === 'SMB' || form.protocol === 'ocfs2' || (form.protocol === 'PreSetup' && hypervisorType !== 'VMware') || form.protocol === 'SharedMountPoint'">
+        <div v-if="form.protocol === 'nfs' || form.protocol === 'SMB' || form.protocol === 'ocfs2' || form.protocol === 'Filesystem' || (form.protocol === 'PreSetup' && hypervisorType !== 'VMware') || form.protocol === 'SharedMountPoint'">
           <a-form-item name="path" ref="path">
             <template #label>
               <tooltip-label :title="$t('label.path')" :tooltip="$t('message.path.description')"/>
@@ -438,6 +440,15 @@ export default {
         this.loading = false
       })
     },
+    changeScope (value) {
+      if (value === 'host') {
+        const cluster = this.clusters.find(cluster => cluster.id === this.form.cluster)
+        this.hypervisorType = cluster.hypervisortype
+        if (this.hypervisorType === 'KVM') {
+          this.protocols.push('Filesystem')
+        }
+      }
+    },
     changeZone (value) {
       this.form.zone = value
       if (this.form.zone === '') {
@@ -504,6 +515,9 @@ export default {
       this.hypervisorType = cluster.hypervisortype
       if (this.hypervisorType === 'KVM') {
         this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'Gluster', 'Linstor', 'custom']
+        if (this.form.scope === 'host') {
+          this.protocols.push('Filesystem')
+        }
       } else if (this.hypervisorType === 'XenServer') {
         this.protocols = ['nfs', 'PreSetup', 'iscsi', 'custom']
       } else if (this.hypervisorType === 'VMware') {
@@ -524,6 +538,20 @@ export default {
         this.form.protocol = this.protocols[0]
       }
     },
+    filesystemURL (hostId, path) {
+      var url
+      if (path.substring(0, 1) !== '/') {
+        path = '/' + path
+      }
+      var hostName
+      this.hosts.forEach(host => {
+        if (host.id === hostId) {
+          hostName = host.name
+        }
+      })
+      url = 'file://' + hostName + path
+      return url
+    },
     nfsURL (server, path) {
       var url
       if (path.substring(0, 1) !== '/') {
@@ -756,6 +784,8 @@ export default {
           if (values.capacityIops && values.capacityIops.length > 0) {
             params.capacityIops = values.capacityIops.split(',').join('')
           }
+        } else if (values.protocol === 'Filesystem') {
+          url = this.filesystemURL(values.host, path)
         }
         params.url = url
         if (values.provider !== 'DefaultPrimary' && values.provider !== 'PowerFlex') {