mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-25 03:41:25 -07:00
* Fix lvg module idempotency In [1] changes were made to ensure that the physical devices were appropriately filtered, but the dev_list which is used to prepare the filter is modified from the original arguments to resolve any symlinks. This results in the existing devices given in the module args to be left out of the filter, resulting in the module trying to add the same device again every time the task is executed. In this PR we change dev_list to be a copy of the module arguments so that we're able to add the given pv list from the module arguments into the filter as well, ensuring that there is idempotence when running the task again. [1] https://github.com/ansible/ansible/pull/38446 * Add lvg module idempotence test To ensure that the lvg module is tested for idempotency, we add a basic integration test. Support for MacOS and FreeBSD are skipped because the module does not currently support those platforms.
278 lines
9.9 KiB
Python
278 lines
9.9 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: (c) 2013, Alexander Bulimov <lazywolf0@gmail.com>
|
|
# Based on lvol module by Jeroen Hoekx <jeroen.hoekx@dsquare.be>
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
'status': ['preview'],
|
|
'supported_by': 'community'}
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
author:
|
|
- Alexander Bulimov (@abulimov)
|
|
module: lvg
|
|
short_description: Configure LVM volume groups
|
|
description:
|
|
- This module creates, removes or resizes volume groups.
|
|
version_added: "1.1"
|
|
options:
|
|
vg:
|
|
description:
|
|
- The name of the volume group.
|
|
required: true
|
|
pvs:
|
|
description:
|
|
- List of comma-separated devices to use as physical devices in this volume group. Required when creating or resizing volume group.
|
|
- The module will take care of running pvcreate if needed.
|
|
pesize:
|
|
description:
|
|
- The size of the physical extent. pesize must be a power of 2, or
|
|
multiple of 128KiB. Since version 2.6, pesize can be optionally suffixed
|
|
by a UNIT (k/K/m/M/g/G), default unit is megabyte.
|
|
default: 4
|
|
pv_options:
|
|
description:
|
|
- Additional options to pass to C(pvcreate) when creating the volume group.
|
|
version_added: "2.4"
|
|
vg_options:
|
|
description:
|
|
- Additional options to pass to C(vgcreate) when creating the volume group.
|
|
version_added: "1.6"
|
|
state:
|
|
description:
|
|
- Control if the volume group exists.
|
|
choices: [ absent, present ]
|
|
default: present
|
|
force:
|
|
description:
|
|
- If C(yes), allows to remove volume group with logical volumes.
|
|
type: bool
|
|
default: 'no'
|
|
notes:
|
|
- This module does not modify PE size for already present volume group.
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
- name: Create a volume group on top of /dev/sda1 with physical extent size = 32MB
|
|
lvg:
|
|
vg: vg.services
|
|
pvs: /dev/sda1
|
|
pesize: 32
|
|
|
|
- name: Create a volume group on top of /dev/sdb with physical extent size = 128KiB
|
|
lvg:
|
|
vg: vg.services
|
|
pvs: /dev/sdb
|
|
pesize: 128K
|
|
|
|
# If, for example, we already have VG vg.services on top of /dev/sdb1,
|
|
# this VG will be extended by /dev/sdc5. Or if vg.services was created on
|
|
# top of /dev/sda5, we first extend it with /dev/sdb1 and /dev/sdc5,
|
|
# and then reduce by /dev/sda5.
|
|
- name: Create or resize a volume group on top of /dev/sdb1 and /dev/sdc5.
|
|
lvg:
|
|
vg: vg.services
|
|
pvs: /dev/sdb1,/dev/sdc5
|
|
|
|
- name: Remove a volume group with name vg.services
|
|
lvg:
|
|
vg: vg.services
|
|
state: absent
|
|
'''
|
|
|
|
import os
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
|
|
def parse_vgs(data):
|
|
vgs = []
|
|
for line in data.splitlines():
|
|
parts = line.strip().split(';')
|
|
vgs.append({
|
|
'name': parts[0],
|
|
'pv_count': int(parts[1]),
|
|
'lv_count': int(parts[2]),
|
|
})
|
|
return vgs
|
|
|
|
|
|
def find_mapper_device_name(module, dm_device):
|
|
dmsetup_cmd = module.get_bin_path('dmsetup', True)
|
|
mapper_prefix = '/dev/mapper/'
|
|
rc, dm_name, err = module.run_command("%s info -C --noheadings -o name %s" % (dmsetup_cmd, dm_device))
|
|
if rc != 0:
|
|
module.fail_json(msg="Failed executing dmsetup command.", rc=rc, err=err)
|
|
mapper_device = mapper_prefix + dm_name.rstrip()
|
|
return mapper_device
|
|
|
|
|
|
def parse_pvs(module, data):
|
|
pvs = []
|
|
dm_prefix = '/dev/dm-'
|
|
for line in data.splitlines():
|
|
parts = line.strip().split(';')
|
|
if parts[0].startswith(dm_prefix):
|
|
parts[0] = find_mapper_device_name(module, parts[0])
|
|
pvs.append({
|
|
'name': parts[0],
|
|
'vg_name': parts[1],
|
|
})
|
|
return pvs
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
vg=dict(type='str', required=True),
|
|
pvs=dict(type='list'),
|
|
pesize=dict(type='str', default=4),
|
|
pv_options=dict(type='str', default=''),
|
|
vg_options=dict(type='str', default=''),
|
|
state=dict(type='str', default='present', choices=['absent', 'present']),
|
|
force=dict(type='bool', default=False),
|
|
),
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
vg = module.params['vg']
|
|
state = module.params['state']
|
|
force = module.boolean(module.params['force'])
|
|
pesize = module.params['pesize']
|
|
pvoptions = module.params['pv_options'].split()
|
|
vgoptions = module.params['vg_options'].split()
|
|
|
|
dev_list = []
|
|
if module.params['pvs']:
|
|
dev_list = list(module.params['pvs'])
|
|
elif state == 'present':
|
|
module.fail_json(msg="No physical volumes given.")
|
|
|
|
# LVM always uses real paths not symlinks so replace symlinks with actual path
|
|
for idx, dev in enumerate(dev_list):
|
|
dev_list[idx] = os.path.realpath(dev)
|
|
|
|
if state == 'present':
|
|
# check given devices
|
|
for test_dev in dev_list:
|
|
if not os.path.exists(test_dev):
|
|
module.fail_json(msg="Device %s not found." % test_dev)
|
|
|
|
# get pv list
|
|
pvs_cmd = module.get_bin_path('pvs', True)
|
|
if dev_list:
|
|
pvs_filter = ' || '. join(['pv_name = {0}'.format(x) for x in dev_list + module.params['pvs']])
|
|
pvs_filter = "--select '%s'" % pvs_filter
|
|
else:
|
|
pvs_filter = ''
|
|
rc, current_pvs, err = module.run_command("%s --noheadings -o pv_name,vg_name --separator ';' %s" % (pvs_cmd, pvs_filter))
|
|
if rc != 0:
|
|
module.fail_json(msg="Failed executing pvs command.", rc=rc, err=err)
|
|
|
|
# check pv for devices
|
|
pvs = parse_pvs(module, current_pvs)
|
|
used_pvs = [pv for pv in pvs if pv['name'] in dev_list and pv['vg_name'] and pv['vg_name'] != vg]
|
|
if used_pvs:
|
|
module.fail_json(msg="Device %s is already in %s volume group." % (used_pvs[0]['name'], used_pvs[0]['vg_name']))
|
|
|
|
vgs_cmd = module.get_bin_path('vgs', True)
|
|
rc, current_vgs, err = module.run_command("%s --noheadings -o vg_name,pv_count,lv_count --separator ';'" % vgs_cmd)
|
|
|
|
if rc != 0:
|
|
module.fail_json(msg="Failed executing vgs command.", rc=rc, err=err)
|
|
|
|
changed = False
|
|
|
|
vgs = parse_vgs(current_vgs)
|
|
|
|
for test_vg in vgs:
|
|
if test_vg['name'] == vg:
|
|
this_vg = test_vg
|
|
break
|
|
else:
|
|
this_vg = None
|
|
|
|
if this_vg is None:
|
|
if state == 'present':
|
|
# create VG
|
|
if module.check_mode:
|
|
changed = True
|
|
else:
|
|
# create PV
|
|
pvcreate_cmd = module.get_bin_path('pvcreate', True)
|
|
for current_dev in dev_list:
|
|
rc, _, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)])
|
|
if rc == 0:
|
|
changed = True
|
|
else:
|
|
module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err)
|
|
vgcreate_cmd = module.get_bin_path('vgcreate')
|
|
rc, _, err = module.run_command([vgcreate_cmd] + vgoptions + ['-s', pesize, vg] + dev_list)
|
|
if rc == 0:
|
|
changed = True
|
|
else:
|
|
module.fail_json(msg="Creating volume group '%s' failed" % vg, rc=rc, err=err)
|
|
else:
|
|
if state == 'absent':
|
|
if module.check_mode:
|
|
module.exit_json(changed=True)
|
|
else:
|
|
if this_vg['lv_count'] == 0 or force:
|
|
# remove VG
|
|
vgremove_cmd = module.get_bin_path('vgremove', True)
|
|
rc, _, err = module.run_command("%s --force %s" % (vgremove_cmd, vg))
|
|
if rc == 0:
|
|
module.exit_json(changed=True)
|
|
else:
|
|
module.fail_json(msg="Failed to remove volume group %s" % (vg), rc=rc, err=err)
|
|
else:
|
|
module.fail_json(msg="Refuse to remove non-empty volume group %s without force=yes" % (vg))
|
|
|
|
# resize VG
|
|
current_devs = [os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg]
|
|
devs_to_remove = list(set(current_devs) - set(dev_list))
|
|
devs_to_add = list(set(dev_list) - set(current_devs))
|
|
|
|
if devs_to_add or devs_to_remove:
|
|
if module.check_mode:
|
|
changed = True
|
|
else:
|
|
if devs_to_add:
|
|
devs_to_add_string = ' '.join(devs_to_add)
|
|
# create PV
|
|
pvcreate_cmd = module.get_bin_path('pvcreate', True)
|
|
for current_dev in devs_to_add:
|
|
rc, _, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)])
|
|
if rc == 0:
|
|
changed = True
|
|
else:
|
|
module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err)
|
|
# add PV to our VG
|
|
vgextend_cmd = module.get_bin_path('vgextend', True)
|
|
rc, _, err = module.run_command("%s %s %s" % (vgextend_cmd, vg, devs_to_add_string))
|
|
if rc == 0:
|
|
changed = True
|
|
else:
|
|
module.fail_json(msg="Unable to extend %s by %s." % (vg, devs_to_add_string), rc=rc, err=err)
|
|
|
|
# remove some PV from our VG
|
|
if devs_to_remove:
|
|
devs_to_remove_string = ' '.join(devs_to_remove)
|
|
vgreduce_cmd = module.get_bin_path('vgreduce', True)
|
|
rc, _, err = module.run_command("%s --force %s %s" % (vgreduce_cmd, vg, devs_to_remove_string))
|
|
if rc == 0:
|
|
changed = True
|
|
else:
|
|
module.fail_json(msg="Unable to reduce %s by %s." % (vg, devs_to_remove_string), rc=rc, err=err)
|
|
|
|
module.exit_json(changed=changed)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|