Add zvol existence check and creation for iSCSI extents
client.py:
- check_iscsi_zvols(): queries pool.dataset.query for VOLUME type,
returns list of missing zvol names
- create_zvol(): creates a single zvol via pool.dataset.create
- create_missing_zvols(): opens a fresh connection and creates a
batch of zvols from a {name: volsize_bytes} dict
summary.py:
- Add zvols_to_check and missing_zvols list fields
- Report shows a WARNING block listing missing zvols when present
migrate.py:
- _migrate_iscsi_extents() populates summary.zvols_to_check with
the dataset name for each DISK-type extent during dry run
cli.py:
- Add _parse_size() to parse human-friendly size strings
(100G, 500GiB, 1T, etc.) to bytes
- run() calls check_iscsi_zvols() during dry run and stores results
in summary.missing_zvols
- Wizard prompts for size and creates missing zvols after the dry
run report, before asking the user to confirm the live run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .archive import parse_archive, list_archive_and_exit
|
||||
from .client import TrueNASClient, check_dataset_paths, create_missing_datasets
|
||||
from .client import TrueNASClient, check_dataset_paths, create_missing_datasets, check_iscsi_zvols, create_missing_zvols
|
||||
from .colors import log, _bold, _bold_cyan, _bold_red, _bold_yellow, _cyan, _dim, _green, _yellow
|
||||
from .csv_source import parse_csv_sources
|
||||
from .migrate import migrate_smb_shares, migrate_nfs_shares, migrate_iscsi
|
||||
@@ -112,6 +112,11 @@ async def run(
|
||||
client, summary.paths_to_create,
|
||||
)
|
||||
|
||||
if args.dry_run and summary.zvols_to_check:
|
||||
summary.missing_zvols = await check_iscsi_zvols(
|
||||
client, summary.zvols_to_check,
|
||||
)
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
@@ -119,6 +124,24 @@ async def run(
|
||||
# Interactive wizard helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _parse_size(s: str) -> int:
|
||||
"""Parse a human-friendly size string to bytes. E.g. '100G', '500GiB', '1T'."""
|
||||
s = s.strip().upper()
|
||||
for suffix, mult in [
|
||||
("PIB", 1 << 50), ("PB", 1 << 50), ("P", 1 << 50),
|
||||
("TIB", 1 << 40), ("TB", 1 << 40), ("T", 1 << 40),
|
||||
("GIB", 1 << 30), ("GB", 1 << 30), ("G", 1 << 30),
|
||||
("MIB", 1 << 20), ("MB", 1 << 20), ("M", 1 << 20),
|
||||
("KIB", 1 << 10), ("KB", 1 << 10), ("K", 1 << 10),
|
||||
]:
|
||||
if s.endswith(suffix):
|
||||
try:
|
||||
return int(float(s[:-len(suffix)]) * mult)
|
||||
except ValueError:
|
||||
pass
|
||||
return int(s) # plain bytes
|
||||
|
||||
|
||||
def _find_debug_archives(directory: str = ".") -> list[Path]:
|
||||
"""Return sorted list of TrueNAS debug archives found in *directory*."""
|
||||
patterns = ("*.tgz", "*.tar.gz", "*.tar", "*.txz", "*.tar.xz")
|
||||
@@ -405,6 +428,29 @@ def interactive_mode() -> None:
|
||||
))
|
||||
print()
|
||||
|
||||
if dry_summary.missing_zvols:
|
||||
print(f"\n {len(dry_summary.missing_zvols)} zvol(s) need to be created for iSCSI extents:")
|
||||
for z in dry_summary.missing_zvols:
|
||||
print(f" • {z}")
|
||||
print()
|
||||
if _confirm(f"Create these {len(dry_summary.missing_zvols)} zvol(s) on {host} now?"):
|
||||
zvol_sizes: dict[str, int] = {}
|
||||
for zvol in dry_summary.missing_zvols:
|
||||
while True:
|
||||
raw = _prompt(f" Size for {zvol} (e.g. 100G, 500GiB, 1T)").strip()
|
||||
if not raw:
|
||||
print(" Size is required.")
|
||||
continue
|
||||
try:
|
||||
zvol_sizes[zvol] = _parse_size(raw)
|
||||
break
|
||||
except ValueError:
|
||||
print(f" Cannot parse {raw!r} — try a format like 100G or 500GiB.")
|
||||
asyncio.run(create_missing_zvols(
|
||||
host=host, port=port, api_key=api_key, zvols=zvol_sizes,
|
||||
))
|
||||
print()
|
||||
|
||||
if not _confirm(f"Apply these changes to {host}?"):
|
||||
print("Aborted – no changes made.")
|
||||
sys.exit(0)
|
||||
|
||||
Reference in New Issue
Block a user