multi local storage handling for kvm (#6699)

Co-authored-by: DK101010 <dirk.klahre@itelligence.de>
Co-authored-by: João Jandre <48719461+JoaoJandre@users.noreply.github.com>
This commit is contained in:
DK101010 2023-11-16 16:43:42 +01:00 committed by GitHub
parent 0735b91037
commit 6001772335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 220 additions and 72 deletions

View File

@ -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;
}

View File

@ -119,6 +119,10 @@ public class ListHostsCmd extends BaseListCmd {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getHostName() {
return hostName;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
List<StoragePoolResponse> poolResponses = ViewResponseHelper.createStoragePoolResponse(result.first().toArray(new StoragePoolJoinVO[result.first().size()]));
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);
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;
}

View File

@ -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) {

View File

@ -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') {