-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Business logic validator plugin implementation #84
Conversation
Can you update the docs on how to use it? |
I figured it was worth waiting for agreement on the approach before adding docs, just in case we decide not to use this particular implementation. |
Just enough for me to understand, I don't actually know how it is working. |
peer_match = data[peer]["interfaces"][peer_int]["peer"] == normal_hostname(host) | ||
peer_int_match = data[peer]["interfaces"][peer_int]["peer_int"] == interface | ||
if not (peer_match and peer_int_match): | ||
raise ValidationError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we should return a list of ValidationResult here instead of expecting an exception to be thrown. It allows the validators to flag more than 1 error at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking out loud, would be useful to provide something in the ModelValidation
class to generate/store ValidationResult easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't updated the check_peer examples to return ValidationResults since cross-host correlation isn't currently supported. The check_interface example is what is supported and it returns a ValidationResult.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of helper functions for generating/storing ValidationResult. I'll have to think about a good way to implement that.
@chipn I forgot to mention it but it's great to see so much progress already and it's exciting to add this capabilities to the schema-enforcer 👍 |
Yes, agreed!! Really great work with little guidance |
docs/custom_validators.md
Outdated
id = "CheckInterface" | ||
left = "interfaces.*[@.type=='core'][] | length([?@])" | ||
right = 2 | ||
operator = "eq" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to support specifying left/right/operator as a single combined string, such as "interfaces.*[@.type=='core'][] | length([?@]) eq 2"
? That seems like it'd be more user-friendly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to support this format, but I couldn't think of a good way to do so since jmespath expressions can be complex and contain spaces/pipes. Any thoughts on how to implement it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably fine to defer to a later PR, but off the top of my head can we regex around the supported operator strings (plus surrounding whitespace)? Something like r"(.*) (eq|lt|gt|lte|gte|ne) (.*)"
? Or can these operators potentially be used within left/right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is likely a valid use case. The optimization doesn’t seem like too much to me, would prefer to do something similar in a json/yaml file outside of python if we are going to spend efforts on it.
"eq": lambda r, v: r == v, | ||
"lt": lambda r, v: int(r) < int(v), | ||
"lte": lambda r, v: int(r) <= int(v), | ||
"contains": lambda r, v: v in r, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we support the actual operators (">", ">=", etc.) as well? Or is that not useful/desirable in a jmespath context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can support the actual operators. However, I wonder if that may be confusing. "gt" is obviously a string, but someone may try to use > (unquoted). Easy to correct by adding quotes, but maybe not always obvious?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah agreed; I think we'd only want to do this if we're able to combine the "left", "right", and "operator" parameters into a single string as discussed above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me!
schema_enforcer/schemas/manager.py
Outdated
@@ -43,6 +44,11 @@ def __init__(self, config): | |||
schema = self.create_schema_from_file(root, filename) | |||
self.schemas[schema.get_id()] = schema | |||
|
|||
# Load validators | |||
full_validator_dir = f"{config.validator_directory}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should remove the fstring since it's not used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't our tests catching this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Chip, looks great
|
||
* `top_level_properties`: Field for mapping of validator to data | ||
* `id`: Schema ID to use for reporting purposes (optional - defaults to class name) | ||
* `left`: Jmespath expression to query your host data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am the one who indicated left/right, but I did so out of lack of imagination. Just want to make sure we are all good with the name here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option is lhs (left-hand side) and rhs (right-hand side).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left / right sounds good to me
@@ -303,8 +303,9 @@ def ansible( | |||
data = hostvars | |||
|
|||
# Validate host vars against schema | |||
for result in schema_obj.validate(data=data, strict=strict): | |||
schema_obj.validate(data=data, strict=strict) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please can you make the same changes to the validate
command , otherwise validators won't work for this command
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validate CLI command calls validate on the InstanceFile object. The InstanceFile validate method was refactored for the new approach:
schema-enforcer/schema_enforcer/instances/file.py
Lines 128 to 134 in 7d557ef
for schema_id, schema in schema_manager.iter_schemas(): | |
if schema_id not in self.matches: | |
continue | |
schema.validate(self.get_content(), strict) | |
results = schema.get_results() | |
errs = itertools.chain(errs, results) | |
schema.clear_results() |
|
||
* `top_level_properties`: Field for mapping of validator to data | ||
* `id`: Schema ID to use for reporting purposes (optional - defaults to class name) | ||
* `left`: Jmespath expression to query your host data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left / right sounds good to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! LGTM. A few comments in line.
@@ -0,0 +1,158 @@ | |||
# Implementing custom validators | |||
|
|||
With custom validators, you can implement business logic in Python. Schema-enforcer will automatically |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An intro of what's meant by "business logic" might be valuable here to set proper context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pinging myself to review
|
||
class CheckInterface(JmesPathModelValidation): # pylint: disable=too-few-public-methods | ||
"""Check that each device has more than one core uplink.""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think pydocstyle requires no newline here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No newline after the comment? I vaguely remember that pydocstyle complained when it isn't in a single line or you miss the period at the end. This validator is also in the test directory and pydocstyle is passing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack. That's interesting -- I've seen pydocstyle flag when a function has space between the docstring and the beginning of the logic executed by the function. It may just be on multiline docstrings though.. IDK. If pydocstyle isn't complaining I'm good :).
def get_results(self) -> list[ValidationResult]: | ||
"""Return all validation results for this validator.""" | ||
if not self._results: | ||
self._results.append(ValidationResult(result="PASS", schema_id=self.id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat adjacent to #46, If there are no validation results, do we want to print a log message indicating that nothing was validated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May defer to @dgarros here. I assumed the intent was to support the use case of no failure is equal to a pass, but that doesn't really fit the current runtime model.
Resolves #24