From 753b5639475448b44641b3d182aa32f49b5bf1ae Mon Sep 17 00:00:00 2001 From: Victor Rodionov Date: Wed, 15 Jan 2014 15:12:02 -0800 Subject: [PATCH] NexentaStor iSCSI volume driver This a NexentaStor iSCSI volume driver. Now implemented only following functions: * create volume * delete volume Currently delete volume still in progress. Signed-off-by: Edison Su --- plugins/pom.xml | 1 + plugins/storage/volume/nexenta/pom.xml | 47 ++ .../module.properties | 18 + .../spring-storage-volume-nexenta-context.xml | 32 ++ .../driver/NexentaPrimaryDataStoreDriver.java | 194 +++++++++ .../NexentaPrimaryDataStoreLifeCycle.java | 159 +++++++ .../provider/NexentaHostListener.java | 17 + .../NexentaPrimaryDataStoreProvider.java | 81 ++++ .../datastore/util/NexentaNmsClient.java | 212 +++++++++ .../storage/datastore/util/NexentaNmsUrl.java | 85 ++++ .../datastore/util/NexentaStorAppliance.java | 401 ++++++++++++++++++ .../storage/datastore/util/NexentaUtil.java | 242 +++++++++++ .../util/NexentaStorApplianceTest.java | 319 ++++++++++++++ .../datastore/util/NexentaUtilTest.java | 117 +++++ 14 files changed, 1925 insertions(+) create mode 100644 plugins/storage/volume/nexenta/pom.xml create mode 100644 plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/module.properties create mode 100644 plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/spring-storage-volume-nexenta-context.xml create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaHostListener.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaPrimaryDataStoreProvider.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsClient.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsUrl.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaStorAppliance.java create mode 100644 plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaUtil.java create mode 100644 plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaStorApplianceTest.java create mode 100644 plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaUtilTest.java diff --git a/plugins/pom.xml b/plugins/pom.xml index 85a7bbee3ee..37382de61bf 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -62,6 +62,7 @@ storage/image/swift storage/image/default storage/image/sample + storage/volume/nexenta storage/volume/solidfire storage/volume/default storage/volume/sample diff --git a/plugins/storage/volume/nexenta/pom.xml b/plugins/storage/volume/nexenta/pom.xml new file mode 100644 index 00000000000..4db205f35ab --- /dev/null +++ b/plugins/storage/volume/nexenta/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + cloud-plugin-storage-volume-nexenta + Apache CloudStack Plugin - Storage Volume Nexenta Provider + + org.apache.cloudstack + cloudstack-plugins + 4.4.0-SNAPSHOT + ../../../pom.xml + + + + org.apache.cloudstack + cloud-engine-storage-volume + ${project.version} + + + + + + maven-surefire-plugin + + true + + + + integration-test + + test + + + + + + + \ No newline at end of file diff --git a/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/module.properties b/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/module.properties new file mode 100644 index 00000000000..c2036006f1f --- /dev/null +++ b/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/module.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +name=storage-volume-nexenta +parent=storage \ No newline at end of file diff --git a/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/spring-storage-volume-nexenta-context.xml b/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/spring-storage-volume-nexenta-context.xml new file mode 100644 index 00000000000..6c83c1ac47f --- /dev/null +++ b/plugins/storage/volume/nexenta/resources/META-INF.cloudstack.storage-volume-solidfire/spring-storage-volume-nexenta-context.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java new file mode 100644 index 00000000000..70f4a4f140c --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.storage.datastore.driver; + +import static org.apache.cloudstack.storage.datastore.util.NexentaUtil.NexentaPluginParameters; + +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; +import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance; +import org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.NexentaStorZvol; +import org.apache.cloudstack.storage.datastore.util.NexentaUtil; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.host.Host; +import com.cloud.storage.Storage; +import com.cloud.storage.StoragePool; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.dao.AccountDao; + +public class NexentaPrimaryDataStoreDriver implements PrimaryDataStoreDriver { + private static final Logger logger = Logger.getLogger(NexentaPrimaryDataStoreDriver.class); + + @Override + public boolean connectVolumeToHost(VolumeInfo volumeInfo, Host host, DataStore dataStore) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void disconnectVolumeFromHost(VolumeInfo volumeInfo, Host host, DataStore dataStore) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public long getVolumeSizeIncludingHypervisorSnapshotReserve(Volume volume, StoragePool pool) { + return 0; //To change body of implemented methods use File | Settings | File Templates. + } + + @Inject + private VolumeDao _volumeDao; + @Inject + PrimaryDataStoreDao _storagePoolDao; + @Inject + private StoragePoolDetailsDao _storagePoolDetailsDao; + @Inject + private AccountDao _accountDao; + + @Override + public Map getCapabilities() { + return null; + } + + @Override + public ChapInfo getChapInfo(VolumeInfo volumeInfo) { + return null; + } + + @Override + public DataTO getTO(DataObject data) { + return null; + } + + @Override + public DataStoreTO getStoreTO(DataStore store) { + return null; + } + + private NexentaStorAppliance getNexentaStorAppliance(long storagePoolId) { + NexentaPluginParameters parameters = new NexentaPluginParameters(); + + parameters.setNmsUrl(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.NMS_URL).getValue()); + parameters.setVolume(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.VOLUME).getValue()); + parameters.setStorageType(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.STORAGE_TYPE).getValue()); + parameters.setStorageHost(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.STORAGE_HOST).getValue()); + parameters.setStoragePort(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.STORAGE_PORT).getValue()); + parameters.setStoragePath(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.STORAGE_PATH).getValue()); + parameters.setSparseVolumes(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.SPARSE_VOLUMES).getValue()); + parameters.setVolumeBlockSize(_storagePoolDetailsDao.findDetail(storagePoolId, NexentaUtil.VOLUME_BLOCK_SIZE).getValue()); + + return new NexentaStorAppliance(parameters); + } + + @Override + public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) {} + + @Override + public void revertSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) {} + + @Override + public void createAsync(DataStore dataStore, DataObject dataObject, AsyncCompletionCallback callback) { + String iqn = null; + String errorMessage = null; + + if (dataObject.getType() != DataObjectType.VOLUME) { + errorMessage = "Invalid DataObjectType (" + dataObject.getType() + ") passed to createAsync"; + } else { + VolumeInfo volumeInfo = (VolumeInfo) dataObject; + + long storagePoolId = dataStore.getId(); + NexentaStorAppliance appliance = getNexentaStorAppliance(storagePoolId); + + // TODO: maybe we should use md5(volume name) as volume name + NexentaStorZvol zvol = (NexentaStorZvol) appliance.createVolume(volumeInfo.getName(), volumeInfo.getSize()); + iqn = zvol.getIqn(); + + VolumeVO volume = this._volumeDao.findById(volumeInfo.getId()); + volume.set_iScsiName(iqn); + volume.setFolder(zvol.getName()); + volume.setPoolType(Storage.StoragePoolType.IscsiLUN); + volume.setPoolId(storagePoolId); + _volumeDao.update(volume.getId(), volume); + + StoragePoolVO storagePool = _storagePoolDao.findById(storagePoolId); + long capacityBytes = storagePool.getCapacityBytes(); + long usedBytes = storagePool.getUsedBytes(); + usedBytes += volumeInfo.getSize(); + storagePool.setUsedBytes(usedBytes > capacityBytes ? capacityBytes : usedBytes); + _storagePoolDao.update(storagePoolId, storagePool); + } + + CreateCmdResult result = new CreateCmdResult(iqn, new Answer(null, errorMessage == null, errorMessage)); + result.setResult(errorMessage); + callback.complete(result); + } + + @Override + public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) { + String errorMessage = null; + if (data.getType() == DataObjectType.VOLUME) { + VolumeInfo volumeInfo = (VolumeInfo) data; + long storagePoolId = store.getId(); + NexentaStorAppliance appliance = getNexentaStorAppliance(storagePoolId); + StoragePoolVO storagePool = _storagePoolDao.findById(storagePoolId); + + + +// _storagePoolDao.update(stoagePoolId); + } else { + errorMessage = String.format( + "Invalid DataObjectType(%s) passed to deleteAsync", + data.getType()); + } + CommandResult result = new CommandResult(); + result.setResult(errorMessage); + callback.complete(result); + } + + @Override + public void copyAsync(DataObject srcdata, DataObject destData, AsyncCompletionCallback callback) {} + + @Override + public boolean canCopy(DataObject srcData, DataObject destData) { + return false; + } + + @Override + public void resize(DataObject data, AsyncCompletionCallback callback) {} +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java new file mode 100644 index 00000000000..775e6b57a85 --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/lifecylce/NexentaPrimaryDataStoreLifeCycle.java @@ -0,0 +1,159 @@ +package org.apache.cloudstack.storage.datastore.lifecylce; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import com.cloud.dc.DataCenterVO; +import com.cloud.host.HostVO; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePoolAutomation; +import org.apache.cloudstack.storage.datastore.util.NexentaUtil; +import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters; +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; +import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; + +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.hypervisor.Hypervisor; + +public class NexentaPrimaryDataStoreLifeCycle + implements PrimaryDataStoreLifeCycle { + private static final Logger logger = + Logger.getLogger(NexentaPrimaryDataStoreLifeCycle.class); + + @Inject + private DataCenterDao zoneDao; + @Inject + private PrimaryDataStoreHelper dataStoreHelper; + @Inject + private ResourceManager _resourceMgr; + @Inject + StorageManager _storageMgr; + @Inject + private StoragePoolAutomation storagePoolAutomation; + + @Override + public DataStore initialize(Map dsInfos) { + String url = (String) dsInfos.get("url"); + Long zoneId = (Long) dsInfos.get("zoneId"); + String storagePoolName = (String) dsInfos.get("name"); + String providerName = (String) dsInfos.get("providerName"); + Long capacityBytes = (Long)dsInfos.get("capacityBytes"); + Long capacityIops = (Long)dsInfos.get("capacityIops"); + String tags = (String)dsInfos.get("tags"); + Map details = (Map) dsInfos.get("details"); + NexentaUtil.NexentaPluginParameters params = NexentaUtil.parseNexentaPluginUrl(url); + DataCenterVO zone = zoneDao.findById(zoneId); + String uuid = String.format("%s_%s_%s", NexentaUtil.PROVIDER_NAME, zone.getUuid(), params.getNmsUrl().getHost()); + + if (capacityBytes == null || capacityBytes <= 0) { + throw new IllegalArgumentException("'capacityBytes' must be present and greater than 0."); + } + + if (capacityIops == null || capacityIops <= 0) { + throw new IllegalArgumentException("'capacityIops' must be present and greater than 0."); + } + + PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters(); + + parameters.setHost(params.getStorageHost()); + parameters.setPort(params.getStoragePort()); + parameters.setPath(params.getStoragePath()); + parameters.setType(params.getStorageType()); + parameters.setUuid(uuid); + parameters.setZoneId(zoneId); + parameters.setName(storagePoolName); + parameters.setProviderName(providerName); + parameters.setManaged(true); + parameters.setCapacityBytes(capacityBytes); + parameters.setUsedBytes(0); + parameters.setCapacityIops(capacityIops); + parameters.setHypervisorType(Hypervisor.HypervisorType.Any); + parameters.setTags(tags); + + details.put(NexentaUtil.NMS_URL, params.getNmsUrl().toString()); + + details.put(NexentaUtil.VOLUME, params.getVolume()); + details.put(NexentaUtil.SPARSE_VOLUMES, params.getSparseVolumes().toString()); + + details.put(NexentaUtil.STORAGE_TYPE, params.getStorageType().toString()); + details.put(NexentaUtil.STORAGE_HOST, params.getStorageHost()); + details.put(NexentaUtil.STORAGE_PORT, params.getStoragePort().toString()); + details.put(NexentaUtil.STORAGE_PATH, params.getStoragePath()); + + parameters.setDetails(details); + + // this adds a row in the cloud.storage_pool table for this SolidFire cluster + return dataStoreHelper.createPrimaryDataStore(parameters); + } + + @Override + public boolean attachCluster(DataStore store, ClusterScope scope) { + return true; + } + + @Override + public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) { + return true; + } + + @Override + public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) { + dataStoreHelper.attachZone(dataStore); + + List xenServerHosts = _resourceMgr.listAllUpAndEnabledHostsInOneZoneByHypervisor(Hypervisor.HypervisorType.XenServer, scope.getScopeId()); + List vmWareServerHosts = _resourceMgr.listAllUpAndEnabledHostsInOneZoneByHypervisor(Hypervisor.HypervisorType.VMware, scope.getScopeId()); + List kvmHosts = _resourceMgr.listAllUpAndEnabledHostsInOneZoneByHypervisor(Hypervisor.HypervisorType.KVM, scope.getScopeId()); + List hosts = new ArrayList(); + + hosts.addAll(xenServerHosts); + hosts.addAll(vmWareServerHosts); + hosts.addAll(kvmHosts); + + for (HostVO host : hosts) { + try { + _storageMgr.connectHostToSharedPool(host.getId(), dataStore.getId()); + } catch (Exception e) { + logger.warn("Unable to establish a connection between " + host + " and " + dataStore, e); + } + } + + return true; + } + + @Override + public boolean maintain(DataStore store) { + storagePoolAutomation.maintain(store); + dataStoreHelper.maintain(store); + + return true; + } + + @Override + public boolean cancelMaintain(DataStore store) { + dataStoreHelper.cancelMaintain(store); + storagePoolAutomation.cancelMaintain(store); + + return true; + } + + @Override + public boolean deleteDataStore(DataStore store) { + return dataStoreHelper.deletePrimaryDataStore(store); + } + + @Override + public boolean migrateToObjectStore(DataStore store) { + return false; + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaHostListener.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaHostListener.java new file mode 100644 index 00000000000..bf6d44266af --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaHostListener.java @@ -0,0 +1,17 @@ +package org.apache.cloudstack.storage.datastore.provider; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; + +public class NexentaHostListener implements HypervisorHostListener { + private static final Logger logger = Logger.getLogger(NexentaHostListener.class); + + public boolean hostConnect(long hostId, long poolId) { + return true; + } + + public boolean hostDisconnected(long hostId, long poolId) { + return true; + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaPrimaryDataStoreProvider.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaPrimaryDataStoreProvider.java new file mode 100644 index 00000000000..afd7e35c257 --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/provider/NexentaPrimaryDataStoreProvider.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.storage.datastore.provider; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Component; + +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreProvider; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; +import org.apache.cloudstack.storage.datastore.driver.NexentaPrimaryDataStoreDriver; +import org.apache.cloudstack.storage.datastore.lifecylce.NexentaPrimaryDataStoreLifeCycle; +import org.apache.cloudstack.storage.datastore.util.NexentaUtil; + +import com.cloud.utils.component.ComponentContext; + +@Component +public class NexentaPrimaryDataStoreProvider implements PrimaryDataStoreProvider { + private DataStoreLifeCycle lifeCycle; + private PrimaryDataStoreDriver driver; + private HypervisorHostListener listener; + + @Override + public DataStoreLifeCycle getDataStoreLifeCycle() { + return lifeCycle; + } + + @Override + public DataStoreDriver getDataStoreDriver() { + return driver; + } + + @Override + public HypervisorHostListener getHostListener() { + return listener; + } + + @Override + public String getName() { + return NexentaUtil.PROVIDER_NAME; + } + + @Override + public boolean configure(Map params) { + lifeCycle = ComponentContext.inject(NexentaPrimaryDataStoreLifeCycle.class); + driver = ComponentContext.inject(NexentaPrimaryDataStoreDriver.class); + listener = ComponentContext.inject(NexentaHostListener.class); + + return true; + } + + @Override + public Set getTypes() { + Set types = new HashSet(); + + types.add(DataStoreProviderType.PRIMARY); + + return types; + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsClient.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsClient.java new file mode 100644 index 00000000000..7ae15777a92 --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsClient.java @@ -0,0 +1,212 @@ +package org.apache.cloudstack.storage.datastore.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import com.cloud.utils.exception.CloudRuntimeException; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.BasicClientConnectionManager; +import org.apache.log4j.Logger; + +public class NexentaNmsClient { + private static final Logger logger = Logger.getLogger(NexentaNmsClient.class); + + protected NexentaNmsUrl nmsUrl = null; + protected DefaultHttpClient httpClient = null; + + NexentaNmsClient(NexentaNmsUrl nmsUrl) { + this.nmsUrl = nmsUrl; + } + + private static boolean isSuccess(int iCode) { + return iCode >= 200 && iCode < 300; + } + + protected DefaultHttpClient getClient() { + if (httpClient == null) { + if (nmsUrl.getSchema().equalsIgnoreCase("http")) { + httpClient = getHttpClient(); + } else { + httpClient = getHttpsClient(); + } + AuthScope authScope = new AuthScope(nmsUrl.getHost(), nmsUrl.getPort(), AuthScope.ANY_SCHEME, "basic"); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(nmsUrl.getUsername(), nmsUrl.getPassword()); + httpClient.getCredentialsProvider().setCredentials(authScope, credentials); + } + return httpClient; + } + + protected DefaultHttpClient getHttpsClient() { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + X509TrustManager tm = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sslContext.init(null, new TrustManager[] {tm}, new SecureRandom()); + + SSLSocketFactory socketFactory = new SSLSocketFactory(sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + SchemeRegistry registry = new SchemeRegistry(); + + registry.register(new Scheme("https", nmsUrl.getPort(), socketFactory)); + + BasicClientConnectionManager mgr = new BasicClientConnectionManager(registry); + + return new DefaultHttpClient(mgr); + } catch (NoSuchAlgorithmException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } catch (KeyManagementException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } + } + + protected DefaultHttpClient getHttpClient() { + return new DefaultHttpClient(); + } + + @SuppressWarnings("unused") + static class NmsRequest { + private String method; + private String object; + private Object[] params; + + NmsRequest(String object, String method) { + this(method, object, null); + } + + NmsRequest(String object, String method, Object... params) { + this.method = method; + this.object = object; + this.params = params; + } + + public String toString() { + StringBuffer b = new StringBuffer(); + b.append("Request to ").append(object).append(" method ").append(method); + if (params != null) { + b.append(" params:"); + for (Object param:params) { + b.append(" ").append(param).append(","); + } + } + return b.toString(); + } + } + + @SuppressWarnings("unused") + static class NmsError { + private String message; + + public String getMesssage() { + return message; + } + } + + @SuppressWarnings("unused") + static class NmsResponse { + @SerializedName("tg_flash") protected String tgFlash; + + protected NmsError error; + + NmsResponse() {} + + NmsResponse(String tgFlash, NmsError error) { + this.tgFlash = tgFlash; + this.error = error; + } + + public String getTgFlash() { + return tgFlash; + } + + public NmsError getError() { + return error; + } + } + + public NmsResponse execute(Class responseClass, String object, String method, Object... params) { + StringBuilder sb = new StringBuilder(); + NmsRequest nmsRequest = new NmsRequest(object, method, params); + if (logger.isDebugEnabled()) { + logger.debug(nmsRequest); + } + final Gson gson = new Gson(); + String jsonRequest = gson.toJson(nmsRequest); + StringEntity input = new StringEntity(jsonRequest, ContentType.APPLICATION_JSON); + HttpPost postRequest = new HttpPost(nmsUrl.toString()); + postRequest.setEntity(input); + + DefaultHttpClient httpClient = getClient(); + try { + HttpResponse response = httpClient.execute(postRequest); + final int status = response.getStatusLine().getStatusCode(); + if (!isSuccess(status)) { + throw new CloudRuntimeException("Failed on JSON-RPC API call. HTTP error code = " + status); + } + BufferedReader buffer = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String tmp; + while ((tmp = buffer.readLine()) != null) { + sb.append(tmp); + } + } catch (ClientProtocolException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } catch (IOException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } finally { + if (httpClient != null) { + try { + httpClient.getConnectionManager().shutdown(); + } catch (Exception t) { + logger.debug(t.getMessage()); + } + } + } + + String responseString = sb.toString(); + if (logger.isDebugEnabled()) { + logger.debug("NexentaStor Appliance response: " + responseString); + } + + NmsResponse nmsResponse = (NmsResponse) gson.fromJson(responseString, responseClass); + if (nmsResponse.getError() != null) { + throw new CloudRuntimeException(nmsResponse.getError().getMesssage()); + } + + return nmsResponse; + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsUrl.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsUrl.java new file mode 100644 index 00000000000..684b8dc1d0c --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaNmsUrl.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.storage.datastore.util; + +public class NexentaNmsUrl { + protected final boolean isAuto; + protected String schema; + protected final String username; + protected final String password; + protected final String host; + protected int port; + + public NexentaNmsUrl(boolean isAuto, String schema, String username, String password, String host, int port) { + this.isAuto = isAuto; + this.schema = schema; + this.username = username; + this.password = password; + this.host = host; + this.port = port; + } + + public boolean isAuto() { + return this.isAuto; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getSchema() { + return this.schema; + } + + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public String getHost() { + return this.host; + } + + public int getPort() { + return this.port; + } + + public String getPath() { + return "/rest/nms"; + } + + public String toString() { + StringBuilder b = new StringBuilder(); + if (isAuto) { + b.append("auto://"); + } else { + b.append(schema).append("://"); + } + if (username != null && password != null) { + b.append(username).append(":").append(password).append("@"); + } else if (username != null) { + b.append(username).append("@"); + } + b.append(host).append(":").append(port).append("/rest/nms/"); + return b.toString(); + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaStorAppliance.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaStorAppliance.java new file mode 100644 index 00000000000..d5cad58660f --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaStorAppliance.java @@ -0,0 +1,401 @@ +package org.apache.cloudstack.storage.datastore.util; + +import java.util.HashMap; +import java.util.LinkedList; + +import com.google.gson.annotations.SerializedName; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.storage.datastore.util.NexentaNmsClient.NmsResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class NexentaStorAppliance { + private static final Logger logger = LogManager.getLogger(NexentaStorAppliance.class); + + protected NexentaNmsClient client; + protected NexentaUtil.NexentaPluginParameters parameters; + + public NexentaStorAppliance(NexentaUtil.NexentaPluginParameters parameters) { + client = new NexentaNmsClient(parameters.getNmsUrl()); + this.parameters = parameters; + } + + NexentaStorAppliance(NexentaNmsClient client, NexentaUtil.NexentaPluginParameters parameters) { + this.client = client; + this.parameters = parameters; + } + + String getVolumeName(String volumeName) { + if (volumeName.startsWith("/")) { + return String.format("%s%s", parameters.getVolume(), volumeName); + } + return String.format("%s/%s", parameters.getVolume(), volumeName); + } + + static String getTargetName(String volumeName) { + return NexentaUtil.ISCSI_TARGET_NAME_PREFIX + volumeName; + } + + static String getTargetGroupName(String volumeName) { + return NexentaUtil.ISCSI_TARGET_GROUP_PREFIX + volumeName; + } + + @SuppressWarnings("unused") + static final class IntegerNmsResponse extends NmsResponse { + Integer result; + + IntegerNmsResponse(int result) { + this.result = Integer.valueOf(result); + } + + public Integer getResult() { + return result; + } + } + + @SuppressWarnings("unused") + static final class IscsiTarget { + protected String status; + protected String protocol; + protected String name; + protected String sessions; + protected String alias; + protected String provider; + + IscsiTarget(String status, String protocol, String name, String sessions, String alias, String provider) { + this.status = status; + this.protocol = protocol; + this.name = name; + this.sessions = sessions; + this.alias = alias; + this.provider = provider; + } + } + + @SuppressWarnings("unused") + static final class ListOfIscsiTargetsNmsResponse extends NmsResponse { + protected HashMap result; + + ListOfIscsiTargetsNmsResponse() {} + + ListOfIscsiTargetsNmsResponse(HashMap result) { + this.result = result; + } + + public HashMap getResult() { + return result; + } + } + + /** + * Checks if iSCSI target exists. + * @param targetName iSCSI target name + * @return true if iSCSI target exists, else false + */ + boolean isIscsiTargetExists(String targetName) { + ListOfIscsiTargetsNmsResponse response = (ListOfIscsiTargetsNmsResponse) client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets"); + if (response == null) { + return false; + } + HashMap result = response.getResult(); + return result != null && result.keySet().contains(targetName); + } + + @SuppressWarnings("unused") + static final class CreateIscsiTargetRequestParams { + @SerializedName("target_name") String targetName; + + CreateIscsiTargetRequestParams(String targetName) { + this.targetName = targetName; + } + + @Override + public boolean equals(Object other) { + return other instanceof CreateIscsiTargetRequestParams && targetName.equals(((CreateIscsiTargetRequestParams) other).targetName); + } + } + + /** + * Creates iSCSI target on NexentaStor Appliance. + * @param targetName iSCSI target name + */ + void createIscsiTarget(String targetName) { + try { + client.execute(NmsResponse.class, "iscsitarget", "create_target", new CreateIscsiTargetRequestParams(targetName)); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("already configured")) { + throw ex; + } + logger.debug("Ignored target creation error: " + ex); + } + } + + @SuppressWarnings("unused") + static final class ListOfStringsNmsResponse extends NmsResponse { + LinkedList result; + + ListOfStringsNmsResponse() {} + + ListOfStringsNmsResponse(LinkedList result) { + this.result = result; + } + + public LinkedList getResult() { + return result; + } + } + + /** + * Checks if iSCSI target group already exists on NexentaStor Appliance. + * @param targetGroupName iSCSI target group name + * @return true if iSCSI target group already exists, else false + */ + boolean isIscsiTargetGroupExists(String targetGroupName) { + ListOfStringsNmsResponse response = (ListOfStringsNmsResponse) client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroups"); + if (response == null) { + return false; + } + LinkedList result = response.getResult(); + return result != null && result.contains(targetGroupName); + } + + /** + * Creates iSCSI target group on NexentaStor Appliance. + * @param targetGroupName iSCSI target group name + */ + void createIscsiTargetGroup(String targetGroupName) { + try { + client.execute(NmsResponse.class, "stmf", "create_targetgroup", targetGroupName); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("already exists") && !ex.getMessage().contains("target must be offline")) { + throw ex; + } + logger.info("Ignored target group creation error: " + ex); + } + } + + /** + * Checks if iSCSI target is member of target group. + * @param targetGroupName iSCSI target group name + * @param targetName iSCSI target name + * @return true if target is member of iSCSI target group, else false + */ + boolean isTargetMemberOfTargetGroup(String targetGroupName, String targetName) { + ListOfStringsNmsResponse response = (ListOfStringsNmsResponse) client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName); + if (response == null) { + return false; + } + LinkedList result = response.getResult(); + return result != null && result.contains(targetName); + } + + /** + * Adds iSCSI target to target group. + * @param targetGroupName iSCSI target group name + * @param targetName iSCSI target name + */ + void addTargetGroupMember(String targetGroupName, String targetName) { + try { + client.execute(NmsResponse.class, "stmf", "add_targetgroup_member", targetGroupName, targetName); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("already exists") && !ex.getMessage().contains("target must be offline")) { + throw ex; + } + logger.debug("Ignored target group member addition error: " + ex); + } + } + + /** + * Checks if LU already exists on NexentaStor appliance. + * @param luName LU name + * @return true if LU already exists, else false + */ + boolean isLuExists(String luName) { + IntegerNmsResponse response; + try { + response = (IntegerNmsResponse) client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", luName); + } catch (CloudRuntimeException ex) { + if (ex.getMessage().contains("does not exist")) { + return false; + } + throw ex; + } + return response!= null && response.getResult() > 0; + } + + @SuppressWarnings("unused") + static final class LuParams { + @Override + public boolean equals(Object other) { + return other instanceof LuParams; + } + } + + /** + * Creates LU for volume. + * @param volumeName volume name + */ + void createLu(String volumeName) { + try { + client.execute(NmsResponse.class, "scsidisk", "create_lu", volumeName, new LuParams()); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("in use")) { + throw ex; + } + logger.info("Ignored LU creation error: " + ex); + } + } + + /** + * Checks if LU shared on NexentaStor appliance. + * @param luName LU name + * @return true if LU was already shared, else false + */ + boolean isLuShared(String luName) { + IntegerNmsResponse response; + try { + response = (IntegerNmsResponse) client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName); + } catch (CloudRuntimeException ex) { + if (ex.getMessage().contains("does not exist")) { + return false; + } + throw ex; + } + return response != null && response.getResult() > 0; + } + + @SuppressWarnings("unused") + static final class MappingEntry { + @SerializedName("target_group") String targetGroup; + String lun; + String zvol; + @SerializedName("host_group") String hostGroup; + @SerializedName("entry_number") String entryNumber; + + MappingEntry(String targetGroup, String lun) { + this.targetGroup = targetGroup; + this.lun = lun; + } + + static boolean isEquals(Object a, Object b) { + return (a == null && b == null) || (a != null && a.equals(b)); + } + + @Override + public boolean equals(Object other) { + if (other instanceof MappingEntry) { + MappingEntry o = (MappingEntry) other; + return isEquals(targetGroup, o.targetGroup) && isEquals(lun, o.lun) && isEquals(zvol, o.zvol) && + isEquals(hostGroup, o.hostGroup) && isEquals(entryNumber, o.entryNumber); + } + return false; + } + } + + @SuppressWarnings("unused") + static final class AddMappingEntryNmsResponse extends NmsResponse { + MappingEntry result; + } + + /** + * Adds LU mapping entry to iSCSI target group. + * @param luName LU name + * @param targetGroupName iSCSI target group name + */ + void addLuMappingEntry(String luName, String targetGroupName) { + MappingEntry mappingEntry = new MappingEntry(targetGroupName, "0"); + try { + client.execute(AddMappingEntryNmsResponse.class, "scsidisk", "add_lun_mapping_entry", luName, mappingEntry); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("view already exists")) { + throw ex; + } + logger.debug("Ignored LU mapping entry addition error " + ex); + } + } + + NexentaStorZvol createIscsiVolume(String volumeName, Long volumeSize) { + final String zvolName = getVolumeName(volumeName); + String volumeSizeString = String.format("%dB", volumeSize); + + client.execute(NmsResponse.class, "zvol", "create", zvolName, volumeSizeString, parameters.getVolumeBlockSize(), parameters.getSparseVolumes()); + + final String targetName = getTargetName(volumeName); + final String targetGroupName = getTargetGroupName(volumeName); + + if (!isIscsiTargetExists(targetName)) { + createIscsiTarget(targetName); + } + + if (!isIscsiTargetGroupExists(targetGroupName)) { + createIscsiTargetGroup(targetGroupName); + } + + if (!isTargetMemberOfTargetGroup(targetGroupName, targetName)) { + addTargetGroupMember(targetGroupName, targetName); + } + + if (!isLuExists(zvolName)) { + createLu(zvolName); + } + + if (!isLuShared(zvolName)) { + addLuMappingEntry(zvolName, targetGroupName); + } + + return new NexentaStorZvol(zvolName, targetName); + } + + static abstract class NexentaStorVolume { + protected String name; + + NexentaStorVolume(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + } + + public static final class NexentaStorZvol extends NexentaStorVolume { + protected String iqn; + + public NexentaStorZvol(String name, String iqn) { + super(name); + this.iqn = iqn; + } + + public String getIqn() { + return iqn; + } + } + + public void deleteIscsiVolume(String volumeName) { + try { + NmsResponse response = client.execute(NmsResponse.class, "zvol", "destroy", volumeName, ""); + } catch (CloudRuntimeException ex) { + if (!ex.getMessage().contains("does not exist")) { + throw ex; + } + logger.debug(String.format( + "Volume %s does not exist, it seems it was already " + + "deleted.", volumeName)); + } + } + + public NexentaStorVolume createVolume(String volumeName, Long volumeSize) { + return createIscsiVolume(volumeName, volumeSize); + } + + public void deleteVolume(String volumeName) { + deleteIscsiVolume(volumeName); + } +} diff --git a/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaUtil.java b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaUtil.java new file mode 100644 index 00000000000..ee6a78ff49c --- /dev/null +++ b/plugins/storage/volume/nexenta/src/org/apache/cloudstack/storage/datastore/util/NexentaUtil.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.storage.datastore.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.StringTokenizer; + +import com.cloud.storage.Storage; + +public class NexentaUtil { + public static final String PROVIDER_NAME = "Nexenta"; + + public static final String NMS_URL = "nmsUrl"; + public static final String VOLUME = "volume"; + + public static final String STORAGE_HOST = "storageHost"; + public static final String STORAGE_PORT = "storagePort"; + public static final String STORAGE_TYPE = "storageType"; + public static final String STORAGE_PATH = "storagePath"; + + public static final String DEFAULT_NMS_USER = "admin"; + public static final String DEFAULT_NMS_PASSWORD = "nexenta"; + + public static final String SPARSE_VOLUMES = "sparseVolumes"; + public static final String VOLUME_BLOCK_SIZE = "volumeBlockSize"; + + public static final int DEFAULT_NMS_PORT = 2000; + public static final int DEFAULT_ISCSI_TARGET_PORTAL_PORT = 3260; + public static final int DEFAULT_NFS_PORT = 2049; + + public static final String ISCSI_TARGET_NAME_PREFIX = "iqn.1986-03.com.sun:02:cloudstack-"; + public static final String ISCSI_TARGET_GROUP_PREFIX = "cloudstack/"; + + /** + * Parse NMS url into normalized parts like scheme, user, host and others. + * + * Example NMS URL: + * auto://admin:nexenta@192.168.1.1:2000/ + * + * NMS URL parts: + * auto true if url starts with auto://, protocol will be automatically switched to https if http not supported; + * scheme (auto) connection protocol (http or https); + * user (admin) NMS user; + * password (nexenta) NMS password; + * host (192.168.1.1) NMS host; + * port (2000) NMS port. + * + * @param nmsUrl url string to parse + * @return instance of NexentaConnection class + */ + public static NexentaNmsUrl parseNmsUrl(String nmsUrl) { + URI uri; + + try { + uri = new URI(nmsUrl); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI: " + nmsUrl); + } + + boolean isAuto = false; + String schema = uri.getScheme(); + if (schema == null || schema.isEmpty() || "auto".equalsIgnoreCase(schema)) { + schema = "http"; + isAuto = true; + } + + String username, password, userInfo = uri.getUserInfo(); + if (userInfo == null) { + username = DEFAULT_NMS_USER; + password = DEFAULT_NMS_PASSWORD; + } else { + if (userInfo.indexOf(':') < 0) { + username = userInfo; + password = DEFAULT_NMS_PASSWORD; + } else { + String[] parts = userInfo.split(":", 2); + username = parts[0]; + password = parts[1]; + } + } + + String host = uri.getHost(); + if (host == null) { + throw new IllegalArgumentException(String.format("NMS host required: %s.", nmsUrl)); + } + + int port = uri.getPort(); + if (port == -1) { + port = DEFAULT_NMS_PORT; + } + + return new NexentaNmsUrl(isAuto, schema, username, password, host, port); + } + + public static Storage.StoragePoolType getStorageType(String v) { + if ("iSCSI".equalsIgnoreCase(v)) { + return Storage.StoragePoolType.Iscsi; + } else if ("NFS".equalsIgnoreCase(v)) { + return Storage.StoragePoolType.NetworkFilesystem; + } + return Storage.StoragePoolType.Iscsi; + } + + public static class NexentaPluginParameters { + protected NexentaNmsUrl nmsUrl; + protected String volume; + protected Storage.StoragePoolType storageType = Storage.StoragePoolType.Iscsi; + protected String storageHost; + protected Integer storagePort; + protected String storagePath; + protected Boolean sparseVolumes = false; + protected String volumeBlockSize = "8K"; + + public void setNmsUrl(String url) { + this.nmsUrl = NexentaUtil.parseNmsUrl(url); + } + + public NexentaNmsUrl getNmsUrl() { + return nmsUrl; + } + + public void setVolume(String volume) { + if (volume.endsWith("/")) { + this.volume = volume.substring(0, volume.length() - 1); + } else { + this.volume = volume; + } + } + + public String getVolume() { + return volume; + } + + public void setStorageType(String storageType) { + this.storageType = NexentaUtil.getStorageType(storageType); + } + + public Storage.StoragePoolType getStorageType() { + return storageType; + } + + public void setStorageHost(String host) { + this.storageHost = host; + } + + public String getStorageHost() { + if (storageHost == null && nmsUrl != null) { + return nmsUrl.getHost(); + } + return storageHost; + } + + public void setStoragePort(String port) { + this.storagePort = Integer.parseInt(port); + } + + public Integer getStoragePort() { + if (storagePort == null && storageType != null) { + if (storageType == Storage.StoragePoolType.Iscsi) { + return DEFAULT_ISCSI_TARGET_PORTAL_PORT; + } else { + return DEFAULT_NFS_PORT; + } + } + return storagePort; + } + + public void setStoragePath(String path) { + this.storagePath = path; + } + + public String getStoragePath() { + return storagePath; + } + + public void setSparseVolumes(String sparseVolumes) { + this.sparseVolumes = Boolean.TRUE.toString().equalsIgnoreCase(sparseVolumes); + } + + public Boolean getSparseVolumes() { + return sparseVolumes; + } + + public void setVolumeBlockSize(String volumeBlockSize) { + this.volumeBlockSize = volumeBlockSize; + } + + public String getVolumeBlockSize() { + return volumeBlockSize; + } + } + + public static NexentaPluginParameters parseNexentaPluginUrl(String url) { + final String delimiter1 = ";"; + final String delimiter2 = "="; + StringTokenizer st = new StringTokenizer(url, delimiter1); + NexentaPluginParameters params = new NexentaPluginParameters(); + while (st.hasMoreElements()) { + String token = st.nextElement().toString(); + int idx = token.indexOf(delimiter2); + if (idx == -1) { + throw new RuntimeException("Invalid URL format"); + } + String[] urlKeyAndValue = token.split(delimiter2, 2); + if (NMS_URL.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setNmsUrl(urlKeyAndValue[1]); + } else if (VOLUME.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setVolume(urlKeyAndValue[1]); + } else if (STORAGE_TYPE.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setStorageType(urlKeyAndValue[1]); + } else if (STORAGE_HOST.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setStorageHost(urlKeyAndValue[1]); + } else if (STORAGE_PORT.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setStoragePort(urlKeyAndValue[1]); + } else if (STORAGE_PATH.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setStoragePath(urlKeyAndValue[1]); + } else if (SPARSE_VOLUMES.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setSparseVolumes(urlKeyAndValue[1]); + } else if (VOLUME_BLOCK_SIZE.equalsIgnoreCase(urlKeyAndValue[0])) { + params.setVolumeBlockSize(urlKeyAndValue[1]); + } + } + return params; + } +} diff --git a/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaStorApplianceTest.java b/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaStorApplianceTest.java new file mode 100644 index 00000000000..af320beaeeb --- /dev/null +++ b/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaStorApplianceTest.java @@ -0,0 +1,319 @@ +package org.apache.cloudstack.storage.datastore.util; + +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.IscsiTarget; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.ListOfIscsiTargetsNmsResponse; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.CreateIscsiTargetRequestParams; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.ListOfStringsNmsResponse; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.IntegerNmsResponse; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.LuParams; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.MappingEntry; +import static org.apache.cloudstack.storage.datastore.util.NexentaStorAppliance.AddMappingEntryNmsResponse; +import static org.apache.cloudstack.storage.datastore.util.NexentaNmsClient.NmsResponse; + + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.LinkedList; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class NexentaStorApplianceTest { + private NexentaNmsClient client; + + private NexentaStorAppliance appliance; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void init() { + final String url = "nmsUrl=https://admin:nexenta@10.1.3.182:8457;volume=cloudstack;storageType=iscsi"; + NexentaUtil.NexentaPluginParameters parameters = NexentaUtil.parseNexentaPluginUrl(url); + //client = new NexentaNmsClient(parameters.getNmsUrl()); + client = mock(NexentaNmsClient.class); + appliance = new NexentaStorAppliance(client, parameters); + } + + @Test + public void testIsIscsiTargetExists() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + + when(client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets")).thenReturn(null); + assertFalse(appliance.isIscsiTargetExists(targetName)); + + when(client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets")).thenReturn(new ListOfIscsiTargetsNmsResponse()); + assertFalse(appliance.isIscsiTargetExists(targetName)); + + final HashMap result = new HashMap(); + + result.put("any", new IscsiTarget("Online", "iSCSI", "any", "0", "-", "iscsit")); + when(client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets")).thenReturn(new ListOfIscsiTargetsNmsResponse(result)); + assertFalse(appliance.isIscsiTargetExists(targetName)); + + result.put(targetName, new IscsiTarget("Online", "iSCSI", targetName, "0", "-", "iscsit")); + when(client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets")).thenReturn(new ListOfIscsiTargetsNmsResponse(result)); + assertTrue(appliance.isIscsiTargetExists(targetName)); + } + + final static String ISCSI_TARGET_ALREADY_CONFIGURED_ERROR = "Unable to create iscsi target\\n iSCSI target %s already configured\\n itadm create-target failed with error " + + "17\\n"; + + @Test + public void testCreateIscsiTarget() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + final CreateIscsiTargetRequestParams p = new CreateIscsiTargetRequestParams(targetName); + + appliance.createIscsiTarget(targetName); + verify(client).execute(NmsResponse.class, "iscsitarget", "create_target", p); + + final String error = String.format(ISCSI_TARGET_ALREADY_CONFIGURED_ERROR, targetName); + when(client.execute(NmsResponse.class, "iscsitarget", "create_target", p)).thenThrow(new CloudRuntimeException(error)); + appliance.createIscsiTarget(targetName); + } + + @Test + public void testCreateIscsiTargetFails() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + final CreateIscsiTargetRequestParams p = new CreateIscsiTargetRequestParams(targetName); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + when(client.execute(NmsResponse.class, "iscsitarget", "create_target", p)).thenThrow(new CloudRuntimeException("any exception")); + appliance.createIscsiTarget(targetName); + } + + @Test + public void testIsIscsiTargetGroupExists() { + final String targetGroup = NexentaStorAppliance.getTargetGroupName("volume1"); + + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroups")).thenReturn(null); + assertFalse(appliance.isIscsiTargetGroupExists(targetGroup)); + + when(client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targetgroups")).thenReturn(new ListOfIscsiTargetsNmsResponse()); + assertFalse(appliance.isIscsiTargetGroupExists(targetGroup)); + + LinkedList result = new LinkedList(); + + result.add("any"); + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroups")).thenReturn(new ListOfStringsNmsResponse(result)); + assertFalse(appliance.isIscsiTargetGroupExists(targetGroup)); + + result.add(targetGroup); + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroups")).thenReturn(new ListOfStringsNmsResponse(result)); + assertTrue(appliance.isIscsiTargetGroupExists(targetGroup)); + } + + final static String ISCSI_TARGET_GROUP_EXISTS_ERROR = "Unable to create targetgroup: stmfadm: %s: already exists\\n"; + + @Test + public void testCreateIscsiTargetGroup() { + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + + appliance.createIscsiTargetGroup(targetGroupName); + verify(client).execute(NmsResponse.class, "stmf", "create_targetgroup", targetGroupName); + + final String error = String.format(ISCSI_TARGET_GROUP_EXISTS_ERROR, targetGroupName); + when(client.execute(NmsResponse.class, "stmf", "create_targetgroup", targetGroupName)).thenThrow(new CloudRuntimeException(error)); + appliance.createIscsiTargetGroup(targetGroupName); + } + + @Test + public void testCreateIscsiTargetGroupFails() { + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + when(client.execute(NmsResponse.class, "stmf", "create_targetgroup", targetGroupName)).thenThrow(new CloudRuntimeException("any exception")); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + appliance.createIscsiTargetGroup(targetGroupName); + } + + @Test + public void testIsMemberOfTargetGroup() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName)).thenReturn(null); + assertFalse(appliance.isTargetMemberOfTargetGroup(targetGroupName, targetName)); + + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName)).thenReturn(new ListOfStringsNmsResponse()); + assertFalse(appliance.isTargetMemberOfTargetGroup(targetGroupName, targetName)); + + LinkedList result = new LinkedList(); + + result.add("any"); + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName)).thenReturn(new ListOfStringsNmsResponse(result)); + assertFalse(appliance.isTargetMemberOfTargetGroup(targetGroupName, targetName)); + + result.add(targetName); + when(client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName)).thenReturn(new ListOfStringsNmsResponse(result)); + assertTrue(appliance.isTargetMemberOfTargetGroup(targetGroupName, targetName)); + } + + @Test + public void testAddTargetGroupMember() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + + appliance.addTargetGroupMember(targetGroupName, targetName); + verify(client).execute(NmsResponse.class, "stmf", "add_targetgroup_member", targetGroupName, targetName); + + String error = String.format(ISCSI_TARGET_ALREADY_EXISTS_IN_TARGET_GROUP_ERROR, targetName); + when(client.execute(NmsResponse.class, "stmf", "add_targetgroup_member", targetGroupName, targetName)).thenThrow(new CloudRuntimeException(error)); + appliance.addTargetGroupMember(targetGroupName, targetName); + } + + final static String ISCSI_TARGET_ALREADY_EXISTS_IN_TARGET_GROUP_ERROR = "Unable to add member to targetgroup: stmfadm: %s: already exists\\n"; + + @Test + public void testAddTargetGroupMemberFails() { + final String targetName = NexentaStorAppliance.getTargetName("volume1"); + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + + when(client.execute(NmsResponse.class, "stmf", "add_targetgroup_member", targetGroupName, targetName)).thenThrow(new CloudRuntimeException("any exception")); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + appliance.addTargetGroupMember(targetGroupName, targetName); + } + + @Test + public void testIsLuExists() { + final String volumeName = appliance.getVolumeName("volume1"); + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", volumeName)).thenReturn(null); + assertFalse(appliance.isLuExists(volumeName)); + + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", volumeName)).thenReturn(new IntegerNmsResponse(0)); + assertFalse(appliance.isLuExists(volumeName)); + + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", volumeName)).thenReturn(new IntegerNmsResponse(1)); + assertTrue(appliance.isLuExists(volumeName)); + + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", volumeName)).thenThrow(new CloudRuntimeException("does not exist")); + assertFalse(appliance.isLuExists(volumeName)); + } + + @Test + public void testIsLuExistsFails() { + final String volumeName = appliance.getVolumeName("volume1"); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", volumeName)).thenThrow(new CloudRuntimeException("any exception")); + assertTrue(appliance.isLuExists(volumeName)); + } + + final static String CREATE_LU_IN_USE_ERROR = "Unable to create lu with " + + "zvol '%s':\\n stmfadm: filename /dev/zvol/rdsk/%s: in use\\n"; + + @Test + public void testCreateLu() { + final String luName = appliance.getVolumeName("volume1"); + final LuParams p = new LuParams(); + + appliance.createLu(luName); + verify(client).execute(NmsResponse.class, "scsidisk", "create_lu", luName, p); + + String error = String.format(CREATE_LU_IN_USE_ERROR, luName, luName); + when(client.execute(NmsResponse.class, "scsidisk", "create_lu", luName, p)).thenThrow(new CloudRuntimeException(error)); + appliance.createLu(luName); + } + + @Test + public void testCreateLuFails() { + final String luName = appliance.getVolumeName("volume1"); + when(client.execute(NmsResponse.class, "scsidisk", "create_lu", luName, new LuParams())).thenThrow(new CloudRuntimeException("any exception")); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + appliance.createLu(luName); + } + + final static String ZVOL_DOES_NOT_EXISTS_ERROR = "Zvol '%s' does not exist"; + + @Test + public void testIsLuShared() { + final String luName = appliance.getVolumeName("volume1"); + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName)).thenReturn(null); + assertFalse(appliance.isLuShared(luName)); + + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName)).thenReturn(new IntegerNmsResponse(0)); + assertFalse(appliance.isLuShared(luName)); + + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName)).thenReturn(new IntegerNmsResponse(1)); + assertTrue(appliance.isLuShared(luName)); + + final String error = String.format(ZVOL_DOES_NOT_EXISTS_ERROR, luName); + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName)).thenThrow(new CloudRuntimeException(error)); + assertFalse(appliance.isLuShared(luName)); + } + + @Test + public void testIsLuSharedFails() { + final String luName = appliance.getVolumeName("volume1"); + when(client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName)).thenThrow(new CloudRuntimeException("any exception")); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + appliance.isLuShared(luName); + } + + final static String ADD_LUN_MAPPING_ENTRY_ERROR = "(rc: 256) Unable to " + + "add view to zvol '%s':\\n add-view: view already exists\\n"; + + @Test + public void testAddLuMappingEntry() { + final String luName = appliance.getVolumeName("volume1"); + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + final MappingEntry mappingEntry = new MappingEntry(targetGroupName, "0"); + appliance.addLuMappingEntry(luName, targetGroupName); + verify(client).execute(AddMappingEntryNmsResponse.class, "scsidisk", "add_lun_mapping_entry", luName, mappingEntry); + + String error = String.format(ADD_LUN_MAPPING_ENTRY_ERROR, luName); + when(client.execute(AddMappingEntryNmsResponse.class, "scsidisk", "add_lun_mapping_entry", luName, mappingEntry)).thenThrow(new CloudRuntimeException(error)); + appliance.addLuMappingEntry(luName, targetGroupName); + } + + @Test + public void testAddLuMappingEntryTest() { + final String luName = appliance.getVolumeName("volume1"); + final String targetGroupName = NexentaStorAppliance.getTargetGroupName("volume1"); + final MappingEntry mappingEntry = new MappingEntry(targetGroupName, "0"); + when(client.execute(AddMappingEntryNmsResponse.class, "scsidisk", "add_lun_mapping_entry", luName, mappingEntry)).thenThrow(new CloudRuntimeException("any exception")); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + appliance.addLuMappingEntry(luName, targetGroupName); + } + + @Test + public void testCreateIscsiVolume() { + final String volumeName = "volume1"; + final Long volumeSize = Long.valueOf(1); + appliance.createIscsiVolume(volumeName, volumeSize); + } + + @Test + public void testDeleteIscsiVolume() { + final String volumeName = appliance.getVolumeName("volume1"); + appliance.deleteIscsiVolume(volumeName); + verify(client).execute(NmsResponse.class, "zvol", "destroy", volumeName, ""); + + when(client.execute(NmsResponse.class, "zvol", "destroy", volumeName, "")).thenThrow(new CloudRuntimeException(String.format("Zvol '%s' does not exist", volumeName))); + appliance.deleteIscsiVolume(volumeName); + } + + @Test + public void testDeleteIscsiVolumeFails() { + final String volumeName = appliance.getVolumeName("volume1"); + exception.expect(CloudRuntimeException.class); + exception.expectMessage("any exception"); + when(client.execute(NmsResponse.class, "zvol", "destroy", volumeName, "")).thenThrow(new CloudRuntimeException("any exception")); + appliance.deleteIscsiVolume(volumeName); + } +} diff --git a/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaUtilTest.java b/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaUtilTest.java new file mode 100644 index 00000000000..5968266dd7d --- /dev/null +++ b/plugins/storage/volume/nexenta/test/org/apache/cloudstack/storage/datastore/util/NexentaUtilTest.java @@ -0,0 +1,117 @@ +package org.apache.cloudstack.storage.datastore.util; + +import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertEquals; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import com.cloud.storage.Storage; + +@RunWith(JUnit4.class) +public class NexentaUtilTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testParseNmsUrl() { + NexentaNmsUrl c; + + c = NexentaUtil.parseNmsUrl("auto://192.168.1.1/"); + assertEquals(c.toString(), "auto://admin:nexenta@192.168.1.1:2000/rest/nms/"); + assertEquals(c.getSchema(), "http"); + + c = NexentaUtil.parseNmsUrl("http://192.168.1.1/"); + assertEquals(c.toString(), "http://admin:nexenta@192.168.1.1:2000/rest/nms/"); + + c = NexentaUtil.parseNmsUrl("http://192.168.1.1:8080"); + assertEquals(c.toString(), "http://admin:nexenta@192.168.1.1:8080/rest/nms/"); + + c = NexentaUtil.parseNmsUrl("https://root@192.168.1.1:8080"); + assertEquals(c.toString(), "https://root:nexenta@192.168.1.1:8080/rest/nms/"); + + c = NexentaUtil.parseNmsUrl("https://root:password@192.168.1.1:8080"); + assertEquals(c.toString(), "https://root:password@192.168.1.1:8080/rest/nms/"); + } + + @Test + public void testGetStorageType() { + assertEquals(NexentaUtil.getStorageType("iscsi"), Storage.StoragePoolType.Iscsi); + assertEquals(NexentaUtil.getStorageType("nfs"), Storage.StoragePoolType.NetworkFilesystem); + assertEquals(NexentaUtil.getStorageType("any"), Storage.StoragePoolType.Iscsi); + } + + @Test + public void testParseNexentaPluginUrl() { + String url = "nmsUrl=http://admin:nexenta@192.168.1.1:2000;"; + + NexentaUtil.NexentaPluginParameters parameters; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getNmsUrl().toString(), "http://admin:nexenta@192.168.1.1:2000/rest/nms/"); + assertNull(parameters.getVolume()); + assertEquals(parameters.getStorageType(), Storage.StoragePoolType.Iscsi); + assertEquals(parameters.getStorageHost(), "192.168.1.1"); + assertEquals((int) parameters.getStoragePort(), NexentaUtil.DEFAULT_ISCSI_TARGET_PORTAL_PORT); + assertNull(parameters.getStoragePath()); + assertEquals((boolean) parameters.getSparseVolumes(), false); + assertEquals(parameters.getVolumeBlockSize(), "8K"); + + url += "volume=cloudstack"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getNmsUrl().toString(), "http://admin:nexenta@192.168.1.1:2000/rest/nms/"); + assertEquals(parameters.getVolume(), "cloudstack"); + + url += "/;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getVolume(), "cloudstack"); + + url += "storageType="; + parameters = NexentaUtil.parseNexentaPluginUrl(url + "nfs"); + assertEquals(parameters.getStorageType(), Storage.StoragePoolType.NetworkFilesystem); + assertEquals((int) parameters.getStoragePort(), NexentaUtil.DEFAULT_NFS_PORT); + + parameters = NexentaUtil.parseNexentaPluginUrl(url + "iscsi"); + assertEquals(parameters.getStorageType(), Storage.StoragePoolType.Iscsi); + assertEquals((int) parameters.getStoragePort(), NexentaUtil.DEFAULT_ISCSI_TARGET_PORTAL_PORT); + + url += "nfs;storageHost=192.168.1.2;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getStorageHost(), "192.168.1.2"); + + url += "storagePort=3000;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals((int) parameters.getStoragePort(), 3000); + + url += "storagePath=/volumes/cloudstack;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getStoragePath(), "/volumes/cloudstack"); + + url += "sparseVolumes=true;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getSparseVolumes(), Boolean.TRUE); + + url += "volumeBlockSize=128K;"; + parameters = NexentaUtil.parseNexentaPluginUrl(url); + assertEquals(parameters.getVolumeBlockSize(), "128K"); + + url += "unknownParameter=value;"; // NOTE: exception should not be raised + parameters = NexentaUtil.parseNexentaPluginUrl(url); + + assertEquals(parameters.getNmsUrl().toString(), "http://admin:nexenta@192.168.1.1:2000/rest/nms/"); + assertEquals(parameters.getVolume(), "cloudstack"); + assertEquals(parameters.getStorageType(), Storage.StoragePoolType.NetworkFilesystem); + assertEquals(parameters.getStorageHost(), "192.168.1.2"); + assertEquals((int) parameters.getStoragePort(), 3000); + assertEquals(parameters.getStoragePath(), "/volumes/cloudstack"); + assertEquals(parameters.getSparseVolumes(), Boolean.TRUE); + assertEquals(parameters.getVolumeBlockSize(), "128K"); + + exception.expect(RuntimeException.class); + exception.expectMessage("Invalid URL format"); + + NexentaUtil.parseNexentaPluginUrl(url + "invalidParameter"); + } +}