|
| 1 | +import json as _json |
1 | 2 | import os |
2 | 3 | import re |
3 | 4 |
|
|
6 | 7 | from linodecli.exit_codes import ExitCodes |
7 | 8 | from tests.integration.helpers import ( |
8 | 9 | BASE_CMDS, |
| 10 | + delete_target_id, |
9 | 11 | exec_failing_test_command, |
10 | 12 | exec_test_command, |
11 | 13 | get_random_text, |
| 14 | + wait_for_condition, |
12 | 15 | ) |
13 | 16 | from tests.integration.volumes.fixtures import volume_instance_id # noqa: F401 |
14 | 17 |
|
@@ -226,3 +229,162 @@ def test_fail_to_update_volume_size(volume_instance_id): |
226 | 229 | "linode-cli volumes update: error: unrecognized arguments: --size=15" |
227 | 230 | in result |
228 | 231 | ) |
| 232 | + |
| 233 | + |
| 234 | +def test_attach_volumes_to_extended_device_slots(linode_cloud_firewall): |
| 235 | + num_volumes = 12 |
| 236 | + device_slots = [ |
| 237 | + "sda", "sdb", "sdc", "sdd", "sde", "sdf", "sdg", "sdh", |
| 238 | + "sdi", "sdj", "sdk", "sdl", |
| 239 | + ] |
| 240 | + regions_data = _json.loads( |
| 241 | + exec_test_command( |
| 242 | + ["linode-cli", "regions", "ls", "--json", "--suppress-warnings"] |
| 243 | + ) |
| 244 | + ) |
| 245 | + eligible = [ |
| 246 | + r["id"] |
| 247 | + for r in regions_data |
| 248 | + if r.get("site_type") == "core" |
| 249 | + and "Linodes" in r.get("capabilities", []) |
| 250 | + and "Block Storage" in r.get("capabilities", []) |
| 251 | + ] |
| 252 | + assert eligible, "No eligible region found with Linodes and Block Storage" |
| 253 | + test_region = eligible[0] |
| 254 | + linode_id = exec_test_command( |
| 255 | + [ |
| 256 | + "linode-cli", |
| 257 | + "linodes", |
| 258 | + "create", |
| 259 | + "--type", |
| 260 | + "g6-standard-6", |
| 261 | + "--region", |
| 262 | + test_region, |
| 263 | + "--firewall_id", |
| 264 | + linode_cloud_firewall, |
| 265 | + "--booted", |
| 266 | + "true", |
| 267 | + "--format", |
| 268 | + "id", |
| 269 | + "--text", |
| 270 | + "--no-headers", |
| 271 | + ] |
| 272 | + ) |
| 273 | + |
| 274 | + vol_ids = [] |
| 275 | + label_base = "vol-ext-" + get_random_text(4) |
| 276 | + |
| 277 | + try: |
| 278 | + for i in range(num_volumes): |
| 279 | + vol_id = exec_test_command( |
| 280 | + BASE_CMDS["volumes"] |
| 281 | + + [ |
| 282 | + "create", |
| 283 | + "--label", |
| 284 | + f"{label_base}-{i}", |
| 285 | + "--region", |
| 286 | + test_region, |
| 287 | + "--size", |
| 288 | + "10", |
| 289 | + "--text", |
| 290 | + "--no-headers", |
| 291 | + "--format", |
| 292 | + "id", |
| 293 | + ] |
| 294 | + ) |
| 295 | + vol_ids.append(vol_id) |
| 296 | + |
| 297 | + def volume_active(vid=vol_id): |
| 298 | + status = exec_test_command( |
| 299 | + BASE_CMDS["volumes"] |
| 300 | + + ["view", vid, "--text", "--no-headers", "--format", "status"] |
| 301 | + ) |
| 302 | + return status.strip() == "active" |
| 303 | + |
| 304 | + wait_for_condition(10, 240, volume_active) |
| 305 | + |
| 306 | + configs_result = exec_test_command( |
| 307 | + BASE_CMDS["linodes"] |
| 308 | + + ["configs-list", linode_id, "--json", "--suppress-warnings"] |
| 309 | + ) |
| 310 | + configs = _json.loads(configs_result) |
| 311 | + assert isinstance(configs, list), "configs-list should return a list" |
| 312 | + |
| 313 | + config_create_result = exec_test_command( |
| 314 | + BASE_CMDS["linodes"] |
| 315 | + + [ |
| 316 | + "config-create", |
| 317 | + linode_id, |
| 318 | + "--label", |
| 319 | + label_base + "-config", |
| 320 | + "--devices.sda.volume_id", |
| 321 | + vol_ids[0], |
| 322 | + "--json", |
| 323 | + "--suppress-warnings", |
| 324 | + ] |
| 325 | + ) |
| 326 | + config_json = _json.loads(config_create_result)[0] |
| 327 | + config_id = str(config_json["id"]) |
| 328 | + |
| 329 | + view_result = exec_test_command( |
| 330 | + BASE_CMDS["linodes"] |
| 331 | + + ["config-view", linode_id, config_id, "--json", "--suppress-warnings"] |
| 332 | + ) |
| 333 | + viewed = _json.loads(view_result)[0] |
| 334 | + assert str(viewed["id"]) == config_id, ( |
| 335 | + "config-view should return the correct config" |
| 336 | + ) |
| 337 | + assert "devices" in viewed, ( |
| 338 | + "config-view response should include a 'devices' field" |
| 339 | + ) |
| 340 | + |
| 341 | + for slot, vol_id in zip(device_slots[1:], vol_ids[1:]): |
| 342 | + exec_test_command( |
| 343 | + BASE_CMDS["linodes"] |
| 344 | + + [ |
| 345 | + "config-update", |
| 346 | + linode_id, |
| 347 | + config_id, |
| 348 | + f"--devices.{slot}.volume_id", |
| 349 | + vol_id, |
| 350 | + "--text", |
| 351 | + "--no-headers", |
| 352 | + ] |
| 353 | + ) |
| 354 | + |
| 355 | + final_config_result = exec_test_command( |
| 356 | + BASE_CMDS["linodes"] |
| 357 | + + ["config-view", linode_id, config_id, "--json", "--suppress-warnings"] |
| 358 | + ) |
| 359 | + final_config = _json.loads(final_config_result)[0] |
| 360 | + devices = final_config.get("devices", {}) |
| 361 | + |
| 362 | + for slot in device_slots: |
| 363 | + assert devices.get(slot) is not None, ( |
| 364 | + f"Extended device slot '{slot}' is unexpectedly empty; " |
| 365 | + "the plan-based volume limit may not be applied correctly." |
| 366 | + ) |
| 367 | + |
| 368 | + populated = [slot for slot in device_slots if devices.get(slot) is not None] |
| 369 | + assert len(populated) == num_volumes, ( |
| 370 | + f"Expected {num_volumes} populated device slots, " |
| 371 | + f"got {len(populated)}: {populated}" |
| 372 | + ) |
| 373 | + |
| 374 | + finally: |
| 375 | + for vol_id in vol_ids: |
| 376 | + delete_target_id( |
| 377 | + target="volumes", |
| 378 | + id=vol_id, |
| 379 | + use_retry=True, |
| 380 | + retries=5, |
| 381 | + delay=15, |
| 382 | + ) |
| 383 | + delete_target_id( |
| 384 | + target="linodes", |
| 385 | + id=linode_id, |
| 386 | + use_retry=True, |
| 387 | + retries=5, |
| 388 | + delay=15, |
| 389 | + ) |
| 390 | + |
0 commit comments