1"""Store configuration options as a singleton.""" 2import os 3import re 4import subprocess 5import sys 6from argparse import Namespace 7from functools import lru_cache 8from typing import Any, Dict, List, Optional, Tuple 9 10from packaging.version import Version 11 12from ansiblelint.constants import ANSIBLE_MISSING_RC 13 14DEFAULT_KINDS = [ 15 # Do not sort this list, order matters. 16 {"jinja2": "**/*.j2"}, # jinja2 templates are not always parsable as something else 17 {"jinja2": "**/*.j2.*"}, 18 {"inventory": "**/inventory/**.yml"}, 19 {"requirements": "**/meta/requirements.yml"}, # v1 only 20 # https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html 21 {"galaxy": "**/galaxy.yml"}, # Galaxy collection meta 22 {"reno": "**/releasenotes/*/*.{yaml,yml}"}, # reno release notes 23 {"playbook": "**/playbooks/*.{yml,yaml}"}, 24 {"playbook": "**/*playbook*.{yml,yaml}"}, 25 {"role": "**/roles/*/"}, 26 {"tasks": "**/tasks/**/*.{yaml,yml}"}, 27 {"handlers": "**/handlers/*.{yaml,yml}"}, 28 {"vars": "**/{host_vars,group_vars,vars,defaults}/**/*.{yaml,yml}"}, 29 {"meta": "**/meta/main.{yaml,yml}"}, 30 {"yaml": ".config/molecule/config.{yaml,yml}"}, # molecule global config 31 { 32 "requirements": "**/molecule/*/{collections,requirements}.{yaml,yml}" 33 }, # molecule old collection requirements (v1), ansible 2.8 only 34 {"yaml": "**/molecule/*/{base,molecule}.{yaml,yml}"}, # molecule config 35 {"requirements": "**/requirements.yml"}, # v2 and v1 36 {"playbook": "**/molecule/*/*.{yaml,yml}"}, # molecule playbooks 37 {"yaml": "**/{.ansible-lint,.yamllint}"}, 38 {"yaml": "**/*.{yaml,yml}"}, 39 {"yaml": "**/.*.{yaml,yml}"}, 40] 41 42BASE_KINDS = [ 43 # These assignations are only for internal use and are only inspired by 44 # MIME/IANA model. Their purpose is to be able to process a file based on 45 # it type, including generic processing of text files using the prefix. 46 { 47 "text/jinja2": "**/*.j2" 48 }, # jinja2 templates are not always parsable as something else 49 {"text/jinja2": "**/*.j2.*"}, 50 {"text": "**/templates/**/*.*"}, # templates are likely not validable 51 {"text/json": "**/*.json"}, # standardized 52 {"text/markdown": "**/*.md"}, # https://tools.ietf.org/html/rfc7763 53 {"text/rst": "**/*.rst"}, # https://en.wikipedia.org/wiki/ReStructuredText 54 {"text/ini": "**/*.ini"}, 55 # YAML has no official IANA assignation 56 {"text/yaml": "**/{.ansible-lint,.yamllint}"}, 57 {"text/yaml": "**/*.{yaml,yml}"}, 58 {"text/yaml": "**/.*.{yaml,yml}"}, 59] 60 61 62options = Namespace( 63 cache_dir=None, 64 colored=True, 65 configured=False, 66 cwd=".", 67 display_relative_path=True, 68 exclude_paths=[], 69 lintables=[], 70 listrules=False, 71 listtags=False, 72 parseable=False, 73 parseable_severity=False, 74 quiet=False, 75 rulesdirs=[], 76 skip_list=[], 77 tags=[], 78 verbosity=False, 79 warn_list=[], 80 kinds=DEFAULT_KINDS, 81 mock_modules=[], 82 mock_roles=[], 83 loop_var_prefix=None, 84 var_naming_pattern=None, 85 offline=False, 86 project_dir=".", # default should be valid folder (do not use None here) 87 extra_vars=None, 88 enable_list=[], 89 skip_action_validation=True, 90 rules=dict(), # Placeholder to set and keep configurations for each rule. 91) 92 93# Used to store detected tag deprecations 94used_old_tags: Dict[str, str] = {} 95 96# Used to store collection list paths (with mock paths if needed) 97collection_list: List[str] = [] 98 99 100def get_rule_config(rule_id: str) -> Dict[str, Any]: 101 """Get configurations for the rule ``rule_id``.""" 102 rule_config = options.rules.get(rule_id, dict()) 103 if not isinstance(rule_config, dict): 104 raise RuntimeError("Invalid rule config for %s: %s" % (rule_id, rule_config)) 105 return rule_config 106 107 108@lru_cache() 109def ansible_collections_path() -> str: 110 """Return collection path variable for current version of Ansible.""" 111 # respect Ansible behavior, which is to load old name if present 112 for env_var in ["ANSIBLE_COLLECTIONS_PATHS", "ANSIBLE_COLLECTIONS_PATH"]: 113 if env_var in os.environ: 114 return env_var 115 116 # https://github.com/ansible/ansible/pull/70007 117 if ansible_version() >= ansible_version("2.10.0.dev0"): 118 return "ANSIBLE_COLLECTIONS_PATH" 119 return "ANSIBLE_COLLECTIONS_PATHS" 120 121 122def parse_ansible_version(stdout: str) -> Tuple[str, Optional[str]]: 123 """Parse output of 'ansible --version'.""" 124 # Ansible can produce extra output before displaying version in debug mode. 125 126 # ansible-core 2.11+: 'ansible [core 2.11.3]' 127 match = re.search(r"^ansible \[(?:core|base) ([^\]]+)\]", stdout, re.MULTILINE) 128 if match: 129 return match.group(1), None 130 # ansible-base 2.10 and Ansible 2.9: 'ansible 2.x.y' 131 match = re.search(r"^ansible ([^\s]+)", stdout, re.MULTILINE) 132 if match: 133 return match.group(1), None 134 return "", "FATAL: Unable parse ansible cli version: %s" % stdout 135 136 137@lru_cache() 138def ansible_version(version: str = "") -> Version: 139 """Return current Version object for Ansible. 140 141 If version is not mentioned, it returns current version as detected. 142 When version argument is mentioned, it return converts the version string 143 to Version object in order to make it usable in comparisons. 144 """ 145 if not version: 146 proc = subprocess.run( 147 ["ansible", "--version"], 148 universal_newlines=True, 149 check=False, 150 stdout=subprocess.PIPE, 151 stderr=subprocess.PIPE, 152 ) 153 if proc.returncode == 0: 154 version, error = parse_ansible_version(proc.stdout) 155 if error is not None: 156 print(error) 157 sys.exit(ANSIBLE_MISSING_RC) 158 else: 159 print( 160 "Unable to find a working copy of ansible executable.", 161 proc, 162 ) 163 sys.exit(ANSIBLE_MISSING_RC) 164 return Version(version) 165 166 167if ansible_collections_path() in os.environ: 168 collection_list = os.environ[ansible_collections_path()].split(':') 169