From 44fa8c1fb285811e502603c1b22ed6770797be31 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 6 May 2016 10:37:41 -0500 Subject: [PATCH] Add ability to analyze the argument_spec for a module --- ansible_testing/module_args.py | 113 +++++++++++++++++++++++++++++++++ ansible_testing/modules.py | 28 ++++++-- 2 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 ansible_testing/module_args.py diff --git a/ansible_testing/module_args.py b/ansible_testing/module_args.py new file mode 100644 index 0000000000..07f3f82a48 --- /dev/null +++ b/ansible_testing/module_args.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016 Matt Martz +# 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 . + +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 {} diff --git a/ansible_testing/modules.py b/ansible_testing/modules.py index bc2ef60d57..f56e83c2dc 100644 --- a/ansible_testing/modules.py +++ b/ansible_testing/modules.py @@ -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