X-Git-Url: https://pwan.org/git/?p=salty_linter.git;a=blobdiff_plain;f=salty_linter%2F__init__.py;h=11e70ca8be062daf8d705dd698b2ffef7932e28c;hp=a5cf233dcde76b40b7bb276da7c34a06b024b2c3;hb=55d062e15f67f50f5bb15eb4a233851295b875da;hpb=9c0a853a3d3328c63336c6f62cde8f3b6404da47 diff --git a/salty_linter/__init__.py b/salty_linter/__init__.py index a5cf233..11e70ca 100644 --- a/salty_linter/__init__.py +++ b/salty_linter/__init__.py @@ -1,10 +1,12 @@ -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) @@ -12,67 +14,50 @@ def lint(sls_yaml, types): 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 "." 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 @@ -80,60 +65,70 @@ def lint(sls_yaml, types): 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 or .: Too many '.'s in {}".format(state_function_name))) + errors.append((value.lc.line + 1, "Expected or .: Too many '.'s in {}.".format(state_function_name))) continue return(errors, warnings) - +