community.general/lib/ansible/modules/system/lvg.py
Julien Girardin 3e303bea4c List correctly current PV in "lvg" module: fix lvg reduce (#49731)
* Refactor integration test for lvg module to introduce grow/reduce test

* List correctly current PV in lvg module: fix lvg reduce

Previous behaviour was to only take into account PV passed in 'pvs'
argument. This lead to reduce not working as expecting:

* with state=present and list of wanted pvs, lvg found only the pvs to
  add or already present and ignored the pv to remove (obviously absent
  from the list of given PV)

* with state=absent and a pv to remove, lvg found that the remaining pvs
  list is empty (ignoring possible other PV in the vg) and decides to
  remove the vg entirely (as supposely no PV are left anymore to store
  lvm metadata)

* Add changelog fragment
2018-12-20 15:43:18 +00:00

283 lines
10 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 itertools
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_pv_name = ' || '.join(
'pv_name = {0}'.format(x)
for x in itertools.chain(dev_list, module.params['pvs'])
)
pvs_filter_vg_name = 'vg_name = {0}'.format(vg)
pvs_filter = "--select '{0} || {1}' ".format(pvs_filter_pv_name, pvs_filter_vg_name)
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()