mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-28 03:00:23 -07:00
lvm_pv: new module for LVM Physical Volumes (#10070)
* Added LVM Physical Volume module * Fixed CI checks * python 2.7 compatibility * Fixed another fprint line not compatible with python 2.x * Applied cosmetic changes * Removed msg from RETURN section * Updated the 'absent state' block logic * Added integration tests * Updated logic for creating loop devices on Alpine Linux * Updated loop device path * Minor, cosmetic changes * Adjust indentation. --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
6bbd1dd7f5
commit
367b28d765
9 changed files with 331 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -902,6 +902,8 @@ files:
|
|||
maintainers: nerzhul
|
||||
$modules/lvg.py:
|
||||
maintainers: abulimov
|
||||
$modules/lvm_pv.py:
|
||||
maintainers: klention
|
||||
$modules/lvg_rename.py:
|
||||
maintainers: lszomor
|
||||
$modules/lvol.py:
|
||||
|
|
192
plugins/modules/lvm_pv.py
Normal file
192
plugins/modules/lvm_pv.py
Normal file
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025, Klention Mali <klention@gmail.com>
|
||||
# Based on lvol module by Jeroen Hoekx <jeroen.hoekx@dsquare.be>
|
||||
# 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
|
||||
short_description: Manage LVM Physical Volumes
|
||||
version_added: "11.0.0"
|
||||
description:
|
||||
- Creates, resizes or removes LVM Physical Volumes.
|
||||
author:
|
||||
- Klention Mali (@klention)
|
||||
options:
|
||||
device:
|
||||
description:
|
||||
- Path to the block device to manage.
|
||||
type: path
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Control if the physical volume exists.
|
||||
type: str
|
||||
choices: [ present, absent ]
|
||||
default: present
|
||||
force:
|
||||
description:
|
||||
- Force the operation.
|
||||
- When O(state=present) (creating a PV), this uses C(pvcreate -f) to force creation.
|
||||
- When O(state=absent) (removing a PV), this uses C(pvremove -ff) to force removal even if part of a volume group.
|
||||
type: bool
|
||||
default: false
|
||||
resize:
|
||||
description:
|
||||
- Resize PV to device size when O(state=present).
|
||||
type: bool
|
||||
default: false
|
||||
notes:
|
||||
- Requires LVM2 utilities installed on the target system.
|
||||
- Device path must exist when creating a PV.
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Creating physical volume on /dev/sdb
|
||||
community.general.lvm_pv:
|
||||
device: /dev/sdb
|
||||
|
||||
- name: Creating and resizing (if needed) physical volume
|
||||
community.general.lvm_pv:
|
||||
device: /dev/sdb
|
||||
resize: true
|
||||
|
||||
- name: Removing physical volume that is not part of any volume group
|
||||
community.general.lvm_pv:
|
||||
device: /dev/sdb
|
||||
state: absent
|
||||
|
||||
- name: Force removing physical volume that is already part of a volume group
|
||||
community.general.lvm_pv:
|
||||
device: /dev/sdb
|
||||
force: true
|
||||
state: absent
|
||||
'''
|
||||
|
||||
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_size(module, device):
|
||||
"""Get current PV size in bytes."""
|
||||
cmd = ['pvs', '--noheadings', '--nosuffix', '--units', 'b', '-o', 'pv_size', device]
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
return int(out.strip())
|
||||
|
||||
|
||||
def rescan_device(module, device):
|
||||
"""Perform storage rescan for the device."""
|
||||
# Extract the base device name (e.g., /dev/sdb -> sdb)
|
||||
base_device = os.path.basename(device)
|
||||
rescan_path = "/sys/block/{0}/device/rescan".format(base_device)
|
||||
|
||||
if os.path.exists(rescan_path):
|
||||
try:
|
||||
with open(rescan_path, 'w') as f:
|
||||
f.write('1')
|
||||
return True
|
||||
except IOError as e:
|
||||
module.warn("Failed to rescan device {0}: {1}".format(device, str(e)))
|
||||
return False
|
||||
else:
|
||||
module.warn("Rescan path not found for device {0}".format(device))
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
device=dict(type='path', required=True),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
force=dict(type='bool', default=False),
|
||||
resize=dict(type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
device = module.params['device']
|
||||
state = module.params['state']
|
||||
force = module.params['force']
|
||||
resize = module.params['resize']
|
||||
changed = False
|
||||
actions = []
|
||||
|
||||
# Validate device existence for present state
|
||||
if state == 'present' and not os.path.exists(device):
|
||||
module.fail_json(msg="Device %s not found" % device)
|
||||
|
||||
is_pv = get_pv_status(module, device)
|
||||
|
||||
if state == 'present':
|
||||
# Create PV if needed
|
||||
if not is_pv:
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
actions.append('would be created')
|
||||
else:
|
||||
cmd = ['pvcreate']
|
||||
if force:
|
||||
cmd.append('-f')
|
||||
cmd.append(device)
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
changed = True
|
||||
actions.append('created')
|
||||
is_pv = True
|
||||
|
||||
# Handle resizing
|
||||
elif resize and is_pv:
|
||||
if module.check_mode:
|
||||
# In check mode, assume resize would change
|
||||
changed = True
|
||||
actions.append('would be resized')
|
||||
else:
|
||||
# Perform device rescan if each time
|
||||
if rescan_device(module, device):
|
||||
actions.append('rescanned')
|
||||
original_size = get_pv_size(module, device)
|
||||
rc, out, err = module.run_command(['pvresize', device], check_rc=True)
|
||||
new_size = get_pv_size(module, device)
|
||||
if new_size != original_size:
|
||||
changed = True
|
||||
actions.append('resized')
|
||||
|
||||
elif state == 'absent':
|
||||
if is_pv:
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
actions.append('would be removed')
|
||||
else:
|
||||
cmd = ['pvremove', '-y']
|
||||
if force:
|
||||
cmd.append('-ff')
|
||||
changed = True
|
||||
cmd.append(device)
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
actions.append('removed')
|
||||
|
||||
# Generate final message
|
||||
if actions:
|
||||
msg = "PV %s: %s" % (device, ', '.join(actions))
|
||||
else:
|
||||
msg = "No changes needed for PV %s" % device
|
||||
module.exit_json(changed=changed, msg=msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
13
tests/integration/targets/lvm_pv/aliases
Normal file
13
tests/integration/targets/lvm_pv/aliases
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (c) Contributors to the 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
|
||||
|
||||
azp/posix/1
|
||||
azp/posix/vm
|
||||
destructive
|
||||
needs/privileged
|
||||
skip/aix
|
||||
skip/freebsd
|
||||
skip/osx
|
||||
skip/macos
|
9
tests/integration/targets/lvm_pv/meta/main.yml
Normal file
9
tests/integration/targets/lvm_pv/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
|
12
tests/integration/targets/lvm_pv/tasks/cleanup.yml
Normal file
12
tests/integration/targets/lvm_pv/tasks/cleanup.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# 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: Detaching loop device
|
||||
ansible.builtin.command: losetup -d {{ loop_device.stdout }}
|
||||
|
||||
- name: Removing loop device file
|
||||
ansible.builtin.file:
|
||||
path: "{{ remote_tmp_dir }}/test_lvm_pv.img"
|
||||
state: absent
|
33
tests/integration/targets/lvm_pv/tasks/creation.yml
Normal file
33
tests/integration/targets/lvm_pv/tasks/creation.yml
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
# 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 for loop device
|
||||
ansible.builtin.command: dd if=/dev/zero of={{ remote_tmp_dir }}/test_lvm_pv.img bs=1M count=50
|
||||
args:
|
||||
creates: "{{ remote_tmp_dir }}/test_lvm_pv.img"
|
||||
|
||||
- name: Creating loop device
|
||||
ansible.builtin.command: losetup -f
|
||||
register: loop_device
|
||||
|
||||
- name: Associating loop device with file
|
||||
ansible.builtin.command: 'losetup {{ loop_device.stdout }} {{ remote_tmp_dir }}/test_lvm_pv.img'
|
||||
|
||||
- name: Creating physical volume
|
||||
community.general.lvm_pv:
|
||||
device: "{{ loop_device.stdout }}"
|
||||
register: result
|
||||
|
||||
- name: Checking physical volume size
|
||||
ansible.builtin.command: pvs --noheadings -o pv_size --units M {{ loop_device.stdout }}
|
||||
register: pv_size_output
|
||||
|
||||
- name: Asserting physical volume was created
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- result.changed == true
|
||||
- (pv_size_output.stdout | trim | regex_replace('M', '') | float) > 45
|
||||
- (pv_size_output.stdout | trim | regex_replace('M', '') | float) < 55
|
||||
- "'created' in result.msg"
|
27
tests/integration/targets/lvm_pv/tasks/main.yml
Normal file
27
tests/integration/targets/lvm_pv/tasks/main.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
####################################################################
|
||||
# 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 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
|
||||
|
||||
- name: Install required packages (Linux)
|
||||
when: ansible_system == 'Linux'
|
||||
ansible.builtin.package:
|
||||
name: lvm2
|
||||
state: present
|
||||
|
||||
- name: Testing lvg_pv module
|
||||
block:
|
||||
- import_tasks: creation.yml
|
||||
|
||||
- import_tasks: resizing.yml
|
||||
|
||||
- import_tasks: removal.yml
|
||||
|
||||
always:
|
||||
- import_tasks: cleanup.yml
|
16
tests/integration/targets/lvm_pv/tasks/removal.yml
Normal file
16
tests/integration/targets/lvm_pv/tasks/removal.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
# 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: Removing physical volume
|
||||
community.general.lvm_pv:
|
||||
device: "{{ loop_device.stdout }}"
|
||||
state: absent
|
||||
register: remove_result
|
||||
|
||||
- name: Asserting physical volume was removed
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- remove_result.changed == true
|
||||
- "'removed' in remove_result.msg"
|
27
tests/integration/targets/lvm_pv/tasks/resizing.yml
Normal file
27
tests/integration/targets/lvm_pv/tasks/resizing.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
# 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: Growing the loop device file to 100MB
|
||||
ansible.builtin.shell: truncate -s 100M {{ remote_tmp_dir }}/test_lvm_pv.img
|
||||
|
||||
- name: Refreshing the loop device
|
||||
ansible.builtin.shell: losetup -c {{ loop_device.stdout }}
|
||||
|
||||
- name: Resizing the physical volume
|
||||
community.general.lvm_pv:
|
||||
device: "{{ loop_device.stdout }}"
|
||||
resize: true
|
||||
register: resize_result
|
||||
|
||||
- name: Checking physical volume size
|
||||
ansible.builtin.command: pvs --noheadings -o pv_size --units M {{ loop_device.stdout }}
|
||||
register: pv_size_output
|
||||
|
||||
- name: Asserting physical volume was resized
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- resize_result.changed == true
|
||||
- (pv_size_output.stdout | trim | regex_replace('M', '') | float) > 95
|
||||
- "'resized' in resize_result.msg"
|
Loading…
Add table
Add a link
Reference in a new issue