mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-08-04 05:04:22 -07:00
Added lvm_pv_move_data module
This commit is contained in:
parent
77cd018427
commit
bd0c613177
8 changed files with 368 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -903,6 +903,8 @@ files:
|
|||
maintainers: abulimov
|
||||
$modules/lvm_pv.py:
|
||||
maintainers: klention
|
||||
$modules/lvm_pv_move_data.py:
|
||||
maintainers: klention
|
||||
$modules/lvg_rename.py:
|
||||
maintainers: lszomor
|
||||
$modules/lvol.py:
|
||||
|
|
167
plugins/modules/lvm_pv_move_data.py
Normal file
167
plugins/modules/lvm_pv_move_data.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025, Klention Mali <klention@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: lvm_pv_move_data
|
||||
short_description: Move data between LVM Physical Volumes (PVs)
|
||||
version_added: "11.2.0"
|
||||
description:
|
||||
- Moves data from one LVM Physical Volume (PV) to another.
|
||||
author:
|
||||
- Klention Mali (@klention)
|
||||
options:
|
||||
source:
|
||||
description:
|
||||
- Path to the source block device to move data from.
|
||||
- Must be an existing PV.
|
||||
type: path
|
||||
required: true
|
||||
destination:
|
||||
description:
|
||||
- Path to the destination block device to move data to.
|
||||
- Must be an existing PV with enough free space.
|
||||
type: path
|
||||
required: true
|
||||
notes:
|
||||
- Requires LVM2 utilities installed on the target system.
|
||||
- Both source and destination devices must exist.
|
||||
- Both source and destination PVs must be in the same volume group.
|
||||
- The destination PV must have enough free space to accommodate the source PV's allocated extents.
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Moving data from /dev/sdb to /dev/sdc
|
||||
community.general.lvm_pv_move_data:
|
||||
source: /dev/sdb
|
||||
destination: /dev/sdc
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def get_pv_status(module, device):
|
||||
"""Check if the device is already a PV."""
|
||||
cmd = ['pvs', '--noheadings', '--readonly', device]
|
||||
return module.run_command(cmd)[0] == 0
|
||||
|
||||
|
||||
def get_pv_vg(module, device):
|
||||
"""Get the VG name of a PV."""
|
||||
cmd = ['pvs', '--noheadings', '-o', 'vg_name', device]
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed to get VG for device %s: %s" % (device, err))
|
||||
vg = out.strip()
|
||||
return None if vg == '' else vg
|
||||
|
||||
|
||||
def get_pv_allocated_pe(module, device):
|
||||
"""Get count of allocated physical extents in a PV."""
|
||||
cmd = ['pvs', '--noheadings', '-o', 'pv_pe_alloc_count', device]
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed to get allocated PE count for device %s: %s" % (device, err))
|
||||
try:
|
||||
return int(out.strip())
|
||||
except ValueError:
|
||||
module.fail_json(msg="Invalid allocated PE count for device %s: %s" % (device, out))
|
||||
|
||||
|
||||
def get_pv_total_pe(module, device):
|
||||
"""Get total number of physical extents in a PV."""
|
||||
cmd = ['pvs', '--noheadings', '-o', 'pv_pe_count', device]
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed to get total PE count for device %s: %s" % (device, err))
|
||||
try:
|
||||
return int(out.strip())
|
||||
except ValueError:
|
||||
module.fail_json(msg="Invalid total PE count for device %s: %s" % (device, out))
|
||||
|
||||
|
||||
def get_pv_free_pe(module, device):
|
||||
"""Calculate free physical extents in a PV."""
|
||||
total_pe = get_pv_total_pe(module, device)
|
||||
allocated_pe = get_pv_allocated_pe(module, device)
|
||||
return total_pe - allocated_pe
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
source=dict(type='path', required=True),
|
||||
destination=dict(type='path', required=True),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
source = module.params['source']
|
||||
destination = module.params['destination']
|
||||
changed = False
|
||||
actions = []
|
||||
|
||||
# Validate device existence
|
||||
if not os.path.exists(source):
|
||||
module.fail_json(msg="Source device %s not found" % source)
|
||||
if not os.path.exists(destination):
|
||||
module.fail_json(msg="Destination device %s not found" % destination)
|
||||
if source == destination:
|
||||
module.fail_json(msg="Source and destination devices must be different")
|
||||
|
||||
# Check both are PVs
|
||||
if not get_pv_status(module, source):
|
||||
module.fail_json(msg="Source device %s is not a PV" % source)
|
||||
if not get_pv_status(module, destination):
|
||||
module.fail_json(msg="Destination device %s is not a PV" % destination)
|
||||
|
||||
# Check both are in the same VG
|
||||
vg_src = get_pv_vg(module, source)
|
||||
vg_dest = get_pv_vg(module, destination)
|
||||
if vg_src != vg_dest:
|
||||
module.fail_json(
|
||||
msg="Source and destination must be in the same VG. Source VG: '%s', Destination VG: '%s'." % (vg_src, vg_dest)
|
||||
)
|
||||
|
||||
# Check source has data to move
|
||||
allocated = get_pv_allocated_pe(module, source)
|
||||
if allocated == 0:
|
||||
actions.append('no allocated extents to move')
|
||||
else:
|
||||
# Check destination has enough free space
|
||||
free_pe_dest = get_pv_free_pe(module, destination)
|
||||
if free_pe_dest < allocated:
|
||||
module.fail_json(
|
||||
msg="Destination device %s has only %d free physical extents, but source device %s has %d allocated extents. Not enough space." %
|
||||
(destination, free_pe_dest, source, allocated)
|
||||
)
|
||||
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
actions.append('would move data from %s to %s' % (source, destination))
|
||||
else:
|
||||
cmd = ['pvmove', source, destination]
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
changed = True
|
||||
actions.append('moved data from %s to %s' % (source, destination))
|
||||
|
||||
if actions:
|
||||
msg = "PV data move: %s" % ', '.join(actions)
|
||||
else:
|
||||
msg = "No data to move from %s" % source
|
||||
module.exit_json(changed=changed, msg=msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
13
tests/integration/targets/lvm_pv_move_data/aliases
Normal file
13
tests/integration/targets/lvm_pv_move_data/aliases
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (c) Contributors to the Ansible project
|
||||
# Based on the integraton test for the lvm_pv module
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
azp/posix/1
|
||||
azp/posix/vm
|
||||
destructive
|
||||
needs/privileged
|
||||
skip/aix
|
||||
skip/freebsd
|
||||
skip/osx
|
||||
skip/macos
|
9
tests/integration/targets/lvm_pv_move_data/meta/main.yml
Normal file
9
tests/integration/targets/lvm_pv_move_data/meta/main.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# Based on the integraton test for the lvg module
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
dependencies:
|
||||
- setup_pkg_mgr
|
||||
- setup_remote_tmp_dir
|
31
tests/integration/targets/lvm_pv_move_data/tasks/cleanup.yml
Normal file
31
tests/integration/targets/lvm_pv_move_data/tasks/cleanup.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Unmounting LV lv_tmp_test from {{ remote_tmp_dir }}/tmp_mount
|
||||
ansible.posix.mount:
|
||||
fstab: '{{ remote_tmp_dir }}/fstab'
|
||||
path: '{{ remote_tmp_dir }}/tmp_mount'
|
||||
state: absent
|
||||
|
||||
- name: Removing xfs filesystem from LV lv_tmp_test
|
||||
community.general.filesystem:
|
||||
dev: /dev/vg_tmp_test/lv_tmp_test
|
||||
status: absent
|
||||
|
||||
- name: Detaching first loop device
|
||||
ansible.builtin.command: losetup -d {{ loop_device_01.stdout }}
|
||||
|
||||
- name: Detaching second loop device
|
||||
ansible.builtin.command: losetup -d {{ loop_device_02.stdout }}
|
||||
|
||||
- name: Removing first loop device file
|
||||
ansible.builtin.file:
|
||||
path: "{{ remote_tmp_dir }}/test_lvm_pv_01.img"
|
||||
state: absent
|
||||
|
||||
- name: Removing second loop device file
|
||||
ansible.builtin.file:
|
||||
path: "{{ remote_tmp_dir }}/test_lvm_pv_02.img"
|
||||
state: absent
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Creating a 3000MB file for the first loop device
|
||||
ansible.builtin.command: dd if=/dev/zero of={{ remote_tmp_dir }}/test_lvm_pv_01.img bs=1M count=3000
|
||||
args:
|
||||
creates: "{{ remote_tmp_dir }}/test_lvm_pv_01.img"
|
||||
|
||||
- name: Creating a 4000MB file for the second loop device
|
||||
ansible.builtin.command: dd if=/dev/zero of={{ remote_tmp_dir }}/test_lvm_pv_02.img bs=1M count=4000
|
||||
args:
|
||||
creates: "{{ remote_tmp_dir }}/test_lvm_pv_02.img"
|
||||
|
||||
- name: Creating loop device
|
||||
ansible.builtin.command: losetup -f
|
||||
register: loop_device_01
|
||||
|
||||
- name: Associating loop device with file
|
||||
ansible.builtin.command: 'losetup {{ loop_device_01.stdout }} {{ remote_tmp_dir }}/test_lvm_pv_01.img'
|
||||
|
||||
- name: Creating loop device
|
||||
ansible.builtin.command: losetup -f
|
||||
register: loop_device_02
|
||||
|
||||
- name: Associating loop device with file
|
||||
ansible.builtin.command: 'losetup {{ loop_device_02.stdout }} {{ remote_tmp_dir }}/test_lvm_pv_02.img'
|
||||
|
||||
- name: Creating physical volume for the first loop device
|
||||
community.general.lvm_pv:
|
||||
device: "{{ loop_device_01.stdout }}"
|
||||
register: pv_creation_result_01
|
||||
|
||||
- name: Checking the first physical volume size
|
||||
ansible.builtin.command: pvs --noheadings -o pv_size --units M {{ loop_device_01.stdout }}
|
||||
register: pv_size_output_01
|
||||
|
||||
- name: Creating physical volume for the second loop device
|
||||
community.general.lvm_pv:
|
||||
device: "{{ loop_device_02.stdout }}"
|
||||
register: pv_creation_result_02
|
||||
|
||||
- name: Checking the second physical volume size
|
||||
ansible.builtin.command: pvs --noheadings -o pv_size --units M {{ loop_device_02.stdout }}
|
||||
register: pv_size_output_02
|
||||
|
||||
- name: Creating volume group on top of first device {{ loop_device_01.stdout }}
|
||||
community.general.lvg:
|
||||
vg: vg_tmp_test
|
||||
pvs: "{{ loop_device_01.stdout }}"
|
||||
register: vg_creation_result
|
||||
|
||||
- name: Creating LV lv_tmp_test on VG vg_tmp_test
|
||||
community.general.lvol:
|
||||
vg: vg_tmp_test
|
||||
lv: lv_tmp_test
|
||||
size: 100%FREE
|
||||
force: true
|
||||
register: lv_creation_result
|
||||
|
||||
- name: Creating xfs filesystem on LV lv_tmp_test
|
||||
community.general.filesystem:
|
||||
dev: /dev/vg_tmp_test/lv_tmp_test
|
||||
fstype: xfs
|
||||
register: fs_creation_result
|
||||
|
||||
#- name: Mounting LV lv_tmp_test to {{ remote_tmp_dir }}/tmp_mount
|
||||
# ansible.builtin.shell: mount -o rw,noauto /dev/vg_tmp_test/lv_tmp_test {{ remote_tmp_dir }}/tmp_mount
|
||||
|
||||
- name: Mounting LV lv_tmp_test to {{ remote_tmp_dir }}/tmp_mount
|
||||
ansible.posix.mount:
|
||||
fstab: '{{ remote_tmp_dir }}/fstab'
|
||||
path: '{{ remote_tmp_dir }}/tmp_mount'
|
||||
src: '/dev/vg_tmp_test/lv_tmp_test'
|
||||
fstype: xfs
|
||||
opts: rw,noauto
|
||||
state: mounted
|
||||
|
||||
- name: Asserting physical volume was created
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pv_creation_result_01.changed == true
|
||||
- pv_creation_result_02.changed == true
|
||||
- vg_creation_result.changed == true
|
||||
- (pv_size_output_01.stdout | trim | regex_replace('M', '') | float) > 95
|
||||
- (pv_size_output_01.stdout | trim | regex_replace('M', '') | float) < 120
|
||||
- (pv_size_output_02.stdout | trim | regex_replace('M', '') | float) > 190
|
||||
- (pv_size_output_02.stdout | trim | regex_replace('M', '') | float) < 220
|
||||
- "'created' in pv_creation_result_01.msg"
|
||||
- "'created' in pv_creation_result_02.msg"
|
25
tests/integration/targets/lvm_pv_move_data/tasks/main.yml
Normal file
25
tests/integration/targets/lvm_pv_move_data/tasks/main.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) Contributors to the Ansible project
|
||||
# Based on the integraton test for the lvm_pv module
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Install required packages (Linux)
|
||||
when: ansible_system == 'Linux'
|
||||
ansible.builtin.package:
|
||||
name: lvm2
|
||||
state: present
|
||||
|
||||
- name: Testing lvm_pv_move_data module
|
||||
block:
|
||||
- import_tasks: creation.yml
|
||||
|
||||
- import_tasks: moving.yml
|
||||
|
||||
always:
|
||||
- import_tasks: cleanup.yml
|
30
tests/integration/targets/lvm_pv_move_data/tasks/moving.yml
Normal file
30
tests/integration/targets/lvm_pv_move_data/tasks/moving.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Creating a 50MB file on the mounted LV
|
||||
ansible.builtin.command: dd if=/dev/zero of={{ remote_tmp_dir }}/tmp_mount/test_file bs=1M count=50
|
||||
args:
|
||||
creates: "{{ remote_tmp_dir }}/tmp_mount/test_file"
|
||||
|
||||
- name: Extending VG vg_tmp_test with second device {{ loop_device_02.stdout }}
|
||||
community.general.lvg:
|
||||
vg: vg_tmp_test
|
||||
pvs: "{{ loop_device_02.stdout }}"
|
||||
|
||||
- name: Moving data from {{ loop_device_01.stdout }} to {{ loop_device_02.stdout }} (both in same VG)
|
||||
community.general.lvm_pv_move_data:
|
||||
source: "{{ loop_device_01.stdout }}"
|
||||
destination: "{{ loop_device_02.stdout }}"
|
||||
register: move_result
|
||||
|
||||
- name: Debugging move result
|
||||
ansible.builtin.debug:
|
||||
var: move_result
|
||||
|
||||
- name: Asserting data was moved successfully
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- move_result.changed == true
|
||||
- "'moved data from' in creation_result.msg"
|
Loading…
Add table
Add a link
Reference in a new issue