import pkg_resources
import pprint
import sys
-import yaml
from ruamel.yaml import YAML
import salty_linter
epilog='Now let\'s get some salt states in ship shop shape !')
argparser.add_argument('--verbose', dest='verbose', action='store_true', help="Let's be super chatty and generate a lot of output.")
argparser.add_argument('--no-werror', dest='werror', action='store_false', help="Don't treat warnings as errors")
-argparser.add_argument('--typedb', nargs='?', default=default_types, type=argparse.FileType('r'), help="alternative type database.")
argparser.add_argument('sls', nargs='+', help="list of salt state files")
argparser.set_defaults(verbose=False, werror=True)
args = argparser.parse_args()
-pp.pprint(args)
-
if args.verbose:
- print("HI I'M SALTY_LINTER. LETS GET CHATTY.")
-
-# Open the type database file and read in the type database
-# Error out of that file doesn't parse / doesn't exist / is unreadab
-typedb = yaml.load(args.typedb)
-
-if args.verbose:
- print("HERE'S YOUR TYPE DATABSE. WALLOW IN IT.")
- pp.pprint(typedb)
- print("WRAP UP YOUR TYPE DATABASE WALLOWING.")
+ print("HELLO EVERYONE I'M SALTY-LINTER AND I'M VERBOSE.")
exit_status = 0
for an_sls_filename in args.sls:
continue
with open(fullpath_sls_filename) as fp:
- yaml = YAML(typ='rt')
+ yaml = YAML(typ='jinja2')
sls_yaml = yaml.load(fp)
- (lint_errors, lint_warnings) = salty_linter.lint(sls_yaml, typedb)
+ (lint_errors, lint_warnings) = salty_linter.lint(sls_yaml)
if lint_errors or (lint_warnings and args.werror):
exit_status = 1
for (line_number, an_error) in lint_errors:
if exit_status == 0:
print("NO OBVIOUS FAILURES.")
-else:
- print("MORE SHIP-SHOPPING FOR YOU !")
+
sys.exit(exit_status)
-import pprint
+import importlib
+import inspect
-def lint(sls_yaml, types):
+from salt.state import STATE_INTERNAL_KEYWORDS
+
+def lint(sls_yaml):
"""
Check the sls_yaml for Salt errors / warnings
- typedb : Nested dictionary of Salt states/functions/parameters (partial)
sls_yaml: ruamel.yaml[jinja2].load()'d output for some Salt state file.
Returns: (errors, warnings)
warnings: List of (line#, warnings) tuples
"""
- pp = pprint.PrettyPrinter(indent=4)
- #pp.pprint(dir(sls_yaml))
- #pp.pprint(sls_yaml.__dict__)
+ module_cache = {}
-
- def lint_parameters(function_name, parameters, parameter_types):
+ def lint_parameters(state_name, function_name, actual_parameters, state_module):
"""
- Input:
- function_name: String of the form "<state_name>.<function>" currently being linted
- parameters: Map of {paramter_name: parameter_data} items currently being linted
- parameter_types: Map of {parameter_name: parameter_type} items
- Output:
- errors: List of error string
- warnings: List of warning strings
"""
- errors = []
- warnings = []
- for parameter in parameters:
- parameter_name = next(iter(parameter.keys()))
-
- ## parameter is either just a string or a {'parameter' : data} map, or always a map ?
-
- if parameter_name in types['globals']:
- continue
- elif parameter_name not in parameter_types:
- s_warning = "Unexpected parameter name {} for {}"
- warnings.append((parameter.lc.line + 1, s_warning.format(parameter_name, function_name)))
- else:
- ## TODO: Add that parameter.values()[0] matches the against parameter_types[parameter] type
- pass
- return (errors, warnings)
+ perrors = []
+ pwarnings = []
+ seen_parameters = []
-
- def lint_function(state_name, function_calls, function_types):
- """
- Input:
- state_name: String of the state_name currently being linted
- function_calls: Map of {function_name: parameters} items currently being linteds
- function_types; Map of {function_name: parameter_types} items
- Output:
- errors: List of error string
- warnings: List of warning strings
- """
- errors = []
- warnings = []
- for function_name, parameters in function_calls.items():
- if function_name not in function_types:
- warnings.append((parameters.lc.line+1, "Unexpected function name {}.{}.".format(state_name, function_name)))
+ try:
+ allowable_parameters = inspect.signature(vars(state_module)[function_name]).parameters
+ except Exception as e:
+ s_error = "Unexpected error looking up {}.{} parameters: {}."
+ perrors.append((1, s_error.format(state_name, function_name, e)))
+ return (perrors, pwarnings)
+
+ for a_parameter in actual_parameters:
+
+ # TODO: A comment explaining this line...
+ parameter_name = next(iter(a_parameter.keys()))
+
+ # Accept any of the global state arguments
+ # (maybe ignore 'state' and 'fun' ??)
+ if parameter_name in STATE_INTERNAL_KEYWORDS:
continue
+ elif parameter_name not in allowable_parameters:
+ s_warning = "Unexpected parameter '{}' for function '{}.{}'."
+ pwarnings.append((a_parameter.lc.line + 1, s_warning.format(parameter_name, state_name, function_name)))
+
+ # If there are duplicate parameters in the state, it looks like salt will use the last seen parameter,
+ # but it looks like that's undefined behavior, but its more likely some copypasta error on your part.
+ if parameter_name in seen_parameters:
+ s_error = "Duplicate parameter '{}' for function '{}.{}'."
+ perrors.append((a_parameter.lc.line + 1, s_error.format(parameter_name, state_name, function_name)))
else:
- (parameter_errors, parameter_warnings) = lint_parameters(state_name + "." + function_name, parameters, function_types[function_name])
- errors += parameter_errors
- warnings += parameter_warnings
- return (errors, warnings)
+ seen_parameters.append(parameter_name)
- ## ------------------------------
+ return (perrors, pwarnings)
- state_types = types['states']
+
+ ## ------------------------------
errors = []
- warnings = []
+ warnings = []
for label, label_value in sls_yaml.items():
# It's possible the label_value is just a 'state.function'. See example_sls/two-items.sls
value = {label_value: []}
else:
value = label_value
-
+
for state_function_name, function_calls_or_parameters in value.items():
# state_function_name should be 'state_name' or 'state_name.function_name'
# sosf => 'state or state.function'
- # = [state_name] or [state_name, function_name]
+ # = [state_name] or [state_name, function_name]
sosf = state_function_name.split('.')
- # Verify that sosf[0] is an expected state:
+ # Load the state module if needed.
+ # If it's already been loaded, use the cached version
state_name = ''
- if sosf[0] not in state_types:
- warnings.append((value.lc.line + 1, "Unexpected state name {}.".format(sosf[0])))
- continue
- else:
+ if sosf[0] in module_cache:
state_name = sosf[0]
+ state_mod = module_cache[state_name]
+ else:
+ try:
+ state_mod = importlib.import_module('salt.states.' + sosf[0])
+ except ModuleNotFoundError:
+ warnings.append((value.lc.line + 1, "Unexpected state name {}.".format(sosf[0])))
+ continue
+ else:
+ state_name = sosf[0]
+ module_cache[state_name] = state_mod
# "state" case
# function_calls_or_parameters[0] will be the function name
- # function_call_or_parameters[1:] will be the parameters
+ # function_calls_or_parameters[1:] will be the parameters
if len(sosf) == 1:
if not function_calls_or_parameters:
errors.append(label_value.lc.line + 1, "Missing function name in parameter list for state {}.".format(state_name))
continue
-
+
function_name = function_calls_or_parameters[0]
- if function_name not in state_types[state_name]:
- warnings.append((state_function_name.lc.line + 1, "Unexpected function name {} for {}.".format(function_name, state_name)))
- continue
- parameters = function_call_or_parameters[1:]
- (function_errors, function_warnings) = lint_function(state_name + "." + function_name, parameters, state_types[state_name][function_name])
- errors += function_errors
- warnings += function_warnings
+ parameters = function_calls_or_parameters[1:]
+
+ if function_name not in vars(state_mod):
+ warnings.append((state_function_name.lc.line + 1, "Unexpected function name '{}' for state {}.".format(function_name, state_name)))
+ continue
+
+ (perrors, pwarnings) = lint_parameters(state_name, function_name, parameters, state_mod)
+ errors += perrors
+ warnings += pwarnings
# "state.function" case
# function_calls_or_parameters will be a list of parameters
elif len(sosf) == 2:
function_name = sosf[1]
- if function_name not in state_types[state_name]:
+ if function_name not in vars(state_mod):
# TODO: This gives the line number at the top of the label block which isn't great.
# It would be better to give the exact like number of the invalid function
- warnings.append((sls_yaml.lc.key(label)[0] + 1, "Unexpected function name {} for {}.".format(function_name, state_name)))
+ warnings.append((sls_yaml.lc.key(label)[0] + 1, "Unexpected function name '{}' for state {}.".format(function_name, state_name)))
continue
- # Children should be parameters of "state.function"
- (parameter_errors, parameter_warnings) = lint_parameters(state_name + "." + function_name, function_calls_or_parameters, state_types[state_name][function_name])
- errors += parameter_errors
- warnings += parameter_warnings
+ parameters = function_calls_or_parameters
+ (perrors, pwarnings) = lint_parameters(state_name, function_name, parameters, state_mod)
+ errors += perrors
+ warnings += pwarnings
# Anything other than "state" or "state.function" is an error.
else:
- errors.append((value.lc.line + 1, "Expected <state> or <state>.<function>: Too many '.'s in {}".format(state_function_name)))
+ errors.append((value.lc.line + 1, "Expected <state> or <state>.<function>: Too many '.'s in {}.".format(state_function_name)))
continue
return(errors, warnings)
-
+