setup.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. """A setuptools based setup module.
  2. """
  3. from __future__ import print_function
  4. import os
  5. import fnmatch
  6. import re
  7. import subprocess
  8. import yaml
  9. # Always prefer setuptools over distutils
  10. from setuptools import setup, Command
  11. from setuptools_lint.setuptools_command import PylintCommand
  12. from six import string_types
  13. from yamllint.config import YamlLintConfig
  14. from yamllint.cli import Format
  15. from yamllint import linter
  16. def find_files(base_dir, exclude_dirs, include_dirs, file_regex):
  17. ''' find files matching file_regex '''
  18. found = []
  19. exclude_regex = ''
  20. include_regex = ''
  21. if exclude_dirs is not None:
  22. exclude_regex = r'|'.join([fnmatch.translate(x) for x in exclude_dirs]) or r'$.'
  23. # Don't use include_dirs, it is broken
  24. if include_dirs is not None:
  25. include_regex = r'|'.join([fnmatch.translate(x) for x in include_dirs]) or r'$.'
  26. for root, dirs, files in os.walk(base_dir):
  27. if exclude_dirs is not None:
  28. # filter out excludes for dirs
  29. dirs[:] = [d for d in dirs if not re.match(exclude_regex, d)]
  30. if include_dirs is not None:
  31. # filter for includes for dirs
  32. dirs[:] = [d for d in dirs if re.match(include_regex, d)]
  33. matches = [os.path.join(root, f) for f in files if re.search(file_regex, f) is not None]
  34. found.extend(matches)
  35. return found
  36. def recursive_search(search_list, field):
  37. """
  38. Takes a list with nested dicts, and searches all dicts for a key of the
  39. field provided. If the items in the list are not dicts, the items are not
  40. processed.
  41. """
  42. fields_found = []
  43. for item in search_list:
  44. if isinstance(item, dict):
  45. for key, value in item.items():
  46. if key == field:
  47. fields_found.append(value)
  48. elif isinstance(value, list):
  49. results = recursive_search(value, field)
  50. for result in results:
  51. fields_found.append(result)
  52. return fields_found
  53. def find_playbooks(base_dir):
  54. ''' find Ansible playbooks'''
  55. all_playbooks = set()
  56. included_playbooks = set()
  57. exclude_dirs = ('adhoc', 'tasks', 'ovirt')
  58. for yaml_file in find_files(
  59. os.path.join(os.getcwd(), base_dir),
  60. exclude_dirs, None, r'^[^\.].*\.ya?ml$'):
  61. with open(yaml_file, 'r') as contents:
  62. for task in yaml.safe_load(contents) or {}:
  63. if not isinstance(task, dict):
  64. # Skip yaml files which are not a dictionary of tasks
  65. continue
  66. if 'include' in task or 'import_playbook' in task:
  67. # Add the playbook and capture included playbooks
  68. all_playbooks.add(yaml_file)
  69. if 'include' in task:
  70. directive = task['include']
  71. else:
  72. directive = task['import_playbook']
  73. included_file_name = directive.split()[0]
  74. included_file = os.path.normpath(
  75. os.path.join(os.path.dirname(yaml_file),
  76. included_file_name))
  77. included_playbooks.add(included_file)
  78. elif 'hosts' in task:
  79. all_playbooks.add(yaml_file)
  80. return all_playbooks, included_playbooks
  81. class OpenShiftAnsibleYamlLint(Command):
  82. ''' Command to run yamllint '''
  83. description = "Run yamllint tests"
  84. user_options = [
  85. ('excludes=', 'e', 'directories to exclude'),
  86. ('config-file=', 'c', 'config file to use'),
  87. ('format=', 'f', 'format to use (standard, parsable)'),
  88. ]
  89. def initialize_options(self):
  90. ''' initialize_options '''
  91. # Reason: Defining these attributes as a part of initialize_options is
  92. # consistent with upstream usage
  93. # Status: permanently disabled
  94. # pylint: disable=attribute-defined-outside-init
  95. self.excludes = None
  96. self.config_file = None
  97. self.format = None
  98. def finalize_options(self):
  99. ''' finalize_options '''
  100. # Reason: These attributes are defined in initialize_options and this
  101. # usage is consistant with upstream usage
  102. # Status: permanently disabled
  103. # pylint: disable=attribute-defined-outside-init
  104. if isinstance(self.excludes, string_types):
  105. self.excludes = self.excludes.split(',')
  106. if self.format is None:
  107. self.format = 'standard'
  108. assert (self.format in ['standard', 'parsable']), (
  109. 'unknown format {0}.'.format(self.format))
  110. if self.config_file is None:
  111. self.config_file = '.yamllint'
  112. assert os.path.isfile(self.config_file), (
  113. 'yamllint config file {0} does not exist.'.format(self.config_file))
  114. def run(self):
  115. ''' run command '''
  116. if self.excludes is not None:
  117. print("Excludes:\n{0}".format(yaml.dump(self.excludes, default_flow_style=False)))
  118. config = YamlLintConfig(file=self.config_file)
  119. has_errors = False
  120. has_warnings = False
  121. if self.format == 'parsable':
  122. format_method = Format.parsable
  123. else:
  124. format_method = Format.standard_color
  125. for yaml_file in find_files(os.getcwd(), self.excludes, None, r'^[^\.].*\.ya?ml$'):
  126. first = True
  127. with open(yaml_file, 'r') as contents:
  128. for problem in linter.run(contents, config):
  129. if first and self.format != 'parsable':
  130. print('\n{0}:'.format(os.path.relpath(yaml_file)))
  131. first = False
  132. print(format_method(problem, yaml_file))
  133. if problem.level == linter.PROBLEM_LEVELS[2]:
  134. has_errors = True
  135. elif problem.level == linter.PROBLEM_LEVELS[1]:
  136. has_warnings = True
  137. if has_errors or has_warnings:
  138. print('yamllint issues found')
  139. raise SystemExit(1)
  140. class OpenShiftAnsiblePylint(PylintCommand):
  141. ''' Class to override the default behavior of PylintCommand '''
  142. # Reason: This method needs to be an instance method to conform to the
  143. # overridden method's signature
  144. # Status: permanently disabled
  145. # pylint: disable=no-self-use
  146. def find_all_modules(self):
  147. ''' find all python files to test '''
  148. exclude_dirs = ('.tox', 'test', 'tests', 'git')
  149. modules = []
  150. for match in find_files(os.getcwd(), exclude_dirs, None, r'\.py$'):
  151. package = os.path.basename(match).replace('.py', '')
  152. modules.append(('openshift_ansible', package, match))
  153. return modules
  154. def get_finalized_command(self, cmd):
  155. ''' override get_finalized_command to ensure we use our
  156. find_all_modules method '''
  157. if cmd == 'build_py':
  158. return self
  159. # Reason: This method needs to be an instance method to conform to the
  160. # overridden method's signature
  161. # Status: permanently disabled
  162. # pylint: disable=no-self-use
  163. def with_project_on_sys_path(self, func, func_args, func_kwargs):
  164. ''' override behavior, since we don't need to build '''
  165. return func(*func_args, **func_kwargs)
  166. class OpenShiftAnsibleSyntaxCheck(Command):
  167. ''' Command to run Ansible syntax check'''
  168. description = "Run Ansible syntax check"
  169. user_options = []
  170. # Colors
  171. FAIL = '\033[31m' # Red
  172. ENDC = '\033[0m' # Reset
  173. def initialize_options(self):
  174. ''' initialize_options '''
  175. pass
  176. def finalize_options(self):
  177. ''' finalize_options '''
  178. pass
  179. def deprecate_jinja2_in_when(self, yaml_contents, yaml_file):
  180. ''' Check for Jinja2 templating delimiters in when conditions '''
  181. test_result = False
  182. failed_items = []
  183. search_results = recursive_search(yaml_contents, 'when')
  184. search_results.append(recursive_search(yaml_contents, 'failed_when'))
  185. for item in search_results:
  186. if isinstance(item, str):
  187. if '{{' in item or '{%' in item:
  188. failed_items.append(item)
  189. else:
  190. for sub_item in item:
  191. if isinstance(sub_item, bool):
  192. continue
  193. if '{{' in sub_item or '{%' in sub_item:
  194. failed_items.append(sub_item)
  195. if len(failed_items) > 0:
  196. print('{}Error: Usage of Jinja2 templating delimiters in when '
  197. 'conditions is deprecated in Ansible 2.3.\n'
  198. ' File: {}'.format(self.FAIL, yaml_file))
  199. for item in failed_items:
  200. print(' Found: "{}"'.format(item))
  201. print(self.ENDC)
  202. test_result = True
  203. return test_result
  204. def deprecate_include(self, yaml_contents, yaml_file):
  205. ''' Check for usage of include directive '''
  206. test_result = False
  207. search_results = recursive_search(yaml_contents, 'include')
  208. if len(search_results) > 0:
  209. print('{}Error: The `include` directive is deprecated in Ansible 2.4.\n'
  210. 'https://github.com/ansible/ansible/blob/devel/CHANGELOG.md\n'
  211. ' File: {}'.format(self.FAIL, yaml_file))
  212. for item in search_results:
  213. print(' Found: "include: {}"'.format(item))
  214. print(self.ENDC)
  215. test_result = True
  216. return test_result
  217. def run(self):
  218. ''' run command '''
  219. has_errors = False
  220. print('#' * 60)
  221. print('Ansible Deprecation Checks')
  222. exclude_dirs = ('adhoc', 'files', 'meta', 'vars', 'defaults', '.tox')
  223. for yaml_file in find_files(
  224. os.getcwd(), exclude_dirs, None, r'^[^\.].*\.ya?ml$'):
  225. with open(yaml_file, 'r') as contents:
  226. yaml_contents = yaml.safe_load(contents)
  227. if not isinstance(yaml_contents, list):
  228. continue
  229. # Check for Jinja2 templating delimiters in when conditions
  230. result = self.deprecate_jinja2_in_when(yaml_contents, yaml_file)
  231. has_errors = result or has_errors
  232. # Check for usage of include: directive
  233. result = self.deprecate_include(yaml_contents, yaml_file)
  234. has_errors = result or has_errors
  235. if not has_errors:
  236. print('...PASSED')
  237. all_playbooks, included_playbooks = find_playbooks('playbooks')
  238. print('#' * 60)
  239. print('Invalid Playbook Include Checks')
  240. invalid_include = []
  241. for playbook in included_playbooks:
  242. # Ignore imported playbooks in 'common', 'private' and 'init'. It is
  243. # expected that these locations would be imported by entry point
  244. # playbooks.
  245. # Ignore playbooks in 'aws', 'azure', 'gcp' and 'openstack' because these
  246. # playbooks do not follow the same component entry point structure.
  247. # Ignore deploy_cluster.yml and prerequisites.yml because these are
  248. # entry point playbooks but are imported by playbooks in the cloud
  249. # provisioning playbooks.
  250. ignored = ('common', 'private', 'init',
  251. 'aws', 'azure', 'gcp', 'openstack',
  252. 'deploy_cluster.yml', 'prerequisites.yml')
  253. if any(x in playbook for x in ignored):
  254. continue
  255. invalid_include.append(playbook)
  256. if invalid_include:
  257. print('{}Invalid included playbook(s) found. Please ensure'
  258. ' component entry point playbooks are not included{}'.format(self.FAIL, self.ENDC))
  259. invalid_include.sort()
  260. for playbook in invalid_include:
  261. print('{}{}{}'.format(self.FAIL, playbook, self.ENDC))
  262. has_errors = True
  263. if not has_errors:
  264. print('...PASSED')
  265. print('#' * 60)
  266. print('Ansible Playbook Entry Point Syntax Checks')
  267. # Evaluate the difference between all playbooks and included playbooks
  268. entrypoint_playbooks = sorted(all_playbooks.difference(included_playbooks))
  269. # Add ci test playbooks
  270. test_playbooks, test_included_playbooks = find_playbooks('test')
  271. test_entrypoint_playbooks = sorted(test_playbooks.difference(test_included_playbooks))
  272. entrypoint_playbooks.extend(test_entrypoint_playbooks)
  273. print('Entry point playbook count: {}'.format(len(entrypoint_playbooks)))
  274. for playbook in entrypoint_playbooks:
  275. print('-' * 60)
  276. print('Syntax checking playbook: {}'.format(playbook))
  277. # Error on any entry points in 'common' or 'private'
  278. invalid_entry_point = ('common', 'private')
  279. if any(x in playbook for x in invalid_entry_point):
  280. print('{}Invalid entry point playbook or orphaned file. Entry'
  281. ' point playbooks are not allowed in \'common\' or'
  282. ' \'private\' directories{}'.format(self.FAIL, self.ENDC))
  283. has_errors = True
  284. # --syntax-check each entry point playbook
  285. try:
  286. # Create a host group list to avoid WARNING on unmatched host patterns
  287. subprocess.check_output(
  288. ['ansible-playbook', '--syntax-check', playbook]
  289. )
  290. except subprocess.CalledProcessError as cpe:
  291. print('{}Execution failed: {}{}'.format(
  292. self.FAIL, cpe, self.ENDC))
  293. has_errors = True
  294. if has_errors:
  295. raise SystemExit(1)
  296. class UnsupportedCommand(Command):
  297. ''' Basic Command to override unsupported commands '''
  298. user_options = []
  299. # Reason: This method needs to be an instance method to conform to the
  300. # overridden method's signature
  301. # Status: permanently disabled
  302. # pylint: disable=no-self-use
  303. def initialize_options(self):
  304. ''' initialize_options '''
  305. pass
  306. # Reason: This method needs to be an instance method to conform to the
  307. # overridden method's signature
  308. # Status: permanently disabled
  309. # pylint: disable=no-self-use
  310. def finalize_options(self):
  311. ''' initialize_options '''
  312. pass
  313. # Reason: This method needs to be an instance method to conform to the
  314. # overridden method's signature
  315. # Status: permanently disabled
  316. # pylint: disable=no-self-use
  317. def run(self):
  318. ''' run command '''
  319. print("Unsupported command for openshift-ansible")
  320. setup(
  321. name='openshift-ansible',
  322. license="Apache 2.0",
  323. cmdclass={
  324. 'install': UnsupportedCommand,
  325. 'develop': UnsupportedCommand,
  326. 'build': UnsupportedCommand,
  327. 'build_py': UnsupportedCommand,
  328. 'build_ext': UnsupportedCommand,
  329. 'egg_info': UnsupportedCommand,
  330. 'sdist': UnsupportedCommand,
  331. 'lint': OpenShiftAnsiblePylint,
  332. 'yamllint': OpenShiftAnsibleYamlLint,
  333. 'ansible_syntax': OpenShiftAnsibleSyntaxCheck,
  334. },
  335. packages=[],
  336. )