Make get_bin_path() always raise an exception (#56813)
This makes it behave in a more idiomatic way * Fix bug in Darwin facts for free memory If the vm_stat command is not found, fact gathering would fail with an unhelpful error message. Handle this gracefully and return a default value for free memory. * Add unit tests
This commit is contained in:
parent
c9a34ae33e
commit
5112feeace
14 changed files with 106 additions and 44 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- get_bin_path() - change the interface to always raise ``ValueError`` if the command is not found (https://github.com/ansible/ansible/pull/56813)
|
|
@ -1973,9 +1973,12 @@ class AnsibleModule(object):
|
||||||
|
|
||||||
bin_path = None
|
bin_path = None
|
||||||
try:
|
try:
|
||||||
bin_path = get_bin_path(arg, required, opt_dirs)
|
bin_path = get_bin_path(arg=arg, opt_dirs=opt_dirs)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.fail_json(msg=to_text(e))
|
if required:
|
||||||
|
self.fail_json(msg=to_text(e))
|
||||||
|
else:
|
||||||
|
return bin_path
|
||||||
|
|
||||||
return bin_path
|
return bin_path
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,14 @@ import os
|
||||||
from ansible.module_utils.common.file import is_executable
|
from ansible.module_utils.common.file import is_executable
|
||||||
|
|
||||||
|
|
||||||
def get_bin_path(arg, required=False, opt_dirs=None):
|
def get_bin_path(arg, opt_dirs=None, required=None):
|
||||||
'''
|
'''
|
||||||
find system executable in PATH.
|
Find system executable in PATH. Raises ValueError if executable is not found.
|
||||||
Optional arguments:
|
Optional arguments:
|
||||||
- required: if executable is not found and required is true it produces an Exception
|
- required: [Deprecated] Prior to 2.10, if executable is not found and required is true it raises an Exception.
|
||||||
|
In 2.10 and later, an Exception is always raised. This parameter will be removed in 2.14.
|
||||||
- opt_dirs: optional list of directories to search in addition to PATH
|
- opt_dirs: optional list of directories to search in addition to PATH
|
||||||
if found return full path; otherwise return None
|
If found return full path, otherwise raise ValueError.
|
||||||
'''
|
'''
|
||||||
opt_dirs = [] if opt_dirs is None else opt_dirs
|
opt_dirs = [] if opt_dirs is None else opt_dirs
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ def get_bin_path(arg, required=False, opt_dirs=None):
|
||||||
if os.path.exists(path) and not os.path.isdir(path) and is_executable(path):
|
if os.path.exists(path) and not os.path.isdir(path) and is_executable(path):
|
||||||
bin_path = path
|
bin_path = path
|
||||||
break
|
break
|
||||||
if required and bin_path is None:
|
if bin_path is None:
|
||||||
raise ValueError('Failed to find required executable %s in paths: %s' % (arg, os.pathsep.join(paths)))
|
raise ValueError('Failed to find required executable %s in paths: %s' % (arg, os.pathsep.join(paths)))
|
||||||
|
|
||||||
return bin_path
|
return bin_path
|
||||||
|
|
|
@ -85,12 +85,17 @@ class DarwinHardware(Hardware):
|
||||||
|
|
||||||
def get_memory_facts(self):
|
def get_memory_facts(self):
|
||||||
memory_facts = {
|
memory_facts = {
|
||||||
'memtotal_mb': int(self.sysctl['hw.memsize']) // 1024 // 1024
|
'memtotal_mb': int(self.sysctl['hw.memsize']) // 1024 // 1024,
|
||||||
|
'memfree_mb': 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
total_used = 0
|
total_used = 0
|
||||||
page_size = 4096
|
page_size = 4096
|
||||||
vm_stat_command = get_bin_path('vm_stat')
|
try:
|
||||||
|
vm_stat_command = get_bin_path('vm_stat')
|
||||||
|
except ValueError:
|
||||||
|
return memory_facts
|
||||||
|
|
||||||
rc, out, err = self.module.run_command(vm_stat_command)
|
rc, out, err = self.module.run_command(vm_stat_command)
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
# Free = Total - (Wired + active + inactive)
|
# Free = Total - (Wired + active + inactive)
|
||||||
|
@ -104,7 +109,7 @@ class DarwinHardware(Hardware):
|
||||||
for k, v in memory_stats.items():
|
for k, v in memory_stats.items():
|
||||||
try:
|
try:
|
||||||
memory_stats[k] = int(v)
|
memory_stats[k] = int(v)
|
||||||
except ValueError as ve:
|
except ValueError:
|
||||||
# Most values convert cleanly to integer values but if the field does
|
# Most values convert cleanly to integer values but if the field does
|
||||||
# not convert to an integer, just leave it alone.
|
# not convert to an integer, just leave it alone.
|
||||||
pass
|
pass
|
||||||
|
@ -116,7 +121,7 @@ class DarwinHardware(Hardware):
|
||||||
if memory_stats.get('Pages inactive'):
|
if memory_stats.get('Pages inactive'):
|
||||||
total_used += memory_stats['Pages inactive'] * page_size
|
total_used += memory_stats['Pages inactive'] * page_size
|
||||||
|
|
||||||
memory_facts['memfree_mb'] = memory_facts['memtotal_mb'] - (total_used // 1024 // 1024)
|
memory_facts['memfree_mb'] = memory_facts['memtotal_mb'] - (total_used // 1024 // 1024)
|
||||||
|
|
||||||
return memory_facts
|
return memory_facts
|
||||||
|
|
||||||
|
|
|
@ -80,22 +80,30 @@ class IscsiInitiatorNetworkCollector(NetworkCollector):
|
||||||
iscsi_facts['iscsi_iqn'] = line.split('=', 1)[1]
|
iscsi_facts['iscsi_iqn'] = line.split('=', 1)[1]
|
||||||
break
|
break
|
||||||
elif sys.platform.startswith('aix'):
|
elif sys.platform.startswith('aix'):
|
||||||
cmd = get_bin_path('lsattr')
|
try:
|
||||||
if cmd:
|
cmd = get_bin_path('lsattr')
|
||||||
cmd += " -E -l iscsi0"
|
except ValueError:
|
||||||
rc, out, err = module.run_command(cmd)
|
return iscsi_facts
|
||||||
if rc == 0 and out:
|
|
||||||
line = self.findstr(out, 'initiator_name')
|
cmd += " -E -l iscsi0"
|
||||||
iscsi_facts['iscsi_iqn'] = line.split()[1].rstrip()
|
rc, out, err = module.run_command(cmd)
|
||||||
|
if rc == 0 and out:
|
||||||
|
line = self.findstr(out, 'initiator_name')
|
||||||
|
iscsi_facts['iscsi_iqn'] = line.split()[1].rstrip()
|
||||||
|
|
||||||
elif sys.platform.startswith('hp-ux'):
|
elif sys.platform.startswith('hp-ux'):
|
||||||
# try to find it in the default PATH and opt_dirs
|
# try to find it in the default PATH and opt_dirs
|
||||||
cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin'])
|
try:
|
||||||
if cmd:
|
cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin'])
|
||||||
cmd += " -l"
|
except ValueError:
|
||||||
rc, out, err = module.run_command(cmd)
|
return iscsi_facts
|
||||||
if out:
|
|
||||||
line = self.findstr(out, 'Initiator Name')
|
cmd += " -l"
|
||||||
iscsi_facts['iscsi_iqn'] = line.split(":", 1)[1].rstrip()
|
rc, out, err = module.run_command(cmd)
|
||||||
|
if out:
|
||||||
|
line = self.findstr(out, 'Initiator Name')
|
||||||
|
iscsi_facts['iscsi_iqn'] = line.split(":", 1)[1].rstrip()
|
||||||
|
|
||||||
return iscsi_facts
|
return iscsi_facts
|
||||||
|
|
||||||
def findstr(self, text, match):
|
def findstr(self, text, match):
|
||||||
|
|
|
@ -79,5 +79,8 @@ class CLIMgr(PkgMgr):
|
||||||
super(CLIMgr, self).__init__()
|
super(CLIMgr, self).__init__()
|
||||||
|
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
self._cli = get_bin_path(self.CLI, False)
|
try:
|
||||||
return bool(self._cli)
|
self._cli = get_bin_path(self.CLI)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
|
@ -291,7 +291,7 @@ class AnsibleModuleError(Exception):
|
||||||
# basic::AnsibleModule() until then but if so, make it a private function so that we don't have to
|
# basic::AnsibleModule() until then but if so, make it a private function so that we don't have to
|
||||||
# keep it for backwards compatibility later.
|
# keep it for backwards compatibility later.
|
||||||
def clear_facls(path):
|
def clear_facls(path):
|
||||||
setfacl = get_bin_path('setfacl', True)
|
setfacl = get_bin_path('setfacl')
|
||||||
# FIXME "setfacl -b" is available on Linux and FreeBSD. There is "setfacl -D e" on z/OS. Others?
|
# FIXME "setfacl -b" is available on Linux and FreeBSD. There is "setfacl -D e" on z/OS. Others?
|
||||||
acl_command = [setfacl, '-b', path]
|
acl_command = [setfacl, '-b', path]
|
||||||
b_acl_command = [to_bytes(x) for x in acl_command]
|
b_acl_command = [to_bytes(x) for x in acl_command]
|
||||||
|
|
|
@ -229,8 +229,14 @@ class RPM(LibMgr):
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli'''
|
''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli'''
|
||||||
we_have_lib = super(RPM, self).is_available()
|
we_have_lib = super(RPM, self).is_available()
|
||||||
if not we_have_lib and get_bin_path('rpm'):
|
|
||||||
module.warn('Found "rpm" but %s' % (missing_required_lib('rpm')))
|
try:
|
||||||
|
get_bin_path('rpm')
|
||||||
|
if not we_have_lib:
|
||||||
|
module.warn('Found "rpm" but %s' % (missing_required_lib('rpm')))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
return we_have_lib
|
return we_have_lib
|
||||||
|
|
||||||
|
|
||||||
|
@ -255,7 +261,11 @@ class APT(LibMgr):
|
||||||
we_have_lib = super(APT, self).is_available()
|
we_have_lib = super(APT, self).is_available()
|
||||||
if not we_have_lib:
|
if not we_have_lib:
|
||||||
for exe in ('apt', 'apt-get', 'aptitude'):
|
for exe in ('apt', 'apt-get', 'aptitude'):
|
||||||
if get_bin_path(exe):
|
try:
|
||||||
|
get_bin_path(exe)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
module.warn('Found "%s" but %s' % (exe, missing_required_lib('apt')))
|
module.warn('Found "%s" but %s' % (exe, missing_required_lib('apt')))
|
||||||
break
|
break
|
||||||
return we_have_lib
|
return we_have_lib
|
||||||
|
|
|
@ -155,7 +155,7 @@ class RoleRequirement(RoleDefinition):
|
||||||
raise AnsibleError("- scm %s is not currently supported" % scm)
|
raise AnsibleError("- scm %s is not currently supported" % scm)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scm_path = get_bin_path(scm, required=True)
|
scm_path = get_bin_path(scm)
|
||||||
except (ValueError, OSError, IOError):
|
except (ValueError, OSError, IOError):
|
||||||
raise AnsibleError("could not find/use %s, it is required to continue with installing %s" % (scm, src))
|
raise AnsibleError("could not find/use %s, it is required to continue with installing %s" % (scm, src))
|
||||||
|
|
||||||
|
|
|
@ -101,10 +101,10 @@ class Connection(ConnectionBase):
|
||||||
if os.path.isabs(self.get_option('chroot_exe')):
|
if os.path.isabs(self.get_option('chroot_exe')):
|
||||||
self.chroot_cmd = self.get_option('chroot_exe')
|
self.chroot_cmd = self.get_option('chroot_exe')
|
||||||
else:
|
else:
|
||||||
self.chroot_cmd = get_bin_path(self.get_option('chroot_exe'))
|
try:
|
||||||
|
self.chroot_cmd = get_bin_path(self.get_option('chroot_exe'))
|
||||||
if not self.chroot_cmd:
|
except ValueError as e:
|
||||||
raise AnsibleError("chroot command (%s) not found in PATH" % to_native(self.get_option('chroot_exe')))
|
raise AnsibleError(to_native(e))
|
||||||
|
|
||||||
super(Connection, self)._connect()
|
super(Connection, self)._connect()
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
|
|
|
@ -108,9 +108,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||||
def _run_command(self, args):
|
def _run_command(self, args):
|
||||||
if not self.DOCKER_MACHINE_PATH:
|
if not self.DOCKER_MACHINE_PATH:
|
||||||
try:
|
try:
|
||||||
self.DOCKER_MACHINE_PATH = get_bin_path('docker-machine', required=True)
|
self.DOCKER_MACHINE_PATH = get_bin_path('docker-machine')
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise AnsibleError('Unable to locate the docker-machine binary.', orig_exc=e)
|
raise AnsibleError(to_native(e))
|
||||||
|
|
||||||
command = [self.DOCKER_MACHINE_PATH]
|
command = [self.DOCKER_MACHINE_PATH]
|
||||||
command.extend(args)
|
command.extend(args)
|
||||||
|
|
|
@ -86,12 +86,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||||
def parse(self, inventory, loader, path, cache=False):
|
def parse(self, inventory, loader, path, cache=False):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._nmap = get_bin_path('nmap', True)
|
self._nmap = get_bin_path('nmap')
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise AnsibleParserError(e)
|
raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work: {0}'.format(to_native(e)))
|
||||||
|
|
||||||
if self._nmap is None:
|
|
||||||
raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work')
|
|
||||||
|
|
||||||
super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
|
super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
|
||||||
|
|
||||||
|
|
|
@ -229,7 +229,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||||
def parse(self, inventory, loader, path, cache=True):
|
def parse(self, inventory, loader, path, cache=True):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._vbox_path = get_bin_path(self.VBOX, True)
|
self._vbox_path = get_bin_path(self.VBOX)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise AnsibleParserError(e)
|
raise AnsibleParserError(e)
|
||||||
|
|
||||||
|
|
33
test/units/module_utils/common/process/test_get_bin_path.py
Normal file
33
test/units/module_utils/common/process/test_get_bin_path.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020 Ansible Project
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible.module_utils.common.process import get_bin_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_bin_path(mocker):
|
||||||
|
path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
|
||||||
|
mocker.patch.dict('os.environ', {'PATH': path})
|
||||||
|
mocker.patch('os.pathsep', ':')
|
||||||
|
|
||||||
|
mocker.patch('os.path.exists', side_effect=[False, True])
|
||||||
|
mocker.patch('os.path.isdir', return_value=False)
|
||||||
|
mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True)
|
||||||
|
|
||||||
|
assert '/usr/local/bin/notacommand' == get_bin_path('notacommand')
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_path_path_raise_valueerror(mocker):
|
||||||
|
mocker.patch.dict('os.environ', {'PATH': ''})
|
||||||
|
|
||||||
|
mocker.patch('os.path.exists', return_value=False)
|
||||||
|
mocker.patch('os.path.isdir', return_value=False)
|
||||||
|
mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='Failed to find required executable notacommand'):
|
||||||
|
get_bin_path('notacommand')
|
Loading…
Reference in a new issue