Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6853f77
feat: add ndcs and npcs attributes to lvol during snapshot management
Hamdy-khader Mar 26, 2026
dd4efab
feat: implement clone endpoint for logical volumes
Hamdy-khader Mar 26, 2026
50dea47
hotfix: ensure lvol mutation lock is acquired before cloning
Hamdy-khader Mar 27, 2026
87e6a36
hotfix: update Docker image tag for lvol clone
Hamdy-khader Mar 27, 2026
100422d
hotfix: add lock parameter to lvol clone function for optional mutati…
Hamdy-khader Mar 27, 2026
002ac5d
return error in func clone_lvol
geoffrey1330 Mar 27, 2026
cea4da2
fixed local variable 'e' referenced before assignment
geoffrey1330 Mar 27, 2026
3073113
feat: add clone-lvol command to CLI for creating logical volume clones
Hamdy-khader Mar 27, 2026
993e32a
hotfix: ensure mutation lock is considered during LVol sync deletion …
Hamdy-khader Mar 27, 2026
558d153
hotfix: fix snapshot UUID reference in lvol cloning logic
Hamdy-khader Mar 27, 2026
54a7947
hotfix: refactor lvol cloning logic to improve snapshot handling and …
Hamdy-khader Mar 27, 2026
541d47b
hotfix: initialize new_size to 0 and parse size from request data in …
Hamdy-khader Mar 27, 2026
a881b16
hotfix: add status field to snapshot details in snapshot controller
Hamdy-khader Mar 27, 2026
ef61fb2
hotfix: add check for maximum logical volumes on storage node during …
Hamdy-khader Mar 27, 2026
8eb9c52
hotfix: correct variable naming for storage node in lvol count check
Hamdy-khader Mar 27, 2026
1f85a96
feat: prevent deletion of snapshots in deletion status unless forced
Hamdy-khader Mar 29, 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
11 changes: 11 additions & 0 deletions simplyblock_cli/cli-reference.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,17 @@ commands:
help: "Logical volume id"
dest: volume_id
type: str
- name: clone-lvol
help: "Create logical volume clone by taking a snapshot and then cloning it."
arguments:
- name: "volume_id"
help: "Logical volume id"
dest: volume_id
type: str
- name: "clone_name"
help: "New lvol clone name"
dest: clone_name
type: str
- name: "control-plane"
help: "Control plane commands"
aliases:
Expand Down
8 changes: 8 additions & 0 deletions simplyblock_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ def init_volume(self):
self.init_volume__get_io_stats(subparser)
self.init_volume__check(subparser)
self.init_volume__inflate(subparser)
self.init_volume__clone_lvol(subparser)


def init_volume__add(self, subparser):
Expand Down Expand Up @@ -681,6 +682,11 @@ def init_volume__inflate(self, subparser):
subcommand = self.add_sub_command(subparser, 'inflate', 'Inflate a logical volume')
subcommand.add_argument('volume_id', help='Logical volume id', type=str)

def init_volume__clone_lvol(self, subparser):
subcommand = self.add_sub_command(subparser, 'clone-lvol', 'Create logical volume clone by taking a snapshot and then cloning it.')
subcommand.add_argument('volume_id', help='Logical volume id', type=str)
subcommand.add_argument('clone_name', help='New lvol clone name', type=str)


def init_control_plane(self):
subparser = self.add_command('control-plane', 'Control plane commands', aliases=['cp','mgmt',])
Expand Down Expand Up @@ -1115,6 +1121,8 @@ def run(self):
ret = self.volume__check(sub_command, args)
elif sub_command in ['inflate']:
ret = self.volume__inflate(sub_command, args)
elif sub_command in ['clone-lvol']:
ret = self.volume__clone_lvol(sub_command, args)
else:
self.parser.print_help()

Expand Down
3 changes: 3 additions & 0 deletions simplyblock_cli/clibase.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,9 @@ def volume__check(self, sub_command, args):
def volume__inflate(self, sub_command, args):
return lvol_controller.inflate_lvol(args.volume_id)

def volume__clone_lvol(self, sub_command, args):
return lvol_controller.clone_lvol(args.volume_id, args.clone_name)

def control_plane__add(self, sub_command, args):
cluster_id = args.cluster_id
cluster_ip = args.cluster_ip
Expand Down
46 changes: 46 additions & 0 deletions simplyblock_core/controllers/lvol_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1778,3 +1778,49 @@ def list_by_node(node_id=None, is_json=False):
if is_json:
return json.dumps(data, indent=2)
return utils.print_table(data)


def clone_lvol(lvol_id, clone_name, new_size=None, pvc_name=None):
db_controller = DBController()
try:
lvol = db_controller.get_lvol_by_id(lvol_id)
except KeyError as e:
logger.error(e)
return False, str(e)

host_node = db_controller.get_storage_node_by_id(lvol.node_id)
lvol_count = len(db_controller.get_lvols_by_node_id(lvol.node_id))
if lvol_count >= host_node.max_lvol:
error = f"Too many lvols on node: {host_node.get_id()}, max lvols reached: {lvol_count}"
logger.error(error)
return False, error

had_lock = None
snapshot_uuid = None
for snap in db_controller.get_snapshots_by_node_id(lvol.node_id):
if snap.snap_name == clone_name:
logger.info(f"Snapshot with name {clone_name} already exists for this LVol: {snap.uuid}, using it for cloning")
snapshot_uuid = snap.uuid
break

if not snapshot_uuid:
had_lock = snapshot_controller._acquire_lvol_mutation_lock(host_node)
snapshot_uuid, err = snapshot_controller.add(lvol_id, clone_name, lock=False)
if err:
snapshot_controller._release_lvol_mutation_lock(host_node, had_lock)
logger.error(err)
return False, str(err)
if not had_lock:
had_lock = snapshot_controller._acquire_lvol_mutation_lock(host_node)
new_lvol_uuid, err = snapshot_controller.clone(
snapshot_uuid, clone_name, new_size, pvc_name, delete_snap_on_lvol_delete=True, lock=False)
if err:
logger.error(err)
if snapshot_uuid:
snapshot_controller.delete(snapshot_uuid)
snapshot_controller._release_lvol_mutation_lock(host_node, had_lock)
return False, str(err)

snapshot_controller._release_lvol_mutation_lock(host_node, had_lock)
return new_lvol_uuid, False

28 changes: 20 additions & 8 deletions simplyblock_core/controllers/snapshot_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _rollback_lvol_creation(lvol, node_ids):
logger.error(f"Failed to rollback lvol {lvol.get_id()} from node {node_id}: {e}")


def add(lvol_id, snapshot_name, backup=False):
def add(lvol_id, snapshot_name, backup=False, lock=True):
try:
lvol = db_controller.get_lvol_by_id(lvol_id)
except KeyError as e:
Expand Down Expand Up @@ -74,7 +74,7 @@ def add(lvol_id, snapshot_name, backup=False):

snode = db_controller.get_storage_node_by_id(lvol.node_id)

if snode.lvol_sync_del():
if snode.lvol_sync_del() and lock:
logger.error(f"LVol sync deletion found on node: {snode.get_id()}")
return False, f"LVol sync deletion found on node: {snode.get_id()}"

Expand Down Expand Up @@ -141,7 +141,8 @@ def add(lvol_id, snapshot_name, backup=False):
secondary_nodes = []
host_node = db_controller.get_storage_node_by_id(snode.get_id())
sec_node = db_controller.get_storage_node_by_id(host_node.secondary_node_id)
had_lock = _acquire_lvol_mutation_lock(host_node)
if lock:
had_lock = _acquire_lvol_mutation_lock(host_node)

try:
# Build nodes list with all secondaries
Expand Down Expand Up @@ -236,7 +237,8 @@ def add(lvol_id, snapshot_name, backup=False):
return False, msg
registered_secs.append(sec)
finally:
_release_lvol_mutation_lock(host_node, had_lock)
if lock:
_release_lvol_mutation_lock(host_node, had_lock)

snap = SnapShot()
snap.uuid = str(uuid.uuid4())
Expand Down Expand Up @@ -297,6 +299,7 @@ def list(node_id=None):
"Created At": time.strftime("%H:%M:%S, %d/%m/%Y", time.gmtime(snap.created_at)),
"Base Snapshot": snap.snap_ref_id,
"Clones": clones,
"Status": snap.status,
})
return utils.print_table(data)

Expand All @@ -308,6 +311,11 @@ def delete(snapshot_uuid, force_delete=False):
logger.error(f"Snapshot not found {snapshot_uuid}")
return False

if snap.status == SnapShot.STATUS_IN_DELETION:
logger.error(f"Snapshot is in deletion {snapshot_uuid}")
if not force_delete:
return True

try:
snode = db_controller.get_storage_node_by_id(snap.lvol.node_id)
except KeyError:
Expand Down Expand Up @@ -413,7 +421,7 @@ def delete(snapshot_uuid, force_delete=False):
return True


def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None, delete_snap_on_lvol_delete=False):
def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None, delete_snap_on_lvol_delete=False, lock=True):
try:
snap = db_controller.get_snapshot_by_id(snapshot_id)
except KeyError as e:
Expand All @@ -439,7 +447,7 @@ def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None
logger.exception(msg)
return False, msg

if snode.lvol_sync_del():
if snode.lvol_sync_del() and lock:
logger.error(f"LVol sync deletion found on node: {snode.get_id()}")
return False, f"LVol sync deletion found on node: {snode.get_id()}"

Expand Down Expand Up @@ -517,6 +525,8 @@ def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None
lvol.subsys_port = snap.lvol.subsys_port
lvol.fabric = snap.fabric
lvol.delete_snap_on_lvol_delete = bool(delete_snap_on_lvol_delete)
lvol.ndcs = snap.lvol.ndcs
lvol.npcs = snap.lvol.npcs

if pvc_name:
lvol.pvc_name = pvc_name
Expand Down Expand Up @@ -584,7 +594,8 @@ def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None
if second_secondary_id:
secondary_ids.append(second_secondary_id)
lvol.nodes = [host_node.get_id()] + secondary_ids
had_lock = _acquire_lvol_mutation_lock(host_node)
if lock:
had_lock = _acquire_lvol_mutation_lock(host_node)

try:
primary_node = None
Expand Down Expand Up @@ -659,7 +670,8 @@ def clone(snapshot_id, clone_name, new_size=0, pvc_name=None, pvc_namespace=None
return False, error
created_nodes.append(sec.get_id())
finally:
_release_lvol_mutation_lock(host_node, had_lock)
if lock:
_release_lvol_mutation_lock(host_node, had_lock)

lvol.status = LVol.STATUS_ONLINE
lvol.write_to_db(db_controller.kv_store)
Expand Down
2 changes: 1 addition & 1 deletion simplyblock_core/env_var
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SIMPLY_BLOCK_COMMAND_NAME=sbcli-dev
SIMPLY_BLOCK_VERSION=26.1.1

SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:R25.10-Hotfix
SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:R25.10-Hotfix-lvol-clone
SIMPLY_BLOCK_SPDK_ULTRA_IMAGE=public.ecr.aws/simply-block/ultra:R25.10-Hotfix-latest
12 changes: 12 additions & 0 deletions simplyblock_web/api/v1/lvol.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,15 @@ def inflate_lvol(uuid):

ret = lvol_controller.inflate_lvol(uuid)
return utils.get_response(ret)

@bp.route('/lvol/clone', methods=['POST'])
def clone():
cl_data = request.get_json()
lvol_id = cl_data['lvol_id']
clone_name = cl_data['clone_name']
pvc_name = cl_data.get('pvc_name', None)
new_size = 0
if 'new_size' in cl_data:
new_size = core_utils.parse_size(cl_data.get('new_size', 0))
clone_id, error = lvol_controller.clone_lvol(lvol_id, clone_name, new_size, pvc_name)
return utils.get_response(clone_id, error, http_code=400)
4 changes: 4 additions & 0 deletions simplyblock_web/api/v2/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,7 @@ def create_snapshot(
cluster_id=cluster.get_id(), pool_id=pool.get_id(), snapshot_id=snapshot_id,
)
return Response(status_code=201, headers={'Location': entity_url})

@instance_api.get('/clone', name='clusters:storage-pools:volumes:clone')
def clone(cluster: Cluster, pool: StoragePool, volume: Volume, clone_name: str) -> bool:
return lvol_controller.clone_lvol(volume.get_id(), clone_name)
Loading