1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Add ability to analyze the argument_spec for a module

This commit is contained in:
Matt Martz 2016-05-06 10:37:41 -05:00 committed by John Barker
parent 7cc11e4ad5
commit 44fa8c1fb2
2 changed files with 137 additions and 4 deletions

View file

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 Matt Martz <matt@sivel.net>
# Copyright (C) 2016 Rackspace US, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import imp
import sys
from modulefinder import ModuleFinder
import mock
MODULE_CLASSES = [
'ansible.module_utils.basic.AnsibleModule',
'ansible.module_utils.vca.VcaAnsibleModule',
'ansible.module_utils.nxos.NetworkModule',
'ansible.module_utils.eos.NetworkModule',
'ansible.module_utils.ios.NetworkModule',
'ansible.module_utils.iosxr.NetworkModule',
'ansible.module_utils.junos.NetworkModule',
'ansible.module_utils.openswitch.NetworkModule',
]
class AnsibleModuleCallError(RuntimeError):
pass
def add_mocks(filename):
gp = mock.patch('ansible.module_utils.basic.get_platform').start()
gp.return_value = 'linux'
module_mock = mock.MagicMock()
mocks = []
for module_class in MODULE_CLASSES:
mocks.append(
mock.patch('ansible.module_utils.basic.AnsibleModule',
new=module_mock)
)
for m in mocks:
p = m.start()
p.side_effect = AnsibleModuleCallError()
finder = ModuleFinder()
try:
finder.run_script(filename)
except:
pass
sys_mock = mock.MagicMock()
sys_mock.__version__ = '0.0.0'
sys_mocks = []
for module, sources in finder.badmodules.items():
if module in sys.modules:
continue
if [s for s in sources if s[:7] in ['ansible', '__main_']]:
parts = module.split('.')
for i in range(len(parts)):
dotted = '.'.join(parts[:i+1])
sys.modules[dotted] = sys_mock
sys_mocks.append(dotted)
return module_mock, mocks, sys_mocks
def remove_mocks(mocks, sys_mocks):
for m in mocks:
m.stop()
for m in sys_mocks:
try:
del sys.modules[m]
except KeyError:
pass
def get_argument_spec(filename):
module_mock, mocks, sys_mocks = add_mocks(filename)
try:
mod = imp.load_source('module', filename)
if not module_mock.call_args:
mod.main()
except AnsibleModuleCallError:
pass
except Exception:
# We can probably remove this branch, it is here for use while testing
pass
remove_mocks(mocks, sys_mocks)
try:
args, kwargs = module_mock.call_args
try:
return kwargs['argument_spec']
except KeyError:
return args[0]
except TypeError:
return {}

View file

@ -34,6 +34,8 @@ from ansible.executor.module_common import REPLACER_WINDOWS
from ansible.plugins import module_loader
from ansible.utils.module_docs import BLACKLIST_MODULES, get_docstring
from module_args import get_argument_spec
from schema import doc_schema, option_schema
from utils import CaptureStd
@ -125,13 +127,15 @@ class ModuleValidator(Validator):
'setup.ps1'
))
def __init__(self, path):
def __init__(self, path, analyze_arg_spec=False):
super(ModuleValidator, self).__init__()
self.path = path
self.basename = os.path.basename(self.path)
self.name, _ = os.path.splitext(self.basename)
self.analyze_arg_spec = analyze_arg_spec
self._python_module_override = False
with open(path) as f:
@ -442,6 +446,16 @@ class ModuleValidator(Validator):
self.errors.append('version_added should be %s. Currently %s' %
(should_be, version_added))
def _validate_argument_spec(self):
if not self.analyze_arg_spec:
return
spec = get_argument_spec(self.path)
for arg, data in spec.items():
if data.get('required') and data.get('default', object) != object:
self.errors.append('"%s" is marked as required but specifies '
'a default. Arguments with a default '
'should not be marked as required' % arg)
def _check_for_new_args(self, doc):
if self._is_new_module():
return
@ -530,6 +544,7 @@ class ModuleValidator(Validator):
self._validate_docs()
if self._python_module() and not self._just_docs():
self._validate_argument_spec()
self._check_for_sys_exit()
self._find_blacklist_imports()
main = self._find_main_call()
@ -593,6 +608,8 @@ def main():
action='store_true')
parser.add_argument('--exclude', help='RegEx exclusion pattern',
type=re_compile)
parser.add_argument('--arg-spec', help='Analyze module argument spec',
action='store_true', default=False)
args = parser.parse_args()
args.modules[:] = [m.rstrip('/') for m in args.modules]
@ -604,7 +621,7 @@ def main():
path = module
if args.exclude and args.exclude.search(path):
sys.exit(0)
mv = ModuleValidator(path)
mv = ModuleValidator(path, analyze_arg_spec=args.arg_spec)
mv.validate()
exit.append(mv.report(args.warnings))
@ -626,7 +643,7 @@ def main():
path = os.path.join(root, filename)
if args.exclude and args.exclude.search(path):
continue
mv = ModuleValidator(path)
mv = ModuleValidator(path, analyze_arg_spec=args.arg_spec)
mv.validate()
exit.append(mv.report(args.warnings))
@ -634,4 +651,7 @@ def main():
if __name__ == '__main__':
main()
try:
main()
except KeyboardInterrupt:
pass