Skip to content

Commit

Permalink
feat: add support for LVM thin volumes
Browse files Browse the repository at this point in the history
Update to ignore thinpool LVs and support think provisioned sources.

Signed-off-by: Todd Gill <[email protected]>
  • Loading branch information
trgill committed May 20, 2024
1 parent bdd50fd commit 27e8d00
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 26 deletions.
113 changes: 87 additions & 26 deletions library/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,54 @@ def get_snapshot_name(lv_name, suffix):
return lv_name + "_" + suffix_str


def lvm_get_attr(vg_name, lv_name):
lvs_command = ["lvs", "--reportformat", "json", vg_name + "/" + lv_name]

rc, output = run_command(lvs_command)

if rc == LVM_NOTFOUND_RC:
return SnapshotStatus.SNAPSHOT_OK, False

if rc:
return SnapshotStatus.ERROR_LVS_FAILED, None

try:
lvs_json = json.loads(output)
except ValueError as error:
logger.info(error)
message = "lvm_is_snapshot: json decode failed : %s" % error.args[0]
return SnapshotStatus.ERROR_JSON_PARSER_ERROR, message

lv_list = lvs_json["report"]

if len(lv_list) > 1 or len(lv_list[0]["lv"]) > 1:
raise LvmBug("'lvs' returned more than 1 lv '%d'" % rc)

lv = lv_list[0]["lv"][0]

lv_attr = lv["lv_attr"]

if len(lv_attr) == 0:
raise LvmBug("'lvs' zero length attr : '%d'" % rc)

return SnapshotStatus.SNAPSHOT_OK, lv_attr


def lvm_is_thinpool(vg_name, lv_name):
rc, lv_attr = lvm_get_attr(vg_name, lv_name)

if rc == LVM_NOTFOUND_RC:
return SnapshotStatus.SNAPSHOT_OK, False

if rc:
return SnapshotStatus.ERROR_LVS_FAILED, None

if lv_attr[0] == "t":
return SnapshotStatus.SNAPSHOT_OK, True
else:
return SnapshotStatus.SNAPSHOT_OK, False


def lvm_lv_exists(vg_name, lv_name):
vg_exists = False
lv_exists = False
Expand Down Expand Up @@ -653,36 +701,15 @@ def lvm_is_inuse(vg_name, lv_name):
return SnapshotStatus.SNAPSHOT_OK, False


def lvm_is_snapshot(vg_name, snapshot_name):
lvs_command = ["lvs", "--reportformat", "json", vg_name + "/" + snapshot_name]

rc, output = run_command(lvs_command)
def lvm_is_snapshot(vg_name, lv_name):
rc, lv_attr = lvm_get_attr(vg_name, lv_name)

if rc == LVM_NOTFOUND_RC:
return SnapshotStatus.SNAPSHOT_OK, False

if rc:
return SnapshotStatus.ERROR_LVS_FAILED, None

try:
lvs_json = json.loads(output)
except ValueError as error:
logger.info(error)
message = "lvm_is_snapshot: json decode failed : %s" % error.args[0]
return SnapshotStatus.ERROR_JSON_PARSER_ERROR, message

lv_list = lvs_json["report"]

if len(lv_list) > 1 or len(lv_list[0]["lv"]) > 1:
raise LvmBug("'lvs' returned more than 1 lv '%d'" % rc)

lv = lv_list[0]["lv"][0]

lv_attr = lv["lv_attr"]

if len(lv_attr) == 0:
raise LvmBug("'lvs' zero length attr : '%d'" % rc)

if lv_attr[0] == "s":
return SnapshotStatus.SNAPSHOT_OK, True
else:
Expand Down Expand Up @@ -720,7 +747,13 @@ def revert_lv(vg_name, snapshot_name, check_mode):
raise LvmBug("'lvs' failed '%d'" % rc)

if lv_exists:
if not lvm_is_snapshot(vg_name, snapshot_name):
rc, is_snapshot = lvm_is_snapshot(vg_name, snapshot_name)
if rc != SnapshotStatus.SNAPSHOT_OK:
return (
SnapshotStatus.ERROR_VERIFY_COMMAND_FAILED,
"revert_lv: command failed for LV lvm_is_snapshot()",
)
if not is_snapshot:
return (
SnapshotStatus.ERROR_REVERT_FAILED,
"LV with name: " + vg_name + "/" + snapshot_name + " is not a snapshot",
Expand Down Expand Up @@ -751,7 +784,13 @@ def extend_lv_snapshot(vg_name, lv_name, suffix, percent_space_required, check_m

changed = False
if lv_exists:
if not lvm_is_snapshot(vg_name, snapshot_name):
rc, is_snapshot = lvm_is_snapshot(vg_name, snapshot_name)
if rc != SnapshotStatus.SNAPSHOT_OK:
return (
SnapshotStatus.ERROR_VERIFY_COMMAND_FAILED,
"extend_lv_snapshot: command failed for LV lvm_is_snapshot()",
)
if not is_snapshot:
return (
SnapshotStatus.ERROR_EXTEND_NOT_SNAPSHOT,
"LV with name: " + vg_name + "/" + snapshot_name + " is not a snapshot",
Expand Down Expand Up @@ -892,7 +931,13 @@ def snapshot_lv(vg_name, lv_name, suffix, snap_size, check_mode):
rc, _vg_exists, lv_exists = lvm_lv_exists(vg_name, snapshot_name)

if lv_exists:
if lvm_is_snapshot(vg_name, snapshot_name):
rc, is_snapshot = lvm_is_snapshot(vg_name, snapshot_name)
if rc != SnapshotStatus.SNAPSHOT_OK:
return (
SnapshotStatus.ERROR_VERIFY_COMMAND_FAILED,
"snapshot_lv: command failed for LV lvm_is_snapshot()",
)
if is_snapshot:
return (
SnapshotStatus.ERROR_ALREADY_EXISTS,
"Snapshot of :" + vg_name + "/" + lv_name + " already exists",
Expand Down Expand Up @@ -2119,8 +2164,24 @@ def get_json_from_args(module_args):
if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
continue

rc, is_snapshot = lvm_is_snapshot(vg_str, lv["lv_name"])
if rc != SnapshotStatus.SNAPSHOT_OK:
return (
SnapshotStatus.ERROR_VERIFY_COMMAND_FAILED,
"get_json_from_args: command failed for LV lvm_is_snapshot()",
)

if is_snapshot:
continue

rc, is_thinpool = lvm_is_thinpool(vg_str, lv["lv_name"])
if rc != SnapshotStatus.SNAPSHOT_OK:
return (
SnapshotStatus.ERROR_VERIFY_COMMAND_FAILED,
"get_json_from_args: command failed for LV lvm_is_thinpool()",
)
if is_thinpool:
continue
volume = {}
volume["name"] = ("snapshot : " + vg_str + "/" + lv["lv_name"],)
volume["vg"] = vg_str
Expand Down
126 changes: 126 additions & 0 deletions tests/tests_set_thin_basic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
- name: Snapshot a set of logical volumes across different volume groups
hosts: all
vars:
test_disk_min_size: "1g"
test_disk_count: 10
test_storage_pools:
- name: test_vg1
disks: "{{ range(0, 6) | map('extract', unused_disks) | list }}"
volumes:
- name: thin_pool_dev
thin_pool_name: thin_pool
thin_pool_size: '3g'
size: "{{ volume1_size }}"
thin: true
- name: lv2
thin_pool_name: thin_pool
size: "15"
thin: true
- name: lv3
thin_pool_name: thin_pool
size: "15%"
thin: true
- name: lv4
thin_pool_name: thin_pool
size: "15%"
thin: true
- name: test_vg2
disks: "{{ range(6, 10) | map('extract', unused_disks) | list }}"
volumes:
- name: lv5
size: "30%"
- name: lv6
size: "25%"
- name: lv7
size: "10%"
- name: lv8
size: "10%"
snapshot_test_set:
name: snapset1
volumes:
- name: snapshot VG1 LV1
vg: test_vg1
lv: lv2
percent_space_required: 20
- name: snapshot VG2 LV3
vg: test_vg2
lv: lv3
percent_space_required: 15
- name: snapshot VG2 LV4
vg: test_vg2
lv: lv4
percent_space_required: 15
- name: snapshot VG3 LV7
vg: test_vg3
lv: lv7
percent_space_required: 15
tasks:
- name: Run tests
block:
- name: Setup
include_tasks: tasks/setup.yml

- name: Run the snapshot role to create snapshot set of LVs
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: snapshot
snapshot_lvm_set: "{{ snapshot_test_set }}"

- name: Assert changes for create snapset
assert:
that: snapshot_cmd["changed"]

- name: Run the snapshot role to verify the set of snapshots for the LVs
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: check
snapshot_lvm_set: "{{ snapshot_test_set }}"
snapshot_lvm_verify_only: true

- name: Create snapset again for idempotence
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: snapshot
snapshot_lvm_set: "{{ snapshot_test_set }}"

- name: Assert no changes for create snapset
assert:
that: not snapshot_cmd["changed"]

- name: Run the snapshot role remove the set
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: remove
snapshot_lvm_set: "{{ snapshot_test_set }}"

- name: Assert changes for remove snapset
assert:
that: snapshot_cmd["changed"]

- name: Run the snapshot role to verify the set is removed
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: remove
snapshot_lvm_set: "{{ snapshot_test_set }}"
snapshot_lvm_verify_only: true

- name: Remove again to check idempotence
include_role:
name: linux-system-roles.snapshot
vars:
snapshot_lvm_action: remove
snapshot_lvm_set: "{{ snapshot_test_set }}"

- name: Assert no changes for remove snapset
assert:
that: not snapshot_cmd["changed"]
always:
- name: Cleanup
include_tasks: tasks/cleanup.yml
tags: tests::cleanup

0 comments on commit 27e8d00

Please sign in to comment.