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:
Sam Doran 2020-01-30 12:54:25 -05:00 committed by GitHub
parent c9a34ae33e
commit 5112feeace
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 106 additions and 44 deletions

View file

@ -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)

View file

@ -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:
if required:
self.fail_json(msg=to_text(e)) self.fail_json(msg=to_text(e))
else:
return bin_path
return bin_path return bin_path

View file

@ -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

View file

@ -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
try:
vm_stat_command = get_bin_path('vm_stat') 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

View file

@ -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'):
try:
cmd = get_bin_path('lsattr') cmd = get_bin_path('lsattr')
if cmd: except ValueError:
return iscsi_facts
cmd += " -E -l iscsi0" cmd += " -E -l iscsi0"
rc, out, err = module.run_command(cmd) rc, out, err = module.run_command(cmd)
if rc == 0 and out: if rc == 0 and out:
line = self.findstr(out, 'initiator_name') line = self.findstr(out, 'initiator_name')
iscsi_facts['iscsi_iqn'] = line.split()[1].rstrip() 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
try:
cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin']) cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin'])
if cmd: except ValueError:
return iscsi_facts
cmd += " -l" cmd += " -l"
rc, out, err = module.run_command(cmd) rc, out, err = module.run_command(cmd)
if out: if out:
line = self.findstr(out, 'Initiator Name') line = self.findstr(out, 'Initiator Name')
iscsi_facts['iscsi_iqn'] = line.split(":", 1)[1].rstrip() 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):

View file

@ -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

View file

@ -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]

View file

@ -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'):
try:
get_bin_path('rpm')
if not we_have_lib:
module.warn('Found "rpm" but %s' % (missing_required_lib('rpm'))) 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

View file

@ -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))

View file

@ -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:
try:
self.chroot_cmd = get_bin_path(self.get_option('chroot_exe')) self.chroot_cmd = get_bin_path(self.get_option('chroot_exe'))
except ValueError as e:
if not self.chroot_cmd: raise AnsibleError(to_native(e))
raise AnsibleError("chroot command (%s) not found in PATH" % to_native(self.get_option('chroot_exe')))
super(Connection, self)._connect() super(Connection, self)._connect()
if not self._connected: if not self._connected:

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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')