84c91cfcf4
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
198 lines
7.3 KiB
Python
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
|