tripleo-validations/.ansible-lint_rules/ValidationHasMetadataRule.py
Jiri Podivin 84c91cfcf4 Ansible lint custom rule expansion
Expands the existing ansible lint rule to aling
with proposed metadata changes.

Depends-On: https://review.opendev.org/c/openstack/validations-libs/+/800721

Signed-off-by: Jiri Podivin <jpodivin@redhat.com>
Change-Id: Ib5fc841e23001816612dcdfd8c3335ae99ba2722
2021-07-29 08:25:56 +02:00

198 lines
7.3 KiB
Python

import os
import yaml
from ansiblelint.errors import MatchError
from ansiblelint.rules import AnsibleLintRule
class ValidationHasMetadataRule(AnsibleLintRule):
id = '750'
shortdesc = 'Validation playbook must have mandatory metadata'
info = """
---
- hosts: localhost
vars:
metadata:
name: Validation Name
description: >
A full description of the validation.
groups:
- group1
- group2
- group3
categories:
- category1
- category2
- category3
products:
- product1
- product2
- product3
"""
description = (
"The Validation playbook must have mandatory metadata:\n"
"```{}```".format(info)
)
severity = 'HIGH'
tags = ['metadata']
no_vars_found = "The validation playbook must contain a 'vars' dictionary"
no_meta_found = (
"The validation playbook must contain "
"a 'metadata' dictionary under vars"
)
no_classification_found = \
"*metadata* should contain a list of {classification}"
unknown_classifications_found = (
"Unkown {classification_key}(s) '{unknown_classification}' found! "
"The official list of {classification_key} are '{known_classification}'. "
)
how_to_add_classification = {
'groups': (
"To add a new validation group, please add it in the groups.yaml "
"file at the root of the tripleo-validations project."
)
}
def get_classifications(self, classification='groups'):
"""Returns a list classification names
defined for tripleo-validations in the '{classification}.yaml' file
located in the base repo directory.
"""
file_path = os.path.abspath(classification + '.yaml')
try:
with open(file_path, "r") as definitions:
contents = yaml.safe_load(definitions)
except (PermissionError, OSError):
raise RuntimeError(
"{}.yaml file at '{}' inacessible.".format(
classification,
file_path))
results = [name for name, _ in contents.items()]
return results
def check_classification(self, metadata, path,
classification_key, strict=False):
"""Check validatity of validation classifications,
such as groups, categories and products.
This one is tricky.
Empty lists in python evaluate as false
So we can't just check for truth value of returned list.
Instead we have to compare the returned value with `None`.
"""
classification = metadata.get(classification_key, None)
if classification is None:
return MatchError(
message=self.no_classification_found.format(
classification=classification_key
),
filename=path,
details=str(metadata))
else:
if not isinstance(classification, list):
return MatchError(
message="*{}* must be a list".format(classification_key),
filename=path,
details=str(metadata))
elif strict:
classifications = self.get_classifications(classification_key)
unknown_classifications = list(
set(classification) - set(classifications))
if unknown_classifications:
message = self.unknown_classifications_found.format(
unknown_classification=unknown_classifications,
known_classification=classifications,
classification_key=classification_key)
message += self.how_to_add_classification.get(classification_key, "")
return MatchError(
message=message,
filename=path,
details=str(metadata))
def matchplay(self, file, data):
results = []
path = file['path']
if file['type'] == 'playbook':
if path.startswith("playbooks/") \
or "tripleo-validations/playbooks/" in path:
# *hosts* line check
hosts = data.get('hosts', None)
if not hosts:
results.append(
MatchError(
message="No *hosts* key found in the playbook",
filename=path,
details=str(data)))
# *vars* lines check
vars = data.get('vars', None)
if not vars:
results.append(
MatchError(
message=self.no_vars_found,
filename=path,
details=str(data)))
else:
if not isinstance(vars, dict):
results.append(
MatchError(
message='*vars* must be a dictionary',
filename=path,
details=str(data)))
# *metadata* lines check
metadata = data['vars'].get('metadata', None)
if metadata:
if not isinstance(metadata, dict):
results.append(
MatchError(
message='*metadata* must be a dictionary',
filename=path,
details=str(data)))
else:
results.append(
MatchError(
message=self.no_meta_found,
filename=path,
details=str(data)))
# *metadata>[name|description] lines check
for info in ['name', 'description']:
if not metadata.get(info, None):
results.append(
MatchError(
message='*metadata* must contain a %s key' % info,
filename=path,
details=str(data)))
continue
if not isinstance(metadata.get(info), str):
results.append(
MatchError(
message='*%s* should be a string' % info,
filename=path,
details=str(data)))
#Checks for metadata we use to classify validations.
#Groups, categories and products
for classification in ['categories', 'products', 'groups']:
classification_error = self.check_classification(
metadata,
path,
classification,
strict=(classification == 'groups'))
if classification_error:
results.append(classification_error)
return results