Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8415629
enabling the hublvol detch on cleanup source
EbiRider Jun 24, 2026
5d53b47
adding a hublvol controller to control detch behaviour
EbiRider Jun 24, 2026
741d056
fixing any issue with back to back migrations
EbiRider Jun 25, 2026
a13a1b2
fixing an issue with tertiary nodes not being registered on the target
EbiRider Jun 25, 2026
e67146b
fixed an issue where the migration will report failure desptie succes…
EbiRider Jun 25, 2026
b87e39e
fixing mypy type checker issue for constants
EbiRider Jun 25, 2026
79cbe59
prevent the migration from trying to delete snapshots migrated from p…
EbiRider Jun 25, 2026
64934f8
the pre-scan now checks for is_snapshot field
EbiRider Jun 25, 2026
735c420
removing the logic to generate a snapshot if the source lvol has none
EbiRider Jun 26, 2026
6c92126
remvoing forced snapshot creation on start_migration
EbiRider Jun 26, 2026
696c79a
fixing an issue with namespace not being added for tertiary node
EbiRider Jun 26, 2026
4746b1b
followup for fixing tertiary ns add issue
EbiRider Jun 26, 2026
6651352
fixing the delete logic on the migration
EbiRider Jun 29, 2026
6d53c5e
removed setting migration flag for snapshots to be deleted
EbiRider Jun 29, 2026
589b9a2
TEMP: testing snapshot resume on retry instead of delete
EbiRider Jun 30, 2026
1302994
TEMP: disabling the cleanup from source and intermediates
EbiRider Jun 30, 2026
6ecbcec
TEMP: disabling the cleanup source
EbiRider Jun 30, 2026
fb9c754
setting the spdk image ref for debugging
EbiRider Jul 3, 2026
05c0e7b
fixing the image ref for spdk
EbiRider Jul 3, 2026
4231193
fixing the image ref for spdk again
EbiRider Jul 3, 2026
9b06344
setting the env_var to main
EbiRider Jul 4, 2026
53ac4d9
removing the status cutover as it's no longer needed
EbiRider Jul 4, 2026
93d7450
fixing ndcs and npcs
EbiRider Jul 4, 2026
fe73f8a
setting env_var for spdk image
EbiRider Jul 4, 2026
f4c08eb
disabling cleanup after migration for testing
EbiRider Jul 4, 2026
1df670b
adding rename logic to fix the issue with migrating the same lvol ove…
EbiRider Jul 4, 2026
fdcf214
fixing an issue with migrating trees
EbiRider Jul 5, 2026
f775a59
fixing an issue where ancestor snapshots would not be renamed after m…
EbiRider Jul 5, 2026
7ef1a90
disabling source cleanup for testing
EbiRider Jul 6, 2026
7e16f1e
enabling cleanup and disabling rename for now
EbiRider Jul 6, 2026
e4072df
enabling lvol rename
EbiRider Jul 6, 2026
d24b0ed
changing the comments after fact checking about rename
EbiRider Jul 7, 2026
c317b76
fixing an issue with namespace not being added for tertiary node
EbiRider Jun 26, 2026
d023ee8
followup for fixing tertiary ns add issue
EbiRider Jun 26, 2026
f1a2b07
TEMP: testing snapshot resume on retry instead of delete
EbiRider Jun 30, 2026
6f9d2d1
TEMP: disabling the cleanup from source and intermediates
EbiRider Jun 30, 2026
c0eb29f
setting the env_var to main
EbiRider Jul 4, 2026
7da99e2
setting env_var for spdk image
EbiRider Jul 4, 2026
2effec6
fixing an issue where ancestor snapshots would not be renamed after m…
EbiRider Jul 5, 2026
b56c429
enabling lvol rename
EbiRider Jul 6, 2026
f0cec34
Apply suggestions from code review
EbiRider Jul 7, 2026
94532a6
fixing linting errors
EbiRider Jul 7, 2026
c99d4f7
fixed an issue that made unit test fail
EbiRider Jul 7, 2026
e9941c7
setting correct image refernces for env var
EbiRider Jul 7, 2026
164e5d6
using request3 for new rpc call
EbiRider Jul 9, 2026
88bda5b
fix: escalate migration errors and deduplicate ancestry helpers
EbiRider Jul 9, 2026
bc45556
fix: dead-code cleanup, exception-safety fixes, stale docs
EbiRider Jul 10, 2026
74d24b4
Merge branch 'main' into migration-followup
EbiRider Jul 10, 2026
4d13362
fixing a linting error
EbiRider Jul 10, 2026
9a4f7fc
fix: dedupe migration-target bdev naming into a shared idempotent helper
EbiRider Jul 10, 2026
aece23c
Merge branch 'main' into migration-followup
EbiRider Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion simplyblock_core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def get_config_var(name, default=None):

# Live volume migration constants
LVOL_MIG_MAX_RETRIES = 5 # max retry attempts before aborting
LVOL_MIG_DEADLINE_SEC = 360 # default 4-hour deadline (0 = no deadline)
LVOL_MIG_DEADLINE_SEC = 3600 # 1-hour deadline (0 = no deadline)
LVOL_MIG_MAX_INTERMEDIATE_SNAPS = 3 # max recursive "shrink" snapshot rounds
LVOL_MIG_INTERMEDIATE_SNAP_THRESHOLD_BYTES = 500 * 1024 * 1024 # 500 MiB — skip if delta is smaller
LVOL_MIG_BDEV_SUFFIX = 'm' # appended to every migration bdev on the target to avoid collision with real bdevs
Expand Down
67 changes: 27 additions & 40 deletions simplyblock_core/controllers/migration_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from datetime import datetime

from simplyblock_core import constants
from simplyblock_core.controllers import migration_events, tasks_controller, snapshot_controller
from simplyblock_core.controllers import migration_events, tasks_controller
from simplyblock_core.exceptions import MigrationConflictError, PreconditionError
from simplyblock_core.controllers.host_auth import _reapply_allowed_hosts
from simplyblock_core.kms import create_kms_connection, lvol_dek_path, pool_kek_name
Expand All @@ -54,7 +54,7 @@
from simplyblock_core.models.nvme_connect import NvmeConnectEntry
from simplyblock_core.models.snapshot import SnapShot
from simplyblock_core.models.storage_node import StorageNode
from simplyblock_core.utils import convert_size
from simplyblock_core.utils import convert_size, lvol_tgt_bdev_name

# Note: JobSchedule is not imported directly here; task creation is delegated to
# tasks_controller.add_lvol_mig_task() which handles event logging consistently.
Expand Down Expand Up @@ -121,12 +121,6 @@ def start_migration(migration_id,
raise ValueError(f"Target node is not online (status={target_node.status})")

snap_plan = get_snapshot_chain(lvol_id, source_node_id)
if not snap_plan:
snap_name = f"_mig_{migration.uuid[:8]}_lvol_{migration.lvol_id[:8]}"
snap_uuid, err = snapshot_controller.add(lvol_id, snap_name, bypass_migration_check=True)
if err:
raise ValueError(f"Failed to create snapshot: {err}")
snap_plan = [snap_uuid]

snaps_found_on_target = [s for s in snap_plan if _is_snap_on_node(s, target_node_id)]
snap_migration_plan = [s for s in snap_plan if s not in snaps_found_on_target]
Expand Down Expand Up @@ -222,7 +216,7 @@ def _cleanup_created(migration):
return

nqn = lvol.nqn
bdev_short = lvol.lvol_bdev + _MIGRATION_BDEV_SUFFIX
bdev_short = lvol_tgt_bdev_name(lvol.lvol_bdev)
composite = f"{tgt_node.lvstore}/{bdev_short}"

# Compute overlap: nodes shared between SRC and TGT paths.
Expand Down Expand Up @@ -423,7 +417,8 @@ def _is_snap_on_node(snap_id, node_id):
snap = db.get_snapshot_by_id(snap_id)
except KeyError:
return False
if snap.lvol.node_id == node_id:
# snap.lvol defaults to None (SnapShot.lvol) — guard before dereferencing.
if snap.lvol and snap.lvol.node_id == node_id:
return True
return any(
inst.get('lvol', {}).get('node_id') == node_id
Expand All @@ -433,10 +428,18 @@ def _is_snap_on_node(snap_id, node_id):

def _get_snap_ancestry(snap_uuid):
"""
Walk the ``snap_ref_id`` chain from *snap_uuid* upward to the root and
Walk the blobstore parent chain from *snap_uuid* upward to the root and
return the UUIDs in root-first order (oldest ancestor first).

``snap_ref_id`` points from a child snapshot to its parent snapshot.
Two parent pointers are tried in order:
- ``snap_ref_id``: set for snapshots taken from a clone volume (points to
the clone's parent snapshot, used for ref-count tracking).
- ``prev_snap_uuid``: set for all snapshots and records the previous
snapshot in the lvol's own sequence — this is the blobstore parent for
snapshots taken from a non-clone volume (e.g. snap_A3 → snap_A2 →
snap_A1 taken from the original lvol_A). Without this fallback, the
ancestry walk stops at the clone's immediate parent and misses the older
ancestor snapshots whose data is not copied standalone.
"""
chain = []
current = snap_uuid
Expand All @@ -448,7 +451,7 @@ def _get_snap_ancestry(snap_uuid):
except KeyError:
break
chain.append(current)
current = snap.snap_ref_id
current = snap.snap_ref_id or snap.prev_snap_uuid
chain.reverse() # oldest → newest
return chain

Expand Down Expand Up @@ -495,7 +498,7 @@ def get_snaps_safe_to_delete_on_source(migration):
if lvol.uuid == migration.lvol_id:
continue
if lvol.cloned_from_snap and lvol.cloned_from_snap in candidates:
_protect_snap_and_ancestors(lvol.cloned_from_snap, candidates)
candidates -= _collect_snap_ancestry(lvol.cloned_from_snap)

return candidates

Expand Down Expand Up @@ -525,48 +528,32 @@ def get_snaps_to_delete_on_target(migration):
if lvol.uuid == migration.lvol_id:
continue
if lvol.cloned_from_snap:
_collect_snap_ancestry(lvol.cloned_from_snap, protected)
protected |= _collect_snap_ancestry(lvol.cloned_from_snap)

return [
uid for uid in migration.snaps_migrated
if uid not in preexisting and uid not in protected
]


def _protect_snap_and_ancestors(snap_uuid, candidate_set):
"""Remove *snap_uuid* and all its ancestors from *candidate_set*."""
def _collect_snap_ancestry(snap_uuid) -> set:
"""Return the set of *snap_uuid* and all its ancestors."""
result: set = set()
current = snap_uuid
visited = set()
while current and current not in visited:
visited.add(current)
candidate_set.discard(current)
try:
snap = db.get_snapshot_by_id(current)
current = snap.snap_ref_id
except KeyError:
break


def _collect_snap_ancestry(snap_uuid, out_set):
"""Add *snap_uuid* and all its ancestors to *out_set*."""
current = snap_uuid
visited = set()
while current and current not in visited:
visited.add(current)
out_set.add(current)
while current and current not in result:
result.add(current)
try:
snap = db.get_snapshot_by_id(current)
current = snap.snap_ref_id
current = snap.snap_ref_id or snap.prev_snap_uuid
except KeyError:
break
return result


# ---------------------------------------------------------------------------
# Post-migration DB updates
# ---------------------------------------------------------------------------

_MIGRATION_BDEV_SUFFIX = constants.LVOL_MIG_BDEV_SUFFIX


def _build_connect_entries(node, port, lvol, nqn, ctrl_loss_tmo, cluster, host_entry, host_nqn):
"""Build NVMe connect-string entries for each data NIC on *node* at *port*."""
Expand Down Expand Up @@ -675,7 +662,7 @@ def create_migration(lvol_id, target_node_id,
cluster = db.get_cluster_by_id(tgt_node.cluster_id)
tgt_rpc = tgt_node.rpc_client()
nqn = lvol.nqn
bdev_short = lvol.lvol_bdev + _MIGRATION_BDEV_SUFFIX
bdev_short = lvol_tgt_bdev_name(lvol.lvol_bdev)
composite = f"{tgt_node.lvstore}/{bdev_short}"
size_in_mib = convert_size(lvol.size, 'MiB')
tgt_port = tgt_node.get_lvol_subsys_port(tgt_node.lvstore)
Expand Down Expand Up @@ -738,7 +725,7 @@ def create_migration(lvol_id, target_node_id,
f"create_migration: secondary registration error (continuing): {_e}")

_pre_ter_node = None
if lvol.ha_type == "ha3" and tgt_node.tertiary_node_id:
if tgt_node.tertiary_node_id:
try:
_pre_ter_node = db.get_storage_node_by_id(tgt_node.tertiary_node_id)
_ter_rpc_reg = _pre_ter_node.rpc_client()
Expand Down
7 changes: 1 addition & 6 deletions simplyblock_core/models/lvol_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ class LVolMigration(BaseModel):
STATUS_NEW = 'new'
STATUS_RUNNING = 'running'
STATUS_SUSPENDED = 'suspended'
# CUTOVER: the target subsystem is live and the client must reconnect before
# the source subsystem is removed. Set by PHASE_LVOL_MIGRATE (tgt_is_src_secondary
# path only); cleared to STATUS_DONE once PHASE_CLEANUP_SOURCE completes.
STATUS_CUTOVER = 'cutover'
STATUS_DONE = 'done'
STATUS_FAILED = 'failed'
STATUS_CANCELLED = 'cancelled'
Expand All @@ -49,7 +45,6 @@ class LVolMigration(BaseModel):
STATUS_NEW: 0,
STATUS_RUNNING: 1,
STATUS_SUSPENDED: 2,
STATUS_CUTOVER: 6,
STATUS_DONE: 3,
STATUS_FAILED: 4,
STATUS_CANCELLED: 5,
Expand Down Expand Up @@ -124,7 +119,7 @@ def write_to_db(self, kv_store=None):
def is_active(self):
return self.status in (
self.STATUS_NEW, self.STATUS_RUNNING,
self.STATUS_SUSPENDED, self.STATUS_CUTOVER,
self.STATUS_SUSPENDED,
)

def has_deadline_passed(self):
Expand Down
3 changes: 3 additions & 0 deletions simplyblock_core/rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,9 @@ def bdev_lvol_get_lvstores(self, name):
params = {"lvs_name": name}
return self._request("bdev_lvol_get_lvstores", params)

def bdev_lvol_rename(self, old_name, new_name):
return self._request3("bdev_lvol_rename", old_name=old_name, new_name=new_name)

def bdev_lvol_resize(self, name, size_in_mib):
params = {
"name": name,
Expand Down
Loading
Loading