diff --git a/client/pom.xml b/client/pom.xml index b9cbae91b7a..aae0006174e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -97,6 +97,11 @@ cloud-plugin-storage-volume-linstor ${project.version} + + org.apache.cloudstack + cloud-plugin-storage-volume-storpool + ${project.version} + org.apache.cloudstack cloud-server @@ -755,6 +760,12 @@ bcpkix-jdk15on false ${project.build.directory}/lib + + + org.apache.cloudstack + cloud-plugin-storage-volume-storpool + false + ${project.build.directory}/lib org.bouncycastle @@ -799,6 +810,7 @@ org.bouncycastle:bcpkix-jdk15on org.bouncycastle:bctls-jdk15on mysql:mysql-connector-java + org.apache.cloudstack:cloud-plugin-storage-volume-storpool diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/CopyCmdAnswer.java b/core/src/main/java/org/apache/cloudstack/storage/command/CopyCmdAnswer.java index 9b581decd66..fef272bfbd9 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/CopyCmdAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/CopyCmdAnswer.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.storage.command; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; import com.cloud.agent.api.to.DataTO; public class CopyCmdAnswer extends Answer { @@ -37,4 +38,8 @@ public class CopyCmdAnswer extends Answer { public CopyCmdAnswer(String errMsg) { super(null, false, errMsg); } + + public CopyCmdAnswer(Command cmd, Exception e) { + super(cmd, e); + } } diff --git a/debian/rules b/debian/rules index ed1559a46df..b590e5d40fd 100755 --- a/debian/rules +++ b/debian/rules @@ -41,6 +41,7 @@ override_dh_auto_install: mkdir $(DESTDIR)/usr/share/$(PACKAGE)-agent/lib install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-$(VERSION).jar $(DESTDIR)/usr/share/$(PACKAGE)-agent/lib/ install -D plugins/hypervisors/kvm/target/dependencies/* $(DESTDIR)/usr/share/$(PACKAGE)-agent/lib/ + install -D plugins/storage/volume/storpool/target/cloud-plugin-storage-volume-storpool-$(VERSION).jar $(DESTDIR)/usr/share/$(PACKAGE)-agent/lib/ install -d -m0755 debian/$(PACKAGE)-agent/lib/systemd/system install -m0644 packaging/systemd/$(PACKAGE)-agent.service debian/$(PACKAGE)-agent/lib/systemd/system/$(PACKAGE)-agent.service diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java index 622dda31f98..540d4f6673a 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/PrimaryDataStoreDriver.java @@ -103,4 +103,33 @@ public interface PrimaryDataStoreDriver extends DataStoreDriver { * returns true if the host can access the storage pool */ boolean canHostAccessStoragePool(Host host, StoragePool pool); + + /** + * Used by storage pools which want to keep VMs' information + * @return true if additional VM info is needed (intended for storage pools). + */ + boolean isVmInfoNeeded(); + + /** + * Provides additional info for a VM (intended for storage pools). + * E.g. the storage pool may want to keep/delete information if the volume is attached/detached to any VM. + * @param vmId The ID of the virtual machine + * @param volumeId the ID of the volume + */ + void provideVmInfo(long vmId, long volumeId); + + /** + * Returns true if the storage have to know about the VM's tags (intended for storage pools). + * @param tagKey The name of the tag + * @return true if the storage have to know about the VM's tags + */ + boolean isVmTagsNeeded(String tagKey); + + /** + * Provide VM's tags to storage (intended for storage pools). + * @param vmId The ID of the virtual machine + * @param volumeId The ID of the volume + * @param tagValue The value of the VM's tag + */ + void provideVmTags(long vmId, long volumeId, String tagValue); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/StorageStrategyFactory.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/StorageStrategyFactory.java index eac9a313278..e309b9842be 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/StorageStrategyFactory.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/StorageStrategyFactory.java @@ -39,9 +39,9 @@ public interface StorageStrategyFactory { /** * Used only for KVM hypervisors when allocating a VM snapshot * @param vmId the ID of the virtual machine + * @param rootPoolId volume pool ID * @param snapshotMemory for VM snapshots with memory * @return VMSnapshotStrategy */ VMSnapshotStrategy getVmSnapshotStrategy(Long vmId, Long rootPoolId, boolean snapshotMemory); - } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java index 223229b5ee8..6372aa3bd94 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VMSnapshotStrategy.java @@ -30,7 +30,7 @@ public interface VMSnapshotStrategy { StrategyPriority canHandle(VMSnapshot vmSnapshot); /** - * Used only for KVM hypervisors when allocating a VM snapshot + * Verifies if the strategy can handle the VM snapshot. This method is used only for KVM hypervisors when allocating a VM snapshot. * @param vmId the ID of the virtual machine * @param snapshotMemory for VM snapshots with memory * @return StrategyPriority diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index c647b11d22c..90f6ccd1f9b 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -52,8 +52,8 @@ import com.cloud.host.dao.HostDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSHypervisorVO; import com.cloud.storage.GuestOSVO; -import com.cloud.storage.VolumeVO; import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; diff --git a/packaging/centos7/cloud.spec b/packaging/centos7/cloud.spec index d8ecf662c63..4288463d2eb 100644 --- a/packaging/centos7/cloud.spec +++ b/packaging/centos7/cloud.spec @@ -351,6 +351,7 @@ install -D agent/target/transformed/cloudstack-agent-profile.sh ${RPM_BUILD_ROOT install -D agent/target/transformed/cloudstack-agent.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-agent install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%name-agent/lib/cloud-plugin-hypervisor-kvm-%{_maventag}.jar cp plugins/hypervisors/kvm/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/storpool/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib # Usage server mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage diff --git a/packaging/centos8/cloud.spec b/packaging/centos8/cloud.spec index e86a98781bf..099ad59b3d5 100644 --- a/packaging/centos8/cloud.spec +++ b/packaging/centos8/cloud.spec @@ -344,6 +344,7 @@ install -D agent/target/transformed/cloudstack-agent-profile.sh ${RPM_BUILD_ROOT install -D agent/target/transformed/cloudstack-agent.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-agent install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%name-agent/lib/cloud-plugin-hypervisor-kvm-%{_maventag}.jar cp plugins/hypervisors/kvm/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/storpool/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib # Usage server mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage diff --git a/packaging/suse15/cloud.spec b/packaging/suse15/cloud.spec index d6be798e095..bf6ef667109 100644 --- a/packaging/suse15/cloud.spec +++ b/packaging/suse15/cloud.spec @@ -346,6 +346,7 @@ install -D agent/target/transformed/cloudstack-agent-profile.sh ${RPM_BUILD_ROOT install -D agent/target/transformed/cloudstack-agent.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-agent install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%name-agent/lib/cloud-plugin-hypervisor-kvm-%{_maventag}.jar cp plugins/hypervisors/kvm/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/storpool/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib # Usage server mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage diff --git a/plugins/pom.xml b/plugins/pom.xml index 04c94a22f91..961884faac6 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -123,6 +123,8 @@ storage/volume/solidfire storage/volume/scaleio storage/volume/linstor + storage/volume/storpool + storage-allocators/random @@ -211,4 +213,4 @@ - \ No newline at end of file + diff --git a/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/driver/DateraPrimaryDataStoreDriver.java b/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/driver/DateraPrimaryDataStoreDriver.java index 91e73aa4424..0002dbe1c17 100644 --- a/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/driver/DateraPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/datera/src/main/java/org/apache/cloudstack/storage/datastore/driver/DateraPrimaryDataStoreDriver.java @@ -1860,4 +1860,22 @@ public class DateraPrimaryDataStoreDriver implements PrimaryDataStoreDriver { public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java index 56d2c3b7b2f..7c8b5fb22c8 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java @@ -491,4 +491,22 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 320860380ce..2a486a11039 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -765,4 +765,22 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java b/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java index 92f8938060b..84051888c5e 100644 --- a/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/nexenta/src/main/java/org/apache/cloudstack/storage/datastore/driver/NexentaPrimaryDataStoreDriver.java @@ -239,4 +239,22 @@ public class NexentaPrimaryDataStoreDriver implements PrimaryDataStoreDriver { public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/driver/SamplePrimaryDataStoreDriverImpl.java b/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/driver/SamplePrimaryDataStoreDriverImpl.java index a4162772343..732786047c4 100644 --- a/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/driver/SamplePrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/sample/src/main/java/org/apache/cloudstack/storage/datastore/driver/SamplePrimaryDataStoreDriverImpl.java @@ -265,4 +265,22 @@ public class SamplePrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java index 8b8c7e7e3e2..3bb6e0d7e63 100644 --- a/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java +++ b/plugins/storage/volume/scaleio/src/main/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriver.java @@ -947,4 +947,22 @@ public class ScaleIOPrimaryDataStoreDriver implements PrimaryDataStoreDriver { String msg = "SDC not connected on the host: " + host.getId() + ", reconnect the SDC to MDM"; alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_HOST, host.getDataCenterId(), host.getPodId(), "SDC disconnected on host: " + host.getUuid(), msg); } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/driver/SolidFirePrimaryDataStoreDriver.java b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/driver/SolidFirePrimaryDataStoreDriver.java index 6bad1efbb73..702bdc3669a 100644 --- a/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/driver/SolidFirePrimaryDataStoreDriver.java +++ b/plugins/storage/volume/solidfire/src/main/java/org/apache/cloudstack/storage/datastore/driver/SolidFirePrimaryDataStoreDriver.java @@ -1619,4 +1619,22 @@ public class SolidFirePrimaryDataStoreDriver implements PrimaryDataStoreDriver { public boolean canHostAccessStoragePool(Host host, StoragePool pool) { return true; } + + @Override + public boolean isVmInfoNeeded() { + return false; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return false; + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + } } diff --git a/plugins/storage/volume/storpool/README.md b/plugins/storage/volume/storpool/README.md new file mode 100644 index 00000000000..7f710732e08 --- /dev/null +++ b/plugins/storage/volume/storpool/README.md @@ -0,0 +1,344 @@ +# StorPool CloudStack Integration + +## CloudStack Overview + +### Primary and Secondary storage + +Primary storage is associated with a cluster or zone, and it stores the virtual disks for all the VMs running on hosts in that cluster/zone. + +Secondary storage stores the following: +* Templates — OS images that can be used to boot VMs and can include additional configuration information, such as installed applications +* ISO images — disc images containing data or bootable media for operating systems +* Disk volume snapshots — saved copies of VM data which can be used for data recovery or to create new templates + + +### ROOT and DATA volumes + +ROOT volumes correspond to the boot disk of a VM. They are created automatically by CloudStack during VM creation. +ROOT volumes are created based on a system disk offering, corresponding to the service offering the user VM +is based on. We may change the ROOT volume disk offering but only to another system created disk offering. + +DATA volumes correspond to additional disks. These can be created by users and then attached/detached to VMs. +DATA volumes are created based on a user-defined disk offering. + + +## Plugin Organization + +The StorPool plugin consists of two parts: + +### KVM hypervisor plugin patch + +Source directory: ./apache-cloudstack-4.17-src/plugins/hypervisors/kvm + +### StorPool primary storage plugin + +Source directory: ./apache-cloudstack-4.17.0-src/plugins/storage/volume + +There is one plugin for both the CloudStack management and agents, in the hope that having all the source +in one place will ease development and maintenance. The plugin itself though is separated into two mainly +independent parts: + + * ./src/com/... directory tree: agent related classes and commands send from management to agent + * ./src/org/... directory tree: management related classes + +The plugin is intended to be self contained and non-intrusive, thus ideally deploying it would consist of only +dropping the jar file into the appropriate places. This is the reason why all StorPool related communication +(ex. data copying, volume resize) is done with StorPool specific commands even when there is a CloudStack command +that does pretty much the same. + +Note that for the present the StorPool plugin may only be used for a single primary storage cluster; support for +multiple clusters is planned. + + +## Build, Install, Setup + +### Build + +Go to the source directory and run: + + mvn -Pdeveloper -DskipTests install + +The resulting jar file is located in the target/ subdirectory. + +Note: checkstyle errors: before compilation a code style check is performed; if this fails compilation is aborted. +In short: no trailing whitespace, indent using 4 spaces, not tabs, comment-out or remove unused imports. + +Note: Need to build both the KVM plugin and the StorPool plugin proper. + +### Install + +#### StorPool primary storage plugin + +For each CloudStack management host: + +```bash +scp ./target/cloud-plugin-storage-volume-storpool-{version}.jar {MGMT_HOST}:/usr/share/cloudstack-management/lib/ +``` + +For each CloudStack agent host: + +```bash +scp ./target/cloud-plugin-storage-volume-storpool-{version}.jar {AGENT_HOST}:/usr/share/cloudstack-agent/plugins/ +``` + +Note: CloudStack managements/agents services must be restarted after adding the plugin to the respective directories + +Note: Agents should have access to the StorPool management API, since attach and detach operations happens on the agent. +This is a CloudStack design issue, can't do much about it. + +### Setup + +#### Setting up StorPool + +Perform the StorPool installation following the StorPool Installation Guide. + +Create a template to be used by CloudStack. Must set *placeHead*, *placeAll*, *placeTail* and *replication*. +No need to set default volume size because it is determined by the CloudStack disks and services offering. + +#### Setting up a StorPool PRIMARY storage pool in CloudStack + +From the WEB UI, go to Infrastructure -> Primary Storage -> Add Primary Storage + +Scope: select Zone-Wide +Hypervisor: select KVM +Zone: pick appropriate zone. +Name: user specified name + +Protocol: select *SharedMountPoint* +Path: enter */dev/storpool* (required argument, actually not needed in practice). + +Provider: select *StorPool* +Managed: leave unchecked (currently ignored) +Capacity Bytes: used for accounting purposes only. May be more or less than the actual StorPool template capacity. +Capacity IOPS: currently not used (may use for max IOPS limitations on volumes from this pool). +URL: enter SP_API_HTTP=address:port;SP_AUTH_TOKEN=token;SP_TEMPLATE=template_name. At present one template can be used for at most one Storage Pool. + +SP_API_HTTP - address of StorPool Api +SP_AUTH_TOKEN - StorPool's token +SP_TEMPLATE - name of StorPool's template + +Storage Tags: If left blank, the StorPool storage plugin will use the pool name to create a corresponding storage tag. +This storage tag may be used later, when defining service or disk offerings. + + +## Plugin Functionality + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Plugin ActionCloudStack Actionmanagement/agentimpl. details
Create ROOT volume from ISOcreate VM from ISOmanagementcreateVolumeAsync
Create ROOT volume from Templatecreate VM from Templatemanagement + agentcopyAsync (T => T, T => V)
Create DATA volumecreate VolumemanagementcreateVolumeAsync
Attach ROOT/DATA volumestart VM (+attach/detach Volume)agentconnectPhysicalDisk
Detach ROOT/DATA volumestop VMagentdisconnectPhysicalDiskByPath
 Migrate VMagentattach + detach
Delete ROOT volumedestroy VM (expunge)managementdeleteAsync
Delete DATA volumedelete Volume (detached)managementdeleteAsync
Create ROOT/DATA volume snapshotsnapshot volumemanagement + agenttakeSnapshot + copyAsync (S => S)
Create volume from snapshootcreate volume from snapshotmanagement + agent(?)copyAsync (S => V)
Create TEMPLATE from ROOT volumecreate template from volumemanagement + agentcopyAsync (V => T)
Create TEMPLATE from snapshotcreate template from snapshotSECONDARY STORAGE 
Download volumedownload volumemanagement + agentcopyAsync (V => V)
Revert ROOT/DATA volume to snapshotrevert to snapshotmanagementrevertSnapshot
(Live) resize ROOT/DATA volumeresize volumemanagement + agentresize + StorpoolResizeCmd
Delete SNAPSHOT (ROOT/DATA)delete snapshotmanagementStorpoolSnapshotStrategy
Delete TEMPLATEdelete templateagentdeletePhysicalDisk
migrate VM/volumemigrate VM/volume to another storagemanagement/management + agentcopyAsync (V => V)
VM snapshotgroup snapshot of VM's disksmanagementStorpoolVMSnapshotStrategy takeVMSnapshot
revert VM snapshotrevert group snapshot of VM's disksmanagementStorpoolVMSnapshotStrategy revertVMSnapshot
delete VM snapshotdelete group snapshot of VM's disksmanagementStorpoolVMSnapshotStrategy deleteVMSnapshot
VM vc_policy tagvc_policy tag for all disks attached to VMmanagementStorPoolCreateTagsCmd
delete VM vc_policy tagremove vc_policy tag for all disks attached to VMmanagementStorPoolDeleteTagsCmd
+ +>NOTE: When using multicluster for each CloudStack cluster in its settings set the value of StorPool's SP_CLUSTER_ID in "sp.cluster.id". +> + +>NOTE: Secondary storage could be bypassed with Configuration setting "sp.bypass.secondary.storage" set to true.
+In this case only snapshots won't be downloaded to secondary storage. +> + +### Creating template from snapshot + +#### If bypass option is enabled + +The snapshot exists only on PRIMARY (StorPool) storage. From this snapshot it will be created a template on SECONADRY. + +#### If bypass option is disabled + +TODO: Maybe we should not use CloudStack functionality, and to use that one when bypass option is enabled + +This is independent of StorPool as snapshots exist on secondary. + +### Creating ROOT volume from templates + +When creating the first volume based on the given template, if snapshot of the template does not exists on StorPool it will be first downloaded (cached) to PRIMARY storage. +This is mapped to a StorPool snapshot so, creating succecutive volumes from the same template does not incur additional +copying of data to PRIMARY storage. + +This cached snapshot is garbage collected when the original template is deleted from CloudStack. This cleanup is done +by a background task in CloudStack. + +### Creating a ROOT volume from an ISO image + +We just need to create the volume. The ISO installation is handled by CloudStack. + +### Creating a DATA volume + +DATA volumes are created by CloudStack the first time it is attached to a VM. + +### Creating volume from snapshot + +We use the fact that the snapshot already exists on PRIMARY, so no data is copied. We will copy snapshots from SECONDARY to StorPool PRIMARY, +when there is no corresponding StorPool snapshot. + +### Resizing volumes + +We need to send a resize cmd to agent, where the VM the volume is attached to is running, so that +the resize is visible by the VM. + +### Creating snapshots + +The snapshot is first created on the PRIMARY storage (i.e. StorPool), then backed-up on SECONDARY storage +(tested with NFS secondary) if bypass option is not enabled. The original StorPool snapshot is kept, so that creating volumes from the snapshot does not need to copy +the data again to PRIMARY. When the snapshot is deleted from CloudStack so is the corresponding StorPool snapshot. + +Currently snapshots are taken in RAW format. + +### Reverting volume to snapshot + +It's handled by StorPool + +### Migrating volumes to other Storage pools + +Tested with storage pools on NFS only. + +### Virtual Machine Snapshot/Group Snapshot + +StorPool supports consistent snapshots of volumes attached to a virtual machine. + +### BW/IOPS limitations + +Max IOPS are kept in StorPool's volumes with the help of custom service offerings, by adding IOPS limits to the +corresponding system disk offering. + +CloudStack has no way to specify max BW. Do they want to be able to specify max BW only is sufficient. diff --git a/plugins/storage/volume/storpool/pom.xml b/plugins/storage/volume/storpool/pom.xml new file mode 100644 index 00000000000..af8d521133c --- /dev/null +++ b/plugins/storage/volume/storpool/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + cloud-plugin-storage-volume-storpool + Apache CloudStack Plugin - Storage Volume StorPool provider + + org.apache.cloudstack + cloudstack-plugins + 4.17.0.0-SNAPSHOT + ../../../pom.xml + + + + org.apache.cloudstack + cloud-engine-storage-volume + ${project.version} + + + org.apache.cloudstack + cloud-engine-storage-snapshot + ${project.version} + + + org.apache.cloudstack + cloud-plugin-hypervisor-kvm + ${project.version} + + + org.apache.cloudstack + cloud-engine-orchestration + ${project.version} + + + org.apache.commons + commons-collections4 + 4.4 + + + + + + maven-surefire-plugin + + true + + + + integration-test + + test + + + + + + + diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupSnapshotCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupSnapshotCommand.java new file mode 100644 index 00000000000..afdd2c270a0 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupSnapshotCommand.java @@ -0,0 +1,30 @@ +/* + * 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 com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.to.SnapshotObjectTO; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolBackupSnapshotCommand extends StorPoolCopyCommand { + public StorPoolBackupSnapshotCommand(final DataTO srcTO, final DataTO dstTO, final int timeout, final boolean executeInSequence) { + super(srcTO, dstTO, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupTemplateFromSnapshotCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupTemplateFromSnapshotCommand.java new file mode 100644 index 00000000000..d15357e9706 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolBackupTemplateFromSnapshotCommand.java @@ -0,0 +1,30 @@ +/* + * 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 com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.to.TemplateObjectTO; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolBackupTemplateFromSnapshotCommand extends StorPoolCopyCommand { + public StorPoolBackupTemplateFromSnapshotCommand(final DataTO srcTO, final DataTO dstTO, final int timeout, final boolean executeInSequence) { + super(srcTO, dstTO, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyCommand.java new file mode 100644 index 00000000000..09fb1ca1dd2 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyCommand.java @@ -0,0 +1,60 @@ +/* + * 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 com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.command.StorageSubSystemCommand; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolCopyCommand extends StorageSubSystemCommand { + private S sourceTO; + private D destinationTO; + private boolean executeInSequence = false; + + public StorPoolCopyCommand(final DataTO sourceTO, final DataTO destinationTO, final int timeout, final boolean executeInSequence) { + super(); + this.sourceTO = (S)sourceTO; + this.destinationTO = (D)destinationTO; + setWait(timeout); + this.executeInSequence = executeInSequence; + } + + public S getSourceTO() { + return sourceTO; + } + + public D getDestinationTO() { + return destinationTO; + } + + public int getWaitInMillSeconds() { + return getWait() * 1000; + } + + @Override + public boolean executeInSequence() { + return executeInSequence; + } + + @Override + public void setExecuteInSequence(final boolean inSeq) { + executeInSequence = inSeq; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyVolumeToSecondaryCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyVolumeToSecondaryCommand.java new file mode 100644 index 00000000000..9bf8b2eefd2 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolCopyVolumeToSecondaryCommand.java @@ -0,0 +1,30 @@ +/* + * 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 com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolCopyVolumeToSecondaryCommand extends StorPoolCopyCommand { + public StorPoolCopyVolumeToSecondaryCommand(final DataTO srcTO, final DataTO dstTO, final int timeout, final boolean executeInSequence) { + super(srcTO, dstTO, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadTemplateCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadTemplateCommand.java new file mode 100644 index 00000000000..6dc0db2071d --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadTemplateCommand.java @@ -0,0 +1,37 @@ +// +// 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolDownloadTemplateCommand extends StorPoolCopyCommand { + protected String objectType; + public StorPoolDownloadTemplateCommand(final DataTO srcTO, final DataTO dstTO, final int timeout, final boolean executeInSequence, String objectType) { + super(srcTO, dstTO, timeout, executeInSequence); + this.objectType = objectType; + } + + public String getObjectType() { + return objectType; + } + public void setObjectType(String objectType) { + this.objectType = objectType; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadVolumeCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadVolumeCommand.java new file mode 100644 index 00000000000..d670ff36fc8 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolDownloadVolumeCommand.java @@ -0,0 +1,30 @@ +/* + * 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 com.cloud.agent.api.storage; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import com.cloud.agent.api.to.DataTO; + +public class StorPoolDownloadVolumeCommand extends StorPoolCopyCommand { + public StorPoolDownloadVolumeCommand(final DataTO srcTO, final DataTO dstTO, final int timeout, final boolean executeInSequence) { + super(srcTO, dstTO, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolMigrateWithVolumesCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolMigrateWithVolumesCommand.java new file mode 100644 index 00000000000..d002267a486 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolMigrateWithVolumesCommand.java @@ -0,0 +1,55 @@ +/* + * 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 com.cloud.agent.api.storage; + +import java.util.ArrayList; +import java.util.List; + +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.to.VirtualMachineTO; + +public class StorPoolMigrateWithVolumesCommand extends MigrateCommand { + private List migrateDiskInfoList = new ArrayList<>(); + + public StorPoolMigrateWithVolumesCommand() { + super(); + } + + public StorPoolMigrateWithVolumesCommand(String vmName, String destIp, boolean isWindows, VirtualMachineTO vmTO, + boolean executeInSequence) { + super(vmName, destIp, isWindows, vmTO, executeInSequence); + } + + public List getMigrateDiskInfoList() { + return migrateDiskInfoList; + } + + public void setMigrateDiskInfoList(List migrateDiskInfoList) { + this.migrateDiskInfoList = migrateDiskInfoList; + } + + public boolean isMigrateStorageManaged() { + return true; + } + + public boolean isMigrateNonSharedInc() { + return false; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java new file mode 100644 index 00000000000..c27f3f81389 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolAnswer.java @@ -0,0 +1,94 @@ +/* + * 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 com.cloud.agent.api.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.ModifyStoragePoolAnswer; +import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.storage.template.TemplateProp; + +public class StorPoolModifyStoragePoolAnswer extends Answer{ + private StoragePoolInfo poolInfo; + private Map templateInfo; + private String localDatastoreName; + private String poolType; + private List datastoreClusterChildren = new ArrayList<>(); + private String clusterId; + + public StorPoolModifyStoragePoolAnswer(StorPoolModifyStoragePoolCommand cmd, long capacityBytes, long availableBytes, Map tInfo, String clusterId) { + super(cmd); + result = true; + poolInfo = new StoragePoolInfo(null, cmd.getPool().getHost(), cmd.getPool().getPath(), cmd.getLocalPath(), cmd.getPool().getType(), capacityBytes, availableBytes); + templateInfo = tInfo; + this.clusterId = clusterId; + } + + public StorPoolModifyStoragePoolAnswer(String errMsg) { + super(null, false, errMsg); + } + + public void setPoolInfo(StoragePoolInfo poolInfo) { + this.poolInfo = poolInfo; + } + + public StoragePoolInfo getPoolInfo() { + return poolInfo; + } + + public void setTemplateInfo(Map templateInfo) { + this.templateInfo = templateInfo; + } + + public Map getTemplateInfo() { + return templateInfo; + } + + public void setLocalDatastoreName(String localDatastoreName) { + this.localDatastoreName = localDatastoreName; + } + + public String getLocalDatastoreName() { + return localDatastoreName; + } + + public String getPoolType() { + return poolType; + } + + public void setPoolType(String poolType) { + this.poolType = poolType; + } + + public List getDatastoreClusterChildren() { + return datastoreClusterChildren; + } + + public void setDatastoreClusterChildren(List datastoreClusterChildren) { + this.datastoreClusterChildren = datastoreClusterChildren; + } + + public String getClusterId() { + return clusterId; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolCommand.java new file mode 100644 index 00000000000..a72a4620de1 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolModifyStoragePoolCommand.java @@ -0,0 +1,36 @@ +/* + * 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.ModifyStoragePoolCommand; +import com.cloud.storage.StoragePool; + +public class StorPoolModifyStoragePoolCommand extends ModifyStoragePoolCommand { + private String volumeName; + + public StorPoolModifyStoragePoolCommand(boolean add, StoragePool pool, String volumeName) { + super(add, pool); + this.volumeName = volumeName; + } + + public String getVolumeName() { + return volumeName; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolResizeVolumeCommand.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolResizeVolumeCommand.java new file mode 100644 index 00000000000..10b40bfeb09 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/agent/api/storage/StorPoolResizeVolumeCommand.java @@ -0,0 +1,39 @@ +/* + * 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.to.StorageFilerTO; + + +public class StorPoolResizeVolumeCommand extends ResizeVolumeCommand { + protected boolean isAttached; + protected StorPoolResizeVolumeCommand() { + super(); + } + + public StorPoolResizeVolumeCommand(String path, StorageFilerTO pool, Long currentSize, Long newSize, boolean shrinkOk, String vmInstance, boolean isAttached) { + super(path, pool, currentSize, newSize, shrinkOk, vmInstance); + this.isAttached = isAttached; + } + + public boolean isAttached() { + return isAttached; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupSnapshotCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupSnapshotCommandWrapper.java new file mode 100644 index 00000000000..f83a4292e20 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupSnapshotCommandWrapper.java @@ -0,0 +1,109 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor.SP_LOG; + +import java.io.File; + +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.StorPoolBackupSnapshotCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = StorPoolBackupSnapshotCommand.class) +public final class StorPoolBackupSnapshotCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolBackupSnapshotCommandWrapper.class); + + @Override + public CopyCmdAnswer execute(final StorPoolBackupSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String srcPath = null; + KVMStoragePool secondaryPool = null; + + try { + final SnapshotObjectTO src = cmd.getSourceTO(); + final SnapshotObjectTO dst = cmd.getDestinationTO(); + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + + SP_LOG("StorpoolBackupSnapshotCommandWrapper.execute: src=" + src.getPath() + "dst=" + dst.getPath()); + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "snapshot", src.getPath()); + srcPath = src.getPath(); + + final QemuImgFile srcFile = new QemuImgFile(srcPath, PhysicalDiskFormat.RAW); + + final DataStoreTO dstDataStore = dst.getDataStore(); + if (!(dstDataStore instanceof NfsTO)) { + return new CopyCmdAnswer("Backup Storpool snapshot: Only NFS secondary supported at present!"); + } + + secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl()); + + final String dstDir = secondaryPool.getLocalPath() + File.separator + dst.getPath(); + FileUtils.forceMkdir(new File(dstDir)); + + final String dstPath = dstDir + File.separator + dst.getName(); + final QemuImgFile dstFile = new QemuImgFile(dstPath, PhysicalDiskFormat.QCOW2); + + final QemuImg qemu = new QemuImg(cmd.getWaitInMillSeconds()); + qemu.convert(srcFile, dstFile); + + SP_LOG("StorpoolBackupSnapshotCommandWrapper srcFileFormat=%s, dstFileFormat=%s", srcFile.getFormat(), dstFile.getFormat()); + final File snapFile = new File(dstPath); + final long size = snapFile.exists() ? snapFile.length() : 0; + + final SnapshotObjectTO snapshot = new SnapshotObjectTO(); + snapshot.setPath(dst.getPath() + File.separator + dst.getName()); + snapshot.setPhysicalSize(size); + + return new CopyCmdAnswer(snapshot); + } catch (final Exception e) { + final String error = String.format("Failed to backup snapshot with id [%s] with a pool %s, due to %s", cmd.getSourceTO().getId(), cmd.getSourceTO().getDataStore().getUuid(), e.getMessage()); + SP_LOG(error); + s_logger.debug(error); + return new CopyCmdAnswer(cmd, e); + } finally { + if (srcPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "snapshot", srcPath); + } + + if (secondaryPool != null) { + try { + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupTemplateFromSnapshotCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupTemplateFromSnapshotCommandWrapper.java new file mode 100644 index 00000000000..518cbb8d5e5 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolBackupTemplateFromSnapshotCommandWrapper.java @@ -0,0 +1,161 @@ +/* + * 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor.SP_LOG; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.StorageLayer; +import com.cloud.storage.template.Processor; +import com.cloud.storage.template.Processor.FormatInfo; +import com.cloud.storage.template.QCOW2Processor; +import com.cloud.storage.template.TemplateLocation; +import com.cloud.storage.template.TemplateProp; + +@ResourceWrapper(handles = StorPoolBackupTemplateFromSnapshotCommand.class) +public class StorPoolBackupTemplateFromSnapshotCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolBackupTemplateFromSnapshotCommandWrapper.class); + + @Override + public CopyCmdAnswer execute(final StorPoolBackupTemplateFromSnapshotCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String srcPath = null; + KVMStoragePool secondaryPool = null; + String objectType = cmd.getSourceTO().getObjectType().toString().toLowerCase(); + + try { + final DataTO src = cmd.getSourceTO(); + final TemplateObjectTO dst = cmd.getDestinationTO(); + String name = null; + String volumeFormatExtension = null; + + if (src instanceof SnapshotObjectTO) { + name = ((SnapshotObjectTO) src).getName(); + volumeFormatExtension = ((SnapshotObjectTO) src).getVolume().getFormat().getFileExtension(); + } else if (src instanceof VolumeObjectTO) { + name = ((VolumeObjectTO) src).getName(); + volumeFormatExtension = ((VolumeObjectTO) src).getFormat().getFileExtension(); + } else { + return new CopyCmdAnswer("Backup of a template is not supported for data object: " + src.getObjectType() ); + } + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + StorageLayer storage = libvirtComputingResource.getStorage(); + Processor processor = new QCOW2Processor(); + String _tmpltpp = "template.properties"; + + SP_LOG("StorpoolBackupTemplateFromSnapshotCommandWrapper.execute: src=" + src.getPath() + "dst=" + dst.getPath()); + StorPoolStorageAdaptor.attachOrDetachVolume("attach", objectType, src.getPath()); + srcPath = src.getPath(); + + final QemuImgFile srcFile = new QemuImgFile(srcPath, PhysicalDiskFormat.RAW); + + final DataStoreTO dstDataStore = dst.getDataStore(); + if (!(dstDataStore instanceof NfsTO)) { + return new CopyCmdAnswer("Backup Storpool snapshot: Only NFS secondary supported at present!"); + } + + secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl()); + + final String dstDir = secondaryPool.getLocalPath() + File.separator + dst.getPath(); + FileUtils.forceMkdir(new File(dstDir)); + + String nameWithExtension = name + "." + volumeFormatExtension; + + final String dstPath = dstDir + File.separator + nameWithExtension; + final QemuImgFile dstFile = new QemuImgFile(dstPath, PhysicalDiskFormat.QCOW2); + + final QemuImg qemu = new QemuImg(cmd.getWaitInMillSeconds()); + qemu.convert(srcFile, dstFile); + + storage.create(dstDir, _tmpltpp); + String metaFileName = dstDir + File.separator + _tmpltpp; + File metaFile = new File(metaFileName); + + try ( FileWriter writer = new FileWriter(metaFile); + BufferedWriter bufferWriter = new BufferedWriter(writer);) { + bufferWriter.write("uniquename=" + dst.getName()); + bufferWriter.write("\n"); + bufferWriter.write("filename=" + nameWithExtension); + } + Map params = new HashMap(); + params.put(StorageLayer.InstanceConfigKey, storage); + + processor.configure("template processor", params); + + FormatInfo info = processor.process(dstDir, null, name); + TemplateLocation loc = new TemplateLocation(storage, dstDir); + loc.create(1, true, dst.getName()); + loc.addFormat(info); + loc.save(); + + TemplateProp prop = loc.getTemplateInfo(); + final TemplateObjectTO template = new TemplateObjectTO(); + template.setPath(dst.getPath() + File.separator + nameWithExtension); + template.setFormat(ImageFormat.QCOW2); + template.setSize(prop.getSize()); + template.setPhysicalSize(prop.getPhysicalSize()); + + return new CopyCmdAnswer(template); + } catch (final Exception e) { + final String error = "failed to backup snapshot: " + e.getMessage(); + SP_LOG(error); + s_logger.debug(error); + return new CopyCmdAnswer(cmd, e); + } finally { + if (srcPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", objectType, srcPath); + } + + if (secondaryPool != null) { + try { + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolCopyVolumeToSecondaryCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolCopyVolumeToSecondaryCommandWrapper.java new file mode 100644 index 00000000000..29e8979bd88 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolCopyVolumeToSecondaryCommandWrapper.java @@ -0,0 +1,124 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor.SP_LOG; + +import java.io.File; + +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.StorPoolCopyVolumeToSecondaryCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = StorPoolCopyVolumeToSecondaryCommand.class) +public final class StorPoolCopyVolumeToSecondaryCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolCopyVolumeToSecondaryCommandWrapper.class); + + @Override + public CopyCmdAnswer execute(final StorPoolCopyVolumeToSecondaryCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String srcPath = null; + KVMStoragePool secondaryPool = null; + + try { + final VolumeObjectTO src = cmd.getSourceTO(); + final VolumeObjectTO dst = cmd.getDestinationTO(); + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + final String destVolumePath = dst.getPath(); + + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: src=" + src.getPath() + "dst=" + dst.getPath()); + + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "snapshot", src.getPath()); + srcPath = src.getPath(); + + final QemuImgFile srcFile = new QemuImgFile(srcPath, PhysicalDiskFormat.RAW); + + final DataStoreTO dstDataStore = dst.getDataStore(); + + final KVMStoragePoolManager poolMgr = libvirtComputingResource.getStoragePoolMgr(); + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: KVMStoragePoolManager " + poolMgr); + KVMStoragePool destPool; + if( dstDataStore instanceof NfsTO ) { + destPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl()); + destPool.createFolder(destVolumePath); + storagePoolMgr.deleteStoragePool(destPool.getType(), destPool.getUuid()); + destPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl() + File.separator + destVolumePath); + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: Nfs destPool=%s ",destPool); + } else if( dstDataStore instanceof PrimaryDataStoreTO ) { + PrimaryDataStoreTO primaryDst = (PrimaryDataStoreTO)dstDataStore; + destPool = poolMgr.getStoragePool(primaryDst.getPoolType(), dstDataStore.getUuid()); + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: not Nfs destPool=%s " ,destPool); + } else { + return new CopyCmdAnswer("Don't know how to copy to " + dstDataStore.getClass().getName() + ", " + dst.getPath() ); + } + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: dstName=%s, dstProvisioningType=%s, srcSize=%s, dstUUID=%s, srcUUID=%s " ,dst.getName(), dst.getProvisioningType(), src.getSize(),dst.getUuid(), src.getUuid()); + + KVMPhysicalDisk newDisk = destPool.createPhysicalDisk(dst.getUuid(), dst.getProvisioningType(), src.getSize()); + SP_LOG("NewDisk path=%s, uuid=%s ", newDisk.getPath(), dst.getUuid()); + String destPath = newDisk.getPath(); + newDisk.setPath(dst.getUuid()); + + PhysicalDiskFormat destFormat = newDisk.getFormat(); + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: KVMPhysicalDisk name=%s, format=%s, path=%s, destinationPath=%s " , newDisk.getName(), newDisk.getFormat(), newDisk.getPath(), destPath); + QemuImgFile destFile = new QemuImgFile(destPath, destFormat); + QemuImg qemu = new QemuImg(cmd.getWaitInMillSeconds()); + qemu.convert(srcFile, destFile); + + final File file = new File(destPath); + final long size = file.exists() ? file.length() : 0; + dst.setPath(destVolumePath + File.separator + dst.getUuid()); + dst.setSize(size); + + return new CopyCmdAnswer(dst); + } catch (final Exception e) { + final String error = "Failed to copy volume to secondary storage: " + e.getMessage(); + s_logger.debug(error); + return new CopyCmdAnswer(error); + } finally { + if (srcPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "snapshot", srcPath); + } + + if (secondaryPool != null) { + try { + SP_LOG("StorpoolCopyVolumeToSecondaryCommandWrapper.execute: secondaryPool=%s " , secondaryPool); + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadTemplateCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadTemplateCommandWrapper.java new file mode 100644 index 00000000000..07b08a1da2b --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadTemplateCommandWrapper.java @@ -0,0 +1,134 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor.SP_LOG; + +import java.io.File; +import java.util.List; + +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.StorPoolDownloadTemplateCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = StorPoolDownloadTemplateCommand.class) +public final class StorPoolDownloadTemplateCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolDownloadTemplateCommandWrapper.class); + + @Override + public CopyCmdAnswer execute(final StorPoolDownloadTemplateCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String dstPath = null; + KVMStoragePool secondaryPool = null; + DataTO src = cmd.getSourceTO(); + DataTO dst = cmd.getDestinationTO(); + + try { + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + SP_LOG("StorpoolDownloadTemplateCommandWrapper.execute: src=" + src.getPath() + " dst=" + dst.getPath()); + + final DataStoreTO srcDataStore = src.getDataStore(); + if (!(srcDataStore instanceof NfsTO)) { + return new CopyCmdAnswer("Download template to Storpool: Only NFS secondary supported at present!"); + } + + final NfsTO nfsImageStore = (NfsTO)srcDataStore; + final String tmplturl = nfsImageStore.getUrl() + File.separator + src.getPath(); + final int index = tmplturl.lastIndexOf("/"); + final String mountpoint = tmplturl.substring(0, index); + String tmpltname = null; + if (index < tmplturl.length() - 1) { + tmpltname = tmplturl.substring(index + 1); + } + + secondaryPool = storagePoolMgr.getStoragePoolByURI(mountpoint); + + KVMPhysicalDisk srcDisk = null; + + if (tmpltname == null) { + secondaryPool.refresh(); + final List disks = secondaryPool.listPhysicalDisks(); + if (CollectionUtils.isEmpty(disks)) { + SP_LOG("Failed to get volumes from pool: " + secondaryPool.getUuid()); + return new CopyCmdAnswer("Failed to get volumes from pool: " + secondaryPool.getUuid()); + } + for (final KVMPhysicalDisk disk : disks) { + if (disk.getName().endsWith("qcow2")) { + srcDisk = disk; + break; + } + } + } else { + srcDisk = secondaryPool.getPhysicalDisk(tmpltname); + } + + if (srcDisk == null) { + SP_LOG("Failed to get template from pool: " + secondaryPool.getUuid()); + return new CopyCmdAnswer("Failed to get template from pool: " + secondaryPool.getUuid()); + } + + SP_LOG("got src path: " + srcDisk.getPath() + " srcSize " + srcDisk.getVirtualSize()); + + final QemuImgFile srcFile = new QemuImgFile(srcDisk.getPath(), srcDisk.getFormat()); + + final QemuImg qemu = new QemuImg(cmd.getWaitInMillSeconds()); + StorPoolStorageAdaptor.resize( Long.toString(srcDisk.getVirtualSize()), dst.getPath()); + + dstPath = dst.getPath(); + StorPoolStorageAdaptor.attachOrDetachVolume("attach", cmd.getObjectType(), dstPath); + + final QemuImgFile dstFile = new QemuImgFile(dstPath, PhysicalDiskFormat.RAW); + + qemu.convert(srcFile, dstFile); + return new CopyCmdAnswer(dst); + } catch (final Exception e) { + final String error = "Failed to copy template to primary: " + e.getMessage(); + s_logger.debug(error); + return new CopyCmdAnswer(cmd, e); + } finally { + if (dstPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", cmd.getObjectType(), dstPath); + } + + if (secondaryPool != null) { + try { + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadVolumeCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadVolumeCommandWrapper.java new file mode 100644 index 00000000000..d1a58a4aeb8 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolDownloadVolumeCommandWrapper.java @@ -0,0 +1,162 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor.SP_LOG; + +import java.util.List; + +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +//import java.io.File; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.StorPoolDownloadVolumeCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; + +@ResourceWrapper(handles = StorPoolDownloadVolumeCommand.class) +public final class StorPoolDownloadVolumeCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolDownloadVolumeCommandWrapper.class); + + @Override + public CopyCmdAnswer execute(final StorPoolDownloadVolumeCommand cmd, final LibvirtComputingResource libvirtComputingResource) { + String dstPath = null; + KVMStoragePool secondaryPool = null; + + try { + final VolumeObjectTO src = cmd.getSourceTO(); + final VolumeObjectTO dst = cmd.getDestinationTO(); + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + SP_LOG("StorpoolDownloadVolumeCommandWrapper.execute: src=" + src.getPath() + " srcName=" + src.getName() + " dst=" + dst.getPath()); + + final DataStoreTO srcDataStore = src.getDataStore(); + KVMPhysicalDisk srcDisk = null; + + if(srcDataStore instanceof NfsTO) { + SP_LOG("StorpoolDownloadVolumeCommandWrapper.execute: srcIsNfsTO"); + + final String tmplturl = srcDataStore.getUrl() + srcDataStore.getPathSeparator() + src.getPath(); + final int index = tmplturl.lastIndexOf("/"); + final String mountpoint = tmplturl.substring(0, index); + String tmpltname = null; + if (index < tmplturl.length() - 1) { + tmpltname = tmplturl.substring(index + 1); + } + + secondaryPool = storagePoolMgr.getStoragePoolByURI(mountpoint); + + if (tmpltname == null) { + secondaryPool.refresh(); + final List disks = secondaryPool.listPhysicalDisks(); + if (disks == null || disks.isEmpty()) { + SP_LOG("Failed to get volumes from pool: " + secondaryPool.getUuid()); + return new CopyCmdAnswer("Failed to get volumes from pool: " + secondaryPool.getUuid()); + } + for (final KVMPhysicalDisk disk : disks) { + if (disk.getName().endsWith("qcow2")) { + srcDisk = disk; + break; + } + } + } else { + srcDisk = secondaryPool.getPhysicalDisk(tmpltname); + } + } else if(srcDataStore instanceof PrimaryDataStoreTO) { + SP_LOG("SrcDisk is Primary Storage"); + PrimaryDataStoreTO primarySrc = (PrimaryDataStoreTO)srcDataStore; + SP_LOG("StorpoolDownloadVolumeCommandWrapper.execute primarySrcPoolType=%s, uuid-%s ", primarySrc.getPoolType(), primarySrc.getUuid()); + final KVMStoragePoolManager poolMgr = libvirtComputingResource.getStoragePoolMgr(); + srcDisk = poolMgr.getPhysicalDisk(primarySrc.getPoolType(), srcDataStore.getUuid(), src.getPath()); + SP_LOG("PhysicalDisk: disk=%s", srcDisk ); + } else { + return new CopyCmdAnswer("Don't know how to copy from " + srcDataStore.getClass().getName() + ", " + src.getPath() ); + } + + if (srcDisk == null) { + SP_LOG("Failed to get src volume"); + return new CopyCmdAnswer("Failed to get src volume"); + } + + SP_LOG("got src path: " + srcDisk.getPath() + " srcSize " + srcDisk.getVirtualSize()); + + String srcPath = null; + boolean isRBDPool = srcDisk.getPool().getType() == StoragePoolType.RBD; + if (isRBDPool) { + KVMStoragePool srcPool = srcDisk.getPool(); + String rbdDestPath = srcPool.getSourceDir() + "/" + srcDisk.getName(); + srcPath = KVMPhysicalDisk.RBDStringBuilder(srcPool.getSourceHost(), + srcPool.getSourcePort(), + srcPool.getAuthUserName(), + srcPool.getAuthSecret(), + rbdDestPath); + } else { + srcPath = srcDisk.getPath(); + } + final QemuImgFile srcFile = new QemuImgFile(srcPath, PhysicalDiskFormat.RAW); + + final QemuImg qemu = new QemuImg(cmd.getWaitInMillSeconds()); + StorPoolStorageAdaptor.resize( Long.toString(srcDisk.getVirtualSize()), dst.getPath()); + + dstPath = dst.getPath(); + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "volume", dstPath); + + final QemuImgFile dstFile = new QemuImgFile(dstPath, srcFile.getFormat()); + SP_LOG("SRC format=%s, DST format=%s",srcFile.getFormat(), dstFile.getFormat()); + qemu.convert(srcFile, dstFile); + SP_LOG("StorpoolDownloadVolumeCommandWrapper VolumeObjectTO format=%s, hypervisor=%s", dst.getFormat(), dst.getHypervisorType()); + if (isRBDPool) { + dst.setFormat(ImageFormat.QCOW2); + } + return new CopyCmdAnswer(dst); + } catch (final Exception e) { + final String error = "Failed to copy volume to primary: " + e.getMessage(); + SP_LOG(error); + s_logger.debug(error); + return new CopyCmdAnswer(cmd, e); + } finally { + if (dstPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "volume", dstPath); + } + + if (secondaryPool != null) { + try { + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java new file mode 100644 index 00000000000..b3579708335 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolModifyStorageCommandWrapper.java @@ -0,0 +1,146 @@ +/* + * 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.StorPoolModifyStoragePoolAnswer; +import com.cloud.agent.api.storage.StorPoolModifyStoragePoolCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.template.TemplateProp; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +@ResourceWrapper(handles = StorPoolModifyStoragePoolCommand.class) +public final class StorPoolModifyStorageCommandWrapper extends CommandWrapper { + private static final Logger log = Logger.getLogger(StorPoolModifyStorageCommandWrapper.class); + + @Override + public Answer execute(final StorPoolModifyStoragePoolCommand command, final LibvirtComputingResource libvirtComputingResource) { + String clusterId = getSpClusterId(); + if (clusterId == null) { + log.debug(String.format("Could not get StorPool cluster id for a command $s", command.getClass())); + return new Answer(command, false, "spNotFound"); + } + try { + String result = attachOrDetachVolume("attach", "volume", command.getVolumeName()); + if (result != null) { + return new Answer(command, false, result); + } + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + final KVMStoragePool storagepool = + storagePoolMgr.createStoragePool(command.getPool().getUuid(), command.getPool().getHost(), command.getPool().getPort(), command.getPool().getPath(), command.getPool() + .getUserInfo(), command.getPool().getType()); + if (storagepool == null) { + log.debug(String.format("Did not find a storage pool [%s]", command.getPool().getId())); + return new Answer(command, false, String.format("Failed to create storage pool [%s]", command.getPool().getId())); + } + + final Map tInfo = new HashMap(); + final StorPoolModifyStoragePoolAnswer answer = new StorPoolModifyStoragePoolAnswer(command, storagepool.getCapacity(), storagepool.getAvailable(), tInfo, clusterId); + + return answer; + } catch (Exception e) { + log.debug(String.format("Could not modify storage due to %s", e.getMessage())); + return new Answer(command, e); + } + } + + private String getSpClusterId() { + Script sc = new Script("storpool_confget", 0, log); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + + String SP_CLUSTER_ID = null; + final String err = sc.execute(parser); + if (err != null) { + final String errMsg = String.format("Could not execute storpool_confget. Error: %s", err); + log.warn(errMsg); + StorPoolStorageAdaptor.SP_LOG("Could not execute storpool_confget. Error: %s", err); + return SP_CLUSTER_ID; + } + + for (String line: parser.getLines().split("\n")) { + String[] toks = line.split("="); + if( toks.length != 2 ) { + continue; + } + if (toks[0].equals("SP_CLUSTER_ID")) { + SP_CLUSTER_ID = toks[1]; + return SP_CLUSTER_ID; + } + } + return SP_CLUSTER_ID; + } + + public String attachOrDetachVolume(String command, String type, String volumeUuid) { + final String name = StorPoolStorageAdaptor.getVolumeNameFromPath(volumeUuid, true); + if (name == null) { + return null; + } + + String err = null; + Script sc = new Script("storpool", 300000, log); + sc.add("-M"); + sc.add("-j"); + sc.add(command); + sc.add(type, name); + sc.add("here"); + sc.add("onRemoteAttached"); + sc.add("export"); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + + String res = sc.execute(parser); + + if (res != null) { + if (!res.equals(Script.ERR_TIMEOUT)) { + try { + Set> obj2 = new JsonParser().parse(res).getAsJsonObject().entrySet(); + for (Entry entry : obj2) { + if (entry.getKey().equals("error")) { + res = entry.getValue().getAsJsonObject().get("name").getAsString(); + } + } + } catch (Exception e) { + } + } + + err = String.format("Unable to %s volume %s. Error: %s", command, name, res); + } + + if (err != null) { + log.warn(err); + } + return res; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolResizeVolumeCommandWrapper.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolResizeVolumeCommandWrapper.java new file mode 100644 index 00000000000..9f9277768e6 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/StorPoolResizeVolumeCommandWrapper.java @@ -0,0 +1,98 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.log4j.Logger; + +import com.cloud.agent.api.storage.ResizeVolumeAnswer; +import com.cloud.agent.api.storage.StorPoolResizeVolumeCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + + +@ResourceWrapper(handles = StorPoolResizeVolumeCommand.class) +public final class StorPoolResizeVolumeCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(StorPoolResizeVolumeCommandWrapper.class); + + @Override + public ResizeVolumeAnswer execute(final StorPoolResizeVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { + final String volid = command.getPath(); + final long newSize = command.getNewSize(); + final long currentSize = command.getCurrentSize(); + final String vmInstanceName = command.getInstanceName(); + final boolean shrinkOk = command.getShrinkOk(); + final StorageFilerTO spool = command.getPool(); + String volPath = null; + + if (currentSize == newSize) { + // nothing to do + s_logger.info("No need to resize volume: current size " + currentSize + " is same as new size " + newSize); + return new ResizeVolumeAnswer(command, true, "success", currentSize); + } + + try { + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + KVMStoragePool pool = storagePoolMgr.getStoragePool(spool.getType(), spool.getUuid()); + + final KVMPhysicalDisk vol = pool.getPhysicalDisk(volid); + final String path = vol.getPath(); + volPath = path; + if (!command.isAttached()) { + StorPoolStorageAdaptor.attachOrDetachVolume("attach", "volume", path); + } + final Script resizecmd = new Script(libvirtComputingResource.getResizeVolumePath(), libvirtComputingResource.getCmdsTimeout(), s_logger); + resizecmd.add("-s", String.valueOf(newSize)); + resizecmd.add("-c", String.valueOf(currentSize)); + resizecmd.add("-p", path); + resizecmd.add("-t", "NOTIFYONLY"); + resizecmd.add("-r", String.valueOf(shrinkOk)); + resizecmd.add("-v", vmInstanceName); + final String result = resizecmd.execute(); + + if (result != null) { + return new ResizeVolumeAnswer(command, true, "Resize succeeded, but need reboot to notify guest"); + } + + /* fetch new size as seen from libvirt, don't want to assume anything */ + pool = storagePoolMgr.getStoragePool(spool.getType(), spool.getUuid()); + pool.refresh(); + + final long finalSize = pool.getPhysicalDisk(volid).getVirtualSize(); + s_logger.debug("after resize, size reports as " + finalSize + ", requested " + newSize); + return new ResizeVolumeAnswer(command, true, "success", finalSize); + } catch (final Exception e) { + final String error = "Failed to resize volume: " + e.getMessage(); + s_logger.debug(error); + return new ResizeVolumeAnswer(command, false, error); + } finally { + if (!command.isAttached() && volPath != null) { + StorPoolStorageAdaptor.attachOrDetachVolume("detach", "volume", volPath); + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java new file mode 100644 index 00000000000..0373e3bdf82 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java @@ -0,0 +1,388 @@ +// 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 com.cloud.hypervisor.kvm.storage; + + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.to.DiskTO; +import com.cloud.storage.Storage; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.ProvisioningType; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; + +@StorageAdaptorInfo(storagePoolType=StoragePoolType.SharedMountPoint) +public class StorPoolStorageAdaptor implements StorageAdaptor { + public static void SP_LOG(String fmt, Object... args) { + try (PrintWriter spLogFile = new PrintWriter(new BufferedWriter(new FileWriter("/var/log/cloudstack/agent/storpool-agent.log", true)))) { + final String line = String.format(fmt, args); + String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,ms").format(Calendar.getInstance().getTime()); + spLogFile.println(timeStamp +" "+line); + spLogFile.flush(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final Logger log = Logger.getLogger(StorPoolStorageAdaptor.class); + + private static final Map storageUuidToStoragePool = new HashMap(); + + @Override + public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, StoragePoolType storagePoolType) { + SP_LOG("StorpooolStorageAdaptor.createStoragePool: uuid=%s, host=%s:%d, path=%s, userInfo=%s, type=%s", uuid, host, port, path, userInfo, storagePoolType); + + StorPoolStoragePool storagePool = new StorPoolStoragePool(uuid, host, port, storagePoolType, this); + storageUuidToStoragePool.put(uuid, storagePool); + return storagePool; + } + + @Override + public KVMStoragePool getStoragePool(String uuid) { + SP_LOG("StorpooolStorageAdaptor.getStoragePool: uuid=%s", uuid); + return storageUuidToStoragePool.get(uuid); + } + + @Override + public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { + SP_LOG("StorpooolStorageAdaptor.getStoragePool: uuid=%s, refresh=%s", uuid, refreshInfo); + return storageUuidToStoragePool.get(uuid); + } + + @Override + public boolean deleteStoragePool(String uuid) { + SP_LOG("StorpooolStorageAdaptor.deleteStoragePool: uuid=%s", uuid); + return storageUuidToStoragePool.remove(uuid) != null; + } + + @Override + public boolean deleteStoragePool(KVMStoragePool pool) { + SP_LOG("StorpooolStorageAdaptor.deleteStoragePool: uuid=%s", pool.getUuid()); + return deleteStoragePool(pool.getUuid()); + } + + private static long getDeviceSize(final String devPath) { + SP_LOG("StorpooolStorageAdaptor.getDeviceSize: path=%s", devPath); + + if (getVolumeNameFromPath(devPath, true) == null) { + return 0; + } + File file = new File(devPath); + if (!file.exists()) { + return 0; + } + Script sc = new Script("blockdev", 0, log); + sc.add("--getsize64", devPath); + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + + String res = sc.execute(parser); + if (res != null) { + SP_LOG("Unable to retrieve device size for %s. Res: %s", devPath, res); + + log.debug(String.format("Unable to retrieve device size for %s. Res: %s", devPath, res)); + return 0; + } + + return Long.parseLong(parser.getLine()); + } + + private static boolean waitForDeviceSymlink(String devPath) { + final int numTries = 10; + final int sleepTime = 100; + + for(int i = 0; i < numTries; i++) { + if (getDeviceSize(devPath) != 0) { + return true; + } else { + try { + Thread.sleep(sleepTime); + } catch (Exception ex) { + // don't do anything + } + } + } + return false; + } + + public static String getVolumeNameFromPath(final String volumeUuid, boolean tildeNeeded) { + if (volumeUuid.startsWith("/dev/storpool/")) { + return volumeUuid.split("/")[3]; + } else if (volumeUuid.startsWith("/dev/storpool-byid/")) { + return tildeNeeded ? "~" + volumeUuid.split("/")[3] : volumeUuid.split("/")[3]; + } + + return null; + } + + public static boolean attachOrDetachVolume(String command, String type, String volumeUuid) { + final String name = getVolumeNameFromPath(volumeUuid, true); + if (name == null) { + return false; + } + + SP_LOG("StorpooolStorageAdaptor.attachOrDetachVolume: cmd=%s, type=%s, uuid=%s, name=%s", command, type, volumeUuid, name); + + final int numTries = 10; + final int sleepTime = 1000; + String err = null; + + for(int i = 0; i < numTries; i++) { + Script sc = new Script("storpool", 0, log); + sc.add("-M"); + sc.add(command); + sc.add(type, name); + sc.add("here"); + if (command.equals("attach")) { + sc.add("onRemoteAttached"); + sc.add("export"); + } + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + + String res = sc.execute(parser); + if (res == null) { + err = null; + break; + } + err = String.format("Unable to %s volume %s. Error: %s", command, name, res); + + if (command.equals("detach")) { + try { + Thread.sleep(sleepTime); + } catch (Exception ex) { + // don't do anything + } + } else { + break; + } + } + + if (err != null) { + SP_LOG(err); + log.warn(err); + throw new CloudRuntimeException(err); + } + + if (command.equals("attach")) { + return waitForDeviceSymlink(volumeUuid); + } else { + return true; + } + } + + public static boolean resize(String newSize, String volumeUuid ) { + final String name = getVolumeNameFromPath(volumeUuid, true); + if (name == null) { + return false; + } + + SP_LOG("StorpooolStorageAdaptor.resize: size=%s, uuid=%s, name=%s", newSize, volumeUuid, name); + + Script sc = new Script("storpool", 0, log); + sc.add("-M"); + sc.add("volume"); + sc.add(name); + sc.add("update"); + sc.add("size"); + sc.add(newSize); + sc.add("shrinkOk"); + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + String res = sc.execute(parser); + if (res == null) { + return true; + } + + String err = String.format("Unable to resize volume %s. Error: %s", name, res); + SP_LOG(err); + log.warn(err); + throw new CloudRuntimeException(err); + } + + @Override + public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { + SP_LOG("StorpooolStorageAdaptor.getPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool); + + log.debug(String.format("getPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool)); + + final long deviceSize = getDeviceSize(volumeUuid); + + KVMPhysicalDisk physicalDisk = new KVMPhysicalDisk(volumeUuid, volumeUuid, pool); + physicalDisk.setFormat(PhysicalDiskFormat.RAW); + physicalDisk.setSize(deviceSize); + physicalDisk.setVirtualSize(deviceSize); + return physicalDisk; + } + + @Override + public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map details) { + SP_LOG("StorpooolStorageAdaptor.connectPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool); + + log.debug(String.format("connectPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool)); + + return attachOrDetachVolume("attach", "volume", volumeUuid); + } + + @Override + public boolean disconnectPhysicalDisk(String volumeUuid, KVMStoragePool pool) { + SP_LOG("StorpooolStorageAdaptor.disconnectPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool); + + log.debug(String.format("disconnectPhysicalDisk: uuid=%s, pool=%s", volumeUuid, pool)); + return attachOrDetachVolume("detach", "volume", volumeUuid); + } + + public boolean disconnectPhysicalDisk(Map volumeToDisconnect) { + String volumeUuid = volumeToDisconnect.get(DiskTO.UUID); + SP_LOG("StorpooolStorageAdaptor.disconnectPhysicalDisk: map. uuid=%s", volumeUuid); + return attachOrDetachVolume("detach", "volume", volumeUuid); + } + + @Override + public boolean disconnectPhysicalDiskByPath(String localPath) { + SP_LOG("StorpooolStorageAdaptor.disconnectPhysicalDiskByPath: localPath=%s", localPath); + + log.debug(String.format("disconnectPhysicalDiskByPath: localPath=%s", localPath)); + return attachOrDetachVolume("detach", "volume", localPath); + } + + // The following do not apply for StorpoolStorageAdaptor? + @Override + public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, KVMStoragePool pool, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size) { + SP_LOG("StorpooolStorageAdaptor.createPhysicalDisk: uuid=%s, pool=%s, format=%s, size=%d", volumeUuid, pool, format, size); + throw new UnsupportedOperationException("Creating a physical disk is not supported."); + } + + @Override + public boolean deletePhysicalDisk(String volumeUuid, KVMStoragePool pool, Storage.ImageFormat format) { + // Should only come here when cleaning-up StorPool snapshots associated with CloudStack templates. + SP_LOG("StorpooolStorageAdaptor.deletePhysicalDisk: uuid=%s, pool=%s, format=%s", volumeUuid, pool, format); + final String name = getVolumeNameFromPath(volumeUuid, true); + if (name == null) { + final String err = String.format("StorpooolStorageAdaptor.deletePhysicalDisk: '%s' is not a StorPool volume?", volumeUuid); + SP_LOG(err); + throw new UnsupportedOperationException(err); + } + + Script sc = new Script("storpool", 0, log); + sc.add("-M"); + sc.add("snapshot", name); + sc.add("delete", name); + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + + String res = sc.execute(parser); + if (res != null) { + final String err = String.format("Unable to delete StorPool snapshot '%s'. Error: %s", name, res); + SP_LOG(err); + log.warn(err); + throw new UnsupportedOperationException(err); + } + return true; // apparently ignored + } + + @Override + public List listPhysicalDisks(String storagePoolUuid, KVMStoragePool pool) { + SP_LOG("StorpooolStorageAdaptor.listPhysicalDisks: uuid=%s, pool=%s", storagePoolUuid, pool); + throw new UnsupportedOperationException("Listing disks is not supported for this configuration."); + } + + @Override + public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String name, PhysicalDiskFormat format, + ProvisioningType provisioningType, long size, KVMStoragePool destPool, int timeout) { + SP_LOG("StorpooolStorageAdaptor.createDiskFromTemplate: template=%s, name=%s, fmt=%s, ptype=%s, size=%d, dst_pool=%s, to=%d", + template, name, format, provisioningType, size, destPool.getUuid(), timeout); + throw new UnsupportedOperationException("Creating a disk from a template is not yet supported for this configuration."); + } + + @Override + public KVMPhysicalDisk createTemplateFromDisk(KVMPhysicalDisk disk, String name, PhysicalDiskFormat format, long size, KVMStoragePool destPool) { + SP_LOG("StorpooolStorageAdaptor.createTemplateFromDisk: disk=%s, name=%s, fmt=%s, size=%d, dst_pool=%s", disk, name, format, size, destPool.getUuid()); + throw new UnsupportedOperationException("Creating a template from a disk is not yet supported for this configuration."); + } + + @Override + public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout) { + SP_LOG("StorpooolStorageAdaptor.copyPhysicalDisk: disk=%s, name=%s, dst_pool=%s, to=%d", disk, name, destPool.getUuid(), timeout); + throw new UnsupportedOperationException("Copying a disk is not supported in this configuration."); + } + + public KVMPhysicalDisk createDiskFromSnapshot(KVMPhysicalDisk snapshot, String snapshotName, String name, KVMStoragePool destPool) { + SP_LOG("StorpooolStorageAdaptor.createDiskFromSnapshot: snap=%s, snap_name=%s, name=%s, dst_pool=%s", snapshot, snapshotName, name, destPool.getUuid()); + throw new UnsupportedOperationException("Creating a disk from a snapshot is not supported in this configuration."); + } + + @Override + public boolean refresh(KVMStoragePool pool) { + SP_LOG("StorpooolStorageAdaptor.refresh: pool=%s", pool); + return true; + } + + @Override + public boolean createFolder(String uuid, String path) { + SP_LOG("StorpooolStorageAdaptor.createFolder: uuid=%s, path=%s", uuid, path); + throw new UnsupportedOperationException("A folder cannot be created in this configuration."); + } + + public KVMPhysicalDisk createDiskFromSnapshot(KVMPhysicalDisk snapshot, String snapshotName, String name, + KVMStoragePool destPool, int timeout) { + SP_LOG("StorpooolStorageAdaptor.createDiskFromSnapshot: snap=%s, snap_name=%s, name=%s, dst_pool=%s", snapshot, + snapshotName, name, destPool.getUuid()); + throw new UnsupportedOperationException( + "Creating a disk from a snapshot is not supported in this configuration."); + } + + public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk template, String name, + PhysicalDiskFormat format, long size, KVMStoragePool destPool, int timeout) { + SP_LOG("StorpooolStorageAdaptor.createDiskFromTemplateBacking: template=%s, name=%s, dst_pool=%s", template, + name, destPool.getUuid()); + throw new UnsupportedOperationException( + "Creating a disk from a template is not supported in this configuration."); + } + + public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFilePath, KVMStoragePool destPool, + boolean isIso) { + SP_LOG("StorpooolStorageAdaptor.createTemplateFromDirectDownloadFile: templateFilePath=%s, dst_pool=%s", + templateFilePath, destPool.getUuid()); + throw new UnsupportedOperationException( + "Creating a template from direct download is not supported in this configuration."); + } + + public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFilePath, String destTemplatePath, + KVMStoragePool destPool, ImageFormat format, int timeout) { + return null; + } + + @Override + public boolean createFolder(String uuid, String path, String localPath) { + return false; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStoragePool.java b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStoragePool.java new file mode 100644 index 00000000000..d031f319d19 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStoragePool.java @@ -0,0 +1,164 @@ +// 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 com.cloud.hypervisor.kvm.storage; + +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; + +import com.cloud.storage.Storage; +import com.cloud.storage.Storage.StoragePoolType; + +public class StorPoolStoragePool implements KVMStoragePool { + private String _uuid; + private String _sourceHost; + private int _sourcePort; + private StoragePoolType _storagePoolType; + private StorageAdaptor _storageAdaptor; + private String _authUsername; + private String _authSecret; + private String _sourceDir; + private String _localPath; + + public StorPoolStoragePool(String uuid, String host, int port, StoragePoolType storagePoolType, StorageAdaptor storageAdaptor) { + _uuid = uuid; + _sourceHost = host; + _sourcePort = port; + _storagePoolType = storagePoolType; + _storageAdaptor = storageAdaptor; + } + + @Override + public String getUuid() { + return _uuid; + } + + @Override + public String getSourceHost() { + return _sourceHost; + } + + @Override + public int getSourcePort() { + return _sourcePort; + } + + @Override + public long getCapacity() { + return 100L*(1024L*1024L*1024L*1024L*1024L); + } + + @Override + public long getUsed() { + return 0; + } + + @Override + public long getAvailable() { + return 0; + } + + @Override + public StoragePoolType getType() { + return _storagePoolType; + } + + @Override + public String getAuthUserName() { + return _authUsername; + } + + @Override + public String getAuthSecret() { + return _authSecret; + } + + @Override + public String getSourceDir() { + return _sourceDir; + } + + @Override + public String getLocalPath() { + return _localPath; + } + + @Override + public PhysicalDiskFormat getDefaultFormat() { + return PhysicalDiskFormat.RAW; + } + + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size) { + return _storageAdaptor.createPhysicalDisk(name, this, format, provisioningType, size); + } + + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, Storage.ProvisioningType provisioningType, long size) { + return _storageAdaptor.createPhysicalDisk(name, this, null, provisioningType, size); + } + + @Override + public boolean connectPhysicalDisk(String name, Map details) { + return _storageAdaptor.connectPhysicalDisk(name, this, details); + } + + @Override + public KVMPhysicalDisk getPhysicalDisk(String volumeUuid) { + return _storageAdaptor.getPhysicalDisk(volumeUuid, this); + } + + @Override + public boolean disconnectPhysicalDisk(String volumeUuid) { + return _storageAdaptor.disconnectPhysicalDisk(volumeUuid, this); + } + + @Override + public boolean deletePhysicalDisk(String volumeUuid, Storage.ImageFormat format) { + return _storageAdaptor.deletePhysicalDisk(volumeUuid, this, format); + } + + @Override + public List listPhysicalDisks() { + return _storageAdaptor.listPhysicalDisks(_uuid, this); + } + + @Override + public boolean refresh() { + return _storageAdaptor.refresh(this); + } + + @Override + public boolean delete() { + return _storageAdaptor.deleteStoragePool(this); + } + + @Override + public boolean createFolder(String path) { + return _storageAdaptor.createFolder(_uuid, path); + } + + @Override + public boolean isExternalSnapshot() { + return false; + } + + public boolean supportsConfigDriveIso() { + return false; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java new file mode 100644 index 00000000000..499d0630b85 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/collector/StorPoolAbandonObjectsCollector.java @@ -0,0 +1,323 @@ +/* + * 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.collector; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +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.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackNoReturn; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.TransactionStatus; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class StorPoolAbandonObjectsCollector extends ManagerBase implements Configurable { + private static Logger log = Logger.getLogger(StorPoolAbandonObjectsCollector.class); + @Inject + private PrimaryDataStoreDao storagePoolDao; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + + private ScheduledExecutorService _volumeTagsUpdateExecutor; + private static final String ABANDON_LOG = "/var/log/cloudstack/management/storpool-abandoned-objects"; + + + static final ConfigKey volumeCheckupTagsInterval = new ConfigKey("Advanced", Integer.class, + "storpool.volume.tags.checkup", "86400", + "Minimal interval (in seconds) to check and report if StorPool volume exists in CloudStack volumes database", + false); + static final ConfigKey snapshotCheckupTagsInterval = new ConfigKey("Advanced", Integer.class, + "storpool.snapshot.tags.checkup", "86400", + "Minimal interval (in seconds) to check and report if StorPool snapshot exists in CloudStack snapshots database", + false); + + @Override + public String getConfigComponentName() { + return StorPoolAbandonObjectsCollector.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { volumeCheckupTagsInterval, snapshotCheckupTagsInterval }; + } + + @Override + public boolean start() { + init(); + return true; + } + + private void init() { + _volumeTagsUpdateExecutor = Executors.newScheduledThreadPool(2, + new NamedThreadFactory("StorPoolAbandonObjectsCollector")); + StorPoolHelper.appendLogger(log, ABANDON_LOG, "abandon"); + if (volumeCheckupTagsInterval.value() > 0) { + _volumeTagsUpdateExecutor.scheduleAtFixedRate(new StorPoolVolumesTagsUpdate(), + volumeCheckupTagsInterval.value(), volumeCheckupTagsInterval.value(), TimeUnit.SECONDS); + } + if (snapshotCheckupTagsInterval.value() > 0) { + _volumeTagsUpdateExecutor.scheduleAtFixedRate(new StorPoolSnapshotsTagsUpdate(), + snapshotCheckupTagsInterval.value(), snapshotCheckupTagsInterval.value(), TimeUnit.SECONDS); + } + } + + class StorPoolVolumesTagsUpdate extends ManagedContextRunnable { + + @Override + @DB + protected void runInContext() { + List spPools = storagePoolDao.findPoolsByProvider(StorPoolUtil.SP_PROVIDER_NAME); + if (CollectionUtils.isEmpty(spPools)) { + return; + } + Map volumes = new HashMap<>(); + for (StoragePoolVO storagePoolVO : spPools) { + try { + JsonArray arr = StorPoolUtil.volumesList(StorPoolUtil.getSpConnection(storagePoolVO.getUuid(), storagePoolVO.getId(), storagePoolDetailsDao, storagePoolDao)); + volumes.putAll(getStorPoolNamesAndCsTag(arr)); + } catch (Exception e) { + log.debug(String.format("Could not collect abandon objects due to %s", e.getMessage()), e); + } + } + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); + + try { + PreparedStatement pstmt = txn.prepareAutoCloseStatement( + "CREATE TEMPORARY TABLE `cloud`.`volumes1`(`id` bigint unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL,`tag` varchar(255) NOT NULL, PRIMARY KEY (`id`))"); + pstmt.executeUpdate(); + + pstmt = txn.prepareAutoCloseStatement( + "CREATE TEMPORARY TABLE `cloud`.`volumes_on_host1`(`id` bigint unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL,`tag` varchar(255) NOT NULL, PRIMARY KEY (`id`))"); + pstmt.executeUpdate(); + + } catch (SQLException e) { + log.info(String.format("[ignored] SQL failed to delete vm work job: %s ", + e.getLocalizedMessage())); + } catch (Throwable e) { + log.info(String.format("[ignored] caught an error during delete vm work job: %s", + e.getLocalizedMessage())); + } + + try { + PreparedStatement pstmt = txn.prepareStatement("INSERT INTO `cloud`.`volumes1` (name, tag) VALUES (?, ?)"); + PreparedStatement volumesOnHostpstmt = txn.prepareStatement("INSERT INTO `cloud`.`volumes_on_host1` (name, tag) VALUES (?, ?)"); + for (Map.Entry volume : volumes.entrySet()) { + if (volume.getValue().equals("volume")) { + addRecordToDb(volume.getKey(), pstmt, volume.getValue(), true); + } else if (volume.getValue().equals("check-volume-is-on-host")) { + addRecordToDb(volume.getKey(), volumesOnHostpstmt, volume.getValue(), true); + } + } + pstmt.executeBatch(); + volumesOnHostpstmt.executeBatch(); + String sql = "SELECT f.* FROM `cloud`.`volumes1` f LEFT JOIN `cloud`.`volumes` v ON f.name=v.path where v.path is NULL OR NOT state=?"; + findMissingRecordsInCS(txn, sql, "volume"); + + String sqlVolumeOnHost = "SELECT f.* FROM `cloud`.`volumes_on_host1` f LEFT JOIN `cloud`.`storage_pool_details` v ON f.name=v.value where v.value is NULL"; + findMissingRecordsInCS(txn, sqlVolumeOnHost, "volumes_on_host"); + } catch (SQLException e) { + log.info(String.format("[ignored] SQL failed due to: %s ", + e.getLocalizedMessage())); + } catch (Throwable e) { + log.info(String.format("[ignored] caught an error: %s", + e.getLocalizedMessage())); + } finally { + try { + PreparedStatement pstmt = txn.prepareStatement("DROP TABLE `cloud`.`volumes1`"); + pstmt.executeUpdate(); + pstmt = txn.prepareStatement("DROP TABLE `cloud`.`volumes_on_host1`"); + pstmt.executeUpdate(); + } catch (SQLException e) { + txn.close(); + log.info(String.format("createTemporaryVolumeTable %s", e.getMessage())); + } + txn.close(); + } + } + }); + } + } + + class StorPoolSnapshotsTagsUpdate extends ManagedContextRunnable { + + @Override + @DB + protected void runInContext() { + List spPools = storagePoolDao.findPoolsByProvider(StorPoolUtil.SP_PROVIDER_NAME); + Map snapshots = new HashMap(); + if (CollectionUtils.isEmpty(spPools)) { + return; + } + for (StoragePoolVO storagePoolVO : spPools) { + try { + JsonArray arr = StorPoolUtil.snapshotsList(StorPoolUtil.getSpConnection(storagePoolVO.getUuid(), storagePoolVO.getId(), storagePoolDetailsDao, storagePoolDao)); + snapshots.putAll(getStorPoolNamesAndCsTag(arr)); + } catch (Exception e) { + log.debug(String.format("Could not collect abandon objects due to %s", e.getMessage())); + } + } + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); + + try{ + PreparedStatement pstmt = txn.prepareAutoCloseStatement( + "CREATE TEMPORARY TABLE `cloud`.`snapshots1`(`id` bigint unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL,`tag` varchar(255) NOT NULL, PRIMARY KEY (`id`))"); + pstmt.executeUpdate(); + + pstmt = txn.prepareAutoCloseStatement( + "CREATE TEMPORARY TABLE `cloud`.`vm_snapshots1`(`id` bigint unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL,`tag` varchar(255) NOT NULL, PRIMARY KEY (`id`))"); + pstmt.executeUpdate(); + + pstmt = txn.prepareAutoCloseStatement( + "CREATE TEMPORARY TABLE `cloud`.`vm_templates1`(`id` bigint unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL,`tag` varchar(255) NOT NULL, PRIMARY KEY (`id`))"); + pstmt.executeUpdate(); + } catch (SQLException e) { + log.info(String.format("[ignored] SQL failed to delete vm work job: %s ", + e.getLocalizedMessage())); + } catch (Throwable e) { + log.info(String.format("[ignored] caught an error during delete vm work job: %s", + e.getLocalizedMessage())); + } + + try { + PreparedStatement snapshotsPstmt = txn.prepareStatement("INSERT INTO `cloud`.`snapshots1` (name, tag) VALUES (?, ?)"); + PreparedStatement groupSnapshotsPstmt = txn.prepareStatement("INSERT INTO `cloud`.`vm_snapshots1` (name, tag) VALUES (?, ?)"); + PreparedStatement templatePstmt = txn.prepareStatement("INSERT INTO `cloud`.`vm_templates1` (name, tag) VALUES (?, ?)"); + for (Map.Entry snapshot : snapshots.entrySet()) { + if (!snapshot.getValue().equals("group") && !snapshot.getValue().equals("template")) { + addRecordToDb(snapshot.getKey(), snapshotsPstmt, snapshot.getValue(), true); + } else if (snapshot.getValue().equals("group")) { + addRecordToDb(snapshot.getKey(), groupSnapshotsPstmt, snapshot.getValue(), true); + } else if (snapshot.getValue().equals("template")) { + addRecordToDb(snapshot.getKey(), templatePstmt, snapshot.getValue(), true); + } + } + snapshotsPstmt.executeBatch(); + groupSnapshotsPstmt.executeBatch(); + templatePstmt.executeBatch(); + + String sqlSnapshots = "SELECT f.* FROM `cloud`.`snapshots1` f LEFT JOIN `cloud`.`snapshot_details` v ON f.name=v.value where v.value is NULL"; + findMissingRecordsInCS(txn, sqlSnapshots, "snapshot"); + + String sqlVmSnapshots = "SELECT f.* FROM `cloud`.`vm_snapshots1` f LEFT JOIN `cloud`.`vm_snapshot_details` v ON f.name=v.value where v.value is NULL"; + findMissingRecordsInCS(txn, sqlVmSnapshots, "snapshot"); + + String sqlTemplates = "SELECT temp.*" + + " FROM `cloud`.`vm_templates1` temp" + + " LEFT JOIN `cloud`.`template_store_ref` store" + + " ON temp.name=store.local_path" + + " LEFT JOIN `cloud`.`template_spool_ref` spool" + + " ON temp.name=spool.local_path" + + " where store.local_path is NULL" + + " and spool.local_path is NULL"; + findMissingRecordsInCS(txn, sqlTemplates, "snapshot"); + } catch (SQLException e) { + log.info(String.format("[ignored] SQL failed due to: %s ", + e.getLocalizedMessage())); + } catch (Throwable e) { + log.info(String.format("[ignored] caught an error: %s", + e.getLocalizedMessage())); + } finally { + try { + PreparedStatement pstmt = txn.prepareStatement("DROP TABLE `cloud`.`snapshots1`"); + pstmt.executeUpdate(); + pstmt = txn.prepareStatement("DROP TABLE `cloud`.`vm_snapshots1`"); + pstmt.executeUpdate(); + pstmt = txn.prepareStatement("DROP TABLE `cloud`.`vm_templates1`"); + pstmt.executeUpdate(); + } catch (SQLException e) { + txn.close(); + log.info(String.format("createTemporaryVolumeTable %s", e.getMessage())); + } + txn.close(); + } + } + }); + } + } + + private void addRecordToDb(String name, PreparedStatement pstmt, String tag, boolean pathNeeded) + throws SQLException { + name = name.startsWith("~") ? name.split("~")[1] : name; + pstmt.setString(1, pathNeeded ? StorPoolUtil.devPath(name) : name); + pstmt.setString(2, tag); + pstmt.addBatch(); + } + + private void findMissingRecordsInCS(TransactionLegacy txn, String sql, String object) throws SQLException { + ResultSet rs; + PreparedStatement pstmt2 = txn.prepareStatement(sql); + if (object.equals("volume")) { + pstmt2.setString(1, "Ready"); + } + rs = pstmt2.executeQuery(); + String name = null; + while (rs.next()) { + name = rs.getString(2); + log.info(String.format( + "CloudStack does not know about StorPool %s %s, it had to be a %s", object, name, rs.getString(3))); + } + } + + private Map getStorPoolNamesAndCsTag(JsonArray arr) { + Map map = new HashMap<>(); + for (int i = 0; i < arr.size(); i++) { + String name = arr.get(i).getAsJsonObject().get("name").getAsString(); + String tag = null; + if (!name.startsWith("*") && !name.contains("@")) { + JsonObject tags = arr.get(i).getAsJsonObject().get("tags").getAsJsonObject(); + if (tags != null && tags.getAsJsonPrimitive("cs") != null && !(arr.get(i).getAsJsonObject().get("deleted") != null && arr.get(i).getAsJsonObject().get("deleted").getAsBoolean())) { + tag = tags.getAsJsonPrimitive("cs").getAsString(); + map.put(name, tag); + } + } + } + return map; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java new file mode 100644 index 00000000000..97b15d847f3 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/driver/StorPoolPrimaryDataStoreDriver.java @@ -0,0 +1,976 @@ +/* + * 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 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.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +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.TemplateInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.command.CreateObjectAnswer; +import org.apache.cloudstack.storage.command.StorageSubSystemCommand; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.ResizeVolumeAnswer; +import com.cloud.agent.api.storage.StorPoolBackupSnapshotCommand; +import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; +import com.cloud.agent.api.storage.StorPoolCopyVolumeToSecondaryCommand; +import com.cloud.agent.api.storage.StorPoolDownloadTemplateCommand; +import com.cloud.agent.api.storage.StorPoolDownloadVolumeCommand; +import com.cloud.agent.api.storage.StorPoolResizeVolumeCommand; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.server.ResourceTag; +import com.cloud.server.ResourceTag.ResourceObjectType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.ResizeVolumePayload; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePool; +import com.cloud.storage.VMTemplateDetailVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VolumeDetailVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.storage.dao.VMTemplatePoolDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.tags.dao.ResourceTagDao; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.VMInstanceDao; + +public class StorPoolPrimaryDataStoreDriver implements PrimaryDataStoreDriver { + + private static final Logger log = Logger.getLogger(StorPoolPrimaryDataStoreDriver.class); + + @Inject + private VolumeDao volumeDao; + @Inject + private StorageManager storageMgr; + @Inject + private PrimaryDataStoreDao primaryStoreDao; + @Inject + private EndPointSelector selector; + @Inject + private ConfigurationDao configDao; + @Inject + private TemplateDataStoreDao vmTemplateDataStoreDao; + @Inject + private VMInstanceDao vmInstanceDao; + @Inject + private ClusterDao clusterDao; + @Inject + private HostDao hostDao; + @Inject + private ResourceTagDao _resourceTagDao; + @Inject + private SnapshotDetailsDao _snapshotDetailsDao; + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + @Inject + private VolumeDetailsDao volumeDetailsDao; + @Inject + private VMTemplateDetailsDao vmTemplateDetailsDao; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + @Inject + private VMTemplatePoolDao vmTemplatePoolDao; + + @Override + public Map getCapabilities() { + return null; + } + + @Override + public DataTO getTO(DataObject data) { + return null; + } + + @Override + public DataStoreTO getStoreTO(DataStore store) { + return null; + } + + @Override + public long getUsedBytes(StoragePool storagePool) { + return 0; + } + + @Override + public long getUsedIops(StoragePool storagePool) { + return 0; + } + + @Override + public boolean grantAccess(DataObject data, Host host, DataStore dataStore) { + return false; + } + + @Override + public void revokeAccess(DataObject data, Host host, DataStore dataStore) { + } + + private void updateStoragePool(final long poolId, final long deltaUsedBytes) { + StoragePoolVO storagePool = primaryStoreDao.findById(poolId); + final long capacity = storagePool.getCapacityBytes(); + final long used = storagePool.getUsedBytes() + deltaUsedBytes; + + storagePool.setUsedBytes(used < 0 ? 0 : (used > capacity ? capacity : used)); + primaryStoreDao.update(poolId, storagePool); + } + + private String getVMInstanceUUID(Long id) { + return id != null ? vmInstanceDao.findById(id).getUuid() : null; + } + + protected void _completeResponse(final CreateObjectAnswer answer, final String err, final AsyncCompletionCallback callback) + { + final CreateCmdResult res = new CreateCmdResult(null, answer); + res.setResult(err); + callback.complete(res); + } + + protected void completeResponse(final DataTO result, final AsyncCompletionCallback callback) + { + _completeResponse(new CreateObjectAnswer(result), null, callback); + } + + protected void completeResponse(final String err, final AsyncCompletionCallback callback) + { + _completeResponse(new CreateObjectAnswer(err), err, callback); + } + + @Override + public long getDataObjectSizeIncludingHypervisorSnapshotReserve(DataObject dataObject, StoragePool pool) { + return dataObject.getSize(); + } + + @Override + public long getBytesRequiredForTemplate(TemplateInfo templateInfo, StoragePool storagePool) { + return 0; + } + + @Override + public ChapInfo getChapInfo(DataObject dataObject) { + return null; + } + + @Override + public void createAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback callback) { + String path = null; + String err = null; + if (data.getType() == DataObjectType.VOLUME) { + try { + VolumeInfo vinfo = (VolumeInfo)data; + String name = vinfo.getUuid(); + Long size = vinfo.getSize(); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, primaryStoreDao); + + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.createAsync volume: name=%s, uuid=%s, isAttached=%s vm=%s, payload=%s, template: %s", vinfo.getName(), vinfo.getUuid(), vinfo.isAttachedVM(), vinfo.getAttachedVmName(), vinfo.getpayload(), conn.getTemplateName()); + SpApiResponse resp = StorPoolUtil.volumeCreate(name, null, size, getVMInstanceUUID(vinfo.getInstanceId()), null, "volume", vinfo.getMaxIops(), conn); + if (resp.getError() == null) { + String volumeName = StorPoolUtil.getNameFromResponse(resp, false); + path = StorPoolUtil.devPath(volumeName); + + VolumeVO volume = volumeDao.findById(vinfo.getId()); + volume.setPoolId(dataStore.getId()); + volume.setPoolType(StoragePoolType.SharedMountPoint); + volume.setPath(path); + volumeDao.update(volume.getId(), volume); + + updateStoragePool(dataStore.getId(), size); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.createAsync volume: name=%s, uuid=%s, isAttached=%s vm=%s, payload=%s, template: %s", volumeName, vinfo.getUuid(), vinfo.isAttachedVM(), vinfo.getAttachedVmName(), vinfo.getpayload(), conn.getTemplateName()); + } else { + err = String.format("Could not create StorPool volume %s. Error: %s", name, resp.getError()); + } + } catch (Exception e) { + err = String.format("Could not create volume due to %s", e.getMessage()); + } + } else { + err = String.format("Invalid object type \"%s\" passed to createAsync", data.getType()); + } + + CreateCmdResult res = new CreateCmdResult(path, new Answer(null, err == null, err)); + res.setResult(err); + if (callback != null) { + callback.complete(res); + } + } + + @Override + public void resize(DataObject data, AsyncCompletionCallback callback) { + String path = null; + String err = null; + ResizeVolumeAnswer answer = null; + + if (data.getType() == DataObjectType.VOLUME) { + VolumeObject vol = (VolumeObject)data; + StoragePool pool = (StoragePool)data.getDataStore(); + ResizeVolumePayload payload = (ResizeVolumePayload)vol.getpayload(); + + final String name = StorPoolStorageAdaptor.getVolumeNameFromPath(vol.getPath(), true); + final long oldSize = vol.getSize(); + Long oldMaxIops = vol.getMaxIops(); + + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(data.getDataStore().getUuid(), data.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.resize: name=%s, uuid=%s, oldSize=%d, newSize=%s, shrinkOk=%s", name, vol.getUuid(), oldSize, payload.newSize, payload.shrinkOk); + + SpApiResponse resp = StorPoolUtil.volumeUpdate(name, payload.newSize, payload.shrinkOk, payload.newMaxIops, conn); + if (resp.getError() != null) { + err = String.format("Could not resize StorPool volume %s. Error: %s", name, resp.getError()); + } else { + StorPoolResizeVolumeCommand resizeCmd = new StorPoolResizeVolumeCommand(vol.getPath(), new StorageFilerTO(pool), vol.getSize(), payload.newSize, payload.shrinkOk, + payload.instanceName, payload.hosts == null ? false : true); + answer = (ResizeVolumeAnswer) storageMgr.sendToPool(pool, payload.hosts, resizeCmd); + + if (answer == null || !answer.getResult()) { + err = answer != null ? answer.getDetails() : "return a null answer, resize failed for unknown reason"; + } else { + path = StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false)); + + vol.setSize(payload.newSize); + vol.update(); + if (payload.newMaxIops != null) { + VolumeVO volume = volumeDao.findById(vol.getId()); + volume.setMaxIops(payload.newMaxIops); + volumeDao.update(volume.getId(), volume); + } + + updateStoragePool(vol.getPoolId(), payload.newSize - oldSize); + } + } + if (err != null) { + // try restoring volume to its initial size + resp = StorPoolUtil.volumeUpdate(name, oldSize, true, oldMaxIops, conn); + if (resp.getError() != null) { + log.debug(String.format("Could not resize StorPool volume %s back to its original size. Error: %s", name, resp.getError())); + } + } + } catch (Exception e) { + log.debug("sending resize command failed", e); + err = e.toString(); + } + } else { + err = String.format("Invalid object type \"%s\" passed to resize", data.getType()); + } + + CreateCmdResult res = new CreateCmdResult(path, answer); + res.setResult(err); + callback.complete(res); + } + + @Override + public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback callback) { + String err = null; + if (data.getType() == DataObjectType.VOLUME) { + VolumeInfo vinfo = (VolumeInfo)data; + String name = StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.deleteAsync delete volume: name=%s, uuid=%s, isAttached=%s vm=%s, payload=%s dataStore=%s", name, vinfo.getUuid(), vinfo.isAttachedVM(), vinfo.getAttachedVmName(), vinfo.getpayload(), dataStore.getUuid()); + if (name == null) { + name = vinfo.getUuid(); + } + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, primaryStoreDao); + + SpApiResponse resp = StorPoolUtil.volumeDelete(name, conn); + if (resp.getError() == null) { + updateStoragePool(dataStore.getId(), - vinfo.getSize()); + VolumeDetailVO detail = volumeDetailsDao.findDetail(vinfo.getId(), StorPoolUtil.SP_PROVIDER_NAME); + if (detail != null) { + volumeDetailsDao.remove(detail.getId()); + } + } else { + if (!resp.getError().getName().equalsIgnoreCase("objectDoesNotExist")) { + err = String.format("Could not delete StorPool volume %s. Error: %s", name, resp.getError()); + } + } + } catch (Exception e) { + err = String.format("Could not delete volume due to %s", e.getMessage()); + } + } else { + err = String.format("Invalid DataObjectType \"%s\" passed to deleteAsync", data.getType()); + } + + if (err != null) { + log.error(err); + StorPoolUtil.spLog(err); + } + + CommandResult res = new CommandResult(); + res.setResult(err); + callback.complete(res); + } + + private void logDataObject(final String pref, DataObject data) { + final DataStore dstore = data.getDataStore(); + String name = null; + Long size = null; + + if (data.getType() == DataObjectType.VOLUME) { + VolumeInfo vinfo = (VolumeInfo)data; + name = vinfo.getName(); + size = vinfo.getSize(); + } else if (data.getType() == DataObjectType.SNAPSHOT) { + SnapshotInfo sinfo = (SnapshotInfo)data; + name = sinfo.getName(); + size = sinfo.getSize(); + } else if (data.getType() == DataObjectType.TEMPLATE) { + TemplateInfo tinfo = (TemplateInfo)data; + name = tinfo.getName(); + size = tinfo.getSize(); + } + + StorPoolUtil.spLog("%s: name=%s, size=%s, uuid=%s, type=%s, dstore=%s:%s:%s", pref, name, size, data.getUuid(), data.getType(), dstore.getUuid(), dstore.getName(), dstore.getRole()); + } + + @Override + public boolean canCopy(DataObject srcData, DataObject dstData) { + return true; + } + + @Override + public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCallback callback) { + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc:"); + logDataObject("SRC", srcData); + logDataObject("DST", dstData); + + final DataObjectType srcType = srcData.getType(); + final DataObjectType dstType = dstData.getType(); + String err = null; + Answer answer = null; + StorageSubSystemCommand cmd = null; + + try { + if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.VOLUME) { + SnapshotInfo sinfo = (SnapshotInfo)srcData; + final String snapshotName = StorPoolHelper.getSnapshotName(srcData.getId(), srcData.getUuid(), snapshotDataStoreDao, _snapshotDetailsDao); + + VolumeInfo vinfo = (VolumeInfo)dstData; + final String volumeName = vinfo.getUuid(); + final Long size = vinfo.getSize(); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + SpApiResponse resp = StorPoolUtil.volumeCreate(volumeName, snapshotName, size, null, null, "volume", sinfo.getBaseVolume().getMaxIops(), conn); + if (resp.getError() == null) { + updateStoragePool(dstData.getDataStore().getId(), size); + + VolumeObjectTO to = (VolumeObjectTO)dstData.getTO(); + to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + to.setSize(size); + + answer = new CopyCmdAnswer(to); + StorPoolUtil.spLog("Created volume=%s with uuid=%s from snapshot=%s with uuid=%s", StorPoolUtil.getNameFromResponse(resp, false), to.getUuid(), snapshotName, sinfo.getUuid()); + } else if (resp.getError().getName().equals("objectDoesNotExist")) { + //check if snapshot is on secondary storage + StorPoolUtil.spLog("Snapshot %s does not exists on StorPool, will try to create a volume from a snopshot on secondary storage", snapshotName); + SnapshotDataStoreVO snap = snapshotDataStoreDao.findBySnapshot(sinfo.getId(), DataStoreRole.Image); + if (snap != null && StorPoolStorageAdaptor.getVolumeNameFromPath(snap.getInstallPath(), false) == null) { + resp = StorPoolUtil.volumeCreate(srcData.getUuid(), null, size, null, "no", "snapshot", sinfo.getBaseVolume().getMaxIops(), conn); + if (resp.getError() == null) { + VolumeObjectTO dstTO = (VolumeObjectTO) dstData.getTO(); + dstTO.setSize(size); + dstTO.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + cmd = new StorPoolDownloadTemplateCommand(srcData.getTO(), dstTO, StorPoolHelper.getTimeout(StorPoolHelper.PrimaryStorageDownloadWait, configDao), VirtualMachineManager.ExecuteInSequence.value(), "volume"); + + EndPoint ep = selector.select(srcData, dstData); + if (ep == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = ep.sendMessage(cmd); + } + + if (answer != null && answer.getResult()) { + SpApiResponse resp2 = StorPoolUtil.volumeFreeze(StorPoolUtil.getNameFromResponse(resp, true), conn); + if (resp2.getError() != null) { + err = String.format("Could not freeze Storpool volume %s. Error: %s", srcData.getUuid(), resp2.getError()); + } else { + String name = StorPoolUtil.getNameFromResponse(resp, false); + SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(sinfo.getId(), sinfo.getUuid()); + if (snapshotDetails != null) { + StorPoolHelper.updateSnapshotDetailsValue(snapshotDetails.getId(), StorPoolUtil.devPath(name), "snapshot"); + }else { + StorPoolHelper.addSnapshotDetails(sinfo.getId(), sinfo.getUuid(), StorPoolUtil.devPath(name), _snapshotDetailsDao); + } + resp = StorPoolUtil.volumeCreate(volumeName, StorPoolUtil.getNameFromResponse(resp, true), size, null, null, "volume", sinfo.getBaseVolume().getMaxIops(), conn); + if (resp.getError() == null) { + updateStoragePool(dstData.getDataStore().getId(), size); + + VolumeObjectTO to = (VolumeObjectTO) dstData.getTO(); + to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + to.setSize(size); + // successfully downloaded snapshot to primary storage + answer = new CopyCmdAnswer(to); + StorPoolUtil.spLog("Created volume=%s with uuid=%s from snapshot=%s with uuid=%s", name, to.getUuid(), snapshotName, sinfo.getUuid()); + + } else { + err = String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, resp.getError()); + } + } + } else { + err = answer != null ? answer.getDetails() : "Unknown error while downloading template. Null answer returned."; + } + } else { + err = String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, resp.getError()); + } + } else { + err = String.format("The snapshot %s does not exists neither on primary, neither on secondary storage. Cannot create volume from snapshot", snapshotName); + } + } else { + err = String.format("Could not create Storpool volume %s from snapshot %s. Error: %s", volumeName, snapshotName, resp.getError()); + } + } else if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.SNAPSHOT) { + // bypass secondary storage + if (StorPoolConfigurationManager.BypassSecondaryStorage.value()) { + SnapshotObjectTO snapshot = (SnapshotObjectTO) srcData.getTO(); + answer = new CopyCmdAnswer(snapshot); + } else { + // copy snapshot to secondary storage (backup snapshot) + cmd = new StorPoolBackupSnapshotCommand(srcData.getTO(), dstData.getTO(), StorPoolHelper.getTimeout(StorPoolHelper.BackupSnapshotWait, configDao), VirtualMachineManager.ExecuteInSequence.value()); + + final String snapName = StorPoolStorageAdaptor.getVolumeNameFromPath(((SnapshotInfo) srcData).getPath(), true); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(srcData.getDataStore().getUuid(), srcData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + try { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(snapName, clusterDao); + EndPoint ep = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); + if (ep == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = ep.sendMessage(cmd); + // if error during snapshot backup, cleanup the StorPool snapshot + if (answer != null && !answer.getResult()) { + StorPoolUtil.spLog(String.format("Error while backing-up snapshot '%s' - cleaning up StorPool snapshot. Error: %s", snapName, answer.getDetails())); + SpApiResponse resp = StorPoolUtil.snapshotDelete(snapName, conn); + if (resp.getError() != null) { + final String err2 = String.format("Failed to cleanup StorPool snapshot '%s'. Error: %s.", snapName, resp.getError()); + log.error(err2); + StorPoolUtil.spLog(err2); + } + } + } + } catch (CloudRuntimeException e) { + err = e.getMessage(); + } + } + } else if (srcType == DataObjectType.VOLUME && dstType == DataObjectType.TEMPLATE) { + // create template from volume + VolumeObjectTO volume = (VolumeObjectTO) srcData.getTO(); + TemplateObjectTO template = (TemplateObjectTO) dstData.getTO(); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(srcData.getDataStore().getUuid(), srcData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + + String volumeName = StorPoolStorageAdaptor.getVolumeNameFromPath(volume.getPath(), true); + + + cmd = new StorPoolBackupTemplateFromSnapshotCommand(volume, template, + StorPoolHelper.getTimeout(StorPoolHelper.PrimaryStorageDownloadWait, configDao), VirtualMachineManager.ExecuteInSequence.value()); + + try { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(volumeName, clusterDao); + EndPoint ep2 = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); + if (ep2 == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = ep2.sendMessage(cmd); + if (answer != null && answer.getResult()) { + SpApiResponse resSnapshot = StorPoolUtil.volumeSnapshot(volumeName, template.getUuid(), null, "template", "no", conn); + if (resSnapshot.getError() != null) { + log.debug(String.format("Could not snapshot volume with ID=%s", volume.getId())); + StorPoolUtil.spLog("Volume snapshot failed with error=%s", resSnapshot.getError().getDescr()); + err = resSnapshot.getError().getDescr(); + } + else { + StorPoolHelper.updateVmStoreTemplate(template.getId(), template.getDataStore().getRole(), StorPoolUtil.devPath(StorPoolUtil.getSnapshotNameFromResponse(resSnapshot, false, StorPoolUtil.GLOBAL_ID)), vmTemplateDataStoreDao); + vmTemplateDetailsDao.persist(new VMTemplateDetailVO(template.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, String.valueOf(srcData.getDataStore().getId()), false)); + } + }else { + err = "Could not copy template to secondary " + answer.getResult(); + } + } + }catch (CloudRuntimeException e) { + err = e.getMessage(); + } + } else if (srcType == DataObjectType.TEMPLATE && dstType == DataObjectType.TEMPLATE) { + // copy template to primary storage + TemplateInfo tinfo = (TemplateInfo)dstData; + Long size = tinfo.getSize(); + if(size == null || size == 0) + size = 1L*1024*1024*1024; + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dstData.getDataStore().getUuid(), dstData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + + TemplateDataStoreVO templDataStoreVO = vmTemplateDataStoreDao.findByTemplate(tinfo.getId(), DataStoreRole.Image); + + String snapshotName = (templDataStoreVO != null && templDataStoreVO.getLocalDownloadPath() != null) + ? StorPoolStorageAdaptor.getVolumeNameFromPath(templDataStoreVO.getLocalDownloadPath(), true) + : null; + String name = tinfo.getUuid(); + + SpApiResponse resp = null; + if (snapshotName != null) { + //no need to copy volume from secondary, because we have it already on primary. Just need to create a child snapshot from it. + //The child snapshot is needed when configuration "storage.cleanup.enabled" is true, not to clean the base snapshot and to lose everything + resp = StorPoolUtil.volumeCreate(name, snapshotName, size, null, "no", "template", null, conn); + if (resp.getError() != null) { + err = String.format("Could not create Storpool volume for CS template %s. Error: %s", name, resp.getError()); + } else { + String volumeNameToSnapshot = StorPoolUtil.getNameFromResponse(resp, true); + SpApiResponse resp2 = StorPoolUtil.volumeFreeze(volumeNameToSnapshot, conn); + if (resp2.getError() != null) { + err = String.format("Could not freeze Storpool volume %s. Error: %s", name, resp2.getError()); + } else { + StorPoolUtil.spLog("Storpool snapshot [%s] for a template exists. Creating template on Storpool with name [%s]", tinfo.getUuid(), name); + TemplateObjectTO dstTO = (TemplateObjectTO) dstData.getTO(); + dstTO.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + dstTO.setSize(size); + answer = new CopyCmdAnswer(dstTO); + } + } + } else { + resp = StorPoolUtil.volumeCreate(name, null, size, null, "no", "template", null, conn); + if (resp.getError() != null) { + err = String.format("Could not create Storpool volume for CS template %s. Error: %s", name, resp.getError()); + } else { + TemplateObjectTO dstTO = (TemplateObjectTO)dstData.getTO(); + dstTO.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + dstTO.setSize(size); + + cmd = new StorPoolDownloadTemplateCommand(srcData.getTO(), dstTO, StorPoolHelper.getTimeout(StorPoolHelper.PrimaryStorageDownloadWait, configDao), VirtualMachineManager.ExecuteInSequence.value(), "volume"); + + EndPoint ep = selector.select(srcData, dstData); + if (ep == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = ep.sendMessage(cmd); + } + + if (answer != null && answer.getResult()) { + // successfully downloaded template to primary storage + SpApiResponse resp2 = StorPoolUtil.volumeFreeze(StorPoolUtil.getNameFromResponse(resp, true), conn); + if (resp2.getError() != null) { + err = String.format("Could not freeze Storpool volume %s. Error: %s", name, resp2.getError()); + } + } else { + err = answer != null ? answer.getDetails() : "Unknown error while downloading template. Null answer returned."; + } + } + } + if (err != null) { + resp = StorPoolUtil.volumeDelete(StorPoolUtil.getNameFromResponse(resp, true), conn); + if (resp.getError() != null) { + log.warn(String.format("Could not clean-up Storpool volume %s. Error: %s", name, resp.getError())); + } + } + } else if (srcType == DataObjectType.TEMPLATE && dstType == DataObjectType.VOLUME) { + // create volume from template on Storpool PRIMARY + TemplateInfo tinfo = (TemplateInfo)srcData; + + VolumeInfo vinfo = (VolumeInfo)dstData; + VMTemplateStoragePoolVO templStoragePoolVO = StorPoolHelper.findByPoolTemplate(vinfo.getPoolId(), tinfo.getId()); + final String parentName = templStoragePoolVO.getLocalDownloadPath() !=null ? StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getLocalDownloadPath(), true) : StorPoolStorageAdaptor.getVolumeNameFromPath(templStoragePoolVO.getInstallPath(), true); + final String name = vinfo.getUuid(); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + + Long snapshotSize = StorPoolUtil.snapshotSize(parentName, conn); + if (snapshotSize == null) { + err = String.format("Snapshot=%s does not exist on StorPool. Will recreate it first on primary", parentName); + vmTemplatePoolDao.remove(templStoragePoolVO.getId()); + } + if (err == null) { + long size = vinfo.getSize(); + if( size < snapshotSize ) + { + StorPoolUtil.spLog(String.format("provided size is too small for snapshot. Provided %d, snapshot %d. Using snapshot size", size, snapshotSize)); + size = snapshotSize; + } + StorPoolUtil.spLog(String.format("volume size is: %d", size)); + Long vmId = vinfo.getInstanceId(); + SpApiResponse resp = StorPoolUtil.volumeCreate(name, parentName, size, getVMInstanceUUID(vmId), + getVcPolicyTag(vmId), "volume", vinfo.getMaxIops(), conn); + if (resp.getError() == null) { + updateStoragePool(dstData.getDataStore().getId(), vinfo.getSize()); + + VolumeObjectTO to = (VolumeObjectTO) vinfo.getTO(); + to.setSize(vinfo.getSize()); + to.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + + answer = new CopyCmdAnswer(to); + } else { + err = String.format("Could not create Storpool volume %s. Error: %s", name, resp.getError()); + } + } + } else if (srcType == DataObjectType.VOLUME && dstType == DataObjectType.VOLUME) { + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.copyAsync src Data Store=%s", srcData.getDataStore().getDriver()); + VolumeInfo dstInfo = (VolumeInfo)dstData; + VolumeInfo srcInfo = (VolumeInfo) srcData; + + if( !(srcData.getDataStore().getDriver() instanceof StorPoolPrimaryDataStoreDriver ) ) { + // copy "VOLUME" to primary storage + String name = dstInfo.getUuid(); + Long size = dstInfo.getSize(); + if(size == null || size == 0) + size = 1L*1024*1024*1024; + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dstData.getDataStore().getUuid(), dstData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + Long vmId = srcInfo.getInstanceId(); + + SpApiResponse resp = StorPoolUtil.volumeCreate(name, null, size, getVMInstanceUUID(vmId), getVcPolicyTag(vmId), "volume", dstInfo.getMaxIops(), conn); + if (resp.getError() != null) { + err = String.format("Could not create Storpool volume for CS template %s. Error: %s", name, resp.getError()); + } else { + //updateVolume(dstData.getId()); + VolumeObjectTO dstTO = (VolumeObjectTO)dstData.getTO(); + dstTO.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false))); + dstTO.setSize(size); + + cmd = new StorPoolDownloadVolumeCommand(srcData.getTO(), dstTO, StorPoolHelper.getTimeout(StorPoolHelper.PrimaryStorageDownloadWait, configDao), VirtualMachineManager.ExecuteInSequence.value()); + + EndPoint ep = selector.select(srcData, dstData); + + if( ep == null) { + StorPoolUtil.spLog("select(srcData, dstData) returned NULL. trying srcOnly"); + ep = selector.select(srcData); // Storpool is zone + } + if (ep == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + StorPoolUtil.spLog("Sending command to %s", ep.getHostAddr()); + answer = ep.sendMessage(cmd); + + if (answer != null && answer.getResult()) { + // successfully downloaded volume to primary storage + } else { + err = answer != null ? answer.getDetails() : "Unknown error while downloading volume. Null answer returned."; + } + } + + if (err != null) { + SpApiResponse resp3 = StorPoolUtil.volumeDelete(name, conn); + if (resp3.getError() != null) { + log.warn(String.format("Could not clean-up Storpool volume %s. Error: %s", name, resp3.getError())); + } + } + } + } else { + // download volume - first copies to secondary + VolumeObjectTO srcTO = (VolumeObjectTO)srcData.getTO(); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc SRC path=%s ", srcTO.getPath()); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc DST canonicalName=%s ", dstData.getDataStore().getClass().getCanonicalName()); + PrimaryDataStoreTO checkStoragePool = dstData.getTO().getDataStore() instanceof PrimaryDataStoreTO ? (PrimaryDataStoreTO)dstData.getTO().getDataStore() : null; + final String name = StorPoolStorageAdaptor.getVolumeNameFromPath(srcTO.getPath(), true); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc DST tmpSnapName=%s ,srcUUID=%s", name, srcTO.getUuid()); + + if (checkStoragePool != null && checkStoragePool.getPoolType().equals(StoragePoolType.SharedMountPoint)) { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dstData.getDataStore().getUuid(), dstData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + String baseOn = StorPoolStorageAdaptor.getVolumeNameFromPath(srcTO.getPath(), true); + //uuid tag will be the same as srcData.uuid + String volumeName = srcData.getUuid(); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc volumeName=%s, baseOn=%s", volumeName, baseOn); + final SpApiResponse response = StorPoolUtil.volumeCopy(volumeName, baseOn, "volume", srcInfo.getMaxIops(), conn); + srcTO.setSize(srcData.getSize()); + srcTO.setPath(StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(response, false))); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc DST to=%s", srcTO); + + answer = new CopyCmdAnswer(srcTO); + } else { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(srcData.getDataStore().getUuid(), srcData.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + final SpApiResponse resp = StorPoolUtil.volumeSnapshot(name, srcTO.getUuid(), srcInfo.getInstanceId() != null ? getVMInstanceUUID(srcInfo.getInstanceId()) : null, "temporary", null, conn); + String snapshotName = StorPoolUtil.getSnapshotNameFromResponse(resp, true, StorPoolUtil.GLOBAL_ID); + if (resp.getError() == null) { + srcTO.setPath(StorPoolUtil.devPath( + StorPoolUtil.getSnapshotNameFromResponse(resp, false, StorPoolUtil.GLOBAL_ID))); + + cmd = new StorPoolCopyVolumeToSecondaryCommand(srcTO, dstData.getTO(), StorPoolHelper.getTimeout(StorPoolHelper.CopyVolumeWait, configDao), VirtualMachineManager.ExecuteInSequence.value()); + + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.copyAsnc command=%s ", cmd); + + try { + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(snapshotName, clusterDao); + EndPoint ep = clusterId != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, hostDao)) : selector.select(srcData, dstData); + StorPoolUtil.spLog("selector.select(srcData, dstData) ", ep); + if (ep == null) { + ep = selector.select(dstData); + StorPoolUtil.spLog("selector.select(srcData) ", ep); + } + + if (ep == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = ep.sendMessage(cmd); + StorPoolUtil.spLog("Answer: details=%s, result=%s", answer.getDetails(), answer.getResult()); + } + } catch (CloudRuntimeException e) { + err = e.getMessage(); + } + } else { + err = String.format("Failed to create temporary StorPool snapshot while trying to download volume %s (uuid %s). Error: %s", srcTO.getName(), srcTO.getUuid(), resp.getError()); + } + final SpApiResponse resp2 = StorPoolUtil.snapshotDelete(snapshotName, conn); + if (resp2.getError() != null) { + final String err2 = String.format("Failed to delete temporary StorPool snapshot %s. Error: %s", StorPoolUtil.getNameFromResponse(resp, true), resp2.getError()); + log.error(err2); + StorPoolUtil.spLog(err2); + } + } + } + } else { + err = String.format("Unsupported copy operation from %s (type %s) to %s (type %s)", srcData.getUuid(), srcType, dstData.getUuid(), dstType); + } + } catch (Exception e) { + StorPoolUtil.spLog("Caught exception: %s", e.toString()); + err = e.toString(); + } + + if (answer != null && !answer.getResult()) { + err = answer.getDetails(); + } + + if (err != null) { + StorPoolUtil.spLog("Failed due to %s", err); + + log.error(err); + answer = new Answer(cmd, false, err); + } + + CopyCommandResult res = new CopyCommandResult(null, answer); + res.setResult(err); + callback.complete(res); + } + + @Override + public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) { + String snapshotName = snapshot.getUuid(); + VolumeInfo vinfo = snapshot.getBaseVolume(); + String volumeName = StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true); + Long vmId = vinfo.getInstanceId(); + if (volumeName != null) { + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriver.takeSnapshot volumename=%s vmInstance=%s",volumeName, vmId); + } else { + throw new UnsupportedOperationException("The path should be: " + StorPoolUtil.SP_DEV_PATH); + } + + CreateObjectAnswer answer = null; + String err = null; + + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + + SpApiResponse resp = StorPoolUtil.volumeSnapshot(volumeName, snapshotName, vmId != null ? getVMInstanceUUID(vmId) : null, "snapshot", null, conn); + + if (resp.getError() != null) { + err = String.format("Could not snapshot StorPool volume %s. Error %s", volumeName, resp.getError()); + answer = new CreateObjectAnswer(err); + } else { + String name = StorPoolUtil.getSnapshotNameFromResponse(resp, true, StorPoolUtil.GLOBAL_ID); + SnapshotObjectTO snapTo = (SnapshotObjectTO)snapshot.getTO(); + snapTo.setPath(StorPoolUtil.devPath(name.split("~")[1])); + answer = new CreateObjectAnswer(snapTo); + StorPoolHelper.addSnapshotDetails(snapshot.getId(), snapshot.getUuid(), snapTo.getPath(), _snapshotDetailsDao); + //add primary storage of snapshot + StorPoolHelper.addSnapshotDetails(snapshot.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, String.valueOf(snapshot.getDataStore().getId()), _snapshotDetailsDao); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.takeSnapshot: snapshot: name=%s, uuid=%s, volume: name=%s, uuid=%s", name, snapshot.getUuid(), volumeName, vinfo.getUuid()); + } + } catch (Exception e) { + err = String.format("Could not take volume snapshot due to %s", e.getMessage()); + } + + CreateCmdResult res = new CreateCmdResult(null, answer); + res.setResult(err); + callback.complete(res); + } + + @Override + public void revertSnapshot(final SnapshotInfo snapshot, final SnapshotInfo snapshotOnPrimaryStore, final AsyncCompletionCallback callback) { + final VolumeInfo vinfo = snapshot.getBaseVolume(); + final String snapshotName = StorPoolHelper.getSnapshotName(snapshot.getId(), snapshot.getUuid(), snapshotDataStoreDao, _snapshotDetailsDao); + final String volumeName = StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true); + StorPoolUtil.spLog("StorpoolPrimaryDataStoreDriverImpl.revertSnapshot: snapshot: name=%s, uuid=%s, volume: name=%s, uuid=%s", snapshotName, snapshot.getUuid(), volumeName, vinfo.getUuid()); + String err = null; + + SpConnectionDesc conn = null; + try { + conn = StorPoolUtil.getSpConnection(vinfo.getDataStore().getUuid(), vinfo.getDataStore().getId(), storagePoolDetailsDao, primaryStoreDao); + } catch (Exception e) { + err = String.format("Could not revert volume due to %s", e.getMessage()); + completeResponse(err, callback); + return; + } + + VolumeDetailVO detail = volumeDetailsDao.findDetail(vinfo.getId(), StorPoolUtil.SP_PROVIDER_NAME); + if (detail != null) { + //Rename volume to its global id only if it was migrated from UUID to global id + SpApiResponse updateVolumeResponse = StorPoolUtil.volumeUpdateRename(StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true), "", StorPoolStorageAdaptor.getVolumeNameFromPath(detail.getValue(), false), conn); + + if (updateVolumeResponse.getError() != null) { + StorPoolUtil.spLog("Could not update StorPool's volume %s to it's globalId due to %s", StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true), updateVolumeResponse.getError().getDescr()); + err = String.format("Could not update StorPool's volume %s to it's globalId due to %s", StorPoolStorageAdaptor.getVolumeNameFromPath(vinfo.getPath(), true), updateVolumeResponse.getError().getDescr()); + completeResponse(err, callback); + return; + } + volumeDetailsDao.remove(detail.getId()); + } + + SpApiResponse resp = StorPoolUtil.detachAllForced(volumeName, false, conn); + if (resp.getError() != null) { + err = String.format("Could not detach StorPool volume %s due to %s", volumeName, resp.getError()); + completeResponse(err, callback); + return; + } + SpApiResponse response = StorPoolUtil.volumeRevert(volumeName, snapshotName, conn); + if (response.getError() != null) { + err = String.format( + "Could not revert StorPool volume %s to the %s snapshot: could not create the new volume: error %s", + volumeName, snapshotName, response.getError()); + completeResponse(err, callback); + return; + } + + if (vinfo.getMaxIops() != null) { + response = StorPoolUtil.volumeUpadateTags(volumeName, null, vinfo.getMaxIops(), conn, null); + if (response.getError() != null) { + StorPoolUtil.spLog("Volume was reverted successfully but max iops could not be set due to %s", response.getError().getDescr()); + } + } + + final VolumeObjectTO to = (VolumeObjectTO)vinfo.getTO(); + completeResponse(to, callback); + } + + private String getVcPolicyTag(Long vmId) { + ResourceTag resourceTag = vmId != null ? _resourceTagDao.findByKey(vmId, ResourceObjectType.UserVm, StorPoolUtil.SP_VC_POLICY) : null; + return resourceTag != null ? resourceTag.getValue() : ""; + } + + public void handleQualityOfServiceForVolumeMigration(VolumeInfo arg0, QualityOfServiceState arg1) { + StorPoolUtil.spLog("handleQualityOfServiceForVolumeMigration with volume name=%s", arg0.getName()); + } + + + public void copyAsync(DataObject srcData, DataObject destData, Host destHost, + AsyncCompletionCallback callback) { + copyAsync(srcData, destData, callback); + } + + public boolean canProvideStorageStats() { + return false; + } + + public Pair getStorageStats(StoragePool storagePool) { + return null; + } + + public boolean canProvideVolumeStats() { + return false; + } + + public Pair getVolumeStats(StoragePool storagePool, String volumeId) { + return null; + } + + public boolean canHostAccessStoragePool(Host host, StoragePool pool) { + return false; + } + + @Override + public boolean isVmInfoNeeded() { + return true; + } + + @Override + public void provideVmInfo(long vmId, long volumeId) { + VolumeVO volume = volumeDao.findById(volumeId); + StoragePoolVO poolVO = primaryStoreDao.findById(volume.getPoolId()); + if (poolVO != null) { + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(poolVO.getUuid(), poolVO.getId(), storagePoolDetailsDao, primaryStoreDao); + String volName = StorPoolStorageAdaptor.getVolumeNameFromPath(volume.getPath(), true); + VMInstanceVO userVM = vmInstanceDao.findById(vmId); + SpApiResponse resp = StorPoolUtil.volumeUpadateTags(volName, volume.getInstanceId() != null ? userVM.getUuid() : "", null, conn, getVcPolicyTag(vmId)); + if (resp.getError() != null) { + log.warn(String.format("Could not update VC policy tags of a volume with id [%s]", volume.getUuid())); + } + } catch (Exception e) { + log.warn(String.format("Could not update Virtual machine tags due to %s", e.getMessage())); + } + } + } + + @Override + public boolean isVmTagsNeeded(String tagKey) { + return tagKey != null && tagKey.equals(StorPoolUtil.SP_VC_POLICY); + } + + @Override + public void provideVmTags(long vmId, long volumeId, String tagValue) { + VolumeVO volume = volumeDao.findById(volumeId); + StoragePoolVO poolVO = primaryStoreDao.findById(volume.getPoolId()); + if (poolVO != null) { + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(poolVO.getUuid(), poolVO.getId(), storagePoolDetailsDao, primaryStoreDao); + String volName = StorPoolStorageAdaptor.getVolumeNameFromPath(volume.getPath(), true); + SpApiResponse resp = StorPoolUtil.volumeUpadateVCTags(volName, conn, getVcPolicyTag(vmId)); + if (resp.getError() != null) { + log.warn(String.format("Could not update VC policy tags of a volume with id [%s]", volume.getUuid())); + } + } catch (Exception e) { + log.warn(String.format("Could not update Virtual machine tags due to %s", e.getMessage())); + } + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java new file mode 100644 index 00000000000..8ed39327f39 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/StorPoolPrimaryDataStoreLifeCycle.java @@ -0,0 +1,321 @@ +/* + * 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.lifecycle; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +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.ZoneScope; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.StoragePoolInfo; +import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.ScopeType; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.StorageManager; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolAutomation; +import com.cloud.storage.VMTemplateDetailVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.storage.dao.VMTemplatePoolDao; +import com.cloud.utils.exception.CloudRuntimeException; + +public class StorPoolPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle { + private static final Logger log = Logger.getLogger(StorPoolPrimaryDataStoreLifeCycle.class); + + @Inject + protected PrimaryDataStoreHelper dataStoreHelper; + @Inject + protected StoragePoolAutomation storagePoolAutmation; + @Inject + private PrimaryDataStoreDao _primaryDataStoreDao; + @Inject + private ResourceManager resourceMgr; + @Inject + private StorageManager storageMgr; + @Inject + private SnapshotDao snapshotDao; + @Inject + private SnapshotDetailsDao snapshotDetailsDao; + @Inject + private VMTemplatePoolDao vmTemplatePoolDao; + @Inject + private VMTemplateDetailsDao vmTemplateDetailsDao; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + + @Override + public DataStore initialize(Map dsInfos) { + StorPoolUtil.spLog("initialize:"); + for (Map.Entry e: dsInfos.entrySet()) { + StorPoolUtil.spLog(" %s=%s", e.getKey(), e.getValue()); + } + StorPoolUtil.spLog(""); + + log.debug("initialize"); + + String name = (String)dsInfos.get("name"); + String providerName = (String)dsInfos.get("providerName"); + Long zoneId = (Long)dsInfos.get("zoneId"); + + String url = (String)dsInfos.get("url"); + SpConnectionDesc conn = new SpConnectionDesc(url); + if (conn.getHostPort() == null) + throw new IllegalArgumentException("No SP_API_HTTP"); + + if (conn.getAuthToken() == null) + throw new IllegalArgumentException("No SP_AUTH_TOKEN"); + + if (conn.getTemplateName() == null) + throw new IllegalArgumentException("No SP_TEMPLATE"); + + if (!StorPoolUtil.templateExists(conn)) { + throw new IllegalArgumentException("No such storpool template " + conn.getTemplateName() + " or credentials are invalid"); + } + + for (StoragePoolVO sp : _primaryDataStoreDao.findPoolsByProvider("StorPool")) { + List spDetails = storagePoolDetailsDao.listDetails(sp.getId()); + String host = null; + String template = null; + String authToken = null; + SpConnectionDesc old = null; + for (StoragePoolDetailVO storagePoolDetailVO : spDetails) { + switch (storagePoolDetailVO.getName()) { + case StorPoolUtil.SP_AUTH_TOKEN: + authToken = storagePoolDetailVO.getValue(); + break; + case StorPoolUtil.SP_HOST_PORT: + host = storagePoolDetailVO.getValue(); + break; + case StorPoolUtil.SP_TEMPLATE: + template = storagePoolDetailVO.getValue(); + break; + default: + break; + } + } + if (host != null && template != null && authToken != null) { + old = new SpConnectionDesc(host, authToken, template); + } else { + old = new SpConnectionDesc(sp.getUuid()); + } + if( old.getHostPort().equals(conn.getHostPort()) && old.getTemplateName().equals(conn.getTemplateName()) ) + throw new IllegalArgumentException("StorPool cluster and template already in use by pool " + sp.getName()); + } + + Long capacityBytes = (Long)dsInfos.get("capacityBytes"); + if (capacityBytes == null) { + throw new IllegalArgumentException("Capcity bytes is required"); + } + + String tags = (String)dsInfos.get("tags"); + if (tags == null || tags.isEmpty()) { + tags = name; + } + + @SuppressWarnings("unchecked") + Map details = (Map)dsInfos.get("details"); + details.put(StorPoolUtil.SP_AUTH_TOKEN, conn.getAuthToken()); + details.put(StorPoolUtil.SP_HOST_PORT, conn.getHostPort()); + details.put(StorPoolUtil.SP_TEMPLATE, conn.getTemplateName()); + + PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters(); + parameters.setName(name); + parameters.setUuid(conn.getTemplateName() + ";" + UUID.randomUUID().toString()); + parameters.setZoneId(zoneId); + parameters.setProviderName(providerName); + parameters.setType(StoragePoolType.SharedMountPoint); + parameters.setHypervisorType(HypervisorType.KVM); + parameters.setManaged(false); + parameters.setHost("n/a"); + parameters.setPort(0); + parameters.setPath(StorPoolUtil.SP_DEV_PATH); + parameters.setUsedBytes(0); + parameters.setCapacityBytes(capacityBytes); + parameters.setTags(tags); + parameters.setDetails(details); + + return dataStoreHelper.createPrimaryDataStore(parameters); + } + + @Override + public void updateStoragePool(StoragePool storagePool, Map details) { + StorPoolUtil.spLog("updateStoragePool:"); + for (Map.Entry e: details.entrySet()) { + StorPoolUtil.spLog(" %s=%s", e.getKey(), e.getValue()); + } + StorPoolUtil.spLog(""); + + log.debug("updateStoragePool"); + return; + } + @Override + public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) { + log.debug("attachHost"); + return true; + } + + @Override + public boolean attachCluster(DataStore store, ClusterScope scope) { + log.debug("attachCluster"); + if (!scope.getScopeType().equals(ScopeType.ZONE)) { + throw new UnsupportedOperationException("Only Zone-Wide scope is supported!"); + } + return true; + } + + @Override + public boolean attachZone(DataStore dataStore, ZoneScope scope, HypervisorType hypervisorType) { + log.debug("attachZone"); + + if (hypervisorType != HypervisorType.KVM) { + throw new UnsupportedOperationException("Only KVM hypervisors supported!"); + } + List kvmHosts = resourceMgr.listAllUpAndEnabledHostsInOneZoneByHypervisor(HypervisorType.KVM, scope.getScopeId()); + for (HostVO host : kvmHosts) { + try { + storageMgr.connectHostToSharedPool(host.getId(), dataStore.getId()); + } catch (Exception e) { + log.warn(String.format("Unable to establish a connection between host %s and pool %s due to %s", host, dataStore, e)); + } + } + dataStoreHelper.attachZone(dataStore, hypervisorType); + return true; + } + + @Override + public boolean maintain(DataStore dataStore) { + log.debug("maintain"); + + storagePoolAutmation.maintain(dataStore); + dataStoreHelper.maintain(dataStore); + return true; + } + + @Override + public boolean cancelMaintain(DataStore store) { + log.debug("cancelMaintain"); + + dataStoreHelper.cancelMaintain(store); + storagePoolAutmation.cancelMaintain(store); + return true; + } + + @Override + public boolean deleteDataStore(DataStore store) { + log.debug("deleteDataStore"); + long storagePoolId = store.getId(); + + List lstSnapshots = snapshotDao.listAll(); + + if (lstSnapshots != null) { + for (SnapshotVO snapshot : lstSnapshots) { + SnapshotDetailsVO snapshotDetails = snapshotDetailsDao.findDetail(snapshot.getId(), StorPoolUtil.SP_STORAGE_POOL_ID); + + // if this snapshot belongs to the storagePool that was passed in + if (snapshotDetails != null && snapshotDetails.getValue() != null && Long.parseLong(snapshotDetails.getValue()) == storagePoolId) { + throw new CloudRuntimeException("This primary storage cannot be deleted because it currently contains one or more snapshots."); + } + } + } + + List lstTemplateDetails = vmTemplateDetailsDao.listAll(); + + if (lstTemplateDetails != null) { + for (VMTemplateDetailVO vmTemplateDetailVO : lstTemplateDetails) { + if (vmTemplateDetailVO.getName().equals(StorPoolUtil.SP_STORAGE_POOL_ID) && Long.parseLong(vmTemplateDetailVO.getValue()) == storagePoolId) { + throw new CloudRuntimeException("This primary storage cannot be deleted because it currently contains one or more template snapshots."); + } + } + } + + List lstTemplatePoolRefs = vmTemplatePoolDao.listByPoolId(storagePoolId); + + SpConnectionDesc conn = null; + try { + conn = StorPoolUtil.getSpConnection(store.getUuid(), store.getId(), storagePoolDetailsDao, _primaryDataStoreDao); + } catch (CloudRuntimeException e) { + throw e; + } + + if (lstTemplatePoolRefs != null) { + for (VMTemplateStoragePoolVO templatePoolRef : lstTemplatePoolRefs) { + SpApiResponse resp = StorPoolUtil.snapshotDelete( + StorPoolStorageAdaptor.getVolumeNameFromPath(templatePoolRef.getLocalDownloadPath(), true), conn); + if (resp.getError() != null) { + throw new CloudRuntimeException(String.format("Could not delete StorPool's snapshot from template_spool_ref table due to %s", resp.getError())); + } + vmTemplatePoolDao.remove(templatePoolRef.getId()); + } + } + boolean isDeleted = dataStoreHelper.deletePrimaryDataStore(store); + if (isDeleted) { + List volumesOnHosts = storagePoolDetailsDao.listDetails(storagePoolId); + for (StoragePoolDetailVO storagePoolDetailVO : volumesOnHosts) { + if (storagePoolDetailVO.getValue() != null && storagePoolDetailVO.getName().contains(StorPoolUtil.SP_VOLUME_ON_CLUSTER)) { + StorPoolUtil.volumeDelete(StorPoolStorageAdaptor.getVolumeNameFromPath(storagePoolDetailVO.getValue(), true), conn); + } + } + storagePoolDetailsDao.removeDetails(storagePoolId); + } + return isDeleted; + } + + @Override + public boolean migrateToObjectStore(DataStore store) { + log.debug("migrateToObjectStore"); + return false; + } + + @Override + public void enableStoragePool(DataStore dataStore) { + log.debug("enableStoragePool"); + dataStoreHelper.enable(dataStore); + } + + @Override + public void disableStoragePool(DataStore dataStore) { + log.debug("disableStoragePool"); + dataStoreHelper.disable(dataStore); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java new file mode 100644 index 00000000000..4a5ce4012d7 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolHostListener.java @@ -0,0 +1,234 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolFeaturesAndFixes; +import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.log4j.Logger; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.StorPoolModifyStoragePoolAnswer; +import com.cloud.agent.api.storage.StorPoolModifyStoragePoolCommand; +import com.cloud.agent.manager.AgentAttache; +import com.cloud.alert.AlertManager; +import com.cloud.dc.ClusterDetailsDao; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.StorageConflictException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.utils.exception.CloudRuntimeException; + +public class StorPoolHostListener implements HypervisorHostListener { + private static final Logger log = Logger.getLogger(StorPoolHostListener .class); + + @Inject + private AgentManager agentMgr; + @Inject + private DataStoreManager dataStoreMgr; + @Inject + private AlertManager alertMgr; + @Inject + private StoragePoolHostDao storagePoolHostDao; + @Inject + private PrimaryDataStoreDao primaryStoreDao; + @Inject + private HostDao hostDao; + @Inject + private ClusterDao clusterDao; + @Inject + private ClusterDetailsDao clusterDetailsDao; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + + @Override + public boolean hostConnect(long hostId, long poolId) throws StorageConflictException { + //Will update storage pool's connection details if they aren't updated in DB, before connecting pool to host + StoragePoolVO poolVO = primaryStoreDao.findById(poolId); + + SpConnectionDesc conn = null; + try { + conn = StorPoolUtil.getSpConnection(poolVO.getUuid(), poolId, storagePoolDetailsDao, primaryStoreDao); + } catch (Exception e) { + return false; + } + + StoragePool pool = (StoragePool)this.dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + + HostVO host = hostDao.findById(hostId); + StoragePoolDetailVO volumeOnPool = verifyVolumeIsOnCluster(poolId, conn, host.getClusterId()); + if (volumeOnPool == null) { + return false; + } + + if (host.isInMaintenanceStates()) { + addModifyCommandToCommandsAllowedInMaintenanceMode(); + } + + List driverSupportedFeatures = StorPoolFeaturesAndFixes.getAllClassConstants(); + List driverFeaturesBeforeUpgrade = StorPoolHelper.listFeaturesUpdates(storagePoolDetailsDao, poolId); + boolean isCurrentVersionSupportsEverythingFromPrevious = StorPoolHelper.isPoolSupportsAllFunctionalityFromPreviousVersion(storagePoolDetailsDao, driverSupportedFeatures, driverFeaturesBeforeUpgrade, poolId); + if (!isCurrentVersionSupportsEverythingFromPrevious) { + String msg = "The current StorPool driver does not support all functionality from the one before upgrade to CS"; + StorPoolUtil.spLog("Storage pool [%s] is not connected to host [%s] because the functionality after the upgrade is not full", + poolId, hostId); + alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_HOST, pool.getDataCenterId(), pool.getPodId(), msg, msg); + return false; + } + + StorPoolModifyStoragePoolCommand cmd = new StorPoolModifyStoragePoolCommand(true, pool, volumeOnPool.getValue()); + final Answer answer = agentMgr.easySend(hostId, cmd); + + StoragePoolHostVO poolHost = storagePoolHostDao.findByPoolHost(pool.getId(), hostId); + + if (answer == null) { + throw new CloudRuntimeException("Unable to get an answer to the modify storage pool command" + pool.getId()); + } + + if (!answer.getResult()) { + if (answer.getDetails() != null) { + if (answer.getDetails().equals("objectDoesNotExist")) { + StorPoolUtil.volumeDelete(StorPoolStorageAdaptor.getVolumeNameFromPath(volumeOnPool.getValue(), true), conn); + storagePoolDetailsDao.remove(volumeOnPool.getId()); + return false; + } else if (answer.getDetails().equals("spNotFound")) { + return false; + } + + } + String msg = "Unable to attach storage pool" + poolId + " to the host" + hostId; + alertMgr.sendAlert(AlertManager.AlertType.ALERT_TYPE_HOST, pool.getDataCenterId(), pool.getPodId(), msg, msg); + throw new CloudRuntimeException("Unable establish connection from storage head to storage pool " + pool.getId() + " due to " + answer.getDetails() + + pool.getId()); + } + + StorPoolUtil.spLog("hostConnect: hostId=%d, poolId=%d", hostId, poolId); + + StorPoolModifyStoragePoolAnswer mspAnswer = (StorPoolModifyStoragePoolAnswer)answer; + if (mspAnswer.getLocalDatastoreName() != null && pool.isShared()) { + String datastoreName = mspAnswer.getLocalDatastoreName(); + List localStoragePools = primaryStoreDao.listLocalStoragePoolByPath(pool.getDataCenterId(), datastoreName); + for (StoragePoolVO localStoragePool : localStoragePools) { + if (datastoreName.equals(localStoragePool.getPath())) { + log.warn("Storage pool: " + pool.getId() + " has already been added as local storage: " + localStoragePool.getName()); + throw new StorageConflictException("Cannot add shared storage pool: " + pool.getId() + " because it has already been added as local storage:" + + localStoragePool.getName()); + } + } + } + + if (poolHost == null) { + poolHost = new StoragePoolHostVO(pool.getId(), hostId, mspAnswer.getPoolInfo().getLocalPath().replaceAll("//", "/")); + storagePoolHostDao.persist(poolHost); + } else { + poolHost.setLocalPath(mspAnswer.getPoolInfo().getLocalPath().replaceAll("//", "/")); + } + + StorPoolHelper.setSpClusterIdIfNeeded(hostId, mspAnswer.getClusterId(), clusterDao, hostDao, clusterDetailsDao); + + log.info("Connection established between storage pool " + pool + " and host " + hostId); + return true; + } + + private synchronized StoragePoolDetailVO verifyVolumeIsOnCluster(long poolId, SpConnectionDesc conn, long clusterId) { + StoragePoolDetailVO volumeOnPool = storagePoolDetailsDao.findDetail(poolId, StorPoolUtil.SP_VOLUME_ON_CLUSTER + "-" + clusterId); + if (volumeOnPool == null) { + SpApiResponse resp = StorPoolUtil.volumeCreate(conn); + if (resp.getError() != null) { + return volumeOnPool; + } + String volumeName = StorPoolUtil.getNameFromResponse(resp, false); + volumeOnPool = new StoragePoolDetailVO(poolId, StorPoolUtil.SP_VOLUME_ON_CLUSTER + "-" + clusterId, StorPoolUtil.devPath(volumeName), false); + storagePoolDetailsDao.persist(volumeOnPool); + } + return volumeOnPool; + } + + @Override + public boolean hostAdded(long hostId) { + return true; + } + + @Override + public boolean hostDisconnected(long hostId, long poolId) { + StorPoolUtil.spLog("hostDisconnected: hostId=%d, poolId=%d", hostId, poolId); + return true; + } + + @Override + public boolean hostAboutToBeRemoved(long hostId) { + return true; + } + + @Override + public boolean hostRemoved(long hostId, long clusterId) { + return true; + } + + //workaround: we need this "hack" to add our command StorPoolModifyStoragePoolCommand in AgentAttache.s_commandsAllowedInMaintenanceMode + //which checks the allowed commands when the host is in maintenance mode + private void addModifyCommandToCommandsAllowedInMaintenanceMode() { + + Class cls = AgentAttache.class; + try { + Field field = cls.getDeclaredField("s_commandsAllowedInMaintenanceMode"); + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + List allowedCmdsInMaintenance = new ArrayList(Arrays.asList(AgentAttache.s_commandsAllowedInMaintenanceMode)); + allowedCmdsInMaintenance.add(StorPoolModifyStoragePoolCommand.class.toString()); + String[] allowedCmdsInMaintenanceNew = new String[allowedCmdsInMaintenance.size()]; + allowedCmdsInMaintenance.toArray(allowedCmdsInMaintenanceNew); + Arrays.sort(allowedCmdsInMaintenanceNew); + field.set(null, allowedCmdsInMaintenanceNew); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + String err = "Could not add StorPoolModifyStoragePoolCommand to s_commandsAllowedInMaintenanceMode array due to: %s"; + StorPoolUtil.spLog(err, e.getMessage()); + log.warn(String.format(err, e.getMessage())); + } + } + + @Override + public boolean hostEnabled(long hostId) { + return true; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolPrimaryDataStoreProvider.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolPrimaryDataStoreProvider.java new file mode 100644 index 00000000000..892d95b8c92 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/provider/StorPoolPrimaryDataStoreProvider.java @@ -0,0 +1,78 @@ +/* + * 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.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle; +import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreProvider; +import org.apache.cloudstack.storage.datastore.driver.StorPoolPrimaryDataStoreDriver; +import org.apache.cloudstack.storage.datastore.lifecycle.StorPoolPrimaryDataStoreLifeCycle; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; + +import com.cloud.utils.component.ComponentContext; + +public class StorPoolPrimaryDataStoreProvider implements PrimaryDataStoreProvider { + + protected DataStoreLifeCycle lifecycle; + protected DataStoreDriver driver; + protected HypervisorHostListener listener; + + StorPoolPrimaryDataStoreProvider() { + } + + @Override + public String getName() { + return StorPoolUtil.SP_PROVIDER_NAME; + } + + @Override + public DataStoreLifeCycle getDataStoreLifeCycle() { + return lifecycle; + } + + @Override + public DataStoreDriver getDataStoreDriver() { + return driver; + } + + @Override + public HypervisorHostListener getHostListener() { + return listener; + } + + @Override + public boolean configure(Map params) { + lifecycle = ComponentContext.inject(StorPoolPrimaryDataStoreLifeCycle.class); + driver = ComponentContext.inject(StorPoolPrimaryDataStoreDriver.class); + listener = ComponentContext.inject(StorPoolHostListener.class); + return true; + } + + @Override + public Set getTypes() { + Set types = new HashSet(); + types.add(DataStoreProviderType.PRIMARY); + return types; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolFeaturesAndFixes.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolFeaturesAndFixes.java new file mode 100644 index 00000000000..f87f36c6b48 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolFeaturesAndFixes.java @@ -0,0 +1,40 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +public class StorPoolFeaturesAndFixes { + + public static List getAllClassConstants() { + List constants = new ArrayList<>(); + + for (Field field : StorPoolFeaturesAndFixes.class.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) { + constants.add(field.getName()); + } + } + return constants; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java new file mode 100644 index 00000000000..9395f134fe1 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolHelper.java @@ -0,0 +1,298 @@ +/* + * 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.io.IOException; +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.log4j.Appender; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.RollingFileAppender; + +import com.cloud.dc.ClusterDetailsDao; +import com.cloud.dc.ClusterDetailsVO; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.server.ResourceTag; +import com.cloud.server.ResourceTag.ResourceObjectType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.tags.dao.ResourceTagDao; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.db.QueryBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; + +public class StorPoolHelper { + + private static final String UPDATE_SNAPSHOT_DETAILS_VALUE = "UPDATE `cloud`.`snapshot_details` SET value=? WHERE id=?"; + private static final String UPDATE_VOLUME_DETAILS_NAME = "UPDATE `cloud`.`volume_details` SET name=? WHERE id=?"; + public static final String PrimaryStorageDownloadWait = "primary.storage.download.wait"; + public static final String CopyVolumeWait = "copy.volume.wait"; + public static final String BackupSnapshotWait = "backup.snapshot.wait"; + + public static void updateVolumeInfo(VolumeObjectTO volumeObjectTO, Long size, SpApiResponse resp, + VolumeDao volumeDao) { + String volumePath = StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(resp, false)); + VolumeVO volume = volumeDao.findById(volumeObjectTO.getId()); + if (volume != null) { + volumeObjectTO.setSize(size); + volumeObjectTO.setPath(volumePath); + volume.setSize(size); + volume.setPath(volumePath); + volumeDao.update(volumeObjectTO.getId(), volume); + } + } + + // If volume is deleted, CloudStack removes records of snapshots created on Primary storage only in database. + // That's why we keep information in snapshot_details table, about all snapshots created on StorPool and we can operate with them + public static void addSnapshotDetails(final Long id, final String uuid, final String snapshotName, + SnapshotDetailsDao snapshotDetailsDao) { + SnapshotDetailsVO details = new SnapshotDetailsVO(id, uuid, snapshotName, false); + snapshotDetailsDao.persist(details); + } + + public static String getSnapshotName(Long snapshotId, String snapshotUuid, SnapshotDataStoreDao snapshotStoreDao, + SnapshotDetailsDao snapshotDetailsDao) { + + SnapshotDetailsVO snapshotDetails = snapshotDetailsDao.findDetail(snapshotId, snapshotUuid); + + if (snapshotDetails != null) { + return StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDetails.getValue(), true); + } else { + List snapshots = snapshotStoreDao.findBySnapshotId(snapshotId); + if (!CollectionUtils.isEmpty(snapshots)) { + for (SnapshotDataStoreVO snapshotDataStoreVO : snapshots) { + String name = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDataStoreVO.getInstallPath(), true); + if (name == null) { + continue; + } else { + addSnapshotDetails(snapshotId, snapshotUuid, snapshotDataStoreVO.getInstallPath(), snapshotDetailsDao); + return name; + } + } + } + } + return null; + } + + public static void updateSnapshotDetailsValue(Long id, String valueOrName, String snapshotOrVolume) { + TransactionLegacy txn = TransactionLegacy.currentTxn(); + PreparedStatement pstmt = null; + try { + String sql = null; + if (snapshotOrVolume.equals("snapshot")) { + sql = UPDATE_SNAPSHOT_DETAILS_VALUE; + } else if (snapshotOrVolume.equals("volume")) { + sql = UPDATE_VOLUME_DETAILS_NAME; + } else { + StorPoolUtil.spLog("Could not update snapshot detail with id=%s", id); + } + if (sql != null) { + pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.setString(1, valueOrName); + pstmt.setLong(2, id); + pstmt.executeUpdate(); + txn.commit(); + } + } catch (Exception e) { + txn.rollback(); + StorPoolUtil.spLog("Could not update snapshot detail with id=%s", id); + } + } + + public static String getVcPolicyTag(Long vmId, ResourceTagDao resourceTagDao) { + if (vmId != null) { + ResourceTag tag = resourceTagDao.findByKey(vmId, ResourceObjectType.UserVm, StorPoolUtil.SP_VC_POLICY); + if (tag != null) { + return tag.getValue(); + } + } + return null; + } + + public static String getVMInstanceUUID(Long id, VMInstanceDao vmInstanceDao) { + if (id != null) { + VMInstanceVO vmInstance = vmInstanceDao.findById(id); + if (vmInstance != null) { + return vmInstance.getUuid(); + } + } + return null; + } + + public static Map addStorPoolTags(String name, String vmUuid, String csTag, String vcPolicy) { + Map tags = new HashMap<>(); + tags.put("uuid", name); + tags.put("cvm", vmUuid); + tags.put(StorPoolUtil.SP_VC_POLICY, vcPolicy); + if (csTag != null) { + tags.put("cs", csTag); + } + return tags; + } + + // Initialize custom logger for updated volume and snapshots + public static void appendLogger(Logger log, String filePath, String kindOfLog) { + Appender appender = null; + PatternLayout patternLayout = new PatternLayout(); + patternLayout.setConversionPattern("%d{YYYY-MM-dd HH:mm:ss.SSS} %m%n"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + String path = filePath + "-" + sdf.format(timestamp) + ".log"; + try { + appender = new RollingFileAppender(patternLayout, path); + log.setAdditivity(false); + log.addAppender(appender); + } catch (IOException e) { + e.printStackTrace(); + } + if (kindOfLog.equals("update")) { + StorPoolUtil.spLog( + "You can find information about volumes and snapshots, which will be updated in Database with their globalIs in %s log file", + path); + } else if (kindOfLog.equals("abandon")) { + StorPoolUtil.spLog( + "You can find information about volumes and snapshots, for which CloudStack doesn't have information in %s log file", + path); + } + } + + public static void setSpClusterIdIfNeeded(long hostId, String clusterId, ClusterDao clusterDao, HostDao hostDao, + ClusterDetailsDao clusterDetails) { + HostVO host = hostDao.findById(hostId); + if (host != null && host.getClusterId() != null) { + ClusterVO cluster = clusterDao.findById(host.getClusterId()); + ClusterDetailsVO clusterDetailsVo = clusterDetails.findDetail(cluster.getId(), + StorPoolConfigurationManager.StorPoolClusterId.key()); + if (clusterDetailsVo == null) { + clusterDetails.persist( + new ClusterDetailsVO(cluster.getId(), StorPoolConfigurationManager.StorPoolClusterId.key(), clusterId)); + } else if (clusterDetailsVo.getValue() == null || !clusterDetailsVo.getValue().equals(clusterId)) { + clusterDetailsVo.setValue(clusterId); + clusterDetails.update(clusterDetailsVo.getId(), clusterDetailsVo); + } + } + } + + public static Long findClusterIdByGlobalId(String globalId, ClusterDao clusterDao) { + List clusterVo = clusterDao.listAll(); + if (clusterVo.size() == 1) { + StorPoolUtil.spLog("There is only one cluster, sending backup to secondary command"); + return null; + } + for (ClusterVO clusterVO2 : clusterVo) { + if (globalId != null && StorPoolConfigurationManager.StorPoolClusterId.valueIn(clusterVO2.getId()) != null + && globalId.contains(StorPoolConfigurationManager.StorPoolClusterId.valueIn(clusterVO2.getId()).toString())) { + StorPoolUtil.spLog("Found cluster with id=%s for object with globalId=%s", clusterVO2.getId(), + globalId); + return clusterVO2.getId(); + } + } + throw new CloudRuntimeException( + "Could not find the right clusterId. to send command. To use snapshot backup to secondary for each CloudStack cluster in its settings set the value of StorPool's cluster-id in \"sp.cluster.id\"."); + } + + public static HostVO findHostByCluster(Long clusterId, HostDao hostDao) { + List host = hostDao.findByClusterId(clusterId); + return host != null ? host.get(0) : null; + } + + public static int getTimeout(String cfg, ConfigurationDao configDao) { + final ConfigurationVO value = configDao.findByName(cfg); + return NumbersUtil.parseInt(value.getValue(), Integer.parseInt(value.getDefaultValue())); + } + + public static VMTemplateStoragePoolVO findByPoolTemplate(long poolId, long templateId) { + QueryBuilder sc = QueryBuilder.create(VMTemplateStoragePoolVO.class); + sc.and(sc.entity().getPoolId(), Op.EQ, poolId); + sc.and(sc.entity().getTemplateId(), Op.EQ, templateId); + return sc.find(); + } + + public static void updateVmStoreTemplate(Long id, DataStoreRole role, String path, + TemplateDataStoreDao templStoreDao) { + TemplateDataStoreVO templ = templStoreDao.findByTemplate(id, role); + templ.setLocalDownloadPath(path); + templStoreDao.persist(templ); + } + + public static List listFeaturesUpdates(StoragePoolDetailsDao storagePoolDetails, long poolId) { + SearchBuilder sb = storagePoolDetails.createSearchBuilder(); + sb.and("pool_id", sb.entity().getResourceId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE); + SearchCriteria sc = sb.create(); + sc.setParameters("pool_id", poolId); + sc.setParameters("name", "SP-FEATURE" + "%"); + return storagePoolDetails.search(sc, null); + } + + public static boolean isPoolSupportsAllFunctionalityFromPreviousVersion(StoragePoolDetailsDao storagePoolDetails, List currentPluginFeatures, List poolFeaturesBeforeUpgrade, long poolId) { + if (CollectionUtils.isEmpty(currentPluginFeatures) && CollectionUtils.isEmpty(poolFeaturesBeforeUpgrade)) { + return true; + } + + List poolDetails = poolFeaturesBeforeUpgrade.stream().map(StoragePoolDetailVO::getName).collect(Collectors.toList()); + List detailsNotContainedInCurrent = new ArrayList<>(CollectionUtils.removeAll(poolDetails, currentPluginFeatures)); + List detailsNotContainedInDataBase = new ArrayList<>(CollectionUtils.removeAll(currentPluginFeatures, poolDetails)); + if (!CollectionUtils.isEmpty(detailsNotContainedInCurrent)) { + return false; + } else if (!CollectionUtils.isEmpty(detailsNotContainedInDataBase)) { + for (String features : detailsNotContainedInDataBase) { + StoragePoolDetailVO storageNewFeatures = new StoragePoolDetailVO(poolId, features, features, false); + storagePoolDetails.persist(storageNewFeatures); + } + return true; + } + return true; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java new file mode 100644 index 00000000000..484a9b98931 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/datastore/util/StorPoolUtil.java @@ -0,0 +1,609 @@ +/* + * 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.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.log4j.Logger; + +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +public class StorPoolUtil { + private static final Logger log = Logger.getLogger(StorPoolUtil.class); + + private static final File spLogFile = new File("/var/log/cloudstack/management/storpool-plugin.log"); + private static PrintWriter spLogPrinterWriter = spLogFileInitialize(); + + private static PrintWriter spLogFileInitialize() { + try { + log.info("INITIALIZE SP-LOG_FILE"); + if (spLogFile.exists()) { + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + final Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + final File spLogFileRename = new File(spLogFile + "-" + sdf.format(timestamp)); + final boolean ret = spLogFile.renameTo(spLogFileRename); + if (!ret) { + log.warn("Unable to rename" + spLogFile + " to " + spLogFileRename); + } else { + log.debug("Renamed " + spLogFile + " to " + spLogFileRename); + } + } else { + spLogFile.getParentFile().mkdirs(); + } + return new PrintWriter(spLogFile); + } catch (Exception e) { + log.info("INITIALIZE SP-LOG_FILE: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + public static void spLog(String fmt, Object... args) { + String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,ms").format(Calendar.getInstance().getTime()); + spLogPrinterWriter.println(String.format(timeStamp + " " + fmt, args)); + spLogPrinterWriter.flush(); + if (spLogFile.length() > 107374182400L) { + spLogPrinterWriter.close(); + spLogPrinterWriter = spLogFileInitialize(); + } + } + + public static final String SP_PROVIDER_NAME = "StorPool"; + public static final String SP_DEV_PATH = "/dev/storpool-byid/"; + public static final String SP_OLD_PATH = "/dev/storpool/"; + public static final String SP_VC_POLICY = "vc-policy"; + public static final String GLOBAL_ID = "snapshotGlobalId"; + public static final String UPDATED_DETAIL = "renamed"; + public static final String SP_STORAGE_POOL_ID = "spStoragePoolId"; + + public static final String SP_HOST_PORT = "SP_API_HTTP_HOST"; + + public static final String SP_TEMPLATE = "SP_TEMPLATE"; + + public static final String SP_AUTH_TOKEN = "SP_AUTH_TOKEN"; + + public static final String SP_VOLUME_ON_CLUSTER = "SP_VOLUME_ON_CLUSTER"; + + public static enum StorpoolRights { + RO("ro"), RW("rw"), DETACH("detach"); + + private final String name; + + private StorpoolRights(String name) { + this.name = name; + } + + public String toString() { + return name; + } + } + + public static final class SpApiError { + private String name; + private String descr; + + public SpApiError() { + } + + public String getName() { + return this.name; + } + + public String getDescr() { + return this.descr; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescr(String descr) { + this.descr = descr; + } + + public String toString() { + return String.format("%s: %s", name, descr); + } + } + + public static class SpConnectionDesc { + private String hostPort; + private String authToken; + private String templateName; + + public SpConnectionDesc(String url) { + String[] urlSplit = url.split(";"); + if (urlSplit.length == 1 && !urlSplit[0].contains("=")) { + this.templateName = url; + + Script sc = new Script("storpool_confget", 0, log); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + + final String err = sc.execute(parser); + if (err != null) { + final String errMsg = String.format("Could not execute storpool_confget. Error: %s", err); + log.warn(errMsg); + throw new CloudRuntimeException(errMsg); + } + + String SP_API_HOST = null; + String SP_API_PORT = null; + + for (String line : parser.getLines().split("\n")) { + String[] toks = line.split("="); + if (toks.length != 2) { + continue; + } + + switch (toks[0]) { + case "SP_API_HTTP_HOST": + SP_API_HOST = toks[1]; + break; + + case "SP_API_HTTP_PORT": + SP_API_PORT = toks[1]; + break; + + case "SP_AUTH_TOKEN": + this.authToken = toks[1]; + break; + } + } + + if (SP_API_HOST == null) + throw new CloudRuntimeException("Invalid StorPool config. Missing SP_API_HTTP_HOST"); + if (SP_API_PORT == null) + throw new CloudRuntimeException("Invalid StorPool config. Missing SP_API_HTTP_PORT"); + if (this.authToken == null) + throw new CloudRuntimeException("Invalid StorPool config. Missing SP_AUTH_TOKEN"); + + this.hostPort = SP_API_HOST + ":" + SP_API_PORT; + } else { + for (String kv : urlSplit) { + String[] toks = kv.split("="); + if (toks.length != 2) + continue; + switch (toks[0]) { + case "SP_API_HTTP": + this.hostPort = toks[1]; + break; + + case "SP_AUTH_TOKEN": + this.authToken = toks[1]; + break; + + case "SP_TEMPLATE": + this.templateName = toks[1]; + break; + } + } + } + } + + public SpConnectionDesc(String host, String authToken2, String templateName2) { + this.hostPort = host; + this.authToken = authToken2; + this.templateName = templateName2; + } + + public String getHostPort() { + return this.hostPort; + } + + public String getAuthToken() { + return this.authToken; + } + + public String getTemplateName() { + return this.templateName; + } + } + + public static SpConnectionDesc getSpConnection(String url, long poolId, StoragePoolDetailsDao poolDetails, + PrimaryDataStoreDao storagePool) { + boolean isAlternateEndpointEnabled = StorPoolConfigurationManager.AlternativeEndPointEnabled.valueIn(poolId); + if (isAlternateEndpointEnabled) { + String alternateEndpoint = StorPoolConfigurationManager.AlternativeEndpoint.valueIn(poolId); + if (StringUtils.isNotEmpty(alternateEndpoint)) { + return new SpConnectionDesc(alternateEndpoint); + } else { + throw new CloudRuntimeException(String.format("Using an alternative endpoint of StorPool primary storage with id [%s] is enabled but no endpoint URL is provided", poolId)); + } + } + List details = poolDetails.listDetails(poolId); + String host = null; + String authToken = null; + String templateName = null; + for (StoragePoolDetailVO storagePoolDetailVO : details) { + switch (storagePoolDetailVO.getName()) { + case SP_HOST_PORT: + host = storagePoolDetailVO.getValue(); + break; + case SP_AUTH_TOKEN: + authToken = storagePoolDetailVO.getValue(); + break; + case SP_TEMPLATE: + templateName = storagePoolDetailVO.getValue(); + break; + } + } + if (host != null && authToken != null && templateName != null) { + return new SpConnectionDesc(host, authToken, templateName); + } else { + return updateStorageAndStorageDetails(url, poolId, poolDetails, storagePool); + } + } + + private static SpConnectionDesc updateStorageAndStorageDetails(String url, long poolId, + StoragePoolDetailsDao poolDetails, PrimaryDataStoreDao storagePool) { + SpConnectionDesc conn = new SpConnectionDesc(url); + poolDetails.persist(new StoragePoolDetailVO(poolId, SP_HOST_PORT, conn.getHostPort(), false)); + poolDetails.persist(new StoragePoolDetailVO(poolId, SP_AUTH_TOKEN, conn.getAuthToken(), false)); + poolDetails.persist(new StoragePoolDetailVO(poolId, SP_TEMPLATE, conn.getTemplateName(), false)); + StoragePoolVO pool = storagePool.findById(poolId); + pool.setUuid(conn.getTemplateName() + ";" + UUID.randomUUID().toString()); + storagePool.update(poolId, pool); + StorPoolUtil.spLog( + "Storage pool with id=%s and template's name=%s was updated and its connection details are hidden from UI.", + pool.getId(), conn.getTemplateName()); + return conn; + } + + public static class SpApiResponse { + private SpApiError error; + public JsonElement fullJson; + + public SpApiResponse() { + } + + public SpApiError getError() { + return this.error; + } + + public void setError(SpApiError error) { + this.error = error; + } + } + + public static String devPath(final String name) { + return String.format("%s%s", SP_DEV_PATH, name); + } + + private static SpApiResponse spApiRequest(HttpRequestBase req, String query, SpConnectionDesc conn) { + + if (conn == null) + conn = new SpConnectionDesc(""); + + if (conn.getHostPort() == null) { + throw new CloudRuntimeException("Invalid StorPool config. Missing SP_API_HTTP_HOST"); + } + + if (conn.getAuthToken() == null) { + throw new CloudRuntimeException("Invalid StorPool config. Missing SP_AUTH_TOKEN"); + } + + try (CloseableHttpClient httpclient = HttpClientBuilder.create().build()) { + final String qry = String.format("http://%s/ctrl/1.0/%s", conn.getHostPort(), query); + final URI uri = new URI(qry); + + req.setURI(uri); + req.addHeader("Authorization", String.format("Storpool v1:%s", conn.getAuthToken())); + + final HttpResponse resp = httpclient.execute(req); + + Gson gson = new Gson(); + BufferedReader br = new BufferedReader(new InputStreamReader(resp.getEntity().getContent())); + + JsonElement el = new JsonParser().parse(br); + + SpApiResponse apiResp = gson.fromJson(el, SpApiResponse.class); + apiResp.fullJson = el; + return apiResp; + } catch (UnsupportedEncodingException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } catch (ClientProtocolException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } catch (IOException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } catch (URISyntaxException ex) { + throw new CloudRuntimeException(ex.getMessage()); + } + } + + private static SpApiResponse GET(String query, SpConnectionDesc conn) { + return spApiRequest(new HttpGet(), query, conn); + } + + private static SpApiResponse POST(String query, Object json, SpConnectionDesc conn) { + HttpPost req = new HttpPost(); + if (json != null) { + Gson gson = new Gson(); + String js = gson.toJson(json); + StringEntity input = new StringEntity(js, ContentType.APPLICATION_JSON); + log.info("Request:" + js); + req.setEntity(input); + } + + return spApiRequest(req, query, conn); + } + + public static boolean templateExists(SpConnectionDesc conn) { + SpApiResponse resp = GET("VolumeTemplateDescribe/" + conn.getTemplateName(), conn); + return resp.getError() == null ? true : objectExists(resp.getError()); + } + + public static boolean snapshotExists(final String name, SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/Snapshot/" + name, conn); + return resp.getError() == null ? true : objectExists(resp.getError()); + } + + public static JsonArray snapshotsList(SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/SnapshotsList", conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonArray data = obj.getAsJsonArray("data"); + return data; + } + + public static JsonArray volumesList(SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/VolumesList", conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonArray data = obj.getAsJsonArray("data"); + return data; + } + + private static boolean objectExists(SpApiError err) { + if (!err.getName().equals("objectDoesNotExist")) { + throw new CloudRuntimeException(err.getDescr()); + } + return false; + } + + public static Long snapshotSize(final String name, SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/Snapshot/" + name, conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + + if (resp.getError() != null && !objectExists(resp.getError())) { + return null; + } + JsonObject data = obj.getAsJsonArray("data").get(0).getAsJsonObject(); + return data.getAsJsonPrimitive("size").getAsLong(); + } + + public static String getSnapshotClusterID(String name, SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/Snapshot/" + name, conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + + JsonObject data = obj.getAsJsonArray("data").get(0).getAsJsonObject(); + JsonPrimitive clusterId = data.getAsJsonPrimitive("clusterId"); + return clusterId != null ? clusterId.getAsString() : null; + } + + public static String getVolumeClusterID(String name, SpConnectionDesc conn) { + SpApiResponse resp = GET("MultiCluster/Volume/" + name, conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + + JsonObject data = obj.getAsJsonArray("data").get(0).getAsJsonObject(); + JsonPrimitive clusterId = data.getAsJsonPrimitive("clusterId"); + return clusterId != null ? clusterId.getAsString() : null; + } + + public static SpApiResponse volumeCreate(final String name, final String parentName, final Long size, String vmUuid, + String vcPolicy, String csTag, Long iops, SpConnectionDesc conn) { + Map json = new LinkedHashMap<>(); + json.put("name", ""); + json.put("iops", iops); + json.put("parent", parentName); + json.put("size", size); + json.put("template", conn.getTemplateName()); + Map tags = StorPoolHelper.addStorPoolTags(name, vmUuid, csTag, vcPolicy); + json.put("tags", tags); + return POST("MultiCluster/VolumeCreate", json, conn); + } + + public static SpApiResponse volumeCreate(SpConnectionDesc conn) { + Map json = new LinkedHashMap<>(); + json.put("name", ""); + json.put("size", 512); + json.put("template", conn.getTemplateName()); + Map tags = new HashMap<>(); + tags.put("cs", "check-volume-is-on-host"); + json.put("tags", tags); + return POST("MultiCluster/VolumeCreate", json, conn); + } + + public static SpApiResponse volumeCopy(final String name, final String baseOn, String csTag, Long iops, + SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("baseOn", baseOn); + if (iops != null) { + json.put("iops", iops); + } + json.put("template", conn.getTemplateName()); + Map tags = StorPoolHelper.addStorPoolTags(name, null, csTag, null); + json.put("tags", tags); + return POST("MultiCluster/VolumeCreate", json, conn); + } + + public static SpApiResponse volumeUpdateRename(final String name, String newName, String uuid, + SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("rename", newName); + Map tags = new HashMap<>(); + tags.put("uuid", uuid); + json.put("tags", tags); + + return POST("MultiCluster/VolumeUpdate/" + name, json, conn); + } + + public static SpApiResponse volumeUpdate(final String name, final Long newSize, final Boolean shrinkOk, Long iops, + SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("iops", iops); + json.put("size", newSize); + json.put("shrinkOk", shrinkOk); + + return POST("MultiCluster/VolumeUpdate/" + name, json, conn); + } + + public static SpApiResponse volumeUpadateTags(final String name, final String uuid, Long iops, + SpConnectionDesc conn, String vcPolicy) { + Map json = new HashMap<>(); + Map tags = StorPoolHelper.addStorPoolTags(null, uuid, null, vcPolicy); + json.put("iops", iops); + json.put("tags", tags); + return POST("MultiCluster/VolumeUpdate/" + name, json, conn); + } + + public static SpApiResponse volumeUpadateCvmTags(final String name, final String uuid, SpConnectionDesc conn) { + Map json = new HashMap<>(); + Map tags = StorPoolHelper.addStorPoolTags(null, uuid, null, null); + json.put("tags", tags); + return POST("MultiCluster/VolumeUpdate/" + name, json, conn); + } + + public static SpApiResponse volumeUpadateVCTags(final String name, SpConnectionDesc conn, String vcPolicy) { + Map json = new HashMap<>(); + Map tags = StorPoolHelper.addStorPoolTags(null, null, null, vcPolicy); + json.put("tags", tags); + return POST("MultiCluster/VolumeUpdate/" + name, json, conn); + } + + public static SpApiResponse volumeSnapshot(final String volumeName, final String snapshotName, String vmUuid, + String csTag, String vcPolicy, SpConnectionDesc conn) { + Map json = new HashMap<>(); + Map tags = StorPoolHelper.addStorPoolTags(snapshotName, vmUuid, csTag, vcPolicy); + json.put("name", ""); + json.put("tags", tags); + + return POST("MultiCluster/VolumeSnapshot/" + volumeName, json, conn); + } + + public static SpApiResponse volumesGroupSnapshot(final List volumeTOs, final String vmUuid, + final String snapshotName, String csTag, SpConnectionDesc conn) { + Map json = new LinkedHashMap<>(); + Map tags = StorPoolHelper.addStorPoolTags(snapshotName, vmUuid, csTag, null); + List> volumes = new ArrayList<>(); + for (VolumeObjectTO volumeTO : volumeTOs) { + Map vol = new LinkedHashMap<>(); + String name = StorPoolStorageAdaptor.getVolumeNameFromPath(volumeTO.getPath(), true); + vol.put("name", ""); + vol.put("volume", name); + volumes.add(vol); + } + json.put("tags", tags); + json.put("volumes", volumes); + log.info("json:" + json); + return POST("MultiCluster/VolumesGroupSnapshot", json, conn); + } + + public static SpApiResponse volumeRevert(final String name, final String snapshotName, SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("toSnapshot", snapshotName); + return POST("MultiCluster/VolumeRevert/" + name, json, conn); + } + + public static SpApiResponse volumeFreeze(final String volumeName, SpConnectionDesc conn) { + return POST("MultiCluster/VolumeFreeze/" + volumeName, null, conn); + } + + public static SpApiResponse volumeAcquire(final String volumeName, SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("onRemoteAttached", "detachForce"); + return POST("MultiCluster/VolumeAcquire/" + volumeName, json, conn); + } + + public static SpApiResponse volumeDelete(final String name, SpConnectionDesc conn) { + Map json = new HashMap<>(); + json.put("onAttached", "detachForce"); + return POST("MultiCluster/VolumeDelete/" + name, json, conn); + } + + public static SpApiResponse snapshotDelete(final String name, SpConnectionDesc conn) { + SpApiResponse resp = detachAllForced(name, true, conn); + return resp.getError() == null ? POST("MultiCluster/SnapshotDelete/" + name, null, conn) : resp; + } + + public static SpApiResponse detachAllForced(final String name, final boolean snapshot, SpConnectionDesc conn) { + final String type = snapshot ? "snapshot" : "volume"; + List> json = new ArrayList<>(); + Map reassignDesc = new HashMap<>(); + reassignDesc.put(type, name); + reassignDesc.put("detach", "all"); + reassignDesc.put("force", true); + json.add(reassignDesc); + + return POST("MultiCluster/VolumesReassign", json, conn); + } + + public static String getSnapshotNameFromResponse(SpApiResponse resp, boolean tildeNeeded, String globalIdOrRemote) { + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonPrimitive data = obj.getAsJsonObject("data").getAsJsonPrimitive(globalIdOrRemote); + String name = data != null ? data.getAsString() : null; + name = name != null ? !tildeNeeded ? name : "~" + name : name; + return name; + } + + public static String getNameFromResponse(SpApiResponse resp, boolean tildeNeeded) { + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonPrimitive data = obj.getAsJsonObject("data").getAsJsonPrimitive("name"); + String name = data != null ? data.getAsString() : null; + name = name != null ? name.startsWith("~") && !tildeNeeded ? name.split("~")[1] : name : name; + return name; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java new file mode 100644 index 00000000000..1608680e41b --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java @@ -0,0 +1,575 @@ +/* + * 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.motion; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; +import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy; +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.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.snapshot.StorPoolConfigurationManager; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.commons.collections.MapUtils; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.MigrateAnswer; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; +import com.cloud.agent.api.ModifyTargetsAnswer; +import com.cloud.agent.api.ModifyTargetsCommand; +import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.StorageManager; +import com.cloud.storage.VMTemplateDetailVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.GuestOSCategoryDao; +import com.cloud.storage.dao.GuestOSDao; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VMTemplateDetailsDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.VMInstanceDao; + +@Component +public class StorPoolDataMotionStrategy implements DataMotionStrategy { + private static final Logger log = Logger.getLogger(StorPoolDataMotionStrategy.class); + + @Inject + private SnapshotDataFactory _snapshotDataFactory; + @Inject + private DataStoreManager _dataStore; + @Inject + private ConfigurationDao _configDao; + @Inject + private EndPointSelector _selector; + @Inject + private TemplateDataStoreDao _templStoreDao; + @Inject + private ClusterDao _clusterDao; + @Inject + private HostDao _hostDao; + @Inject + private SnapshotDetailsDao _snapshotDetailsDao; + @Inject + private VMTemplateDetailsDao _vmTemplateDetailsDao; + @Inject + private SnapshotDataStoreDao _snapshotStoreDao; + @Inject + private StoragePoolDetailsDao _storagePoolDetails; + @Inject + private PrimaryDataStoreDao _storagePool; + @Inject + private VolumeDao _volumeDao; + @Inject + private VolumeDataFactory _volumeDataFactory; + @Inject + private VMInstanceDao _vmDao; + @Inject + private GuestOSDao _guestOsDao; + @Inject + private VolumeService _volumeService; + @Inject + private GuestOSCategoryDao _guestOsCategoryDao; + @Inject + private SnapshotDao _snapshotDao; + @Inject + private AgentManager _agentManager; + @Inject + private PrimaryDataStoreDao _storagePoolDao; + + @Override + public StrategyPriority canHandle(DataObject srcData, DataObject destData) { + DataObjectType srcType = srcData.getType(); + DataObjectType dstType = destData.getType(); + if (srcType == DataObjectType.SNAPSHOT && dstType == DataObjectType.TEMPLATE + && StorPoolConfigurationManager.BypassSecondaryStorage.value()) { + SnapshotInfo sinfo = (SnapshotInfo) srcData; + VolumeInfo volume = sinfo.getBaseVolume(); + StoragePoolVO storagePool = _storagePool.findById(volume.getPoolId()); + if (!storagePool.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { + return StrategyPriority.CANT_HANDLE; + } + String snapshotName = StorPoolHelper.getSnapshotName(sinfo.getId(), sinfo.getUuid(), _snapshotStoreDao, + _snapshotDetailsDao); + StorPoolUtil.spLog("StorPoolDataMotionStrategy.canHandle snapshot name=%s", snapshotName); + if (snapshotName != null) { + return StrategyPriority.HIGHEST; + } + } + return StrategyPriority.CANT_HANDLE; + } + + @Override + public void copyAsync(DataObject srcData, DataObject destData, Host destHost, + AsyncCompletionCallback callback) { + SnapshotObjectTO snapshot = (SnapshotObjectTO) srcData.getTO(); + TemplateObjectTO template = (TemplateObjectTO) destData.getTO(); + DataStore store = _dataStore.getDataStore(snapshot.getVolume().getDataStore().getUuid(), + snapshot.getVolume().getDataStore().getRole()); + SnapshotInfo sInfo = _snapshotDataFactory.getSnapshot(snapshot.getId(), store); + + VolumeInfo vInfo = sInfo.getBaseVolume(); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(vInfo.getDataStore().getUuid(), + vInfo.getDataStore().getId(), _storagePoolDetails, _storagePool); + String name = template.getUuid(); + String volumeName = ""; + + String parentName = StorPoolHelper.getSnapshotName(sInfo.getId(), sInfo.getUuid(), _snapshotStoreDao, + _snapshotDetailsDao); + // TODO volume tags cs - template + SpApiResponse res = StorPoolUtil.volumeCreate(name, parentName, sInfo.getSize(), null, "no", "template", null, + conn); + CopyCmdAnswer answer = null; + String err = null; + if (res.getError() != null) { + log.debug(String.format("Could not create volume from snapshot with ID=%s", snapshot.getId())); + StorPoolUtil.spLog("Volume create failed with error=%s", res.getError().getDescr()); + err = res.getError().getDescr(); + } else { + volumeName = StorPoolUtil.getNameFromResponse(res, true); + SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(sInfo.getId(), sInfo.getUuid()); + + snapshot.setPath(snapshotDetails.getValue()); + Command backupSnapshot = new StorPoolBackupTemplateFromSnapshotCommand(snapshot, template, + StorPoolHelper.getTimeout(StorPoolHelper.BackupSnapshotWait, _configDao), + VirtualMachineManager.ExecuteInSequence.value()); + + try { + // final String snapName = + // StorpoolStorageAdaptor.getVolumeNameFromPath(((SnapshotInfo) + // srcData).getPath(), true); + Long clusterId = StorPoolHelper.findClusterIdByGlobalId(parentName, _clusterDao); + EndPoint ep2 = clusterId != null + ? RemoteHostEndPoint + .getHypervisorHostEndPoint(StorPoolHelper.findHostByCluster(clusterId, _hostDao)) + : _selector.select(sInfo, destData); + if (ep2 == null) { + err = "No remote endpoint to send command, check if host or ssvm is down?"; + } else { + answer = (CopyCmdAnswer) ep2.sendMessage(backupSnapshot); + if (answer != null && answer.getResult()) { + SpApiResponse resSnapshot = StorPoolUtil.volumeFreeze(volumeName, conn); + if (resSnapshot.getError() != null) { + log.debug(String.format("Could not snapshot volume with ID=%s", snapshot.getId())); + StorPoolUtil.spLog("Volume freeze failed with error=%s", resSnapshot.getError().getDescr()); + err = resSnapshot.getError().getDescr(); + StorPoolUtil.volumeDelete(volumeName, conn); + } else { + StorPoolHelper.updateVmStoreTemplate(template.getId(), template.getDataStore().getRole(), + StorPoolUtil.devPath(StorPoolUtil.getNameFromResponse(res, false)), _templStoreDao); + } + } else { + err = "Could not copy template to secondary " + answer.getResult(); + StorPoolUtil.volumeDelete(StorPoolUtil.getNameFromResponse(res, true), conn); + } + } + } catch (CloudRuntimeException e) { + err = e.getMessage(); + } + } + _vmTemplateDetailsDao.persist(new VMTemplateDetailVO(template.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, + String.valueOf(vInfo.getDataStore().getId()), false)); + StorPoolUtil.spLog("StorPoolDataMotionStrategy.copyAsync Creating snapshot=%s for StorPool template=%s", + volumeName, conn.getTemplateName()); + final CopyCommandResult cmd = new CopyCommandResult(null, answer); + cmd.setResult(err); + callback.complete(cmd); + } + + @Override + public StrategyPriority canHandle(Map volumeMap, Host srcHost, Host destHost) { + return canHandleLiveMigrationOnStorPool(volumeMap, srcHost, destHost); + } + + final StrategyPriority canHandleLiveMigrationOnStorPool(Map volumeMap, Host srcHost, + Host destHost) { + if (srcHost.getId() != destHost.getId() && isDestinationStorPoolPrimaryStorage(volumeMap)) { + return StrategyPriority.HIGHEST; + } + return StrategyPriority.CANT_HANDLE; + } + + private boolean isDestinationStorPoolPrimaryStorage(Map volumeMap) { + if (MapUtils.isNotEmpty(volumeMap)) { + for (DataStore dataStore : volumeMap.values()) { + StoragePoolVO storagePoolVO = _storagePool.findById(dataStore.getId()); + if (storagePoolVO == null + || !storagePoolVO.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { + return false; + } + } + } else { + return false; + } + return true; + } + + @Override + public void copyAsync(Map volumeDataStoreMap, VirtualMachineTO vmTO, Host srcHost, + Host destHost, AsyncCompletionCallback callback) { + String errMsg = null; + String newVolName = null; + SpConnectionDesc conn = null; + + try { + if (srcHost.getHypervisorType() != HypervisorType.KVM) { + throw new CloudRuntimeException(String.format("Invalid hypervisor type [%s]. Only KVM supported", srcHost.getHypervisorType())); + } + + VMInstanceVO vmInstance = _vmDao.findById(vmTO.getId()); + vmTO.setState(vmInstance.getState()); + List migrateDiskInfoList = new ArrayList(); + + Map migrateStorage = new HashMap<>(); + Map srcVolumeInfoToDestVolumeInfo = new HashMap<>(); + + for (Map.Entry entry : volumeDataStoreMap.entrySet()) { + VolumeInfo srcVolumeInfo = entry.getKey(); + DataStore destDataStore = entry.getValue(); + + VolumeVO srcVolume = _volumeDao.findById(srcVolumeInfo.getId()); + StoragePoolVO destStoragePool = _storagePool.findById(destDataStore.getId()); + + VolumeVO destVolume = duplicateVolumeOnAnotherStorage(srcVolume, destStoragePool); + + VolumeInfo destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore); + + destVolumeInfo.processEvent(Event.MigrationCopyRequested); + destVolumeInfo.processEvent(Event.MigrationCopySucceeded); + destVolumeInfo.processEvent(Event.MigrationRequested); + + conn = StorPoolUtil.getSpConnection(destDataStore.getUuid(), destDataStore.getId(), _storagePoolDetails, + _storagePool); + SpApiResponse resp = StorPoolUtil.volumeCreate(srcVolume.getUuid(), null, srcVolume.getSize(), + vmTO.getUuid(), null, "volume", srcVolume.getMaxIops(), conn); + + if (resp.getError() == null) { + newVolName = StorPoolUtil.getNameFromResponse(resp, true); + } + + String volumeName = StorPoolUtil.getNameFromResponse(resp, false); + destVolume.setPath(StorPoolUtil.devPath(volumeName)); + _volumeDao.update(destVolume.getId(), destVolume); + destVolume = _volumeDao.findById(destVolume.getId()); + + destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore); + + String destPath = generateDestPath(destHost, destStoragePool, destVolumeInfo); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath); + migrateDiskInfoList.add(migrateDiskInfo); + + migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); + + srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo); + } + + PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); + + try { + Answer pfma = _agentManager.send(destHost.getId(), pfmc); + + if (pfma == null || !pfma.getResult()) { + String details = pfma != null ? pfma.getDetails() : "null answer returned"; + errMsg = String.format("Unable to prepare for migration due to the following: %s", details); + + throw new AgentUnavailableException(errMsg, destHost.getId()); + } + } catch (final OperationTimedoutException e) { + errMsg = String.format("Operation timed out due to %s", e.getMessage()); + throw new AgentUnavailableException(errMsg, destHost.getId()); + } + + VMInstanceVO vm = _vmDao.findById(vmTO.getId()); + boolean isWindows = _guestOsCategoryDao.findById(_guestOsDao.findById(vm.getGuestOSId()).getCategoryId()) + .getName().equalsIgnoreCase("Windows"); + + MigrateCommand migrateCommand = new MigrateCommand(vmTO.getName(), + destHost.getPrivateIpAddress(), isWindows, vmTO, true); + migrateCommand.setWait(StorageManager.KvmStorageOnlineMigrationWait.value()); + migrateCommand.setMigrateStorage(migrateStorage); + migrateCommand.setMigrateStorageManaged(true); + migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList); + + boolean kvmAutoConvergence = StorageManager.KvmAutoConvergence.value(); + + migrateCommand.setAutoConvergence(kvmAutoConvergence); + + MigrateAnswer migrateAnswer = (MigrateAnswer) _agentManager.send(srcHost.getId(), migrateCommand); + + boolean success = migrateAnswer != null && migrateAnswer.getResult(); + + handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + + if (migrateAnswer == null) { + throw new CloudRuntimeException("Unable to get an answer to the migrate command"); + } + + if (!migrateAnswer.getResult()) { + errMsg = migrateAnswer.getDetails(); + + throw new CloudRuntimeException(errMsg); + } + } catch (AgentUnavailableException | OperationTimedoutException | CloudRuntimeException ex) { + + errMsg = String.format( + "Copy volume(s) of VM [%s] to storage(s) [%s] and VM to host [%s] failed in StorPoolDataMotionStrategy.copyAsync. Error message: [%s].", + vmTO.getId(), srcHost.getId(), destHost.getId(), ex.getMessage()); + log.error(errMsg, ex); + + throw new CloudRuntimeException(errMsg); + } finally { + if (errMsg != null) { + deleteVolumeOnFail(newVolName, conn); + } + CopyCmdAnswer copyCmdAnswer = new CopyCmdAnswer(errMsg); + + CopyCommandResult result = new CopyCommandResult(null, copyCmdAnswer); + + result.setResult(errMsg); + + callback.complete(result); + } + } + + private void deleteVolumeOnFail(String newVolName, SpConnectionDesc conn) { + if (newVolName != null && conn != null) { + StorPoolUtil.volumeDelete(newVolName, conn); + } + } + + private VolumeVO duplicateVolumeOnAnotherStorage(Volume volume, StoragePoolVO storagePoolVO) { + Long lastPoolId = volume.getPoolId(); + + VolumeVO newVol = new VolumeVO(volume); + + newVol.setInstanceId(null); + newVol.setChainInfo(null); + newVol.setPath(null); + newVol.setFolder(null); + newVol.setPodId(storagePoolVO.getPodId()); + newVol.setPoolId(storagePoolVO.getId()); + newVol.setLastPoolId(lastPoolId); + + return _volumeDao.persist(newVol); + } + + private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, + VirtualMachineTO vmTO, Host destHost) { + if (!success) { + try { + PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); + + pfmc.setRollback(true); + + Answer pfma = _agentManager.send(destHost.getId(), pfmc); + + if (pfma == null || !pfma.getResult()) { + String details = pfma != null ? pfma.getDetails() : "null answer returned"; + String msg = "Unable to rollback prepare for migration due to the following: " + details; + + throw new AgentUnavailableException(msg, destHost.getId()); + } + } catch (Exception e) { + log.debug("Failed to disconnect one or more (original) dest volumes", e); + } + } + + for (Map.Entry entry : srcVolumeInfoToDestVolumeInfo.entrySet()) { + VolumeInfo srcVolumeInfo = entry.getKey(); + VolumeInfo destVolumeInfo = entry.getValue(); + + if (success) { + srcVolumeInfo.processEvent(Event.OperationSuccessed); + destVolumeInfo.processEvent(Event.OperationSuccessed); + + _volumeDao.updateUuid(srcVolumeInfo.getId(), destVolumeInfo.getId()); + + VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); + + volumeVO.setFormat(ImageFormat.QCOW2); + + _volumeDao.update(volumeVO.getId(), volumeVO); + + try { + _volumeService.destroyVolume(srcVolumeInfo.getId()); + + srcVolumeInfo = _volumeDataFactory.getVolume(srcVolumeInfo.getId()); + + AsyncCallFuture destroyFuture = _volumeService.expungeVolumeAsync(srcVolumeInfo); + + if (destroyFuture.get().isFailed()) { + log.debug("Failed to clean up source volume on storage"); + } + } catch (Exception e) { + log.debug("Failed to clean up source volume on storage", e); + } + + // Update the volume ID for snapshots on secondary storage + if (!_snapshotDao.listByVolumeId(srcVolumeInfo.getId()).isEmpty()) { + _snapshotDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); + _snapshotStoreDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); + } + } else { + try { + disconnectHostFromVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.getPath()); + } catch (Exception e) { + log.debug("Failed to disconnect (new) dest volume", e); + } + + try { + _volumeService.revokeAccess(destVolumeInfo, destHost, destVolumeInfo.getDataStore()); + } catch (Exception e) { + log.debug("Failed to revoke access from dest volume", e); + } + + destVolumeInfo.processEvent(Event.OperationFailed); + srcVolumeInfo.processEvent(Event.OperationFailed); + + try { + _volumeService.destroyVolume(destVolumeInfo.getId()); + + destVolumeInfo = _volumeDataFactory.getVolume(destVolumeInfo.getId()); + + AsyncCallFuture destroyFuture = _volumeService.expungeVolumeAsync(destVolumeInfo); + + if (destroyFuture.get().isFailed()) { + log.debug("Failed to clean up dest volume on storage"); + } + } catch (Exception e) { + log.debug("Failed to clean up dest volume on storage", e); + } + } + } + } + + private String generateDestPath(Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) { + return connectHostToVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.getPath()); + } + + private String connectHostToVolume(Host host, long storagePoolId, String iqn) { + ModifyTargetsCommand modifyTargetsCommand = getModifyTargetsCommand(storagePoolId, iqn, true); + + return sendModifyTargetsCommand(modifyTargetsCommand, host.getId()).get(0); + } + + private void disconnectHostFromVolume(Host host, long storagePoolId, String iqn) { + ModifyTargetsCommand modifyTargetsCommand = getModifyTargetsCommand(storagePoolId, iqn, false); + + sendModifyTargetsCommand(modifyTargetsCommand, host.getId()); + } + + private ModifyTargetsCommand getModifyTargetsCommand(long storagePoolId, String iqn, boolean add) { + StoragePoolVO storagePool = _storagePoolDao.findById(storagePoolId); + + Map details = new HashMap<>(); + + details.put(ModifyTargetsCommand.IQN, iqn); + details.put(ModifyTargetsCommand.STORAGE_TYPE, storagePool.getPoolType().name()); + details.put(ModifyTargetsCommand.STORAGE_UUID, storagePool.getUuid()); + details.put(ModifyTargetsCommand.STORAGE_HOST, storagePool.getHostAddress()); + details.put(ModifyTargetsCommand.STORAGE_PORT, String.valueOf(storagePool.getPort())); + + ModifyTargetsCommand cmd = new ModifyTargetsCommand(); + + List> targets = new ArrayList<>(); + + targets.add(details); + + cmd.setTargets(targets); + cmd.setApplyToAllHostsInCluster(true); + cmd.setAdd(add); + cmd.setTargetTypeToRemove(ModifyTargetsCommand.TargetTypeToRemove.DYNAMIC); + + return cmd; + } + + private List sendModifyTargetsCommand(ModifyTargetsCommand cmd, long hostId) { + ModifyTargetsAnswer modifyTargetsAnswer = (ModifyTargetsAnswer) _agentManager.easySend(hostId, cmd); + + if (modifyTargetsAnswer == null) { + throw new CloudRuntimeException("Unable to get an answer to the modify targets command"); + } + + if (!modifyTargetsAnswer.getResult()) { + String msg = "Unable to modify targets on the following host: " + hostId; + + throw new CloudRuntimeException(msg); + } + + return modifyTargetsAnswer.getConnectedPaths(); + } + + protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath) { + return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), + MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, MigrateCommand.MigrateDiskInfo.DriverType.RAW, + MigrateCommand.MigrateDiskInfo.Source.DEV, destPath); + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java new file mode 100644 index 00000000000..782d8133813 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolConfigurationManager.java @@ -0,0 +1,46 @@ +/* + * 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.snapshot; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +public class StorPoolConfigurationManager implements Configurable { + + public static final ConfigKey BypassSecondaryStorage = new ConfigKey(Boolean.class, "sp.bypass.secondary.storage", "Advanced", "false", + "For StorPool Managed storage backup to secondary", true, ConfigKey.Scope.Global, null); + public static final ConfigKey StorPoolClusterId = new ConfigKey(String.class, "sp.cluster.id", "Advanced", "n/a", + "For StorPool multi cluster authorization", true, ConfigKey.Scope.Cluster, null); + public static final ConfigKey AlternativeEndPointEnabled = new ConfigKey(Boolean.class, "sp.enable.alternative.endpoint", "Advanced", "false", + "Used for StorPool primary storage, definse if there is a need to be used alternative endpoint", true, ConfigKey.Scope.StoragePool, null); + + public static final ConfigKey AlternativeEndpoint = new ConfigKey(String.class, "sp.alternative.endpoint", "Advanced", "", + "Used for StorPool primary storage for an alternative endpoint. Structure of the endpoint is - SP_API_HTTP=address:port;SP_AUTH_TOKEN=token;SP_TEMPLATE=template_name", true, ConfigKey.Scope.StoragePool, null); + + @Override + public String getConfigComponentName() { + return StorPoolConfigurationManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { BypassSecondaryStorage, StorPoolClusterId, AlternativeEndPointEnabled, AlternativeEndpoint }; + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java new file mode 100644 index 00000000000..f66617d8f31 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolSnapshotStrategy.java @@ -0,0 +1,289 @@ +// 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.snapshot; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.State; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.util.StorPoolHelper; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; + + +@Component +public class StorPoolSnapshotStrategy implements SnapshotStrategy { + private static final Logger log = Logger.getLogger(StorPoolSnapshotStrategy.class); + + @Inject + private SnapshotDao _snapshotDao; + @Inject + private PrimaryDataStoreDao _primaryDataStoreDao; + @Inject + private VolumeDao _volumeDao; + @Inject + private SnapshotDataStoreDao _snapshotStoreDao; + @Inject + private SnapshotDetailsDao _snapshotDetailsDao; + @Inject + private SnapshotService snapshotSvr; + @Inject + private SnapshotDataFactory snapshotDataFactory; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + + @Override + public SnapshotInfo backupSnapshot(SnapshotInfo snapshotInfo) { + SnapshotObject snapshotObj = (SnapshotObject) snapshotInfo; + try { + snapshotObj.processEvent(Snapshot.Event.BackupToSecondary); + snapshotObj.processEvent(Snapshot.Event.OperationSucceeded); + } catch (NoTransitionException ex) { + StorPoolUtil.spLog("Failed to change state: " + ex.toString()); + try { + snapshotObj.processEvent(Snapshot.Event.OperationFailed); + } catch (NoTransitionException ex2) { + StorPoolUtil.spLog("Failed to change state: " + ex2.toString()); + } + } + return snapshotInfo; + } + + @Override + public boolean deleteSnapshot(Long snapshotId) { + + final SnapshotVO snapshotVO = _snapshotDao.findById(snapshotId); + VolumeVO volume = _volumeDao.findByIdIncludingRemoved(snapshotVO.getVolumeId()); + String name = StorPoolHelper.getSnapshotName(snapshotId, snapshotVO.getUuid(), _snapshotStoreDao, _snapshotDetailsDao); + boolean res = false; + // clean-up snapshot from Storpool storage pools + StoragePoolVO storage = _primaryDataStoreDao.findById(volume.getPoolId()); + if (storage.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { + try { + SpConnectionDesc conn = StorPoolUtil.getSpConnection(storage.getUuid(), storage.getId(), storagePoolDetailsDao, _primaryDataStoreDao); + SpApiResponse resp = StorPoolUtil.snapshotDelete(name, conn); + if (resp.getError() != null) { + final String err = String.format("Failed to clean-up Storpool snapshot %s. Error: %s", name, resp.getError()); + StorPoolUtil.spLog(err); + } else { + SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(snapshotId, snapshotVO.getUuid()); + if (snapshotDetails != null) { + _snapshotDetailsDao.removeDetails(snapshotId); + } + res = deleteSnapshotFromDb(snapshotId); + StorPoolUtil.spLog("StorpoolSnapshotStrategy.deleteSnapshot: executed successfuly=%s, snapshot uuid=%s, name=%s", res, snapshotVO.getUuid(), name); + } + } catch (Exception e) { + String errMsg = String.format("Cannot delete snapshot due to %s", e.getMessage()); + throw new CloudRuntimeException(errMsg); + } + } + + return res; + } + + @Override + public StrategyPriority canHandle(Snapshot snapshot, SnapshotOperation op) { + StorPoolUtil.spLog("StorpoolSnapshotStrategy.canHandle: snapshot=%s, uuid=%s, op=%s", snapshot.getName(), snapshot.getUuid(), op); + + if (op != SnapshotOperation.DELETE) { + return StrategyPriority.CANT_HANDLE; + } + + String name = StorPoolHelper.getSnapshotName(snapshot.getId(), snapshot.getUuid(), _snapshotStoreDao, _snapshotDetailsDao); + if (name != null) { + StorPoolUtil.spLog("StorpoolSnapshotStrategy.canHandle: globalId=%s", name); + + return StrategyPriority.HIGHEST; + } + SnapshotDetailsVO snapshotDetails = _snapshotDetailsDao.findDetail(snapshot.getId(), snapshot.getUuid()); + if (snapshotDetails != null) { + _snapshotDetailsDao.remove(snapshotDetails.getId()); + } + return StrategyPriority.CANT_HANDLE; + } + + private boolean deleteSnapshotChain(SnapshotInfo snapshot) { + log.debug("delete snapshot chain for snapshot: " + snapshot.getId()); + boolean result = false; + boolean resultIsSet = false; + try { + while (snapshot != null && + (snapshot.getState() == Snapshot.State.Destroying || snapshot.getState() == Snapshot.State.Destroyed || snapshot.getState() == Snapshot.State.Error)) { + SnapshotInfo child = snapshot.getChild(); + + if (child != null) { + log.debug("the snapshot has child, can't delete it on the storage"); + break; + } + log.debug("Snapshot: " + snapshot.getId() + " doesn't have children, so it's ok to delete it and its parents"); + SnapshotInfo parent = snapshot.getParent(); + boolean deleted = false; + if (parent != null) { + if (parent.getPath() != null && parent.getPath().equalsIgnoreCase(snapshot.getPath())) { + log.debug("for empty delta snapshot, only mark it as destroyed in db"); + snapshot.processEvent(Event.DestroyRequested); + snapshot.processEvent(Event.OperationSuccessed); + deleted = true; + if (!resultIsSet) { + result = true; + resultIsSet = true; + } + } + } + if (!deleted) { + SnapshotInfo snap = snapshotDataFactory.getSnapshot(snapshot.getId(), DataStoreRole.Image); + if (StorPoolStorageAdaptor.getVolumeNameFromPath(snap.getPath(), true) == null) { + try { + boolean r = snapshotSvr.deleteSnapshot(snapshot); + if (r) { + List cacheSnaps = snapshotDataFactory.listSnapshotOnCache(snapshot.getId()); + for (SnapshotInfo cacheSnap : cacheSnaps) { + log.debug("Delete snapshot " + snapshot.getId() + " from image cache store: " + cacheSnap.getDataStore().getName()); + cacheSnap.delete(); + } + } + if (!resultIsSet) { + result = r; + resultIsSet = true; + } + } catch (Exception e) { + log.debug("Failed to delete snapshot on storage. ", e); + } + } + } else { + result = true; + } + snapshot = parent; + } + } catch (Exception e) { + log.debug("delete snapshot failed: ", e); + } + return result; + } + + private boolean deleteSnapshotFromDb(Long snapshotId) { + SnapshotVO snapshotVO = _snapshotDao.findById(snapshotId); + + if (snapshotVO.getState() == Snapshot.State.Allocated) { + _snapshotDao.remove(snapshotId); + return true; + } + + if (snapshotVO.getState() == Snapshot.State.Destroyed) { + return true; + } + + if (Snapshot.State.Error.equals(snapshotVO.getState())) { + List storeRefs = _snapshotStoreDao.findBySnapshotId(snapshotId); + for (SnapshotDataStoreVO ref : storeRefs) { + _snapshotStoreDao.expunge(ref.getId()); + } + _snapshotDao.remove(snapshotId); + return true; + } + + if (snapshotVO.getState() == Snapshot.State.CreatedOnPrimary) { + snapshotVO.setState(Snapshot.State.Destroyed); + _snapshotDao.update(snapshotId, snapshotVO); + return true; + } + + if (!Snapshot.State.BackedUp.equals(snapshotVO.getState()) && !Snapshot.State.Error.equals(snapshotVO.getState()) && + !Snapshot.State.Destroying.equals(snapshotVO.getState())) { + throw new InvalidParameterValueException("Can't delete snapshotshot " + snapshotId + " due to it is in " + snapshotVO.getState() + " Status"); + } + + SnapshotInfo snapshotOnImage = snapshotDataFactory.getSnapshot(snapshotId, DataStoreRole.Image); + if (snapshotOnImage == null) { + log.debug("Can't find snapshot on backup storage, delete it in db"); + _snapshotDao.remove(snapshotId); + return true; + } + + SnapshotObject obj = (SnapshotObject)snapshotOnImage; + try { + obj.processEvent(Snapshot.Event.DestroyRequested); + } catch (NoTransitionException e) { + log.debug("Failed to set the state to destroying: ", e); + return false; + } + + try { + boolean result = deleteSnapshotChain(snapshotOnImage); + obj.processEvent(Snapshot.Event.OperationSucceeded); + if (result) { + SnapshotDataStoreVO snapshotOnPrimary = _snapshotStoreDao.findBySnapshot(snapshotId, DataStoreRole.Primary); + if (snapshotOnPrimary != null) { + snapshotOnPrimary.setState(State.Destroyed); + _snapshotStoreDao.update(snapshotOnPrimary.getId(), snapshotOnPrimary); + } + } + } catch (Exception e) { + log.debug("Failed to delete snapshot: ", e); + try { + obj.processEvent(Snapshot.Event.OperationFailed); + } catch (NoTransitionException e1) { + log.debug("Failed to change snapshot state: " + e.toString()); + } + return false; + } + return true; + } + + @Override + public SnapshotInfo takeSnapshot(SnapshotInfo snapshot) { + return null; + } + + @Override + public boolean revertSnapshot(SnapshotInfo snapshot) { + return false; + } + + @Override + public void postSnapshotCreation(SnapshotInfo snapshot) { + } +} diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolVMSnapshotStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolVMSnapshotStrategy.java new file mode 100644 index 00000000000..ec7e89a2391 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/snapshot/StorPoolVMSnapshotStrategy.java @@ -0,0 +1,387 @@ +// +//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.snapshot; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +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.StorPoolUtil; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse; +import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.vmsnapshot.DefaultVMSnapshotStrategy; +import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.api.VMSnapshotTO; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.VolumeDetailVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.uservm.UserVm; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +@Component +public class StorPoolVMSnapshotStrategy extends DefaultVMSnapshotStrategy { + private static final Logger log = Logger.getLogger(StorPoolVMSnapshotStrategy.class); + + @Inject + private VMSnapshotHelper vmSnapshotHelper; + @Inject + private UserVmDao userVmDao; + @Inject + private VMSnapshotDao vmSnapshotDao; + @Inject + private VolumeDao volumeDao; + @Inject + private DiskOfferingDao diskOfferingDao; + @Inject + private PrimaryDataStoreDao storagePool; + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject + private VolumeDataFactory volFactory; + @Inject + private VolumeDetailsDao volumeDetailsDao; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + @Inject + private DataStoreManager dataStoreManager; + int _wait; + + @Override + public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { + log.info("KVMVMSnapshotStrategy take snapshot"); + UserVm userVm = userVmDao.findById(vmSnapshot.getVmId()); + VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot; + + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshotVO, VMSnapshot.Event.CreateRequested); + } catch (NoTransitionException e) { + throw new CloudRuntimeException("No transiontion " + e.getMessage()); + } + + boolean result = false; + try { + + List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid()); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool); + + long prev_chain_size = 0; + long virtual_size = 0; + for (VolumeObjectTO volume : volumeTOs) { + virtual_size += volume.getSize(); + VolumeVO volumeVO = volumeDao.findById(volume.getId()); + prev_chain_size += volumeVO.getVmSnapshotChainSize() == null ? 0 : volumeVO.getVmSnapshotChainSize(); + } + + VMSnapshotTO current = null; + VMSnapshotVO currentSnapshot = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId()); + if (currentSnapshot != null) { + current = vmSnapshotHelper.getSnapshotWithParents(currentSnapshot); + } + + if (current == null) { + vmSnapshotVO.setParent(null); + } else { + vmSnapshotVO.setParent(current.getId()); + } + + SpApiResponse resp = StorPoolUtil.volumesGroupSnapshot(volumeTOs, userVm.getUuid(), vmSnapshotVO.getUuid(), "group", conn); + JsonObject obj = resp.fullJson.getAsJsonObject(); + JsonArray snapshots = obj.getAsJsonObject("data").getAsJsonArray("snapshots"); + StorPoolUtil.spLog("Volumes=%s attached to virtual machine", volumeTOs.toString()); + for (VolumeObjectTO vol : volumeTOs) { + for (JsonElement jsonElement : snapshots) { + JsonObject snapshotObject = jsonElement.getAsJsonObject(); + String snapshot = StorPoolUtil + .devPath(snapshotObject.getAsJsonPrimitive(StorPoolUtil.GLOBAL_ID).getAsString()); + if (snapshotObject.getAsJsonPrimitive("volume").getAsString().equals(StorPoolStorageAdaptor.getVolumeNameFromPath(vol.getPath(), true)) + || snapshotObject.getAsJsonPrimitive("volumeGlobalId").getAsString().equals(StorPoolStorageAdaptor.getVolumeNameFromPath(vol.getPath(), false))) { + VMSnapshotDetailsVO vmSnapshotDetailsVO = new VMSnapshotDetailsVO(vmSnapshot.getId(), vol.getUuid(), snapshot, false); + vmSnapshotDetailsDao.persist(vmSnapshotDetailsVO); + Long poolId = volumeDao.findById(vol.getId()).getPoolId(); + if (poolId != null) { + VMSnapshotDetailsVO vmSnapshotDetailStoragePoolId = new VMSnapshotDetailsVO( + vmSnapshot.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, String.valueOf(poolId), false); + vmSnapshotDetailsDao.persist(vmSnapshotDetailStoragePoolId); + } + StorPoolUtil.spLog("Snapshot=%s of volume=%s for a group snapshot=%s.", snapshot, vol.getUuid(), vmSnapshot.getUuid()); + } + } + } + + if (resp.getError() == null) { + StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.takeSnapshot answer=%s", resp.getError()); + finalizeCreate(vmSnapshotVO, volumeTOs); + result = vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded); + long new_chain_size = 0; + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_CREATE, vmSnapshot, userVm, volumeObjectTO); + new_chain_size += volumeObjectTO.getSize(); + log.info("EventTypes.EVENT_VM_SNAPSHOT_CREATE publishUsageEvent" + volumeObjectTO); + } + publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_ON_PRIMARY, vmSnapshot, userVm, new_chain_size - prev_chain_size, virtual_size); + } else { + throw new CloudRuntimeException("Could not create vm snapshot"); + } + return vmSnapshot; + } catch (Exception e) { + log.debug("Could not create VM snapshot:" + e.getMessage()); + throw new CloudRuntimeException("Could not create VM snapshot:" + e.getMessage()); + } finally { + if (!result) { + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed); + log.info(String.format("VMSnapshot.Event.OperationFailed vmSnapshot=%s", vmSnapshot)); + } catch (NoTransitionException nte) { + log.error("Cannot set vm state:" + nte.getMessage()); + } + } + } + } + + @Override + public StrategyPriority canHandle(VMSnapshot vmSnapshot) { + return areAllVolumesOnStorPool(vmSnapshot.getVmId()); + } + + public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { + if (snapshotMemory) { + return StrategyPriority.CANT_HANDLE; + } + return areAllVolumesOnStorPool(vmId); + } + + private StrategyPriority areAllVolumesOnStorPool(Long vmId) { + List volumeTOs = vmSnapshotHelper.getVolumeTOList(vmId); + if (volumeTOs == null || volumeTOs.isEmpty()) { + return StrategyPriority.CANT_HANDLE; + } + for (VolumeObjectTO volumeTO : volumeTOs) { + Long poolId = volumeTO.getPoolId(); + StoragePoolVO pool = storagePool.findById(poolId); + if (!pool.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) { + return StrategyPriority.CANT_HANDLE; + } + } + return StrategyPriority.HIGHEST; + } + + @Override + public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { + UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId()); + VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot; + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.ExpungeRequested); + } catch (NoTransitionException e) { + log.debug("Failed to change vm snapshot state with event ExpungeRequested"); + throw new CloudRuntimeException( + "Failed to change vm snapshot state with event ExpungeRequested: " + e.getMessage()); + } + + List volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshot.getVmId()); + DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid()); + SpConnectionDesc conn = null; + try { + conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool); + } catch (CloudRuntimeException e) { + throw e; + } + + SpApiResponse resp = null; + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + String err = null; + VMSnapshotDetailsVO snapshotDetailsVO = vmSnapshotDetailsDao.findDetail(vmSnapshot.getId(), volumeObjectTO.getUuid()); + String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDetailsVO.getValue(), true); + if (snapshotName == null) { + err = String.format("Could not find StorPool's snapshot vm snapshot uuid=%s and volume uui=%s", + vmSnapshot.getUuid(), volumeObjectTO.getUuid()); + log.error("Could not delete snapshot for vm:" + err); + } + StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.deleteVMSnapshot snapshotName=%s", snapshotName); + resp = StorPoolUtil.snapshotDelete(snapshotName, conn); + if (resp.getError() != null) { + err = String.format("Could not delete storpool vm error=%s", resp.getError()); + log.error("Could not delete snapshot for vm:" + err); + } else { + // do we need to clean database? + if (snapshotDetailsVO != null) { + vmSnapshotDetailsDao.remove(snapshotDetailsVO.getId()); + } + } + if (err != null) { + StorPoolUtil.spLog( + "StorpoolVMSnapshotStrategy.deleteVMSnapshot delete snapshot=%s of gropusnapshot=%s failed due to %s", + snapshotName, userVm.getInstanceName(), err); + throw new CloudRuntimeException("Delete vm snapshot " + vmSnapshot.getName() + " of vm " + + userVm.getInstanceName() + " failed due to " + err); + } + } + vmSnapshotDetailsDao.removeDetails(vmSnapshot.getId()); + + finalizeDelete(vmSnapshotVO, volumeTOs); + vmSnapshotDao.remove(vmSnapshot.getId()); + + long full_chain_size = 0; + for (VolumeObjectTO volumeTo : volumeTOs) { + publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshot, userVm, volumeTo); + full_chain_size += volumeTo.getSize(); + } + publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshot, userVm, full_chain_size, 0L); + return true; + } + + @Override + public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { + log.debug("Revert vm snapshot"); + VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot; + UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId()); + + if (userVm.getState() == VirtualMachine.State.Running && vmSnapshotVO.getType() == VMSnapshot.Type.Disk) { + throw new CloudRuntimeException("Virtual machine should be in stopped state for revert operation"); + } + + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshotVO, VMSnapshot.Event.RevertRequested); + } catch (NoTransitionException e) { + throw new CloudRuntimeException(e.getMessage()); + } + + boolean result = false; + try { + List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + + DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid()); + SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool); + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + String err = null; + VMSnapshotDetailsVO snapshotDetailsVO = vmSnapshotDetailsDao.findDetail(vmSnapshot.getId(), + volumeObjectTO.getUuid()); + String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDetailsVO.getValue(), true); + if (snapshotName == null) { + err = String.format("Could not find StorPool's snapshot vm snapshot uuid=%s and volume uui=%s", + vmSnapshot.getUuid(), volumeObjectTO.getUuid()); + log.error("Could not delete snapshot for vm:" + err); + } + String volumeName = StorPoolStorageAdaptor.getVolumeNameFromPath(volumeObjectTO.getPath(), true); + VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeObjectTO.getId(), StorPoolUtil.SP_PROVIDER_NAME); + if (detail != null) { + SpApiResponse updateVolumeResponse = StorPoolUtil.volumeUpdateRename(volumeName, "", StorPoolStorageAdaptor.getVolumeNameFromPath(detail.getValue(), false), conn); + + if (updateVolumeResponse.getError() != null) { + StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.canHandle - Could not update StorPool's volume %s to it's globalId due to %s", volumeName, updateVolumeResponse.getError().getDescr()); + err = String.format("StorpoolVMSnapshotStrategy.canHandle - Could not update StorPool's volume %s to it's globalId due to %s", volumeName, updateVolumeResponse.getError().getDescr()); + } else { + volumeDetailsDao.remove(detail.getId()); + } + } + + SpApiResponse resp = StorPoolUtil.detachAllForced(volumeName, false, conn); + if (resp.getError() != null) { + err = String.format("Could not detach StorPool volume %s from a group snapshot, due to %s", + volumeName, resp.getError()); + throw new CloudRuntimeException(err); + } + resp = StorPoolUtil.volumeRevert(volumeName, snapshotName, conn); + if (resp.getError() != null) { + err = String.format("Create Could not complete revert task for volumeName=%s , and snapshotName=%s", + volumeName, snapshotName); + throw new CloudRuntimeException(err); + } + VolumeInfo vinfo = volFactory.getVolume(volumeObjectTO.getId()); + if (vinfo.getMaxIops() != null) { + resp = StorPoolUtil.volumeUpadateTags(volumeName, null, vinfo.getMaxIops(), conn, null); + + if (resp.getError() != null) { + StorPoolUtil.spLog("Volume was reverted successfully but max iops could not be set due to %s", + resp.getError().getDescr()); + } + } + } + finalizeRevert(vmSnapshotVO, volumeTOs); + result = vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded); + } catch (CloudRuntimeException | NoTransitionException e) { + String errMsg = String.format("Error while finalize create vm snapshot [%s] due to %s", vmSnapshot.getName(), e.getMessage()); + log.error(errMsg, e); + throw new CloudRuntimeException(errMsg); + } finally { + if (!result) { + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed); + } catch (NoTransitionException e1) { + log.error("Cannot set vm snapshot state due to: " + e1.getMessage()); + } + } + } + return result; + } + + private void publishUsageEvents(String type, VMSnapshot vmSnapshot, UserVm userVm, VolumeObjectTO volumeTo) { + VolumeVO volume = volumeDao.findById(volumeTo.getId()); + Long diskOfferingId = volume.getDiskOfferingId(); + Long offeringId = null; + if (diskOfferingId != null) { + DiskOfferingVO offering = diskOfferingDao.findById(diskOfferingId); + if (offering != null && offering.isComputeOnly()) { + offeringId = offering.getId(); + } + } + UsageEventUtils.publishUsageEvent(type, vmSnapshot.getAccountId(), userVm.getDataCenterId(), userVm.getId(), + vmSnapshot.getName(), offeringId, volume.getId(), volumeTo.getSize(), VMSnapshot.class.getName(), vmSnapshot.getUuid()); + } + + private void publishUsageEvents(String type, VMSnapshot vmSnapshot, UserVm userVm, Long vmSnapSize, Long virtualSize) { + try { + UsageEventUtils.publishUsageEvent(type, vmSnapshot.getAccountId(), userVm.getDataCenterId(), userVm.getId(), + vmSnapshot.getName(), 0L, 0L, vmSnapSize, virtualSize, VMSnapshot.class.getName(), + vmSnapshot.getUuid()); + } catch (Exception e) { + log.error("Failed to publis usage event " + type, e); + } + } +} diff --git a/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/module.properties b/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/module.properties new file mode 100644 index 00000000000..af4456ea373 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/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-storpool +parent=storage diff --git a/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/spring-storage-volume-storpool-context.xml b/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/spring-storage-volume-storpool-context.xml new file mode 100644 index 00000000000..cf1db3a8bf2 --- /dev/null +++ b/plugins/storage/volume/storpool/src/main/resources/META-INF/cloudstack/storage-volume-storpool/spring-storage-volume-storpool-context.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 10288983477..2c741a205c0 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -57,10 +57,12 @@ import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationSer import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; 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.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; @@ -2639,6 +2641,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (volume.getPoolId() != null) { DataStore dataStore = dataStoreMgr.getDataStore(volume.getPoolId(), DataStoreRole.Primary); volService.revokeAccess(volFactory.getVolume(volume.getId()), host, dataStore); + provideVMInfo(dataStore, vmId, volumeId); } if (volumePool != null && hostId != null) { handleTargetsForVMware(hostId, volumePool.getHostAddress(), volumePool.getPort(), volume.get_iScsiName()); @@ -3884,6 +3887,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (attached) { ev = Volume.Event.OperationSucceeded; s_logger.debug("Volume: " + volInfo.getName() + " successfully attached to VM: " + volInfo.getAttachedVmName()); + provideVMInfo(dataStore, vm.getId(), volInfo.getId()); } else { s_logger.debug("Volume: " + volInfo.getName() + " failed to attach to VM: " + volInfo.getAttachedVmName()); } @@ -3892,6 +3896,17 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return _volsDao.findById(volumeToAttach.getId()); } + private void provideVMInfo(DataStore dataStore, long vmId, Long volumeId) { + DataStoreDriver dataStoreDriver = dataStore != null ? dataStore.getDriver() : null; + + if (dataStoreDriver instanceof PrimaryDataStoreDriver) { + PrimaryDataStoreDriver storageDriver = (PrimaryDataStoreDriver)dataStoreDriver; + if (storageDriver.isVmInfoNeeded()) { + storageDriver.provideVmInfo(vmId, volumeId); + } + } + } + private int getMaxDataVolumesSupported(UserVmVO vm) { Long hostId = vm.getHostId(); if (hostId == null) { diff --git a/server/src/main/java/com/cloud/tags/TaggedResourceManagerImpl.java b/server/src/main/java/com/cloud/tags/TaggedResourceManagerImpl.java index db6cac0e393..23e757dfe23 100644 --- a/server/src/main/java/com/cloud/tags/TaggedResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/tags/TaggedResourceManagerImpl.java @@ -25,8 +25,10 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; -import com.cloud.server.ResourceManagerUtil; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.commons.collections.MapUtils; import org.apache.log4j.Logger; @@ -40,11 +42,14 @@ import com.cloud.network.vpc.NetworkACLItemVO; import com.cloud.network.vpc.NetworkACLVO; import com.cloud.network.vpc.VpcVO; import com.cloud.projects.ProjectVO; +import com.cloud.server.ResourceManagerUtil; import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.server.TaggedResourceService; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.SnapshotPolicyVO; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@ -78,6 +83,10 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso AccountDao _accountDao; @Inject ResourceManagerUtil resourceManagerUtil; + @Inject + VolumeDao volumeDao; + @Inject + DataStoreManager dataStoreMgr; @Override public boolean configure(String name, Map params) throws ConfigurationException { @@ -196,6 +205,9 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso throw new CloudRuntimeException(String.format("tag %s already on %s with id %s", resourceTag.getKey(), resourceType.toString(), resourceId),e); } resourceTags.add(resourceTag); + if (ResourceObjectType.UserVm.equals(resourceType)) { + informStoragePoolForVmTags(id, key, value); + } } } } @@ -275,6 +287,9 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso _resourceTagDao.remove(tagToRemove.getId()); s_logger.debug("Removed the tag '" + tagToRemove + "' for resources (" + String.join(", ", resourceIds) + ")"); + if (ResourceObjectType.UserVm.equals(resourceType)) { + informStoragePoolForVmTags(tagToRemove.getResourceId(), tagToRemove.getKey(), tagToRemove.getValue()); + } } } }); @@ -292,4 +307,19 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso List listResourceTags = listByResourceTypeAndId(type, resourceId); return listResourceTags == null ? null : listResourceTags.stream().collect(Collectors.toMap(ResourceTag::getKey, ResourceTag::getValue)); } + + + private void informStoragePoolForVmTags(long vmId, String key, String value) { + List volumeVos = volumeDao.findByInstance(vmId); + for (VolumeVO volume : volumeVos) { + DataStore dataStore = dataStoreMgr.getDataStore(volume.getPoolId(), DataStoreRole.Primary); + if (dataStore == null || !(dataStore.getDriver() instanceof PrimaryDataStoreDriver)) { + continue; + } + PrimaryDataStoreDriver dataStoreDriver = (PrimaryDataStoreDriver) dataStore.getDriver(); + if (dataStoreDriver.isVmTagsNeeded(key)) { + dataStoreDriver.provideVmTags(vmId, volume.getId(), value); + } + } + } } diff --git a/test/integration/plugins/storpool/MigrateVolumeToStorPool.py b/test/integration/plugins/storpool/MigrateVolumeToStorPool.py new file mode 100644 index 00000000000..e6b9fbbbe2d --- /dev/null +++ b/test/integration/plugins/storpool/MigrateVolumeToStorPool.py @@ -0,0 +1,439 @@ +# 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. + +# Import Local Modules +import pprint +import random +import subprocess +import time +import uuid + +from marvin.cloudstackAPI import (listOsTypes, + listTemplates, + listHosts, + createTemplate, + createVolume, + resizeVolume, + startVirtualMachine, + migrateVirtualMachine, + migrateVolume + ) +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from marvin.configGenerator import configuration, cluster +from marvin.lib.base import (Account, + Configurations, + ServiceOffering, + Snapshot, + StoragePool, + Template, + Tag, + VirtualMachine, + VmSnapshot, + Volume, + SecurityGroup, + ) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_disk_offering, + list_snapshots, + list_storage_pools, + list_volumes, + list_virtual_machines, + list_configurations, + list_service_offering, + list_clusters, + list_zones) +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual +from nose.plugins.attrib import attr + +from storpool import spapi +from sp_util import (TestData, StorPoolHelper) + + +class TestMigrateVolumeToAnotherPool(cloudstackTestCase): + @classmethod + def setUpClass(cls): + super(TestMigrateVolumeToAnotherPool, cls).setUpClass() + try: + cls.setUpCloudStack() + except Exception: + cls.cleanUpCloudStack() + raise + + @classmethod + def setUpCloudStack(cls): + cls.spapi = spapi.Api(host="10.2.23.248", port="81", auth="6549874687", multiCluster=True) + testClient = super(TestMigrateVolumeToAnotherPool, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + + cls._cleanup = [] + + cls.unsupportedHypervisor = False + cls.hypervisor = testClient.getHypervisorInfo() + if cls.hypervisor.lower() in ("hyperv", "lxc"): + cls.unsupportedHypervisor = True + return + + cls.services = testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = None + zones = list_zones(cls.apiclient) + + for z in zones: + if z.name == cls.getClsConfig().mgtSvr[0].zone: + cls.zone = z + + assert cls.zone is not None + + td = TestData() + cls.testdata = td.testdata + cls.helper = StorPoolHelper() + storpool_primary_storage = cls.testdata[TestData.primaryStorage] + cls.template_name = storpool_primary_storage.get("name") + storpool_service_offerings = cls.testdata[TestData.serviceOffering] + + nfs_service_offerings = cls.testdata[TestData.serviceOfferingsPrimary] + ceph_service_offerings = cls.testdata[TestData.serviceOfferingsCeph] + + + storage_pool = list_storage_pools( + cls.apiclient, + name=cls.template_name + ) + + nfs_storage_pool = list_storage_pools( + cls.apiclient, + name='nfs' + ) + + ceph_primary_storage = cls.testdata[TestData.primaryStorage4] + + cls.ceph_storage_pool = list_storage_pools( + cls.apiclient, + name=ceph_primary_storage.get("name") + )[0] + + service_offerings = list_service_offering( + cls.apiclient, + name=cls.template_name + ) + nfs_service_offering = list_service_offering( + cls.apiclient, + name='nfs' + ) + + ceph_service_offering = list_service_offering( + cls.apiclient, + name=ceph_primary_storage.get("name") + ) + + disk_offerings = list_disk_offering( + cls.apiclient, + name="ssd" + ) + + cls.disk_offerings = disk_offerings[0] + if storage_pool is None: + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage) + else: + storage_pool = storage_pool[0] + cls.storage_pool = storage_pool + if service_offerings is None: + service_offerings = ServiceOffering.create(cls.apiclient, storpool_service_offerings) + else: + service_offerings = service_offerings[0] + if nfs_service_offering is None: + nfs_service_offering = ServiceOffering.create(cls.apiclient, nfs_service_offerings) + else: + nfs_service_offering = nfs_service_offering[0] + + if ceph_service_offering is None: + ceph_service_offering = ServiceOffering.create(cls.apiclient, ceph_service_offerings) + else: + ceph_service_offering = ceph_service_offering[0] + #The version of CentOS has to be supported + template = get_template( + cls.apiclient, + cls.zone.id, + account = "system" + ) + + cls.nfs_storage_pool = nfs_storage_pool[0] + if cls.nfs_storage_pool.state == "Maintenance": + cls.nfs_storage_pool = StoragePool.cancelMaintenance(cls.apiclient, cls.nfs_storage_pool.id) + + if cls.ceph_storage_pool.state == "Maintenance": + cls.ceph_storage_pool = StoragePool.cancelMaintenance(cls.apiclient, cls.ceph_storage_pool.id) + + cls.account = cls.helper.create_account( + cls.apiclient, + cls.services["account"], + accounttype = 1, + domainid=cls.domain.id, + roleid = 1 + ) + cls._cleanup.append(cls.account) + + securitygroup = SecurityGroup.list(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid)[0] + cls.helper.set_securityGroups(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid, id = securitygroup.id) + + cls.vm = VirtualMachine.create(cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=nfs_service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + cls.vm2 = VirtualMachine.create(cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=nfs_service_offering.id, + hypervisor= cls.hypervisor, + rootdisksize=10 + ) + cls.vm3 = VirtualMachine.create(cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=nfs_service_offering.id, + hypervisor= cls.hypervisor, + rootdisksize=10 + ) + cls.vm4 = VirtualMachine.create(cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=ceph_service_offering.id, + hypervisor= cls.hypervisor, + rootdisksize=10 + ) + cls.vm5 = VirtualMachine.create(cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=ceph_service_offering.id, + hypervisor= cls.hypervisor, + rootdisksize=10 + ) + cls.storage_pool = StoragePool.update(cls.apiclient, + id=cls.storage_pool.id, + tags = ["ssd, nfs"]) + + + if template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + cls.services["domainid"] = cls.domain.id + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["templates"]["ostypeid"] = template.ostypeid + cls.services["zoneid"] = cls.zone.id + + + cls.service_offering = service_offerings + cls.nfs_service_offering = nfs_service_offering + + cls.template = template + cls.random_data_0 = random_gen(size=100) + cls.test_dir = "/tmp" + cls.random_data = "random.data" + return + + @classmethod + def tearDownClass(cls): + cls.cleanUpCloudStack() + + @classmethod + def cleanUpCloudStack(cls): + try: + if cls.nfs_storage_pool.state is not "Maintenance": + cls.nfs_storage_pool = StoragePool.enableMaintenance(cls.apiclient, cls.nfs_storage_pool.id) + + if cls.ceph_storage_pool.state is not "Maintenance": + cls.ceph_storage_pool = StoragePool.enableMaintenance(cls.apiclient, cls.ceph_storage_pool.id) + + cls.storage_pool = StoragePool.update(cls.apiclient, + id=cls.storage_pool.id, + tags = ["ssd"]) + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + return + + def tearDown(self): + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_1_migrate_vm_from_nfs_to_storpool(self): + ''' Test migrate virtual machine from NFS primary storage to StorPool''' + + self.vm.stop(self.apiclient, forced=True) + cmd = migrateVirtualMachine.migrateVirtualMachineCmd() + cmd.virtualmachineid = self.vm.id + cmd.storageid = self.storage_pool.id + migrated_vm = self.apiclient.migrateVirtualMachine(cmd) + volumes = list_volumes( + self.apiclient, + virtualmachineid = migrated_vm.id, + listall=True + ) + for v in volumes: + name = v.path.split("/")[3] + try: + sp_volume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + self.assertEqual(v.storageid, self.storage_pool.id, "Did not migrate virtual machine from NFS to StorPool") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_2_migrate_volume_from_nfs_to_storpool(self): + ''' Test migrate volume from NFS primary storage to StorPool''' + + self.vm2.stop(self.apiclient, forced=True) + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.vm2.id, + listall=True + ) + for v in volumes: + cmd = migrateVolume.migrateVolumeCmd() + cmd.storageid = self.storage_pool.id + cmd.volumeid = v.id + volume = self.apiclient.migrateVolume(cmd) + self.assertEqual(volume.storageid, self.storage_pool.id, "Did not migrate volume from NFS to StorPool") + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.vm2.id, + listall=True + ) + for v in volumes: + name = v.path.split("/")[3] + try: + sp_volume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_3_migrate_volume_from_nfs_to_storpool(self): + '''Test write on disk before migrating volume from NFS primary storage + Check that data is on disk after migration''' + + try: + # Login to VM and write data to file system + ssh_client = self.vm3.get_ssh_client(reconnect = True) + + cmds = [ + "echo %s > %s/%s" % + (self.random_data_0, self.test_dir, self.random_data), + "sync", + "sleep 1", + "sync", + "sleep 1", + "cat %s/%s" % + (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.vm3.ipaddress) + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data has be write into temp file!" + ) + + self.vm3.stop(self.apiclient, forced=True) + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.vm3.id, + listall=True + ) + time.sleep(30) + for v in volumes: + cmd = migrateVolume.migrateVolumeCmd() + cmd.storageid = self.storage_pool.id + cmd.volumeid = v.id + volume = self.apiclient.migrateVolume(cmd) + self.assertEqual(volume.storageid, self.storage_pool.id, "Did not migrate volume from NFS to StorPool") + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.vm3.id, + listall=True + ) + for v in volumes: + name = v.path.split("/")[3] + try: + sp_volume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + self.vm3.start(self.apiclient) + try: + ssh_client = self.vm3.get_ssh_client(reconnect=True) + + cmds = [ + "cat %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.vm3.ipaddress) + + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data is equal with the ramdom file!" + ) diff --git a/test/integration/plugins/storpool/TestStorPoolVolumes.py b/test/integration/plugins/storpool/TestStorPoolVolumes.py new file mode 100644 index 00000000000..4560038d268 --- /dev/null +++ b/test/integration/plugins/storpool/TestStorPoolVolumes.py @@ -0,0 +1,2153 @@ +# 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. + +# Import Local Modules +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual +from marvin.lib.base import (Account, + Cluster, + Configurations, + ServiceOffering, + Snapshot, + StoragePool, + Template, + VirtualMachine, + VmSnapshot, + Volume, + SecurityGroup, + Role, + ) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_disk_offering, + list_hosts, + list_snapshots, + list_storage_pools, + list_volumes, + list_virtual_machines, + list_configurations, + list_service_offering, + list_clusters, + list_zones) +from marvin.cloudstackAPI import (listOsTypes, + listTemplates, + listHosts, + createTemplate, + createVolume, + getVolumeSnapshotDetails, + resizeVolume, + listZones) +import time +import pprint +import random +import subprocess +from storpool import spapi +from storpool import sptypes +from marvin.configGenerator import configuration +import uuid +from sp_util import (TestData, StorPoolHelper) + +class TestStoragePool(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + super(TestStoragePool, cls).setUpClass() + try: + cls.setUpCloudStack() + except Exception: + cls.cleanUpCloudStack() + raise + + @classmethod + def setUpCloudStack(cls): + testClient = super(TestStoragePool, cls).getClsTestClient() + + cls._cleanup = [] + + cls.apiclient = testClient.getApiClient() + cls.helper = StorPoolHelper() + + cls.unsupportedHypervisor = False + cls.hypervisor = testClient.getHypervisorInfo() + if cls.hypervisor.lower() in ("hyperv", "lxc"): + cls.unsupportedHypervisor = True + return + + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = None + zones = list_zones(cls.apiclient) + + for z in zones: + if z.name == cls.getClsConfig().mgtSvr[0].zone: + cls.zone = z + + assert cls.zone is not None + + cls.sp_template_1 = "ssd" + storpool_primary_storage = { + "name" : cls.sp_template_1, + "zoneid": cls.zone.id, + "url": "SP_API_HTTP=10.2.23.248:81;SP_AUTH_TOKEN=6549874687;SP_TEMPLATE=%s" % cls.sp_template_1, + "scope": "zone", + "capacitybytes": 564325555333, + "capacityiops": 155466, + "hypervisor": "kvm", + "provider": "StorPool", + "tags": cls.sp_template_1 + } + + cls.storpool_primary_storage = storpool_primary_storage + host, port, auth = cls.getCfgFromUrl(url = storpool_primary_storage["url"]) + cls.spapi = spapi.Api(host=host, port=port, auth=auth, multiCluster=True) + + storage_pool = list_storage_pools( + cls.apiclient, + name=storpool_primary_storage["name"] + ) + + if storage_pool is None: + newTemplate = sptypes.VolumeTemplateCreateDesc(name = storpool_primary_storage["name"],placeAll = "virtual", placeTail = "virtual", placeHead = "virtual", replication=1) + template_on_local = cls.spapi.volumeTemplateCreate(newTemplate) + + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage) + else: + storage_pool = storage_pool[0] + cls.primary_storage = storage_pool + + + storpool_service_offerings_ssd = { + "name": cls.sp_template_1, + "displaytext": "SP_CO_2 (Min IOPS = 10,000; Max IOPS = 15,000)", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "tags": cls.sp_template_1 + } + + service_offerings_ssd = list_service_offering( + cls.apiclient, + name=storpool_service_offerings_ssd["name"] + ) + + if service_offerings_ssd is None: + service_offerings_ssd = ServiceOffering.create(cls.apiclient, storpool_service_offerings_ssd) + else: + service_offerings_ssd = service_offerings_ssd[0] + + cls.service_offering = service_offerings_ssd + cls.debug(pprint.pformat(cls.service_offering)) + + + cls.sp_template_2 = "ssd2" + + storpool_primary_storage2 = { + "name" : cls.sp_template_2, + "zoneid": cls.zone.id, + "url": "SP_API_HTTP=10.2.23.248:81;SP_AUTH_TOKEN=6549874687;SP_TEMPLATE=%s" % cls.sp_template_2, + "scope": "zone", + "capacitybytes": 564325555333, + "capacityiops": 1554, + "hypervisor": "kvm", + "provider": "StorPool", + "tags": cls.sp_template_2 + } + + cls.storpool_primary_storage2 = storpool_primary_storage2 + storage_pool = list_storage_pools( + cls.apiclient, + name=storpool_primary_storage2["name"] + ) + + if storage_pool is None: + newTemplate = sptypes.VolumeTemplateCreateDesc(name = storpool_primary_storage2["name"],placeAll = "virtual", placeTail = "virtual", placeHead = "virtual", replication=1) + + template_on_local = cls.spapi.volumeTemplateCreate(newTemplate) + + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage2) + + else: + storage_pool = storage_pool[0] + cls.primary_storage2 = storage_pool + + storpool_service_offerings_ssd2 = { + "name": cls.sp_template_2, + "displaytext": "SP_CO_2", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "tags": cls.sp_template_2 + } + + service_offerings_ssd2 = list_service_offering( + cls.apiclient, + name=storpool_service_offerings_ssd2["name"] + ) + + if service_offerings_ssd2 is None: + service_offerings_ssd2 = ServiceOffering.create(cls.apiclient, storpool_service_offerings_ssd2) + else: + service_offerings_ssd2 = service_offerings_ssd2[0] + + cls.service_offering2 = service_offerings_ssd2 + + disk_offerings = list_disk_offering( + cls.apiclient, + name="Small" + ) + + disk_offering_20 = list_disk_offering( + cls.apiclient, + name="Medium" + ) + + disk_offering_100 = list_disk_offering( + cls.apiclient, + name="Large" + ) + + + cls.disk_offerings = disk_offerings[0] + cls.disk_offering_20 = disk_offering_20[0] + cls.disk_offering_100 = disk_offering_100[0] + + #The version of CentOS has to be supported + template = get_template( + cls.apiclient, + cls.zone.id, + account = "system" + ) + + if template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + cls.services["domainid"] = cls.domain.id + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["templates"]["ostypeid"] = template.ostypeid + cls.services["zoneid"] = cls.zone.id + cls.services["diskofferingid"] = cls.disk_offerings.id + + role = Role.list(cls.apiclient, name='Admin') + + # Create VMs, VMs etc + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id, + roleid = role[0].id + ) + + securitygroup = SecurityGroup.list(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid)[0] + cls.helper.set_securityGroups(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid, id = securitygroup.id) + cls._cleanup.append(cls.account) + + cls.volume_1 = Volume.create( + cls.apiclient, + {"diskname":"StorPoolDisk-1" }, + zoneid=cls.zone.id, + diskofferingid=disk_offerings[0].id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + + cls.volume_2 = Volume.create( + cls.apiclient, + {"diskname":"StorPoolDisk-2" }, + zoneid=cls.zone.id, + diskofferingid=disk_offerings[0].id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + + cls.volume = Volume.create( + cls.apiclient, + {"diskname":"StorPoolDisk-3" }, + zoneid=cls.zone.id, + diskofferingid=disk_offerings[0].id, + account=cls.account.name, + domainid=cls.account.domainid, + ) + + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.virtual_machine2 = VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.vm_migrate = VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=cls.account.name, + domainid=cls.account.domainid, + zoneid=cls.zone.id, + templateid=template.id, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.template = template + cls.hostid = cls.virtual_machine.hostid + cls.random_data_0 = random_gen(size=100) + cls.test_dir = "/tmp" + cls.random_data = "random.data" + return + + @classmethod + def tearDownClass(cls): + cls.cleanUpCloudStack() + + @classmethod + def cleanUpCloudStack(cls): + try: + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + return + + def tearDown(self): + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_01_snapshot_to_template(self): + ''' Create template from snapshot without bypass secondary storage + ''' + volume = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + snapshotid = snapshot.id + ) + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + ssh_client = virtual_machine.get_ssh_client() + + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + self._cleanup.append(template) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_02_snapshot_to_template_bypass_secondary(self): + ''' Test Create Template from snapshot bypassing secondary storage + ''' + ##cls.virtual_machine + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + try: + name = volume[0].path.split("/")[3] + sp_volume = self.spapi.volumeList(volumeName = "~" + name) + except spapi.ApiError as err: + raise Exception(err) + + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snasphot in snapshot_details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + snapshotid = snapshot.id + ) + + flag = False + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + flag = True + break + else: + continue + break + + if flag is False: + raise Exception("Template does not exists in Storpool") + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + ssh_client = virtual_machine.get_ssh_client() + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + self._cleanup.append(template) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_03_snapshot_volume_with_secondary(self): + ''' + Test Create snapshot and backup to secondary + ''' + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot_details") + except spapi.ApiError as err: + raise Exception(err) + self.assertIsNotNone(snapshot, "Could not create snapshot") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_04_snapshot_volume_bypass_secondary(self): + ''' + Test snapshot bypassing secondary + ''' + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except spapi.ApiError as err: + raise Exception(err) + self.assertIsNotNone(snapshot, "Could not create snapshot") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_05_delete_template_bypassed_secondary(self): + ''' Test delete template from snapshot bypassed secondary storage + ''' + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + try: + name = volume[0].path.split("/")[3] + sp_volume = self.spapi.volumeList(volumeName = "~" + name) + except spapi.ApiError as err: + raise Exception(err) + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + snapshotid = snapshot.id + ) + + flag = False + storpoolGlId = None + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + storpoolGlId = "~" + snap.globalId + flag = True + break + else: + continue + break + + if flag is False: + raise Exception("Template does not exists in Storpool") + + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + temp = Template.delete(template, self.apiclient, self.zone.id) + self.assertIsNone(temp, "Template was not deleted") + + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = storpoolGlId) + if sp_snapshot is not None: + self.debug("Snapshot exists on StorPool name " + storpoolGlId) + except spapi.ApiError as err: + self.debug("Do nothing the template has to be deleted") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_06_template_from_snapshot(self): + ''' Test create template bypassing secondary from snapshot which is backed up on secondary storage + ''' + ##cls.virtual_machine + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + try: + name = volume[0].path.split("/")[3] + sp_volume = self.spapi.volumeList(volumeName = "~" + name) + except spapi.ApiError as err: + raise Exception(err) + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapsho details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + snapshotid = snapshot.id + ) + + flag = False + globalId = None + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + flag = True + globalId = snap.globalId + break + else: + continue + break + + if flag is False: + raise Exception("Template does not exists in Storpool") + + + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + temp = Template.delete(template, self.apiclient, self.zone.id) + self.assertIsNone(temp, "Template was not deleted") + + if globalId is not None: + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + globalId) + if sp_snapshot is not None: + self.debug("Snapshot exists on Storpool name " + globalId) + except spapi.ApiError as err: + self.debug("Do nothing the template has to be deleted") + else: + flag = False + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + flag = True + break + else: + continue + break + + if flag is True: + raise Exception("Template should not exists in Storpool") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_07_delete_snapshot_of_deleted_volume(self): + ''' Delete snapshot and template if volume is already deleted, not bypassing secondary + ''' + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + + volume = Volume.create( + self.apiclient, + {"diskname":"StorPoolDisk-Delete" }, + zoneid = self.zone.id, + diskofferingid = self.disk_offerings.id, + account=self.account.name, + domainid=self.account.domainid, + ) + delete = volume + self.virtual_machine2.stop(self.apiclient, forced=True) + self.virtual_machine2.attach_volume( + self.apiclient, + volume + ) + self.virtual_machine2.detach_volume( + self.apiclient, + volume + ) + + volume = list_volumes(self.apiclient, id = volume.id) + + name = volume[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + except spapi.ApiError as err: + raise Exception(err) + if flag == False: + raise Exception("Could not finad snapshot in snapshot details") + except Exception as err: + raise Exception(err) + + template = self.create_template_from_snapshot(self.apiclient, self.services, snapshotid = snapshot.id) + + template_from_volume = self.create_template_from_snapshot(self.apiclient, self.services, volumeid = volume[0].id) + + Volume.delete(delete, self.apiclient, ) + Snapshot.delete(snapshot, self.apiclient) + + flag = False + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + if snapshot_details is not None: + try: + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + except spapi.ApiError as err: + flag = False + + if flag is True: + raise Exception("Snapshot was not deleted") + except Exception as err: + self.debug('Snapshot was deleted %s' % err) + + Template.delete(template, self.apiclient, zoneid = self.zone.id) + Template.delete(template_from_volume, self.apiclient, zoneid = self.zone.id) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_08_delete_snapshot_of_deleted_volume(self): + ''' Delete snapshot and template if volume is already deleted, bypassing secondary + ''' + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + + volume = Volume.create( + self.apiclient, + {"diskname":"StorPoolDisk-Delete" }, + zoneid = self.zone.id, + diskofferingid = self.disk_offerings.id, + account=self.account.name, + domainid=self.account.domainid, + ) + delete = volume + self.virtual_machine2.attach_volume( + self.apiclient, + volume + ) + self.virtual_machine2.detach_volume( + self.apiclient, + volume + ) + + volume = list_volumes(self.apiclient, id = volume.id) + + name = volume[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + if snapshot_details is not None: + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + except spapi.ApiError as err: + raise Exception(err) + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except Exception as err: + raise Exception(err) + + template = self.create_template_from_snapshot(self.apiclient, self.services, snapshotid = snapshot.id) + + Volume.delete(delete, self.apiclient, ) + Snapshot.delete(snapshot, self.apiclient) + + flag = False + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + if snapshot_details is not None: + try: + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + except spapi.ApiError as err: + flag = False + + if flag is True: + raise Exception("Snapshot was not deleted") + except Exception as err: + self.debug('Snapshot was deleted %s' % err) + + + Template.delete(template, self.apiclient, zoneid = self.zone.id) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_09_vm_from_bypassed_template(self): + '''Create virtual machine with sp.bypass.secondary.storage=false + from template created on StorPool and Secondary Storage''' + + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + + name = volume[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "true") + + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + except spapi.ApiError as err: + raise Exception(err) + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except Exception as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + snapshotid = snapshot.id + ) + self._cleanup.append(template) + + flag = False + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + flag = True + break + else: + continue + break + + if flag is False: + raise Exception("Template does not exists in Storpool") + + + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + + backup_config = Configurations.update(self.apiclient, + name = "sp.bypass.secondary.storage", + value = "false") + + vm = VirtualMachine.create( + self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid = template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10, + ) + + ssh_client = vm.get_ssh_client(reconnect=True) + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_10_create_vm_snapshots(self): + """Test to create VM snapshots + """ + volume_attached = self.virtual_machine.attach_volume( + self.apiclient, + self.volume + ) + + vol = list_volumes(self.apiclient, virtualmachineid=self.virtual_machine.id, id=volume_attached.id) + name = vol[0].path.split("/")[3] + sp_volume = self.spapi.volumeList(volumeName = "~" + name) + self.assertEqual(volume_attached.id, self.volume.id, "Is not the same volume ") + try: + # Login to VM and write data to file system + ssh_client = self.virtual_machine.get_ssh_client() + + cmds = [ + "echo %s > %s/%s" % + (self.random_data_0, self.test_dir, self.random_data), + "sync", + "sleep 1", + "sync", + "sleep 1", + "cat %s/%s" % + (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data has be write into temp file!" + ) + + time.sleep(30) + MemorySnapshot = False + vm_snapshot = VmSnapshot.create( + self.apiclient, + self.virtual_machine.id, + MemorySnapshot, + "TestSnapshot", + "Display Text" + ) + self.assertEqual( + vm_snapshot.state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_11_revert_vm_snapshots(self): + """Test to revert VM snapshots + """ + + try: + ssh_client = self.virtual_machine.get_ssh_client() + + cmds = [ + "rm -rf %s/%s" % (self.test_dir, self.random_data), + "ls %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + if str(result[0]).index("No such file or directory") == -1: + self.fail("Check the random data has be delete from temp file!") + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + + self.assertEqual( + list_snapshot_response[0].state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + self.virtual_machine.stop(self.apiclient, forced=True) + + VmSnapshot.revertToSnapshot( + self.apiclient, + list_snapshot_response[0].id + ) + + self.virtual_machine.start(self.apiclient) + + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + cmds = [ + "cat %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data is equal with the ramdom file!" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_12_delete_vm_snapshots(self): + """Test to delete vm snapshots + """ + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + VmSnapshot.deleteVMSnapshot( + self.apiclient, + list_snapshot_response[0].id) + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + #vmid=self.virtual_machine.id, + virtualmachineid=self.virtual_machine.id, + listall=False) + self.debug('list_snapshot_response -------------------- %s' % list_snapshot_response) + + self.assertIsNone(list_snapshot_response, "snapshot is already deleted") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_13_detach_volume(self): + '''Attach volume on VM on 2nd zone''' + self.virtual_machine.stop(self.apiclient) + self.virtual_machine.detach_volume( + self.apiclient, + self.volume + ) + vol = list_volumes(self.apiclient, id=self.volume.id) + name = vol[0].path.split("/")[3] + spvolume = self.spapi.volumeList(volumeName = "~" + name) + self.assertEqual(vol[0].id, self.volume.id, "Is not the same volume ") + tags = spvolume[0].tags + for t in tags: + self.assertFalse(t.lower() == 'cvm'.lower(), "cvm tag still set on detached volume") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_14_attach_detach_volume_to_running_vm(self): + ''' Test Attach Volume To Running Virtual Machine + ''' + time.sleep(60) + self.assertEqual(VirtualMachine.RUNNING, self.virtual_machine.state, "Running") + volume = self.virtual_machine.attach_volume( + self.apiclient, + self.volume_1 + ) + print(volume) + self.assertIsNotNone(volume, "Volume is not None") + + list_vm_volumes = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + id= volume.id + ) + print(list_vm_volumes) + self.assertEqual(volume.id, list_vm_volumes[0].id, "Is true") + + name = list_vm_volumes[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + volume = self.virtual_machine.detach_volume( + self.apiclient, + self.volume_1 + ) + list_vm_volumes = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + id = volume.id + ) + + print(list_vm_volumes) + self.assertIsNone(list_vm_volumes, "Is None") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_15_resize_root_volume_on_working_vm(self): + ''' Test Resize Root volume on Running Virtual Machine + ''' + self.assertEqual(VirtualMachine.RUNNING, self.virtual_machine2.state, "Running") + volume = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine2.id, + type = "ROOT", + listall = True, + ) + volume = volume[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + self.assertEqual(volume.type, 'ROOT', "Volume is not of ROOT type") + shrinkOk = False + if volume.size > int((self.disk_offering_20.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = volume.id + cmd.size = 20 + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + + new_size = Volume.list( + self.apiclient, + id=volume.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_20.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + volume = new_size[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if volume.size > int((self.disk_offering_100.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = volume.id + cmd.size = 100 + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=volume.id + ) + + volume = new_size[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_100.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_16_resize_attached_volume_on_working_vm(self): + ''' Test Resize Volume Attached To Running Virtual Machine + ''' + self.assertEqual(VirtualMachine.RUNNING, self.virtual_machine.state, "Running") + volume = self.virtual_machine.attach_volume( + self.apiclient, + self.volume_1 + ) + + listvol = Volume.list( + self.apiclient, + id=volume.id + ) + name = listvol[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != listvol[0].size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if volume.size > int((self.disk_offering_20.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = volume.id + cmd.diskofferingid = self.disk_offering_20.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + + new_size = Volume.list( + self.apiclient, + id=volume.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_20.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + volume = new_size[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if volume.size > int((self.disk_offering_100.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = volume.id + cmd.diskofferingid = self.disk_offering_100.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=volume.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_100.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + + # return to small disk + volume = new_size[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if volume.size > int((self.disk_offerings.disksize)* (1024**3)): + shrinkOk= True + + cmd.diskofferingid = self.disk_offerings.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=volume.id + ) + + volume = new_size[0] + + name = volume.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + self.assertTrue( + (new_size[0].size == int((self.disk_offerings.disksize)*(1024**3))), + "Could not return to Small disk" + ) + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_17_attach_detach_volume_to_stopped_vm(self): + ''' Test Attach Volume To Stopped Virtual Machine + ''' + virtual_machine = self.virtual_machine.stop( + self.apiclient, + forced=True + ) + + time.sleep(60) + volume_2 = self.virtual_machine.attach_volume( + self.apiclient, + self.volume_2 + ) + list_vm_volumes = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + id= volume_2.id + ) + + name = list_vm_volumes[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + print(list_vm_volumes) + self.assertEqual(volume_2.id,list_vm_volumes[0].id, "Is true") + + time.sleep(90) + volume_2 = self.virtual_machine.detach_volume( + self.apiclient, + self.volume_2 + ) + list_vm_volumes = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + id = volume_2.id + ) + print(list_vm_volumes) + self.assertIsNone(list_vm_volumes, "Is None") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_18_resize_attached_volume(self): + ''' Test Resize Volume Attached To Virtual Machine + ''' + + shrinkOk = False + if self.volume_1.size > int((self.disk_offering_20.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = self.volume_1.id + cmd.diskofferingid = self.disk_offering_20.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + + new_size = Volume.list( + self.apiclient, + id=self.volume_1.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_20.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + self.volume_1 = new_size[0] + + name = self.volume_1.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != self.volume_1.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if self.volume_1.size > int((self.disk_offering_100.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = self.volume_1.id + cmd.diskofferingid = self.disk_offering_100.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=self.volume_1.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_100.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + + # return to small disk + self.volume_1 = new_size[0] + + name = self.volume_1.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != self.volume_1.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if self.volume_1.size > int((self.disk_offerings.disksize)* (1024**3)): + shrinkOk= True + + cmd.diskofferingid = self.disk_offerings.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=self.volume_1.id + ) + + name = new_size[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != new_size[0].size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + self.assertTrue( + (new_size[0].size == int((self.disk_offerings.disksize)*(1024**3))), + "Could not return to Small disk" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_19_resize_detached_volume(self): + ''' Test Resize Volume Detached To Virtual Machine + ''' + list_vm_volumes = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + id= self.volume_2.id + ) + + #check that the volume is not attached to VM + self.assertIsNone(list_vm_volumes, "List volumes is not None") + + shrinkOk = False + if self.volume_2.size > int((self.disk_offering_20.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = self.volume_2.id + cmd.diskofferingid = self.disk_offering_20.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + + new_size = Volume.list( + self.apiclient, + id=self.volume_2.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_20.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + self.volume_2 = new_size[0] + + name = self.volume_2.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != self.volume_2.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if self.volume_2.size > int((self.disk_offering_100.disksize) * (1024**3)): + shrinkOk= True + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = self.volume_2.id + cmd.diskofferingid = self.disk_offering_100.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=self.volume_2.id + ) + + self.assertTrue( + (new_size[0].size == int((self.disk_offering_100.disksize) * (1024**3))), + "New size is not int((self.disk_offering_20) * (1024**3)" + ) + + # return to small disk + self.volume_2 = new_size[0] + + name = self.volume_2.path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != self.volume_2.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + shrinkOk = False + if self.volume_2.size > int((self.disk_offerings.disksize)* (1024**3)): + shrinkOk= True + + cmd.diskofferingid = self.disk_offerings.id + cmd.shrinkok = shrinkOk + + self.apiclient.resizeVolume(cmd) + new_size = Volume.list( + self.apiclient, + id=self.volume_2.id + ) + + name = new_size[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + if spvolume[0].size != new_size[0].size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + self.assertTrue( + (new_size[0].size == int((self.disk_offerings.disksize)*(1024**3))), + "Could not return to Small disk" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_20_snapshot_to_volume(self): + ''' Create volume from snapshot + ''' + snapshot = Snapshot.create( + self.apiclient, + volume_id = self.volume_2.id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + volume = self.create_volume( + self.apiclient, + zoneid = self.zone.id, + snapshotid = snapshot.id, + account=self.account.name, + domainid=self.account.domainid + ) + + listvol = Volume.list( + self.apiclient, + id=volume.id + ) + name = listvol[0].path.split("/")[3] + try: + spvolume = self.spapi.volumeList(volumeName="~" + name) + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(volume, "Could not create volume from snapshot") + self.assertIsInstance(volume, Volume, "Volume is not instance of Volume") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_21_snapshot_detached_volume(self): + ''' Test Snapshot Detached Volume + ''' + self.virtual_machine.stop( + self.apiclient, + forced = True + ) + self.volume = self.virtual_machine.attach_volume( + self.apiclient, + self.volume + ) + self.assertIsNotNone(self.volume, "Attach: Is none") + self.volume = self.virtual_machine.detach_volume( + self.apiclient, + self.volume + ) + + self.assertIsNotNone(self.volume, "Detach: Is none") + + snapshot = Snapshot.create( + self.apiclient, + self.volume.id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Snapshot is None") + + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not Instance of Snappshot") + + snapshot = Snapshot.delete( + snapshot, + self.apiclient + ) + + self.assertIsNone(snapshot, "Snapshot was not deleted") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_22_snapshot_root_disk(self): + ''' Test ROOT Disk Snapshot + ''' + vm = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid = self.zone.id, + templateid = self.template.id, + serviceofferingid = self.service_offering.id, + hypervisor = self.hypervisor, + rootdisksize = 10 + ) + list_volumes_of_vm = list_volumes( + self.apiclient, + virtualmachineid = vm.id, + listall = True, + ) + self.assertIs(len(list_volumes_of_vm), 1, "VM has more disk than 1") + + snapshot = Snapshot.create( + self.apiclient, + list_volumes_of_vm[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = self.apiclient.getVolumeSnapshotDetails(cmd) + flag = False + for s in snapshot_details: + if s["snapshotDetailsName"] == snapshot.id: + name = s["snapshotDetailsValue"].split("/")[3] + sp_snapshot = self.spapi.snapshotList(snapshotName = "~" + name) + flag = True + if flag == False: + raise Exception("Could not find snapshot in snapshot details") + except spapi.ApiError as err: + raise Exception(err) + + self.assertIsNotNone(snapshot, "Snapshot is None") + + self.assertEqual(list_volumes_of_vm[0].id, snapshot.volumeid, "Snapshot is not for the same volume") + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_23_volume_to_template(self): + ''' Create Template From ROOT Volume + ''' + volume = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + + self.virtual_machine.stop(self.apiclient) + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + volumeid = volume[0].id + ) + + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + ssh_client = virtual_machine.get_ssh_client() + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + self._cleanup.append(template) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_24_migrate_vm_to_another_storage(self): + ''' Migrate VM to another Primary Storage + ''' + list_volumes_of_vm = list_volumes( + self.apiclient, + virtualmachineid = self.vm_migrate.id, + listall = True, + ) + + self.assertTrue(len(list_volumes_of_vm) == 1, "There are more volumes attached to VM") + + if list_volumes_of_vm[0].storageid is self.primary_storage.id: + cmd = migrateVirtualMachine.migrateVirtualMachineCmd() + cmd.virtualmachineid = self.vm_migrate.id + if hostid: + cmd.hostid = hostid + vm = apiclient.migrateVirtualMachine(cmd) + volume = list_volumes( + self.apiclient, + virtualmachineid = vm.id + )[0] + self.assertNotEqual(volume.storageid, self.primary_storage.id, "Could not migrate VM") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_25_migrate_volume_to_another_storage(self): + ''' Migrate Volume To Another Primary Storage + ''' + self.assertFalse(hasattr(self.volume, 'virtualmachineid') , "Volume is not detached") + + self.assertFalse(hasattr(self.volume, 'storageid') , "Volume is not detached") + volume = Volume.migrate( + self.apiclient, + volumeid = self.volume.id, + storageid = self.primary_storage2.id + ) + + self.assertIsNotNone(volume, "Volume is None") + + self.assertEqual(volume.storageid, self.primary_storage2.id, "Storage is the same") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_26_create_vm_on_another_storpool_storage(self): + """ Create Virtual Machine on another StorPool primary StoragePool""" + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=self.template.id, + serviceofferingid=self.service_offering2.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + self.assertIsNotNone(virtual_machine, "Could not create virtual machine on another Storpool primary storage") + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_27_snapshot_to_volume_of_root_disk(self): + ''' Create volume from snapshot + ''' + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=self.template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + volume1 = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiclient, + volume_id = volume1[0].id, + account=self.account.name, + domainid=self.account.domainid, + ) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + self.assertIsInstance(snapshot, Snapshot, "Snapshot is not an instance of Snapshot") + + volume = self.create_volume( + self.apiclient, + zoneid = self.zone.id, + snapshotid = snapshot.id, + account=self.account.name, + domainid=self.account.domainid + ) + + self.assertIsNotNone(volume, "Could not create volume from snapshot") + self.assertIsInstance(volume, Volume, "Volume is not instance of Volume") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_28_download_volume(self): + vol = self.volume.extract( + self.apiclient, + volume_id = self.volume.id, + zoneid = self.zone.id, + mode = "HTTP_DOWNLOAD" + ) + self.assertIsNotNone(vol, "Volume is None") + self.assertIsNotNone(vol.url, "No URL provided") + Volume.delete(vol, self.apiclient) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_29_create_vm_from_template_not_on_storpool(self): + ''' Create virtual machine from template which for some reason is deleted from StorPool, but exists in template_spoool_ref DB tables ''' + + volume = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + + self.virtual_machine.stop(self.apiclient) + + template = self.create_template_from_snapshot( + self.apiclient, + self.services, + volumeid = volume[0].id + ) + + virtual_machine = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + ssh_client = virtual_machine.get_ssh_client(reconnect= True) + name = 'ssd-' + template.id + flag = False + storpoolGlId = None + + sp_snapshots = self.spapi.snapshotsList() + for snap in sp_snapshots: + tags = snap.tags + for t in tags: + if tags[t] == template.id: + storpoolGlId = snap.globalId + flag = True + break + else: + continue + break + + if flag is False: + try: + sp_snapshot = self.spapi.snapshotList(snapshotName = name) + except spapi.ApiError as err: + raise Exception(err) + + + self.spapi.snapshotDelete(snapshotName ="~" + storpoolGlId) + + virtual_machine2 = VirtualMachine.create(self.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + accountid=self.account.name, + domainid=self.account.domainid, + zoneid=self.zone.id, + templateid=template.id, + serviceofferingid=self.service_offering.id, + hypervisor=self.hypervisor, + rootdisksize=10 + ) + + ssh_client = virtual_machine2.get_ssh_client(reconnect= True) + self.assertIsNotNone(template, "Template is None") + self.assertIsInstance(template, Template, "Template is instance of template") + self._cleanup.append(template) + + @classmethod + def create_volume(self, apiclient, zoneid=None, snapshotid=None, account=None, domainid=None): + """Create Volume""" + cmd = createVolume.createVolumeCmd() + cmd.name = "Test" + + if zoneid: + cmd.zoneid = zoneid + + if snapshotid: + cmd.snapshotid = snapshotid + + if account: + cmd.account=account + + if domainid: + cmd.domainid=domainid + return Volume(apiclient.createVolume(cmd).__dict__) + + @classmethod + def get_local_cluster(cls): + storpool_clusterid = subprocess.check_output(['storpool_confshow', 'CLUSTER_ID']) + clusterid = storpool_clusterid.split("=") + cls.debug(storpool_clusterid) + clusters = list_clusters(cls.apiclient) + for c in clusters: + configuration = list_configurations( + cls.apiclient, + clusterid = c.id + ) + for conf in configuration: + if conf.name == 'sp.cluster.id' and (conf.value in clusterid[1]): + return c + + @classmethod + def get_remote_cluster(cls): + storpool_clusterid = subprocess.check_output(['storpool_confshow', 'CLUSTER_ID']) + clusterid = storpool_clusterid.split("=") + cls.debug(storpool_clusterid) + clusters = list_clusters(cls.apiclient) + for c in clusters: + configuration = list_configurations( + cls.apiclient, + clusterid = c.id + ) + for conf in configuration: + if conf.name == 'sp.cluster.id' and (conf.value not in clusterid[1]): + return c + + @classmethod + def list_hosts_by_cluster_id(cls, clusterid): + """List all Hosts matching criteria""" + cmd = listHosts.listHostsCmd() + cmd.clusterid = clusterid + return(cls.apiclient.listHosts(cmd)) + + + def start(cls, vmid, hostid): + """Start the instance""" + cmd = startVirtualMachine.startVirtualMachineCmd() + cmd.id = vmid + cmd.hostid = hostid + return (cls.apiclient.startVirtualMachine(cmd)) + + + @classmethod + def create_template_from_snapshot(self, apiclient, services, snapshotid=None, volumeid=None): + """Create template from Volume""" + # Create template from Virtual machine and Volume ID + cmd = createTemplate.createTemplateCmd() + cmd.displaytext = "StorPool_Template" + cmd.name = "-".join(["StorPool-", random_gen()]) + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + # Find OSTypeId from Os type + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) + + if not isinstance(ostypes, list): + raise Exception( + "Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + raise Exception( + "Unable to find Ostype is required for creating template") + + cmd.isfeatured = True + cmd.ispublic = True + cmd.isextractable = False + + if snapshotid: + cmd.snapshotid = snapshotid + if volumeid: + cmd.volumeid = volumeid + return Template(apiclient.createTemplate(cmd).__dict__) + + @classmethod + def getCfgFromUrl(cls, url): + cfg = dict([ + option.split('=') + for option in url.split(';') + ]) + host, port = cfg['SP_API_HTTP'].split(':') + auth = cfg['SP_AUTH_TOKEN'] + return host, int(port), auth + + diff --git a/test/integration/plugins/storpool/TestTagsOnStorPool.py b/test/integration/plugins/storpool/TestTagsOnStorPool.py new file mode 100644 index 00000000000..f37e4cfd532 --- /dev/null +++ b/test/integration/plugins/storpool/TestTagsOnStorPool.py @@ -0,0 +1,576 @@ +# 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. + +# Import Local Modules +import pprint +import random +import subprocess +import time +import json + +from marvin.cloudstackAPI import (listOsTypes, + listTemplates, + listHosts, + createTemplate, + createVolume, + resizeVolume, + revertSnapshot, + startVirtualMachine) +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from marvin.configGenerator import configuration, cluster +from marvin.lib.base import (Account, + Configurations, + ServiceOffering, + Snapshot, + StoragePool, + Template, + Tag, + VirtualMachine, + VmSnapshot, + Volume, + SecurityGroup, + ) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_disk_offering, + list_snapshots, + list_storage_pools, + list_volumes, + list_virtual_machines, + list_configurations, + list_service_offering, + list_clusters, + list_zones) +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual +from nose.plugins.attrib import attr + +from storpool import spapi +import uuid +from sp_util import (TestData, StorPoolHelper) + +class TestStoragePool(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + super(TestStoragePool, cls).setUpClass() + try: + cls.setUpCloudStack() + except Exception: + cls.cleanUpCloudStack() + raise + + @classmethod + def setUpCloudStack(cls): + cls.spapi = spapi.Api(host="10.2.23.248", port="81", auth="6549874687", multiCluster=True) + testClient = super(TestStoragePool, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.unsupportedHypervisor = False + cls.hypervisor = testClient.getHypervisorInfo() + if cls.hypervisor.lower() in ("hyperv", "lxc"): + cls.unsupportedHypervisor = True + return + + cls._cleanup = [] + + cls.services = testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = None + zones = list_zones(cls.apiclient) + + for z in zones: + if z.name == cls.getClsConfig().mgtSvr[0].zone: + cls.zone = z + + assert cls.zone is not None + + td = TestData() + cls.testdata = td.testdata + cls.helper = StorPoolHelper() + + cls.account = cls.helper.create_account( + cls.apiclient, + cls.services["account"], + accounttype = 1, + domainid=cls.domain.id, + roleid = 1 + ) + cls._cleanup.append(cls.account) + + securitygroup = SecurityGroup.list(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid)[0] + cls.helper.set_securityGroups(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid, id = securitygroup.id) + + storpool_primary_storage = cls.testdata[TestData.primaryStorage] + + storpool_service_offerings = cls.testdata[TestData.serviceOffering] + + cls.template_name = storpool_primary_storage.get("name") + + storage_pool = list_storage_pools( + cls.apiclient, + name=cls.template_name + ) + + service_offerings = list_service_offering( + cls.apiclient, + name=cls.template_name + ) + + disk_offerings = list_disk_offering( + cls.apiclient, + name="ssd" + ) + + cls.disk_offerings = disk_offerings[0] + if storage_pool is None: + storage_pool = StoragePool.create(cls.apiclient, storpool_primary_storage) + else: + storage_pool = storage_pool[0] + cls.storage_pool = storage_pool + cls.debug(pprint.pformat(storage_pool)) + if service_offerings is None: + service_offerings = ServiceOffering.create(cls.apiclient, storpool_service_offerings) + else: + service_offerings = service_offerings[0] + #The version of CentOS has to be supported + template = get_template( + cls.apiclient, + cls.zone.id, + account = "system" + ) + + cls.debug(pprint.pformat(template)) + cls.debug(pprint.pformat(cls.hypervisor)) + + if template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + cls.services["domainid"] = cls.domain.id + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["templates"]["ostypeid"] = template.ostypeid + cls.services["zoneid"] = cls.zone.id + cls.services["diskofferingid"] = cls.disk_offerings.id + + cls.service_offering = service_offerings + cls.debug(pprint.pformat(cls.service_offering)) + + cls.volume_1 = Volume.create( + cls.apiclient, + cls.services, + account=cls.account.name, + domainid=cls.account.domainid, + size=5 + ) + cls.volume_2 = Volume.create( + cls.apiclient, + cls.services, + account=cls.account.name, + domainid=cls.account.domainid, + size =5 + ) + cls.volume = Volume.create( + cls.apiclient, + cls.services, + account=cls.account.name, + domainid=cls.account.domainid, + size=5 + ) + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + cls.virtual_machine2= VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + cls.template = template + cls.random_data_0 = random_gen(size=100) + cls.test_dir = "/tmp" + cls.random_data = "random.data" + return + + @classmethod + def tearDownClass(cls): + cls.cleanUpCloudStack() + + @classmethod + def cleanUpCloudStack(cls): + try: + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + return + + def tearDown(self): + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_01_set_vcpolicy_tag_to_vm_with_attached_disks(self): + ''' Test set vc-policy tag to VM with one attached disk + ''' + volume_attached = self.virtual_machine.attach_volume( + self.apiclient, + self.volume_1 + ) + tag = Tag.create( + self.apiclient, + resourceIds=self.virtual_machine.id, + resourceType='UserVm', + tags={'vc-policy': 'testing_vc-policy'} + ) + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine.id, listall=True) + vm_tags = vm[0].tags + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, listall=True + ) + + self.vc_policy_tags(volumes, vm_tags, vm) + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_02_set_vcpolicy_tag_to_attached_disk(self): + """ Test set vc-policy tag to new disk attached to VM""" + volume_attached = self.virtual_machine.attach_volume( + self.apiclient, + self.volume_2 + ) + volume = list_volumes(self.apiclient, id = volume_attached.id, listall=True) + name = volume[0].path.split("/")[3] + sp_volume = self.spapi.volumeList(volumeName="~" + name) + + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine.id, listall=True) + vm_tags = vm[0].tags + for vm_tag in vm_tags: + for sp_tag in sp_volume[0].tags: + if sp_tag == vm_tag.key: + self.assertEqual(sp_tag, vm_tag.key, "StorPool tag is not the same as the Virtual Machine tag") + self.assertEqual(sp_volume[0].tags[sp_tag], vm_tag.value, "StorPool tag value is not the same as the Virtual Machine tag value") + if sp_tag == 'cvm': + self.assertEqual(sp_volume[0].tags[sp_tag], vm[0].id, "cvm tag is not the expected value") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_03_create_vm_snapshot_vc_policy_tag(self): + """Test to create VM snapshots with VC policy tags + """ + volume_attached = self.virtual_machine.attach_volume( + self.apiclient, + self.volume + ) + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, + listall=True) + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine.id, listall=True) + vm_tags = vm[0].tags + + self.vc_policy_tags(volumes, vm_tags, vm) + + + self.assertEqual(volume_attached.id, self.volume.id, "Is not the same volume ") + try: + # Login to VM and write data to file system + ssh_client = self.virtual_machine.get_ssh_client() + + cmds = [ + "echo %s > %s/%s" % + (self.random_data_0, self.test_dir, self.random_data), + "sync", + "sleep 1", + "sync", + "sleep 1", + "cat %s/%s" % + (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data has be write into temp file!" + ) + + time.sleep(30) + MemorySnapshot = False + vm_snapshot = VmSnapshot.create( + self.apiclient, + self.virtual_machine.id, + MemorySnapshot, + "TestSnapshot", + "Display Text" + ) + self.assertEqual( + vm_snapshot.state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_04_revert_vm_snapshots_vc_policy_tag(self): + """Test to revert VM snapshots with VC policy tag + """ + + try: + ssh_client = self.virtual_machine.get_ssh_client() + + cmds = [ + "rm -rf %s/%s" % (self.test_dir, self.random_data), + "ls %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + if str(result[0]).index("No such file or directory") == -1: + self.fail("Check the random data has be delete from temp file!") + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + + self.assertEqual( + list_snapshot_response[0].state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + self.virtual_machine.stop(self.apiclient, forced=True) + + VmSnapshot.revertToSnapshot( + self.apiclient, + list_snapshot_response[0].id + ) + + self.virtual_machine.start(self.apiclient) + + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + cmds = [ + "cat %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, listall=True + ) + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine.id, listall=True) + vm_tags = vm[0].tags + + self.vc_policy_tags(volumes, vm_tags, vm) + + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data is equal with the ramdom file!" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_05_delete_vm_snapshots(self): + """Test to delete vm snapshots + """ + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + VmSnapshot.deleteVMSnapshot( + self.apiclient, + list_snapshot_response[0].id) + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + #vmid=self.virtual_machine.id, + virtualmachineid=self.virtual_machine.id, + listall=False) + self.debug('list_snapshot_response -------------------- %s' % list_snapshot_response) + + self.assertIsNone(list_snapshot_response, "snapshot is already deleted") + + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_06_remove_vcpolicy_tag_when_disk_detached(self): + """ Test remove vc-policy tag to disk detached from VM""" + time.sleep(60) + volume_detached = self.virtual_machine.detach_volume( + self.apiclient, + self.volume_2 + ) + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine.id, listall=True) + vm_tags = vm[0].tags + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, listall=True + ) + + self.vc_policy_tags( volumes, vm_tags, vm) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_07_delete_vcpolicy_tag(self): + """ Test delete vc-policy tag of VM""" + Tag.delete(self.apiclient, + resourceIds=self.virtual_machine.id, + resourceType='UserVm', + tags={'vc-policy': 'testing_vc-policy'}) + + volumes = list_volumes( + self.apiclient, + virtualmachineid = self.virtual_machine.id, listall=True + ) + for v in volumes: + name = v.path.split("/")[3] + spvolume = self.spapi.volumeList(volumeName="~" + name) + tags = spvolume[0].tags + for t in tags: + self.assertFalse(t.lower() == 'vc-policy'.lower(), "There is VC Policy tag") + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_08_vcpolicy_tag_to_reverted_disk(self): + tag = Tag.create( + self.apiclient, + resourceIds=self.virtual_machine2.id, + resourceType='UserVm', + tags={'vc-policy': 'testing_vc-policy'} + ) + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine2.id, listall=True) + vm_tags = vm[0].tags + + volume = Volume.list( + self.apiclient, + virtualmachineid = self.virtual_machine2.id, listall=True, + type = "ROOT" + ) + self.vc_policy_tags(volume, vm_tags, vm) + + snapshot = Snapshot.create( + self.apiclient, + volume[0].id, + account=self.account.name, + domainid=self.account.domainid + ) + + virtual_machine = self.virtual_machine2.stop( + self.apiclient, + forced=True + ) + + cmd = revertSnapshot.revertSnapshotCmd() + cmd.id = snapshot.id + revertedn = self.apiclient.revertSnapshot(cmd) + + vm = list_virtual_machines(self.apiclient,id = self.virtual_machine2.id) + vm_tags = vm[0].tags + + vol = list_volumes(self.apiclient, id = snapshot.volumeid, listall=True) + self.vc_policy_tags(vol, vm_tags, vm) + + + def vc_policy_tags(self, volumes, vm_tags, vm): + flag = False + for v in volumes: + name = v.path.split("/")[3] + spvolume = self.spapi.volumeList(volumeName="~" + name) + tags = spvolume[0].tags + for t in tags: + for vm_tag in vm_tags: + if t == vm_tag.key: + flag = True + self.assertEqual(tags[t], vm_tag.value, "Tags are not equal") + if t == 'cvm': + self.assertEqual(tags[t], vm[0].id, "CVM tag is not the same as vm UUID") + #self.assertEqual(tag.tags., second, msg) + self.assertTrue(flag, "There aren't volumes with vm tags") diff --git a/test/integration/plugins/storpool/TestVmSnapshots.py b/test/integration/plugins/storpool/TestVmSnapshots.py new file mode 100644 index 00000000000..b1daa93edad --- /dev/null +++ b/test/integration/plugins/storpool/TestVmSnapshots.py @@ -0,0 +1,369 @@ +# 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. + +# Import Local Modules +import random +import time + +from marvin.cloudstackAPI import (listTemplates) +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from marvin.lib.base import (Account, + ServiceOffering, + VirtualMachine, + VmSnapshot, + User, + Volume, + SecurityGroup, + ) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_clusters, + list_snapshots, + list_virtual_machines, + list_configurations, + list_disk_offering, + list_accounts, + list_storage_pools, + list_service_offering, + list_zones + ) +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual, get_hypervisor_type +from nose.plugins.attrib import attr +import uuid +from sp_util import (TestData, StorPoolHelper) + +class TestVmSnapshot(cloudstackTestCase): + @classmethod + def setUpClass(cls): + super(TestVmSnapshot, cls).setUpClass() + try: + cls.setUpCloudStack() + except Exception: + cls.cleanUpCloudStack() + raise + + @classmethod + def setUpCloudStack(cls): + testClient = super(TestVmSnapshot, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls._cleanup = [] + cls.unsupportedHypervisor = False + + # Setup test data + td = TestData() + cls.testdata = td.testdata + cls.helper = StorPoolHelper() + + + cls.services = testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = None + zones = list_zones(cls.apiclient) + + for z in zones: + if z.name == cls.getClsConfig().mgtSvr[0].zone: + cls.zone = z + + assert cls.zone is not None + + cls.cluster = list_clusters(cls.apiclient)[0] + cls.hypervisor = get_hypervisor_type(cls.apiclient) + + #The version of CentOS has to be supported + template = get_template( + cls.apiclient, + cls.zone.id, + account = "system" + ) + + + if template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + cls.template = template + + cls.account = cls.helper.create_account( + cls.apiclient, + cls.services["account"], + accounttype = 1, + domainid=cls.domain.id, + roleid = 1 + ) + cls._cleanup.append(cls.account) + + securitygroup = SecurityGroup.list(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid)[0] + cls.helper.set_securityGroups(cls.apiclient, account = cls.account.name, domainid= cls.account.domainid, id = securitygroup.id) + + primarystorage = cls.testdata[TestData.primaryStorage] + + serviceOffering = cls.testdata[TestData.serviceOffering] + storage_pool = list_storage_pools( + cls.apiclient, + name = primarystorage.get("name") + ) + cls.primary_storage = storage_pool[0] + + disk_offering = list_disk_offering( + cls.apiclient, + name="ssd" + ) + + assert disk_offering is not None + + + service_offering_only = list_service_offering( + cls.apiclient, + name="ssd" + ) + if service_offering_only is not None: + cls.service_offering_only = service_offering_only[0] + else: + cls.service_offering_only = ServiceOffering.create( + cls.apiclient, + serviceOffering) + assert cls.service_offering_only is not None + + cls.disk_offering = disk_offering[0] + + # Create 1 data volume_1 + cls.volume = Volume.create( + cls.apiclient, + cls.testdata[TestData.volume_1], + account=cls.account.name, + domainid=cls.domain.id, + zoneid=cls.zone.id, + diskofferingid=cls.disk_offering.id, + size=10 + ) + + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + {"name":"StorPool-%s" % uuid.uuid4() }, + zoneid=cls.zone.id, + templateid=cls.template.id, + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.service_offering_only.id, + hypervisor=cls.hypervisor, + rootdisksize=10 + ) + + cls.random_data_0 = random_gen(size=100) + cls.test_dir = "/tmp" + cls.random_data = "random.data" + return + + @classmethod + def tearDownClass(cls): + cls.cleanUpCloudStack() + + @classmethod + def cleanUpCloudStack(cls): + try: + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + return + + def tearDown(self): + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_01_create_vm_snapshots(self): + """Test to create VM snapshots + """ + volume_attached = self.virtual_machine.attach_volume( + self.apiclient, + self.volume + ) + + self.assertEqual(volume_attached.id, self.volume.id, "Is not the same volume ") + try: + # Login to VM and write data to file system + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + cmds = [ + "echo %s > %s/%s" % + (self.random_data_0, self.test_dir, self.random_data), + "sync", + "sleep 1", + "sync", + "sleep 1", + "cat %s/%s" % + (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data has be write into temp file!" + ) + + time.sleep(30) + MemorySnapshot = False + vm_snapshot = VmSnapshot.create( + self.apiclient, + self.virtual_machine.id, + MemorySnapshot, + "TestSnapshot", + "Display Text" + ) + self.assertEqual( + vm_snapshot.state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + return + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_02_revert_vm_snapshots(self): + """Test to revert VM snapshots + """ + + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + cmds = [ + "rm -rf %s/%s" % (self.test_dir, self.random_data), + "ls %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + if str(result[0]).index("No such file or directory") == -1: + self.fail("Check the random data has be delete from temp file!") + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + + self.assertEqual( + list_snapshot_response[0].state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + self.virtual_machine.stop(self.apiclient, forced=True) + + VmSnapshot.revertToSnapshot( + self.apiclient, + list_snapshot_response[0].id + ) + + self.virtual_machine.start(self.apiclient) + + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + cmds = [ + "cat %s/%s" % (self.test_dir, self.random_data) + ] + + for c in cmds: + self.debug(c) + result = ssh_client.execute(c) + self.debug(result) + + except Exception: + self.fail("SSH failed for Virtual machine: %s" % + self.virtual_machine.ipaddress) + + self.assertEqual( + self.random_data_0, + result[0], + "Check the random data is equal with the ramdom file!" + ) + + @attr(tags=["advanced", "advancedns", "smoke"], required_hardware="true") + def test_03_delete_vm_snapshots(self): + """Test to delete vm snapshots + """ + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + VmSnapshot.deleteVMSnapshot( + self.apiclient, + list_snapshot_response[0].id) + + time.sleep(30) + + list_snapshot_response = VmSnapshot.list( + self.apiclient, + #vmid=self.virtual_machine.id, + virtualmachineid=self.virtual_machine.id, + listall=False) + self.debug('list_snapshot_response -------------------- %s' % list_snapshot_response) + + self.assertIsNone(list_snapshot_response, "snapshot is already deleted") diff --git a/test/integration/plugins/storpool/sp_util.py b/test/integration/plugins/storpool/sp_util.py new file mode 100644 index 00000000000..10160507231 --- /dev/null +++ b/test/integration/plugins/storpool/sp_util.py @@ -0,0 +1,748 @@ +# 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. +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual +from marvin.lib.base import (Account, + Cluster, + Configurations, + ServiceOffering, + Snapshot, + StoragePool, + Template, + VirtualMachine, + VmSnapshot, + Volume) +from marvin.lib.common import (get_zone, + get_domain, + get_template, + list_disk_offering, + list_hosts, + list_snapshots, + list_storage_pools, + list_volumes, + list_virtual_machines, + list_configurations, + list_service_offering, + list_clusters, + list_zones) +from marvin.cloudstackAPI import (listOsTypes, + listTemplates, + listHosts, + createTemplate, + createVolume, + getVolumeSnapshotDetails, + resizeVolume, + authorizeSecurityGroupIngress, + migrateVirtualMachineWithVolume, + destroyVirtualMachine, + deployVirtualMachine, + createAccount, + startVirtualMachine, + ) +import time +import pprint +import random +from marvin.configGenerator import configuration +import uuid +import logging +import subprocess +import json +from storpool import spapi +from storpool import sptypes + +class TestData(): + account = "account" + capacityBytes = "capacitybytes" + capacityIops = "capacityiops" + clusterId = "clusterId" + diskName = "diskname" + diskOffering = "diskoffering" + diskOffering2 = "diskoffering2" + cephDiskOffering = "cephDiskOffering" + nfsDiskOffering = "nfsDiskOffering" + domainId = "domainId" + hypervisor = "hypervisor" + login = "login" + mvip = "mvip" + password = "password" + port = "port" + primaryStorage = "primarystorage" + primaryStorage2 = "primarystorage2" + primaryStorage3 = "primarystorage3" + primaryStorage4 = "primaryStorage4" + provider = "provider" + serviceOffering = "serviceOffering" + serviceOfferingssd2 = "serviceOffering-ssd2" + serviceOfferingsPrimary = "serviceOfferingsPrimary" + serviceOfferingsIops = "serviceOfferingsIops" + serviceOfferingsCeph = "serviceOfferingsCeph" + scope = "scope" + StorPool = "StorPool" + storageTag = ["ssd", "ssd2"] + tags = "tags" + virtualMachine = "virtualmachine" + virtualMachine2 = "virtualmachine2" + volume_1 = "volume_1" + volume_2 = "volume_2" + volume_3 = "volume_3" + volume_4 = "volume_4" + volume_5 = "volume_5" + volume_6 = "volume_6" + volume_7 = "volume_7" + zoneId = "zoneId" + + def __init__(self): + sp_template_1 = 'ssd' + sp_template_2 = 'ssd2' + sp_template_3 = 'test-primary' + self.testdata = { + TestData.primaryStorage: { + "name": sp_template_1, + TestData.scope: "ZONE", + "url": sp_template_1, + TestData.provider: "StorPool", + "path": "/dev/storpool", + TestData.capacityBytes: 2251799813685248, + TestData.hypervisor: "KVM" + }, + TestData.primaryStorage2: { + "name": sp_template_2, + TestData.scope: "ZONE", + "url": sp_template_2, + TestData.provider: "StorPool", + "path": "/dev/storpool", + TestData.capacityBytes: 2251799813685248, + TestData.hypervisor: "KVM" + }, + TestData.primaryStorage3: { + "name": sp_template_3, + TestData.scope: "ZONE", + "url": sp_template_3, + TestData.provider: "StorPool", + "path": "/dev/storpool", + TestData.capacityBytes: 2251799813685248, + TestData.hypervisor: "KVM" + }, + TestData.primaryStorage4: { + "name": "ceph", + TestData.scope: "ZONE", + TestData.provider: "RBD", + TestData.hypervisor: "KVM" + }, + TestData.virtualMachine: { + "name": "TestVM", + "displayname": "TestVM", + "privateport": 22, + "publicport": 22, + "protocol": "tcp" + }, + TestData.virtualMachine2: { + "name": "TestVM2", + "displayname": "TestVM2", + "privateport": 22, + "publicport": 22, + "protocol": "tcp" + }, + TestData.serviceOffering:{ + "name": sp_template_1, + "displaytext": "SP_CO_2 (Min IOPS = 10,000; Max IOPS = 15,000)", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "tags": sp_template_1 + }, + TestData.serviceOfferingssd2:{ + "name": sp_template_2, + "displaytext": "SP_CO_2 (Min IOPS = 10,000; Max IOPS = 15,000)", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "tags": sp_template_2 + }, + TestData.serviceOfferingsPrimary:{ + "name": "nfs", + "displaytext": "SP_CO_2 (Min IOPS = 10,000; Max IOPS = 15,000)", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "tags": "nfs" + }, + TestData.serviceOfferingsCeph:{ + "name": "ceph", + "displaytext": "Ceph Service offerings", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": False, + "hypervisorsnapshotreserve": 200, + "tags": "ceph" + }, + TestData.serviceOfferingsIops:{ + "name": "iops", + "displaytext": "Testing IOPS on StorPool", + "cpunumber": 1, + "cpuspeed": 500, + "memory": 512, + "storagetype": "shared", + "customizediops": True, + "tags": sp_template_1, + }, + TestData.diskOffering: { + "name": "SP_DO_1", + "displaytext": "SP_DO_1 (5GB Min IOPS = 300; Max IOPS = 500)", + "disksize": 5, + "customizediops": False, + "miniops": 300, + "maxiops": 500, + "hypervisorsnapshotreserve": 200, + TestData.tags: sp_template_1, + "storagetype": "shared" + }, + TestData.diskOffering2: { + "name": "SP_DO_1", + "displaytext": "SP_DO_1 (5GB Min IOPS = 300; Max IOPS = 500)", + "disksize": 5, + "customizediops": False, + "miniops": 300, + "maxiops": 500, + "hypervisorsnapshotreserve": 200, + TestData.tags: sp_template_2, + "storagetype": "shared" + }, + TestData.cephDiskOffering: { + "name": "ceph", + "displaytext": "Ceph fixed disk offering", + "disksize": 5, + "customizediops": False, + "miniops": 300, + "maxiops": 500, + "hypervisorsnapshotreserve": 200, + TestData.tags: "ceph", + "storagetype": "shared" + }, + TestData.nfsDiskOffering: { + "name": "nfs", + "displaytext": "NFS fixed disk offering", + "disksize": 5, + "customizediops": False, + "miniops": 300, + "maxiops": 500, + "hypervisorsnapshotreserve": 200, + TestData.tags: "nfs", + "storagetype": "shared" + }, + TestData.volume_1: { + TestData.diskName: "test-volume-1", + }, + TestData.volume_2: { + TestData.diskName: "test-volume-2", + }, + TestData.volume_3: { + TestData.diskName: "test-volume-3", + }, + TestData.volume_4: { + TestData.diskName: "test-volume-4", + }, + TestData.volume_5: { + TestData.diskName: "test-volume-5", + }, + TestData.volume_6: { + TestData.diskName: "test-volume-6", + }, + TestData.volume_7: { + TestData.diskName: "test-volume-7", + }, + } +class StorPoolHelper(): + + @classmethod + def create_template_from_snapshot(self, apiclient, services, snapshotid=None, volumeid=None): + """Create template from Volume""" + # Create template from Virtual machine and Volume ID + cmd = createTemplate.createTemplateCmd() + cmd.displaytext = "StorPool_Template" + cmd.name = "-".join(["StorPool-", random_gen()]) + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + # Find OSTypeId from Os type + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) + + if not isinstance(ostypes, list): + raise Exception( + "Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + raise Exception( + "Unable to find Ostype is required for creating template") + + cmd.isfeatured = True + cmd.ispublic = True + cmd.isextractable = False + + if snapshotid: + cmd.snapshotid = snapshotid + if volumeid: + cmd.volumeid = volumeid + return Template(apiclient.createTemplate(cmd).__dict__) + + @classmethod + def getCfgFromUrl(self, url): + cfg = dict([ + option.split('=') + for option in url.split(';') + ]) + host, port = cfg['SP_API_HTTP'].split(':') + auth = cfg['SP_AUTH_TOKEN'] + return host, int(port), auth + + @classmethod + def get_remote_storpool_cluster(cls): + logging.debug("######################## get_remote_storpool_cluster") + + storpool_clusterid = subprocess.check_output(['storpool_confshow', 'CLUSTER_ID']).strip() + clusterid = storpool_clusterid.split("=")[1].split(".")[1] + logging.debug("######################## %s" % storpool_clusterid) + cmd = ["storpool", "-j", "cluster", "list"] + proc = subprocess.Popen(cmd,stdout=subprocess.PIPE).stdout.read() + csl = json.loads(proc) + logging.debug("######################## %s" % csl) + + clusters = csl.get("data").get("clusters") + logging.debug("######################## %s" % clusters) + + for c in clusters: + c_id = c.get("id") + if c_id != clusterid: + return c.get("name") + + @classmethod + def get_local_cluster(cls, apiclient, zoneid): + storpool_clusterid = subprocess.check_output(['storpool_confshow', 'CLUSTER_ID']) + clusterid = storpool_clusterid.split("=") + logging.debug(storpool_clusterid) + clusters = list_clusters(apiclient, zoneid = zoneid) + for c in clusters: + configuration = list_configurations( + apiclient, + clusterid = c.id + ) + for conf in configuration: + if conf.name == 'sp.cluster.id' and (conf.value in clusterid[1]): + return c + + @classmethod + def get_remote_cluster(cls, apiclient, zoneid): + storpool_clusterid = subprocess.check_output(['storpool_confshow', 'CLUSTER_ID']) + clusterid = storpool_clusterid.split("=") + logging.debug(storpool_clusterid) + clusters = list_clusters(apiclient, zoneid = zoneid) + for c in clusters: + configuration = list_configurations( + apiclient, + clusterid = c.id + ) + for conf in configuration: + if conf.name == 'sp.cluster.id' and (conf.value not in clusterid[1]): + return c + + @classmethod + def get_snapshot_template_id(self, apiclient, snapshot, storage_pool_id): + try: + cmd = getVolumeSnapshotDetails.getVolumeSnapshotDetailsCmd() + cmd.snapshotid = snapshot.id + snapshot_details = apiclient.getVolumeSnapshotDetails(cmd) + logging.debug("Snapshot details %s" % snapshot_details) + logging.debug("Snapshot with uuid %s" % snapshot.id) + for s in snapshot_details: + if s["snapshotDetailsName"] == storage_pool_id: + return s["snapshotDetailsValue"] + except Exception as err: + raise Exception(err) + + return None + + @classmethod + def getDestinationHost(self, hostsToavoid, hosts): + destinationHost = None + for host in hosts: + if host.id not in hostsToavoid: + destinationHost = host + break + return destinationHost + + + @classmethod + def getDestinationPool(self, + poolsToavoid, + migrateto, + pools + ): + """ Get destination pool which has scope same as migrateto + and which is not in avoid set + """ + + destinationPool = None + + # Get Storage Pool Id to migrate to + for storagePool in pools: + if storagePool.scope == migrateto: + if storagePool.name not in poolsToavoid: + destinationPool = storagePool + break + + return destinationPool + + @classmethod + def get_destination_pools_hosts(self, apiclient, vm, hosts): + vol_list = list_volumes( + apiclient, + virtualmachineid=vm.id, + listall=True) + # Get destination host + destinationHost = self.getDestinationHost(vm.hostid, hosts) + return destinationHost, vol_list + + @classmethod + def list_hosts_by_cluster_id(cls, apiclient, clusterid): + """List all Hosts matching criteria""" + cmd = listHosts.listHostsCmd() + cmd.clusterid = clusterid + return(apiclient.listHosts(cmd)) + + @classmethod + def set_securityGroups(cls, apiclient, account, domainid, id): + cmd = authorizeSecurityGroupIngress.authorizeSecurityGroupIngressCmd() + cmd.protocol = 'TCP' + cmd.startport = 22 + cmd.endport = 22 + cmd.cidrlist = '0.0.0.0/0' + cmd.securitygroupid = id + cmd.account = account + cmd.domainid = domainid + + apiclient.authorizeSecurityGroupIngress(cmd) + + cmd.protocol = 'ICMP' + cmd.icmptype = "-1" + cmd.icmpcode = "-1" + # Authorize to only account not CIDR + cmd.securitygroupid = id + cmd.account = account + cmd.domainid = domainid + apiclient.authorizeSecurityGroupIngress(cmd) + + @classmethod + def migrateVm(self, apiclient, vm, destinationHost): + """ + This method is to migrate a VM using migrate virtual machine API + """ + + vm.migrate( + apiclient, + hostid=destinationHost.id, + ) + vm.getState( + apiclient, + "Running" + ) + # check for the VM's host and volume's storage post migration + migrated_vm_response = list_virtual_machines(apiclient, id=vm.id) + assert isinstance(migrated_vm_response, list), "Check list virtual machines response for valid list" + + assert migrated_vm_response[0].hostid == destinationHost.id, "VM did not migrate to a specified host" + return migrated_vm_response[0] + + @classmethod + def migrateVmWithVolumes(self, apiclient, vm, destinationHost, volumes, pool): + """ + This method is used to migrate a vm and its volumes using migrate virtual machine with volume API + INPUTS: + 1. vm -> virtual machine object + 2. destinationHost -> the host to which VM will be migrated + 3. volumes -> list of volumes which are to be migrated + 4. pools -> list of destination pools + """ + vol_pool_map = {vol.id: pool.id for vol in volumes} + + cmd = migrateVirtualMachineWithVolume.migrateVirtualMachineWithVolumeCmd() + cmd.hostid = destinationHost.id + cmd.migrateto = [] + cmd.virtualmachineid = self.virtual_machine.id + for volume, pool1 in vol_pool_map.items(): + cmd.migrateto.append({ + 'volume': volume, + 'pool': pool1 + }) + apiclient.migrateVirtualMachineWithVolume(cmd) + + vm.getState( + apiclient, + "Running" + ) + # check for the VM's host and volume's storage post migration + migrated_vm_response = list_virtual_machines(apiclient, id=vm.id) + assert isinstance(migrated_vm_response, list), "Check list virtual machines response for valid list" + + assert migrated_vm_response[0].hostid == destinationHost.id, "VM did not migrate to a specified host" + + for vol in volumes: + migrated_volume_response = list_volumes( + apiclient, + virtualmachineid=migrated_vm_response[0].id, + name=vol.name, + listall=True) + assert isinstance(migrated_volume_response, list), "Check list virtual machines response for valid list" + assert migrated_volume_response[0].storageid == pool.id, "Volume did not migrate to a specified pool" + + assert str(migrated_volume_response[0].state).lower().eq('ready'), "Check migrated volume is in Ready state" + + return migrated_vm_response[0] + + @classmethod + def create_sp_template_and_storage_pool(self, apiclient, template_name, primary_storage, zoneid): + spapiRemote = spapi.Api.fromConfig() + logging.debug("================ %s" % spapiRemote) + + sp_api = spapi.Api.fromConfig(multiCluster= True) + logging.debug("================ %s" % sp_api) + + remote_cluster = self.get_remote_storpool_cluster() + logging.debug("================ %s" % remote_cluster) + + newTemplate = sptypes.VolumeTemplateCreateDesc(name = template_name, placeAll = "ssd", placeTail = "ssd", placeHead = "ssd", replication=1) + template_on_remote = spapiRemote.volumeTemplateCreate(newTemplate, clusterName = remote_cluster) + template_on_local = spapiRemote.volumeTemplateCreate(newTemplate) + storage_pool = StoragePool.create(apiclient, primary_storage, zoneid = zoneid,) + return storage_pool, spapiRemote, sp_api + + @classmethod + def destroy_vm(self, apiclient, virtualmachineid): + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.id = virtualmachineid + cmd.expunge = True + apiclient.destroyVirtualMachine(cmd) + + @classmethod + def check_storpool_volume_size(cls, volume, spapi): + name = volume.path.split("/")[3] + try: + spvolume = spapi.volumeList(volumeName = "~" + name) + if spvolume[0].size != volume.size: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + @classmethod + def check_storpool_volume_iops(cls, spapi, volume,): + name = volume.path.split("/")[3] + try: + spvolume = spapi.volumeList(volumeName = "~" + name) + logging.debug(spvolume[0].iops) + logging.debug(volume.maxiops) + if spvolume[0].iops != volume.maxiops: + raise Exception("Storpool volume size is not the same as CloudStack db size") + except spapi.ApiError as err: + raise Exception(err) + + @classmethod + def create_custom_disk(cls, apiclient, services, size = None, miniops = None, maxiops =None, diskofferingid=None, zoneid=None, account=None, domainid=None, snapshotid=None): + """Create Volume from Custom disk offering""" + cmd = createVolume.createVolumeCmd() + cmd.name = services["diskname"] + + if diskofferingid: + cmd.diskofferingid = diskofferingid + + if size: + cmd.size = size + + if miniops: + cmd.miniops = miniops + + if maxiops: + cmd.maxiops = maxiops + + if account: + cmd.account = account + + if domainid: + cmd.domainid = domainid + + if snapshotid: + cmd.snapshotid = snapshotid + + cmd.zoneid = zoneid + + return Volume(apiclient.createVolume(cmd).__dict__) + + @classmethod + def create_vm_custom(cls, apiclient, services, templateid=None, zoneid=None, + serviceofferingid=None, method='GET', hypervisor=None, + cpuNumber=None, cpuSpeed=None, memory=None, minIops=None, + maxIops=None, hostid=None, rootdisksize=None, account=None, domainid=None + ): + """Create the instance""" + + cmd = deployVirtualMachine.deployVirtualMachineCmd() + + if serviceofferingid: + cmd.serviceofferingid = serviceofferingid + elif "serviceoffering" in services: + cmd.serviceofferingid = services["serviceoffering"] + + if zoneid: + cmd.zoneid = zoneid + elif "zoneid" in services: + cmd.zoneid = services["zoneid"] + + if hypervisor: + cmd.hypervisor = hypervisor + + if hostid: + cmd.hostid = hostid + + if "displayname" in services: + cmd.displayname = services["displayname"] + + if "name" in services: + cmd.name = services["name"] + + if templateid: + cmd.templateid = templateid + elif "template" in services: + cmd.templateid = services["template"] + + cmd.details = [{}] + + if cpuNumber: + cmd.details[0]["cpuNumber"] = cpuNumber + + if cpuSpeed: + cmd.details[0]["cpuSpeed"] = cpuSpeed + + if memory: + cmd.details[0]["memory"] = memory + + if minIops: + cmd.details[0]["minIops"] = minIops + + if maxIops: + cmd.details[0]["maxIops"] = maxIops + + if rootdisksize >= 0: + cmd.details[0]["rootdisksize"] = rootdisksize + + if account: + cmd.account = account + + if domainid: + cmd.domainid = domainid + + virtual_machine = apiclient.deployVirtualMachine(cmd, method=method) + + return VirtualMachine(virtual_machine.__dict__, services) + + + @classmethod + def resize_volume(cls, apiclient, volume, shrinkOk=None, disk_offering =None, size=None, maxiops=None, miniops=None): + + cmd = resizeVolume.resizeVolumeCmd() + cmd.id = volume.id + + if disk_offering: + cmd.diskofferingid = disk_offering.id + if size: + cmd.size = size + + if maxiops: + cmd.maxiops = maxiops + if miniops: + cmd.miniops + + cmd.shrinkok = shrinkOk + apiclient.resizeVolume(cmd) + + new_size = Volume.list( + apiclient, + id=volume.id + ) + volume_size = new_size[0].size + return new_size[0] + + @classmethod + def create_account(cls, apiclient, services, accounttype=None, domainid=None, roleid=None): + """Creates an account""" + cmd = createAccount.createAccountCmd() + + # 0 - User, 1 - Root Admin, 2 - Domain Admin + if accounttype: + cmd.accounttype = accounttype + else: + cmd.accounttype = 1 + + cmd.email = services["email"] + cmd.firstname = services["firstname"] + cmd.lastname = services["lastname"] + + cmd.password = services["password"] + username = services["username"] + # Limit account username to 99 chars to avoid failure + # 6 chars start string + 85 chars apiclientid + 6 chars random string + 2 chars joining hyphen string = 99 + username = username[:6] + apiclientid = apiclient.id[-85:] if len(apiclient.id) > 85 else apiclient.id + cmd.username = "-".join([username, + random_gen(id=apiclientid, size=6)]) + + if "accountUUID" in services: + cmd.accountid = "-".join([services["accountUUID"], random_gen()]) + + if "userUUID" in services: + cmd.userid = "-".join([services["userUUID"], random_gen()]) + + if domainid: + cmd.domainid = domainid + + if roleid: + cmd.roleid = roleid + + account = apiclient.createAccount(cmd) + + return Account(account.__dict__) + + def start(cls, apiclient, vmid, hostid): + """Start the instance""" + cmd = startVirtualMachine.startVirtualMachineCmd() + cmd.id = vmid + cmd.hostid = hostid + return (apiclient.startVirtualMachine(cmd)) +