Linux mount/fs (lsblk) facts fixes and tests. (#17036)

Fixes #10779

Refactor some of the block device, mount point, and
mtab/fstab facts collection for linux for better
performance on systems with lots of block devices.

Instead of invoking 'lsblk' for every entry in mtab,
invoke it once, then map the results to mtab entries.

Change the args used for invoking 'findmnt' since the
previous combination of args conflicts, so this would
always fail on some systems depending on version.

Add test cases for facts Hardware()/Network()/Virtual() classes
__new__ method and verify they create the proper subclass based
on the platform.system() results.

Split out all the 'invoke some command and grab it's output'
bits related to linux mount paths into their own methods so
it is easier to mock them in unit tests.

Fix the DragonFly* classes that did not defined a 'platform'
class attribute. This caused FreeBSD systems to potentially
get the DragonFly* subclasses incorrectly. In practice it
didnt matter much since the DragonFly* subclasses duplicated
the FreeBSD ones. Actual DragonFly systems would end up with
the generic Hardware() etc instead of the DragonFly* classes.

Fix Hardware.__new__() on PY3, passing args to __new__
would cause "object() takes no parameters" errors. So
check for PY3 and just call __new__ without the args

See
https://hg.python.org/cpython/file/44ed0cd3dc6d/Objects/typeobject.c#l2818
for some explaination.
This commit is contained in:
Adrian Likins
2016-08-17 20:58:51 -04:00
committed by Toshio Kuratomi
parent 9fedcdfc47
commit 7bd57acda4
2 changed files with 683 additions and 50 deletions

View File

@@ -38,7 +38,9 @@ try:
except ImportError:
# python3
import configparser
from ansible.module_utils.basic import get_all_subclasses
from ansible.module_utils.six import PY3
# py2 vs py3; replace with six via ansiballz
try:
@@ -971,7 +973,10 @@ class Hardware(Facts):
for sc in get_all_subclasses(Hardware):
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
if PY3:
return super(cls, subclass).__new__(subclass)
else:
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def populate(self):
return self.facts
@@ -997,6 +1002,12 @@ class LinuxHardware(Hardware):
# Now we have all of these in a dict structure
MEMORY_FACTS = ORIGINAL_MEMORY_FACTS.union(('Buffers', 'Cached', 'SwapCached'))
# regex used against findmnt output to detect bind mounts
BIND_MOUNT_RE = re.compile(r'.*\]')
# regex used against mtab content to find entries that are bind mounts
MTAB_BIND_MOUNT_RE = re.compile(r'.*bind.*"')
def populate(self):
self.get_cpu_facts()
self.get_memory_facts()
@@ -1208,58 +1219,118 @@ class LinuxHardware(Hardware):
else:
self.facts[k] = 'NA'
@timeout(10)
def get_mount_facts(self):
uuids = dict()
self.facts['mounts'] = []
bind_mounts = []
findmntPath = self.module.get_bin_path("findmnt")
if findmntPath:
rc, out, err = self.module.run_command("%s -lnur" % ( findmntPath ), use_unsafe_shell=True)
if rc == 0:
# find bind mounts, in case /etc/mtab is a symlink to /proc/mounts
for line in out.split('\n'):
fields = line.rstrip('\n').split()
if(len(fields) < 2):
continue
if(re.match(".*\]",fields[1])):
bind_mounts.append(fields[0])
def _run_lsblk(self, lsblk_path):
args = ['--list', '--noheadings', '--paths', '--output', 'NAME,UUID']
cmd = [lsblk_path] + args
rc, out, err = self.module.run_command(cmd)
return rc, out, err
def _lsblk_uuid(self):
uuids = {}
lsblk_path = self.module.get_bin_path("lsblk")
if not lsblk_path:
return uuids
rc, out, err = self._run_lsblk(lsblk_path)
if rc != 0:
return uuids
# each line will be in format:
# <devicename><some whitespace><uuid>
# /dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
for lsblk_line in out.splitlines():
if not lsblk_line:
continue
line = lsblk_line.strip()
fields = line.rsplit(None, 1)
if len(fields) < 2:
continue
device_name, uuid = fields[0].strip(), fields[1].strip()
if device_name in uuids:
continue
uuids[device_name] = uuid
return uuids
def _run_findmnt(self, findmnt_path):
args = ['--list', '--noheadings', '--notruncate']
cmd = [findmnt_path] + args
rc, out, err = self.module.run_command(cmd)
return rc, out, err
def _find_bind_mounts(self):
bind_mounts = set()
findmnt_path = self.module.get_bin_path("findmnt")
if not findmnt_path:
return bind_mounts
rc, out, err = self._run_findmnt(findmnt_path)
if rc != 0:
return bind_mounts
# find bind mounts, in case /etc/mtab is a symlink to /proc/mounts
for line in out.splitlines():
fields = line.split()
# fields[0] is the TARGET, fields[1] is the SOURCE
if len(fields) < 2:
continue
# bind mounts will have a [/directory_name] in the SOURCE column
if self.BIND_MOUNT_RE.match(fields[1]):
bind_mounts.add(fields[0])
return bind_mounts
def _mtab_entries(self):
mtab = get_file_content('/etc/mtab', '')
for line in mtab.split('\n'):
fields = line.rstrip('\n').split()
mtab_entries = []
for line in mtab.splitlines():
fields = line.split()
if len(fields) < 4:
continue
if fields[0].startswith('/') or ':/' in fields[0]:
if(fields[2] != 'none'):
size_total, size_available = self._get_mount_size_facts(fields[1])
if fields[0] in uuids:
uuid = uuids[fields[0]]
else:
uuid = 'NA'
lsblkPath = self.module.get_bin_path("lsblk")
if lsblkPath:
rc, out, err = self.module.run_command("%s -ln --output UUID %s" % (lsblkPath, fields[0]), use_unsafe_shell=True)
mtab_entries.append(fields)
return mtab_entries
if rc == 0:
uuid = out.strip()
uuids[fields[0]] = uuid
@timeout(10)
def get_mount_facts(self):
self.facts['mounts'] = []
if fields[1] in bind_mounts:
# only add if not already there, we might have a plain /etc/mtab
if not re.match(".*bind.*", fields[3]):
fields[3] += ",bind"
bind_mounts = self._find_bind_mounts()
uuids = self._lsblk_uuid()
mtab_entries = self._mtab_entries()
self.facts['mounts'].append(
{'mount': fields[1],
'device':fields[0],
'fstype': fields[2],
'options': fields[3],
# statvfs data
'size_total': size_total,
'size_available': size_available,
'uuid': uuid,
})
mounts = []
for fields in mtab_entries:
device, mount, fstype, options = fields[0], fields[1], fields[2], fields[3]
if not device.startswith('/') and ':/' not in device:
continue
if fstype == 'none':
continue
size_total, size_available = self._get_mount_size_facts(mount)
if mount in bind_mounts:
# only add if not already there, we might have a plain /etc/mtab
if not self.MTAB_BIND_MOUNT_RE.match(options):
options += ",bind"
mount_info = {'mount': mount,
'device': device,
'fstype': fstype,
'options': options,
# statvfs data
'size_total': size_total,
'size_available': size_available,
'uuid': uuids.get(device, 'N/A')}
mounts.append(mount_info)
self.facts['mounts'] = mounts
def get_holders(self, block_dev_dict, sysdir):
block_dev_dict['holders'] = []
@@ -1709,8 +1780,10 @@ class FreeBSDHardware(Hardware):
else:
self.facts[k] = 'NA'
class DragonFlyHardware(FreeBSDHardware):
pass
platform = 'DragonFly'
class NetBSDHardware(Hardware):
"""
@@ -2068,7 +2141,10 @@ class Network(Facts):
for sc in get_all_subclasses(Network):
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
if PY3:
return super(cls, subclass).__new__(subclass)
else:
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def populate(self):
return self.facts
@@ -2619,6 +2695,7 @@ class FreeBSDNetwork(GenericBsdIfconfigNetwork):
"""
platform = 'FreeBSD'
class DragonFlyNetwork(GenericBsdIfconfigNetwork):
"""
This is the DragonFly Network Class.
@@ -2626,6 +2703,7 @@ class DragonFlyNetwork(GenericBsdIfconfigNetwork):
"""
platform = 'DragonFly'
class AIXNetwork(GenericBsdIfconfigNetwork):
"""
This is the AIX Network Class.
@@ -2858,7 +2936,11 @@ class Virtual(Facts):
for sc in get_all_subclasses(Virtual):
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
if PY3:
return super(cls, subclass).__new__(subclass)
else:
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def populate(self):
return self.facts
@@ -3055,7 +3137,7 @@ class FreeBSDVirtual(Virtual):
self.facts['virtualization_role'] = ''
class DragonFlyVirtual(FreeBSDVirtual):
pass
platform = 'DragonFly'
class OpenBSDVirtual(Virtual):
"""