#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2025, Klention Mali # 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 auto_answer: description: - Answer yes to all prompts automatically. type: bool default: false atomic: description: - Makes the C(pvmove) operation atomic, ensuring that all affected LVs are moved to the destination PV, or none are if the operation is aborted. type: bool default: true autobackup: description: - Automatically backup metadata before changes (strongly advised!). type: bool default: true requirements: - LVM2 utilities - Both O(source) and O(destination) devices must exist, and the PVs must be in the same volume group. - The O(destination) PV must have enough free space to accommodate the O(source) PV's allocated extents. - Verbosity is automatically controlled by Ansible's verbosity level (using multiple C(-v) flags). """ 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""" actions: description: List of actions performed during module execution. returned: success type: list elements: str sample: [ "moved data from /dev/sdb to /dev/sdc", "no allocated extents to move", "would move data from /dev/sdb to /dev/sdc" ] """ import os from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt def main(): module = AnsibleModule( argument_spec=dict( source=dict(type='path', required=True), destination=dict(type='path', required=True), auto_answer=dict(type='bool', default=False), atomic=dict(type='bool', default=True), autobackup=dict(type='bool', default=True), ), supports_check_mode=True, ) pvs_runner = CmdRunner( module, command="pvs", arg_formats=dict( noheadings=cmd_runner_fmt.as_fixed("--noheadings"), readonly=cmd_runner_fmt.as_fixed("--readonly"), vg_name=cmd_runner_fmt.as_fixed("-o", "vg_name"), pv_pe_alloc_count=cmd_runner_fmt.as_fixed("-o", "pv_pe_alloc_count"), pv_pe_count=cmd_runner_fmt.as_fixed("-o", "pv_pe_count"), device=cmd_runner_fmt.as_list(), ) ) source = module.params['source'] destination = module.params['destination'] changed = False actions = [] result = {'changed': False} # 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") def run_pvs_command(arguments, device): with pvs_runner(arguments) as ctx: rc, out, err = ctx.run(device=device) if rc != 0: module.fail_json( msg="Command failed: %s" % err, stdout=out, stderr=err, rc=rc, cmd=ctx.cmd, arguments=arguments, device=device, ) return out.strip() def is_pv(device): with pvs_runner("noheadings readonly device", check_rc=False) as ctx: rc, out, err = ctx.run(device=device) return rc == 0 if not is_pv(source): module.fail_json(msg="Source device %s is not a PV" % source) if not is_pv(destination): module.fail_json(msg="Destination device %s is not a PV" % destination) vg_src = run_pvs_command("noheadings vg_name device", source) vg_dest = run_pvs_command("noheadings vg_name device", 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) ) def get_allocated_pe(device): try: return int(run_pvs_command("noheadings pv_pe_alloc_count device", device)) except ValueError: module.fail_json(msg="Invalid allocated PE count for device %s" % device) allocated = get_allocated_pe(source) if allocated == 0: actions.append('no allocated extents to move') else: # Check destination has enough free space def get_total_pe(device): try: return int(run_pvs_command("noheadings pv_pe_count device", device)) except ValueError: module.fail_json(msg="Invalid total PE count for device %s" % device) def get_free_pe(device): return get_total_pe(device) - get_allocated_pe(device) free_pe_dest = get_free_pe(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: pvmove_runner = CmdRunner( module, command="pvmove", arg_formats=dict( auto_answer=cmd_runner_fmt.as_bool("-y"), atomic=cmd_runner_fmt.as_bool("--atomic"), autobackup=cmd_runner_fmt.as_fixed("--autobackup", "y" if module.params['autobackup'] else "n"), verbosity=cmd_runner_fmt.as_func(lambda v: ['-' + 'v' * v] if v > 0 else []), source=cmd_runner_fmt.as_list(), destination=cmd_runner_fmt.as_list(), ) ) verbosity = module._verbosity with pvmove_runner("auto_answer atomic autobackup verbosity source destination") as ctx: rc, out, err = ctx.run( verbosity=verbosity, source=source, destination=destination ) result['stdout'] = out result['stderr'] = err changed = True actions.append('moved data from %s to %s' % (source, destination)) result['changed'] = changed result['actions'] = actions if actions: result['msg'] = "PV data move: %s" % ", ".join(actions) else: result['msg'] = "No data to move from %s" % source module.exit_json(**result) if __name__ == '__main__': main()