|
16 | 16 | specific language governing permissions and limitations |
17 | 17 | under the License. |
18 | 18 | --> |
19 | | -# ONTAP plugin — Marvin integration tests |
| 19 | +# NetApp ONTAP Integration Tests — README |
20 | 20 |
|
21 | | -Add Marvin tests here (e.g. `test_ontap_smoke.py`). They can be included in Apache upstream PRs when ready. |
| 21 | +This folder contains end-to-end integration tests for the NetApp ONTAP primary storage plugin in Apache CloudStack. The tests use the **Marvin** framework to drive real CloudStack API calls against a live management server and verify outcomes on a real ONTAP storage system. |
22 | 22 |
|
23 | 23 | CI wiring: |
24 | | - |
25 | 24 | - Bundles: `private-cicd/marvin/bundles.txt` |
26 | 25 | - Zone config: `private-cicd/marvin/zones/` (downstream only) |
27 | 26 |
|
28 | | -Follow patterns in `test/integration/plugins/solidfire/` and `test/integration/plugins/linstor/`. |
| 27 | +--- |
| 28 | + |
| 29 | +## Directory layout |
| 30 | + |
| 31 | +``` |
| 32 | +test/integration/plugins/ontap/ |
| 33 | +├── ontap.cfg # Environment config (IPs, credentials, zone info) |
| 34 | +├── ontap_test_base.py # Shared base class and ONTAP REST client |
| 35 | +├── TEST_CASES.md # Full test case reference table (62 tests) |
| 36 | +├── README.md # This file |
| 37 | +│ |
| 38 | +├── nfs3/ |
| 39 | +│ ├── pool/ |
| 40 | +│ │ ├── test_pool_lifecycle.py # Pool create/disable/enable/maintenance/delete |
| 41 | +│ │ ├── test_pool_with_volumes.py # Same lifecycle with a CS volume present |
| 42 | +│ │ └── test_zone_scoped_pool.py # Zone-scoped pool (attachZone) |
| 43 | +│ ├── volume/ |
| 44 | +│ │ └── test_volume_lifecycle.py # Volume create/delete/negative-delete |
| 45 | +│ └── instance/ |
| 46 | +│ └── test_vm_volume_attach.py # Pool + volume + VM + attach/detach |
| 47 | +│ |
| 48 | +└── iscsi/ |
| 49 | + ├── pool/ |
| 50 | + │ ├── test_pool_lifecycle.py # iSCSI pool lifecycle + igroup assertions |
| 51 | + │ ├── test_pool_with_volumes.py # Same lifecycle with a LUN-backed volume |
| 52 | + │ └── test_zone_scoped_pool.py # Zone-scoped iSCSI pool |
| 53 | + ├── volume/ |
| 54 | + │ └── test_volume_lifecycle.py # LUN create/delete/negative-delete |
| 55 | + └── instance/ |
| 56 | + └── test_vm_volume_attach.py # Pool + LUN + VM + attach/LUN-map lifecycle |
| 57 | +``` |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## What is being tested |
| 62 | + |
| 63 | +The ONTAP plugin (`plugins/storage/volume/ontap/`) integrates CloudStack's primary storage API with the NetApp ONTAP REST API. Every test suite verifies **both sides** of an operation: |
| 64 | + |
| 65 | +1. **CloudStack side** — the expected `listStoragePools` / `listVolumes` / `listVirtualMachines` state after each API call. |
| 66 | +2. **ONTAP side** — the actual ONTAP object state (FlexVol, LUN, igroup, export policy, LUN-map) via direct REST API queries. |
| 67 | + |
| 68 | +### NFS3 vs iSCSI — key differences |
| 69 | + |
| 70 | +| Aspect | NFS3 | iSCSI | |
| 71 | +|--------|------|-------| |
| 72 | +| ONTAP object per pool | FlexVol + export policy | FlexVol + igroup per KVM host | |
| 73 | +| ONTAP object per CS volume | None (FlexVol is shared) | One LUN inside the FlexVol | |
| 74 | +| Host connectivity | NFS mount | iSCSI login (IQN-based) | |
| 75 | +| Volume detach from running VM | Works via virtio hot-unplug | Requires KVM guest to support SCSI hot-unplug | |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +## Prerequisites |
| 80 | + |
| 81 | +Before running any test: |
| 82 | + |
| 83 | +1. **CloudStack management server** running with the ONTAP plugin deployed (jar in `/usr/share/cloudstack-management/lib/`). |
| 84 | +2. **Integration API port 8096 enabled** — run on the management server: |
| 85 | + ```sql |
| 86 | + UPDATE configuration SET value='8096' WHERE name='integration.api.port'; |
| 87 | + ``` |
| 88 | + Then restart: `systemctl restart cloudstack-management` |
| 89 | +3. **MySQL accessible remotely** from your laptop (port 3306). If not: |
| 90 | + ```bash |
| 91 | + sudo sed -i 's/^bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf |
| 92 | + sudo iptables -I INPUT -p tcp --dport 3306 -j ACCEPT |
| 93 | + sudo systemctl restart mysql |
| 94 | + ``` |
| 95 | +4. **ONTAP SVM** with NFS3 service and/or iSCSI service enabled, and at least one data LIF per protocol. |
| 96 | +5. **KVM cluster** registered in CloudStack. For iSCSI tests, every KVM host must have iSCSI configured (its `storageUrl` starts with `iqn.`). |
| 97 | +6. **`ontap.cfg` populated** — see the [Configuration](#configuration) section. |
| 98 | + |
| 99 | +### Python / Marvin setup |
| 100 | + |
| 101 | +```bash |
| 102 | +# Install Marvin from the repo's bundled tarball |
| 103 | +python3 -m pip install --user \ |
| 104 | + "$(ls tools/marvin/dist/Marvin-*.tar.gz | tail -1)" |
| 105 | + |
| 106 | +# Verify |
| 107 | +python3 -c "import marvin; print('Marvin OK')" |
| 108 | +``` |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Configuration — `ontap.cfg` |
| 113 | + |
| 114 | +`ontap.cfg` is a JSON file that tells Marvin where CloudStack and ONTAP are. **Never commit real credentials.** |
| 115 | + |
| 116 | +Key sections: |
| 117 | + |
| 118 | +```json |
| 119 | +{ |
| 120 | + "mgtSvr": [{ "mgtSvrIp": "<CS_IP>", "port": 8096, "user": "admin", "passwd": "password" }], |
| 121 | + "dbSvr": { "dbSvr": "<CS_IP>", "port": 3306, "user": "cloud", "passwd": "cloud" }, |
| 122 | + "ontap": { "storageIP": "<ONTAP_IP>", "svmName": "<SVM>", "username": "admin", "password": "<pw>" } |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +The test classes read `storageIP`, `svmName`, `username`, and `password` from the `ontap` section at runtime. **No credentials appear in test code.** |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## Running the tests |
| 131 | + |
| 132 | +**Always run from the repo root** so that `PYTHONPATH` picks up `ontap_test_base.py`: |
| 133 | + |
| 134 | +```bash |
| 135 | +# All ONTAP tests (takes ~60–90 min) |
| 136 | +PYTHONPATH=test/integration/plugins/ontap \ |
| 137 | +python3 -m nose --with-marvin \ |
| 138 | + --marvin-config=test/integration/plugins/ontap/ontap.cfg \ |
| 139 | + test/integration/plugins/ontap/ -v |
| 140 | + |
| 141 | +# Single suite (e.g. NFS3 pool lifecycle) |
| 142 | +PYTHONPATH=test/integration/plugins/ontap \ |
| 143 | +python3 -m nose --with-marvin \ |
| 144 | + --marvin-config=test/integration/plugins/ontap/ontap.cfg \ |
| 145 | + test/integration/plugins/ontap/nfs3/pool/test_pool_lifecycle.py -v |
| 146 | + |
| 147 | +# By tag (e.g. all iSCSI workflow tests) |
| 148 | +PYTHONPATH=test/integration/plugins/ontap \ |
| 149 | +python3 -m nose --with-marvin \ |
| 150 | + --marvin-config=test/integration/plugins/ontap/ontap.cfg \ |
| 151 | + -a tags=iscsi_workflow \ |
| 152 | + test/integration/plugins/ontap/ -v |
| 153 | +``` |
| 154 | + |
| 155 | +> **Important:** `PYTHONPATH=test/integration/plugins/ontap` is always required. The test files in subdirectories import `ontap_test_base` from the parent directory; without this prefix, Python cannot find it. |
| 156 | +
|
| 157 | +Test results are written to: |
| 158 | +- `/tmp/marvin_last_run.txt` — stdout/stderr summary |
| 159 | +- `/tmp/MarvinLogs/<timestamp>/results.txt` — per-test pass/fail |
| 160 | +- `/tmp/MarvinLogs/<timestamp>/runinfo.txt` — full trace with API call details |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## Code structure — how a test file is organised |
| 165 | + |
| 166 | +Every test file follows the same layout: |
| 167 | + |
| 168 | +``` |
| 169 | +1. Apache 2.0 license header |
| 170 | +2. Module docstring ← workflow summary, prerequisites, run command |
| 171 | +3. Imports |
| 172 | +4. TestData class ← holds all config values read from ontap.cfg; builds the |
| 173 | + createStoragePool command parameters |
| 174 | +5. Test class (extends OntapTestBase) |
| 175 | + ├── Class-level state attributes (pool, volume, vm, etc.) initialised to None |
| 176 | + ├── setUpClass() ← connects to CloudStack; creates a test account and |
| 177 | + │ disk offering; resolves zone/cluster/hosts |
| 178 | + ├── tearDownClass() ← best-effort cleanup: deletes pool (forced=True), |
| 179 | + │ volume, account, disk offering |
| 180 | + ├── Helper methods ← _create_pool(), _create_volume(), _poll_pool_state(), |
| 181 | + │ _lun_maps() (iSCSI only), etc. |
| 182 | + └── test_01 … test_N ← sequential, numbered test methods |
| 183 | +``` |
| 184 | + |
| 185 | +### Key patterns to know |
| 186 | + |
| 187 | +**Sequential state sharing — always use `self.__class__.<attr>`** |
| 188 | + |
| 189 | +Tests share state via class attributes, never instance attributes: |
| 190 | +```python |
| 191 | +# Correct |
| 192 | +self.__class__.pool = pool |
| 193 | +pool = self.__class__.pool |
| 194 | + |
| 195 | +# Wrong — state is lost between test method invocations |
| 196 | +self.pool = pool |
| 197 | +``` |
| 198 | + |
| 199 | +**Guard assertion at the start of every test (except test_01)** |
| 200 | + |
| 201 | +Every test after the first starts with an assertion that the previous step's resource exists. This produces a clear, readable failure message instead of a confusing `AttributeError`: |
| 202 | +```python |
| 203 | +def test_03_enable_storage_pool(self): |
| 204 | + self.assertIsNotNone(self.__class__.pool, "Pool absent — test_01 must pass first") |
| 205 | +``` |
| 206 | + |
| 207 | +**Creating a storage pool — always use indexed `details[N].key` syntax** |
| 208 | + |
| 209 | +The CloudStack API for `createStoragePool` requires plugin details to be passed as indexed parameters. **Never call `StoragePool.create()` directly** — it does not support this syntax: |
| 210 | +```python |
| 211 | +count = 1 |
| 212 | +for key, value in ps["details"].items(): |
| 213 | + setattr(cmd, "details[{}].{}".format(count, key), value) |
| 214 | + count += 1 |
| 215 | +``` |
| 216 | + |
| 217 | +**Polling for async state changes** |
| 218 | + |
| 219 | +CloudStack operations are asynchronous. Use `_poll_pool_state()` rather than reading state immediately after an API call: |
| 220 | +```python |
| 221 | +result = self._poll_pool_state(pool.id, "Maintenance", timeout=120) |
| 222 | +self.assertEqual(result.state, "Maintenance") |
| 223 | +``` |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +## Shared base — `ontap_test_base.py` |
| 228 | + |
| 229 | +`OntapTestBase` provides everything individual test classes inherit: |
| 230 | + |
| 231 | +| What | Purpose | |
| 232 | +|------|---------| |
| 233 | +| `_setup_cloudstack_resources()` | Creates a test account, domain, disk offering; resolves zone/cluster/hosts | |
| 234 | +| `tearDownClass()` | Best-effort cleanup: deletes pool (forced=True), volume, disk offering, account | |
| 235 | +| `_poll_pool_state(pool_id, state, timeout)` | Polls `listStoragePools` until pool reaches the target state | |
| 236 | +| `_create_volume(pool_id)` | Creates a CloudStack data volume on the given pool | |
| 237 | +| `_delete_pool(pool_id, forced)` | Enters Maintenance then calls `deleteStoragePool` | |
| 238 | +| `_parse_pool_details(pool)` | Extracts key→value pairs from the pool's `details` list | |
| 239 | +| `OntapRestClient` | Thin HTTPS client for ONTAP REST API calls | |
| 240 | + |
| 241 | +### `OntapRestClient` methods at a glance |
| 242 | + |
| 243 | +| Method | What it checks | Used in | |
| 244 | +|--------|---------------|---------| |
| 245 | +| `get_volume(name)` | FlexVol existence and state | All suites | |
| 246 | +| `get_export_policy(name)` | NFS export policy existence | NFS3 suites | |
| 247 | +| `get_data_lifs(svm_name)` | NFS data LIF count | NFS3 pool lifecycle | |
| 248 | +| `get_igroup(svm_name, name)` | iSCSI igroup existence and initiator list | iSCSI suites | |
| 249 | +| `list_luns_in_volume(svm_name, vol_name)` | LUNs present in a FlexVol | iSCSI volume/instance suites | |
| 250 | +| `list_lun_maps_for_volume(svm_name, vol_name)` | Active LUN-maps for a volume | iSCSI instance suite | |
| 251 | +| `list_files_in_volume(svm_name, vol_name)` | Files inside a FlexVol | NFS3 instance suite | |
| 252 | + |
| 253 | +--- |
| 254 | + |
| 255 | +## Test suite quick reference |
| 256 | + |
| 257 | +| Suite | File | Tests | What it covers | |
| 258 | +|-------|------|-------|---------------| |
| 259 | +| NFS3 Pool Lifecycle | `nfs3/pool/test_pool_lifecycle.py` | 8 | Create, disable, enable, maintenance, delete | |
| 260 | +| NFS3 Pool with Volumes | `nfs3/pool/test_pool_with_volumes.py` | 7 | Same + live volume present; negative delete guard | |
| 261 | +| NFS3 Zone-Scoped Pool | `nfs3/pool/test_zone_scoped_pool.py` | 4 | Zone scope — all hosts connected via `attachZone` | |
| 262 | +| NFS3 Volume Lifecycle | `nfs3/volume/test_volume_lifecycle.py` | 5 | Volume is metadata-only; FlexVol unchanged on delete | |
| 263 | +| NFS3 VM + Volume Attach | `nfs3/instance/test_vm_volume_attach.py` | 8 | Full VM lifecycle with hot-plug/detach | |
| 264 | +| iSCSI Pool Lifecycle | `iscsi/pool/test_pool_lifecycle.py` | 8 | Create, disable, enable, maintenance, delete + igroups | |
| 265 | +| iSCSI Pool with Volumes | `iscsi/pool/test_pool_with_volumes.py` | 7 | Same + live LUN present; negative delete guard | |
| 266 | +| iSCSI Zone-Scoped Pool | `iscsi/pool/test_zone_scoped_pool.py` | 4 | Zone scope | |
| 267 | +| iSCSI Volume Lifecycle | `iscsi/volume/test_volume_lifecycle.py` | 5 | LUN created per CS volume; LUN removed on delete | |
| 268 | +| iSCSI VM + Volume Attach | `iscsi/instance/test_vm_volume_attach.py` | 8 | Full VM lifecycle; LUN-maps on VM start/stop/detach | |
| 269 | + |
| 270 | +For the goal, dependencies, and exact success criteria of every individual test, see [TEST_CASES.md](TEST_CASES.md). |
| 271 | + |
| 272 | +--- |
| 273 | + |
| 274 | +## Troubleshooting |
| 275 | + |
| 276 | +| Symptom | Likely cause | Fix | |
| 277 | +|---------|-------------|-----| |
| 278 | +| `ModuleNotFoundError: No module named 'ontap_test_base'` | Missing `PYTHONPATH` prefix | Prefix every run with `PYTHONPATH=test/integration/plugins/ontap` | |
| 279 | +| `Marvin Init Failed` | CloudStack API unreachable | Check `mgtSvrIp:8096` is reachable; restart `cloudstack-management` | |
| 280 | +| `Lost connection to MySQL` | MySQL not accepting remote connections | Enable remote MySQL access (see Prerequisites §3) | |
| 281 | +| `sh: python: command not found` (repeated) | Marvin internal call — harmless on macOS | Ignore; Marvin Init still succeeds | |
| 282 | +| Pool state never reaches `Maintenance` | KVM agent not responding | Check `cloudstack-agent` on KVM host; verify host is connected in CloudStack UI | |
| 283 | +| iSCSI `test_07` error 530 | KVM guest does not ACK SCSI hot-unplug | Known environment limitation — see TEST_CASES.md Suite 10 note | |
| 284 | +| ONTAP REST `401 Unauthorized` | Wrong credentials in `ontap.cfg` | Verify `username`/`password` under `ontap` section | |
| 285 | +| `No ready KVM user template available` | Template still downloading | Wait for template `isready=true` in the CloudStack UI, then rerun | |
| 286 | + |
| 287 | +--- |
| 288 | + |
| 289 | +## Adding new test cases |
| 290 | + |
| 291 | +1. Pick the existing file closest to what you need and copy its structure. |
| 292 | +2. Read `ontap_test_base.py` for the exact method signatures you can reuse. |
| 293 | +3. Copy the `_create_pool()` helper from an existing file that matches your protocol — **never** call `StoragePool.create()`. |
| 294 | +4. Number your methods `test_01`, `test_02`, … and add `@attr(tags=["<your_tag>"], required_hardware=True)` to each. |
| 295 | +5. Use `self.__class__.<attr>` for all state shared between test methods. |
| 296 | +6. Syntax-check before the first full run: `python3 -m py_compile <your_file>.py` |
| 297 | +7. Add your test cases to [TEST_CASES.md](TEST_CASES.md). |
0 commit comments