From 12af1a8e208b24f4413c9a6c16822e50ef8f1837 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Fri, 20 Nov 2020 14:25:01 -0800 Subject: [PATCH 01/22] Initial Commit --- examples/ansible/group_vars/leaf.yml | 7 ++- examples/ansible/group_vars/nyc.yml | 3 - examples/ansible/group_vars/spine.yml | 4 ++ examples/ansible/pyproject.toml | 2 +- schema_enforcer/ansible_inventory.py | 30 ++++++++++ schema_enforcer/cli.py | 80 +++++++++++++++++++-------- schema_enforcer/schemas/jsonschema.py | 5 +- schema_enforcer/validation.py | 24 ++++++-- tests/test_ansible_inventory.py | 1 - 9 files changed, 119 insertions(+), 37 deletions(-) diff --git a/examples/ansible/group_vars/leaf.yml b/examples/ansible/group_vars/leaf.yml index 4249e1a..d0f2e8c 100644 --- a/examples/ansible/group_vars/leaf.yml +++ b/examples/ansible/group_vars/leaf.yml @@ -1,4 +1,9 @@ --- dns_servers: - - address: 12 + - address: "10.1.1.1" - address: "10.2.2.2" + +schema_enforcer_schemas: + - "schemas/dns_servers" + +schema_enforcer_strict: true diff --git a/examples/ansible/group_vars/nyc.yml b/examples/ansible/group_vars/nyc.yml index 131ef04..ed97d53 100644 --- a/examples/ansible/group_vars/nyc.yml +++ b/examples/ansible/group_vars/nyc.yml @@ -1,4 +1 @@ --- -jsonschema_mapping: - dns_servers: ["schemas/dns_servers"] - interfaces: ["schemas/interfaces"] diff --git a/examples/ansible/group_vars/spine.yml b/examples/ansible/group_vars/spine.yml index 53b3c25..04417a6 100644 --- a/examples/ansible/group_vars/spine.yml +++ b/examples/ansible/group_vars/spine.yml @@ -7,3 +7,7 @@ interfaces: role: "uplink" swp2: role: "uplink" + +schema_enforcer_schemas: + - "schemas/dns_servers" + - "schemas/interfaces" diff --git a/examples/ansible/pyproject.toml b/examples/ansible/pyproject.toml index 597b5f9..d318418 100644 --- a/examples/ansible/pyproject.toml +++ b/examples/ansible/pyproject.toml @@ -1,2 +1,2 @@ -[tool.jsonschema_testing] +[tool.schema_enforcer] ansible_inventory = "inventory.ini" \ No newline at end of file diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 095df54..67c9d7d 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -77,6 +77,7 @@ def get_clean_host_vars(self, host): "groups", "omit", "ansible_version", + "ansible_config_file", ] hostvars = self.get_host_vars(host) @@ -86,3 +87,32 @@ def get_clean_host_vars(self, host): del hostvars[key] return hostvars + + @staticmethod + def get_applicable_schemas(hostvars, smgr, mapping): + """Get applicable schemas. + + Search an explicit mapping to determine the schemas which should be used to validate hostvars + for a given host. + + If an explicit mapping is not defined, correlate top level keys in the structured data with top + level properties in the schema to acquire applicable schemas. + + Args: + hostvars (dict): dictionary of cleaned host vars which will be evaluated against schema + + Returns: + applicable_schemas (dict): dictionary mapping schema_id to schema obj for all applicable schemas + """ + applicable_schemas = {} + for key in hostvars.keys(): + if mapping and key in mapping: + applicable_schemas = {schema_id: smgr.schemas[schema_id] for schema_id in mapping[key]} + else: + applicable_schemas = { + schema.id: smgr.schemas[schema.id] + for schema in smgr.schemas.values() + if key in schema.top_level_properties + } + + return applicable_schemas diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index 01b5656..89d76b6 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -163,7 +163,7 @@ def schema(check, generate_invalid, list_schemas): # noqa: D417 @click.option("--inventory", "-i", help="Ansible inventory file.", required=False) @click.option("--host", "-h", "limit", help="Limit the execution to a single host.", required=False) @click.option("--show-pass", default=False, help="Shows validation checks that passed", is_flag=True, show_default=True) -def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,too-many-locals +def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,too-many-locals,too-many-statements r"""Validate the hostvar for all hosts within an Ansible inventory. The hostvar are dynamically rendered based on groups. @@ -231,39 +231,71 @@ def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,t continue # Generate host_var and automatically remove all keys inserted by ansible - hostvar = inv.get_clean_host_vars(host) + hostvars = inv.get_clean_host_vars(host) - # if jsonschema_mapping variable is defined, used it to determine which schema to use to validate each key - # if jsonschema_mapping is not defined, validate each key in the inventory agains all schemas in the SchemaManager + # Extrapt mapping from hostvar setting mapping = None - if "jsonschema_mapping" in hostvar: - mapping = hostvar["jsonschema_mapping"] - del hostvar["jsonschema_mapping"] + if "schema_enforcer_schemas" in hostvars: + if not isinstance(hostvars["schema_enforcer_schemas"], list): + raise TypeError(f"'schema_enforcer_schemas' attribute defined for {host.name} must be of type list") + mapping = hostvars["schema_enforcer_schemas"] + del hostvars["schema_enforcer_schemas"] + + # extract whether to use a strict validator or a loose validator from hostvar setting + schema_enforcer_strict = False + if "schema_enforcer_strict" in hostvars: + if not isinstance(hostvars["schema_enforcer_strict"], bool): + raise TypeError(f"'schema_enforcer_strict' attribute defined for {host.name} must be of type bool") + schema_enforcer_strict = hostvars["schema_enforcer_strict"] + del hostvars["schema_enforcer_strict"] + + # Raise error if settings are set incorrectly + if schema_enforcer_strict and not mapping: + msg = ( + f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter does not declare a schema id. " + "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + ) + raise ValueError(msg) + + if schema_enforcer_strict and mapping and len(mapping) > 1: + if mapping: + msg = f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter declares more than one schema id. " + msg += "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + raise ValueError(msg) + + # Acquire schemas applicable to the given host + applicable_schemas = inv.get_applicable_schemas(hostvars, smgr, mapping) - applicable_schemas = {} + for _, schema_obj in applicable_schemas.items(): + # Combine host attributes to those defined in top level of schema + if not schema_enforcer_strict: + data = dict() + for var in schema_obj.top_level_properties: + data.update({var: hostvars.get(var)}) - error_exists = False - for key, value in hostvar.items(): - if mapping and key in mapping.keys(): - applicable_schemas = {schema_id: smgr.schemas[schema_id] for schema_id in mapping[key]} - else: - applicable_schemas = smgr.schemas + # If the schema_enforcer_strict bool is set, hostvars should match a single schema exactly, + # Thus, we do not want to extract only those vars which are defined in schema properties out + # of the vars passed in. + if schema_enforcer_strict: + data = hostvars - for _, schema_obj in applicable_schemas.items(): - for result in schema_obj.validate({key: value}): + # Validate host vars against schema + for result in schema_obj.validate(data=data, strict=schema_enforcer_strict): - result.instance_type = "VAR" - result.instance_name = key - result.instance_location = host.name + result.instance_type = "HOST" + result.instance_hostname = host.name - if not result.passed(): - error_exists = True - result.print() + if not result.passed(): + error_exists = True + result.print() - elif result.passed() and show_pass: - result.print() + elif result.passed() and show_pass: + result.print() if not error_exists: print(colored("ALL SCHEMA VALIDATION CHECKS PASSED", "green")) else: sys.exit(1) + + +# def get_schema_id_from_property(smgr, ) diff --git a/schema_enforcer/schemas/jsonschema.py b/schema_enforcer/schemas/jsonschema.py index c856390..27151af 100644 --- a/schema_enforcer/schemas/jsonschema.py +++ b/schema_enforcer/schemas/jsonschema.py @@ -16,7 +16,7 @@ class JsonSchema: schematype = "jsonchema" def __init__(self, schema, filename, root): - """Initiliz a new JsonSchema object from a dict. + """Initilize a new JsonSchema object from a dict. Args: schema (dict): Data representing the schema. Must be jsonschema valid @@ -27,6 +27,9 @@ def __init__(self, schema, filename, root): self.root = root self.data = schema self.id = self.data.get("$id") # pylint: disable=invalid-name + self.top_level_properties = [ + prop for prop in self.data.get("properties") # pylint: disable=unnecessary-comprehension + ] self.validator = None self.strict_validator = None diff --git a/schema_enforcer/validation.py b/schema_enforcer/validation.py index f8c87c0..44fc8df 100644 --- a/schema_enforcer/validation.py +++ b/schema_enforcer/validation.py @@ -19,6 +19,7 @@ class ValidationResult(BaseModel): instance_name: Optional[str] instance_location: Optional[str] instance_type: Optional[str] + instance_hostname: Optional[str] source: Any = None strict: bool = False @@ -53,12 +54,23 @@ def print(self): def print_failed(self): """Print the result of the test to CLI when the test failed.""" - print( - colored("FAIL", "red") + f" | [ERROR] {self.message}" - f" [{self.instance_type}] {self.instance_location}/{self.instance_name}" - f" [PROPERTY] {':'.join(str(item) for item in self.absolute_path)}" - ) + # Construct the message dynamically based on the instance_type + msg = colored("FAIL", "red") + f" | [ERROR] {self.message}" + if self.instance_type == "FILE": + msg += f" [{self.instance_type}] {self.instance_location}/{self.instance_name}" + + if self.instance_type == "HOST": + msg += f" [{self.instance_type}] {self.instance_hostname}" + + msg += f" [PROPERTY] {':'.join(str(item) for item in self.absolute_path)}" + + # print the msg + print(msg) def print_passed(self): """Print the result of the test to CLI when the test passed.""" - print(colored("PASS", "green") + f" [{self.instance_type}] {self.instance_location}/{self.instance_name}") + if self.instance_type == "FILE": + print(colored("PASS", "green") + f" [{self.instance_type}] {self.instance_location}/{self.instance_name}") + + if self.instance_type == "HOST": + print(colored("PASS", "green") + f" [{self.instance_type}] {self.instance_hostname}") diff --git a/tests/test_ansible_inventory.py b/tests/test_ansible_inventory.py index 844884b..a793266 100644 --- a/tests/test_ansible_inventory.py +++ b/tests/test_ansible_inventory.py @@ -83,5 +83,4 @@ def test_get_clean_host_vars(ansible_inv): } host3 = ansible_inv.inv_mgr.get_host("host3") host3_cleaned_vars = ansible_inv.get_clean_host_vars(host3) - host3_cleaned_vars.pop("ansible_config_file") assert expected == host3_cleaned_vars From 33ccecdfe3699eb48fc67aea8ca87e8747bd78af Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Fri, 20 Nov 2020 16:11:15 -0800 Subject: [PATCH 02/22] Refactor ansible.py functions --- .../ansible/schema/schemas/interfaces.yml | 2 + schema_enforcer/ansible_inventory.py | 54 +++++++++++++++++++ schema_enforcer/cli.py | 50 ++++------------- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/examples/ansible/schema/schemas/interfaces.yml b/examples/ansible/schema/schemas/interfaces.yml index 9598984..8292876 100644 --- a/examples/ansible/schema/schemas/interfaces.yml +++ b/examples/ansible/schema/schemas/interfaces.yml @@ -13,3 +13,5 @@ properties: type: "string" description: type: "string" + role: + type: "string" diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 67c9d7d..6fcf3eb 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -78,6 +78,8 @@ def get_clean_host_vars(self, host): "omit", "ansible_version", "ansible_config_file", + "schema_enforcer_schemas", + "schema_enforcer_strict", ] hostvars = self.get_host_vars(host) @@ -116,3 +118,55 @@ def get_applicable_schemas(hostvars, smgr, mapping): } return applicable_schemas + + def get_schema_validation_settings(self, host): + """Parse Ansible Schema Validation Settings from a host object. + + Validate settings to ensure an error is raised in the event an invalid parameter is + configured in the host file. + + Args: + host (AnsibleInventory.host): Ansible Inventory Host Object + + Raises: + TypeError: Raised when one of the scehma configuration parameters is of the wrong type + ValueError: Raised when one of the schema configuration parameters is incorrectly configured + + Returns: + mapping (list): List of schema IDs against which to validate ansible vars + strict (bool): Whether or not to use strict validation while validating the schema + """ + # Generate host_var and automatically remove all keys inserted by ansible + hostvars = self.get_host_vars(host) + + # Extract mapping from hostvar setting + mapping = None + if "schema_enforcer_schemas" in hostvars: + if not isinstance(hostvars["schema_enforcer_schemas"], list): + raise TypeError(f"'schema_enforcer_schemas' attribute defined for {host.name} must be of type list") + mapping = hostvars["schema_enforcer_schemas"] + del hostvars["schema_enforcer_schemas"] + + # Extract whether to use a strict validator or a loose validator from hostvar setting + strict = False + if "schema_enforcer_strict" in hostvars: + if not isinstance(hostvars["schema_enforcer_strict"], bool): + raise TypeError(f"'schema_enforcer_strict' attribute defined for {host.name} must be of type bool") + strict = hostvars["schema_enforcer_strict"] + del hostvars["schema_enforcer_strict"] + + # Raise error if settings are set incorrectly + if strict and not mapping: + msg = ( + f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter does not declare a schema id. " + "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + ) + raise ValueError(msg) + + if strict and mapping and len(mapping) > 1: + if mapping: + msg = f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter declares more than one schema id. " + msg += "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + raise ValueError(msg) + + return mapping, strict diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index 89d76b6..d5b9192 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -230,57 +230,30 @@ def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,t if limit and host.name != limit: continue - # Generate host_var and automatically remove all keys inserted by ansible + # Acquire Host Variables hostvars = inv.get_clean_host_vars(host) - # Extrapt mapping from hostvar setting - mapping = None - if "schema_enforcer_schemas" in hostvars: - if not isinstance(hostvars["schema_enforcer_schemas"], list): - raise TypeError(f"'schema_enforcer_schemas' attribute defined for {host.name} must be of type list") - mapping = hostvars["schema_enforcer_schemas"] - del hostvars["schema_enforcer_schemas"] - - # extract whether to use a strict validator or a loose validator from hostvar setting - schema_enforcer_strict = False - if "schema_enforcer_strict" in hostvars: - if not isinstance(hostvars["schema_enforcer_strict"], bool): - raise TypeError(f"'schema_enforcer_strict' attribute defined for {host.name} must be of type bool") - schema_enforcer_strict = hostvars["schema_enforcer_strict"] - del hostvars["schema_enforcer_strict"] - - # Raise error if settings are set incorrectly - if schema_enforcer_strict and not mapping: - msg = ( - f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter does not declare a schema id. " - "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." - ) - raise ValueError(msg) - - if schema_enforcer_strict and mapping and len(mapping) > 1: - if mapping: - msg = f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter declares more than one schema id. " - msg += "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." - raise ValueError(msg) + # Acquire validation settings for the given host + mapping, strict = inv.get_schema_validation_settings(host) # Acquire schemas applicable to the given host applicable_schemas = inv.get_applicable_schemas(hostvars, smgr, mapping) for _, schema_obj in applicable_schemas.items(): - # Combine host attributes to those defined in top level of schema - if not schema_enforcer_strict: + # Combine host attributes into a single data structure matching to properties defined at the top level of the schema definition + if not strict: data = dict() for var in schema_obj.top_level_properties: data.update({var: hostvars.get(var)}) - # If the schema_enforcer_strict bool is set, hostvars should match a single schema exactly, - # Thus, we do not want to extract only those vars which are defined in schema properties out - # of the vars passed in. - if schema_enforcer_strict: + # If the schema_enforcer_strict bool is set, hostvars should match a single schema exactly. + # Thus, we want to pass the entirety of the cleaned host vars into the validate method rather + # than creating a data structure with only the top level vars defined by the schema. + if strict: data = hostvars # Validate host vars against schema - for result in schema_obj.validate(data=data, strict=schema_enforcer_strict): + for result in schema_obj.validate(data=data, strict=strict): result.instance_type = "HOST" result.instance_hostname = host.name @@ -296,6 +269,3 @@ def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,t print(colored("ALL SCHEMA VALIDATION CHECKS PASSED", "green")) else: sys.exit(1) - - -# def get_schema_id_from_property(smgr, ) From b26f0ef1df9c916fc50d65bb00e396166969790f Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sat, 21 Nov 2020 01:55:40 -0800 Subject: [PATCH 03/22] Update schema-enforcer ansible command --- examples/ansible/group_vars/leaf.yml | 5 +- examples/ansible/group_vars/spine.yml | 6 +- schema_enforcer/ansible_inventory.py | 97 ++++++++++++++++++++------- schema_enforcer/cli.py | 22 ++++-- schema_enforcer/exceptions.py | 17 +++++ schema_enforcer/schemas/manager.py | 12 ++++ schema_enforcer/validation.py | 4 +- 7 files changed, 127 insertions(+), 36 deletions(-) create mode 100644 schema_enforcer/exceptions.py diff --git a/examples/ansible/group_vars/leaf.yml b/examples/ansible/group_vars/leaf.yml index d0f2e8c..a786313 100644 --- a/examples/ansible/group_vars/leaf.yml +++ b/examples/ansible/group_vars/leaf.yml @@ -3,7 +3,6 @@ dns_servers: - address: "10.1.1.1" - address: "10.2.2.2" -schema_enforcer_schemas: - - "schemas/dns_servers" +# schema_enforcer_schema_ids: +# - "schemas/dns_servers" -schema_enforcer_strict: true diff --git a/examples/ansible/group_vars/spine.yml b/examples/ansible/group_vars/spine.yml index 04417a6..ead0c60 100644 --- a/examples/ansible/group_vars/spine.yml +++ b/examples/ansible/group_vars/spine.yml @@ -1,6 +1,6 @@ --- dns_servers: - - address: "10.1.1.1" + - address: false - address: "10.2.2.2" interfaces: swp1: @@ -8,6 +8,8 @@ interfaces: swp2: role: "uplink" -schema_enforcer_schemas: +schema_enforcer_schema_ids: - "schemas/dns_servers" - "schemas/interfaces" + +schema_enforcer_automap_default: true \ No newline at end of file diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 6fcf3eb..9114170 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -3,6 +3,10 @@ from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.template import Templar +from termcolor import colored +import logging + +logger = logging.getLogger(__name__) # Referenced https://github.com/fgiorgetti/qpid-dispatch-tests/ for the below class @@ -78,8 +82,9 @@ def get_clean_host_vars(self, host): "omit", "ansible_version", "ansible_config_file", - "schema_enforcer_schemas", + "schema_enforcer_schema_ids", "schema_enforcer_strict", + "schema_enforcer_automap_default", ] hostvars = self.get_host_vars(host) @@ -91,7 +96,7 @@ def get_clean_host_vars(self, host): return hostvars @staticmethod - def get_applicable_schemas(hostvars, smgr, mapping): + def get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap): """Get applicable schemas. Search an explicit mapping to determine the schemas which should be used to validate hostvars @@ -102,20 +107,26 @@ def get_applicable_schemas(hostvars, smgr, mapping): Args: hostvars (dict): dictionary of cleaned host vars which will be evaluated against schema + smgr (schema_enforcer.schemas.manager.SchemaManager): SchemaManager object + declared_schema_ids: list of declared schema IDs inferred from schema_enforcer_schemas variable + automap: Returns: applicable_schemas (dict): dictionary mapping schema_id to schema obj for all applicable schemas """ applicable_schemas = {} for key in hostvars.keys(): - if mapping and key in mapping: - applicable_schemas = {schema_id: smgr.schemas[schema_id] for schema_id in mapping[key]} - else: - applicable_schemas = { - schema.id: smgr.schemas[schema.id] - for schema in smgr.schemas.values() - if key in schema.top_level_properties - } + # extract applicable schema ID to JsonSchema objects if schema_ids are declared + if declared_schema_ids: + for schema_id in declared_schema_ids: + applicable_schemas[schema_id] = smgr.schemas[schema_id] + + # extract applicable schema ID to JsonSchema objects based on host var to top level property mapping. + if not declared_schema_ids and automap: + for schema in smgr.schemas.values(): + if key in schema.top_level_properties: + applicable_schemas[schema.id] = schema + continue return applicable_schemas @@ -139,13 +150,12 @@ def get_schema_validation_settings(self, host): # Generate host_var and automatically remove all keys inserted by ansible hostvars = self.get_host_vars(host) - # Extract mapping from hostvar setting - mapping = None - if "schema_enforcer_schemas" in hostvars: - if not isinstance(hostvars["schema_enforcer_schemas"], list): - raise TypeError(f"'schema_enforcer_schemas' attribute defined for {host.name} must be of type list") - mapping = hostvars["schema_enforcer_schemas"] - del hostvars["schema_enforcer_schemas"] + # Extract declared_schema_ids from hostvar setting + declared_schema_ids = [] + if "schema_enforcer_schema_ids" in hostvars: + if not isinstance(hostvars["schema_enforcer_schema_ids"], list): + raise TypeError(f"'schema_enforcer_schema_ids' attribute defined for {host.name} must be of type list") + declared_schema_ids = hostvars["schema_enforcer_schema_ids"] # Extract whether to use a strict validator or a loose validator from hostvar setting strict = False @@ -153,20 +163,57 @@ def get_schema_validation_settings(self, host): if not isinstance(hostvars["schema_enforcer_strict"], bool): raise TypeError(f"'schema_enforcer_strict' attribute defined for {host.name} must be of type bool") strict = hostvars["schema_enforcer_strict"] - del hostvars["schema_enforcer_strict"] + + automap = True + if "schema_enforcer_automap_default" in hostvars: + if not isinstance(hostvars["schema_enforcer_automap_default"], bool): + raise TypeError(f"'schema_enforcer_automap_default' attribute defined for {host.name} must be of type bool") + automap = hostvars["schema_enforcer_automap_default"] # Raise error if settings are set incorrectly - if strict and not mapping: + if strict and not declared_schema_ids: msg = ( - f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter does not declare a schema id. " + f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schema_ids' parameter does not declare a schema id. " "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." ) raise ValueError(msg) - if strict and mapping and len(mapping) > 1: - if mapping: - msg = f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schemas' parameter declares more than one schema id. " - msg += "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + if strict and declared_schema_ids and len(declared_schema_ids) > 1: + msg = ( + f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schema_ids' parameter declares more than one schema id. " + "The 'schema_enforcer_schema_ids' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + ) raise ValueError(msg) - return mapping, strict + return declared_schema_ids, strict, automap + + def print_schema_mapping(self, hosts, limit, smgr): + print_dict = {} + for host in hosts: + if limit and host.name != limit: + continue + + # Get hostvars + hostvars = self.get_clean_host_vars(host) + + # Acquire validation settings for the given host + declared_schema_ids, strict, automap = self.get_schema_validation_settings(host) + + # Validate declared schemas exist + smgr.validate_schemas_exist(declared_schema_ids) + + # Acquire schemas applicable to the given host + applicable_schemas = self.get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap) + + # Add an element to the print dict for this host + print_dict[host.name] = [schema_id for schema_id in applicable_schemas.keys()] + + if print_dict: + print("{:25} Schema ID".format("Ansible Host")) + print("-" * 80) + print_strings = [] + for hostname, schema_ids in print_dict.items(): + print_strings.append(f"{hostname:25} {schema_ids}") + print("\n".join(sorted(print_strings))) + + diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index d5b9192..ca8c4d9 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -34,7 +34,7 @@ def main(): @click.option( "--show-checks", default=False, - help="Shows the schemas to be checked for each instance file", + help="Shows the schemas to be checked for each structured data file", is_flag=True, show_default=True, ) @@ -163,7 +163,14 @@ def schema(check, generate_invalid, list_schemas): # noqa: D417 @click.option("--inventory", "-i", help="Ansible inventory file.", required=False) @click.option("--host", "-h", "limit", help="Limit the execution to a single host.", required=False) @click.option("--show-pass", default=False, help="Shows validation checks that passed", is_flag=True, show_default=True) -def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,too-many-locals,too-many-statements +@click.option( + "--show-checks", + default=False, + help="Shows the schemas to be checked for each ansible host", + is_flag=True, + show_default=True, +) +def ansible(inventory, limit, show_pass, show_checks): # pylint: disable=too-many-branches,too-many-locals,too-many-statements r"""Validate the hostvar for all hosts within an Ansible inventory. The hostvar are dynamically rendered based on groups. @@ -224,6 +231,10 @@ def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,t hosts = inv.get_hosts_containing() print(f"Found {len(hosts)} hosts in the inventory") + if show_checks: + inv.print_schema_mapping(hosts, limit, smgr) + sys.exit(0) + error_exists = False for host in hosts: @@ -234,10 +245,13 @@ def ansible(inventory, limit, show_pass): # pylint: disable=too-many-branches,t hostvars = inv.get_clean_host_vars(host) # Acquire validation settings for the given host - mapping, strict = inv.get_schema_validation_settings(host) + declared_schema_ids, strict, automap = inv.get_schema_validation_settings(host) + + # Validate declared schemas exist + smgr.validate_schemas_exist(declared_schema_ids) # Acquire schemas applicable to the given host - applicable_schemas = inv.get_applicable_schemas(hostvars, smgr, mapping) + applicable_schemas = inv.get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap) for _, schema_obj in applicable_schemas.items(): # Combine host attributes into a single data structure matching to properties defined at the top level of the schema definition diff --git a/schema_enforcer/exceptions.py b/schema_enforcer/exceptions.py new file mode 100644 index 0000000..8b95b5c --- /dev/null +++ b/schema_enforcer/exceptions.py @@ -0,0 +1,17 @@ +"""Exception classes used in Schema Enforcer. + +Copyright (c) 2020 Network To Code, LLC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +class SchemaNotDefinedError(Exception): + """Raised when a schema is declared but not defiled""" \ No newline at end of file diff --git a/schema_enforcer/schemas/manager.py b/schema_enforcer/schemas/manager.py index 03cbbee..c3242c6 100644 --- a/schema_enforcer/schemas/manager.py +++ b/schema_enforcer/schemas/manager.py @@ -221,3 +221,15 @@ def generate_invalid_tests_expected(self, schema_id): def _get_test_directory(self): """Return the path to the main schema test directory.""" return f"{self.config.main_directory}/{self.config.test_directory}" + + def validate_schemas_exist(self, schema_ids): + """Validate that each schema ID in a list of schema IDs exists. + + Args: schema_ids (list): A list of schema IDs, each of which should exist as a schema object + """ + + if not isinstance(schema_ids, list): + raise TypeError("schema_ids argument passed into validate_schemas_exist must be of type list") + for schema_id in schema_ids: + if not self.schemas.get(schema_id, None): + raise SchemaNotDefined(f"Schema ID {schema_id} declared but not defined") diff --git a/schema_enforcer/validation.py b/schema_enforcer/validation.py index 44fc8df..aae0d28 100644 --- a/schema_enforcer/validation.py +++ b/schema_enforcer/validation.py @@ -70,7 +70,7 @@ def print_failed(self): def print_passed(self): """Print the result of the test to CLI when the test passed.""" if self.instance_type == "FILE": - print(colored("PASS", "green") + f" [{self.instance_type}] {self.instance_location}/{self.instance_name}") + print(colored("PASS", "green") + f" | [{self.instance_type}] {self.instance_location}/{self.instance_name}") if self.instance_type == "HOST": - print(colored("PASS", "green") + f" [{self.instance_type}] {self.instance_hostname}") + print(colored("PASS", "green") + f" | [{self.instance_type}] {self.instance_hostname} [SCHEMA ID] {self.schema_id}") From 0e61e21cb5764f2d73c4986c60d88c02a5d20464 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 23 Nov 2020 12:25:35 -0800 Subject: [PATCH 04/22] Update code to make linting tests pass --- schema_enforcer/ansible_inventory.py | 32 +++++++++++++++++----------- schema_enforcer/cli.py | 10 +++++++-- schema_enforcer/exceptions.py | 7 ++++-- schema_enforcer/schemas/manager.py | 4 ++-- schema_enforcer/validation.py | 5 ++++- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 9114170..b7d3b25 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -3,10 +3,6 @@ from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.template import Templar -from termcolor import colored -import logging - -logger = logging.getLogger(__name__) # Referenced https://github.com/fgiorgetti/qpid-dispatch-tests/ for the below class @@ -144,8 +140,7 @@ def get_schema_validation_settings(self, host): ValueError: Raised when one of the schema configuration parameters is incorrectly configured Returns: - mapping (list): List of schema IDs against which to validate ansible vars - strict (bool): Whether or not to use strict validation while validating the schema + (dict): Dict of validation settings with keys "declared_schema_ids", "strict", and "automap" """ # Generate host_var and automatically remove all keys inserted by ansible hostvars = self.get_host_vars(host) @@ -167,7 +162,9 @@ def get_schema_validation_settings(self, host): automap = True if "schema_enforcer_automap_default" in hostvars: if not isinstance(hostvars["schema_enforcer_automap_default"], bool): - raise TypeError(f"'schema_enforcer_automap_default' attribute defined for {host.name} must be of type bool") + raise TypeError( + f"'schema_enforcer_automap_default' attribute defined for {host.name} must be of type bool" + ) automap = hostvars["schema_enforcer_automap_default"] # Raise error if settings are set incorrectly @@ -185,9 +182,20 @@ def get_schema_validation_settings(self, host): ) raise ValueError(msg) - return declared_schema_ids, strict, automap + return { + "declared_schema_ids": declared_schema_ids, + "strict": strict, + "automap": automap, + } def print_schema_mapping(self, hosts, limit, smgr): + """Print host to schema IDs mapping. + + Args: + hosts (list): A list of ansible.inventory.host.Host objects for which the mapping should be printed + limit (str): The host to which to limit the search + smgr (schema_enforcer.schemas.manager.SchemaManager): Schema manager which handles schema objects + """ print_dict = {} for host in hosts: if limit and host.name != limit: @@ -197,7 +205,9 @@ def print_schema_mapping(self, hosts, limit, smgr): hostvars = self.get_clean_host_vars(host) # Acquire validation settings for the given host - declared_schema_ids, strict, automap = self.get_schema_validation_settings(host) + schema_validation_settings = self.get_schema_validation_settings(host) + declared_schema_ids = schema_validation_settings["declared_schema_ids"] + automap = schema_validation_settings["automap"] # Validate declared schemas exist smgr.validate_schemas_exist(declared_schema_ids) @@ -206,7 +216,7 @@ def print_schema_mapping(self, hosts, limit, smgr): applicable_schemas = self.get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap) # Add an element to the print dict for this host - print_dict[host.name] = [schema_id for schema_id in applicable_schemas.keys()] + print_dict[host.name] = list(applicable_schemas.keys()) if print_dict: print("{:25} Schema ID".format("Ansible Host")) @@ -215,5 +225,3 @@ def print_schema_mapping(self, hosts, limit, smgr): for hostname, schema_ids in print_dict.items(): print_strings.append(f"{hostname:25} {schema_ids}") print("\n".join(sorted(print_strings))) - - diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index ca8c4d9..ef9ef25 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -170,7 +170,9 @@ def schema(check, generate_invalid, list_schemas): # noqa: D417 is_flag=True, show_default=True, ) -def ansible(inventory, limit, show_pass, show_checks): # pylint: disable=too-many-branches,too-many-locals,too-many-statements +def ansible( + inventory, limit, show_pass, show_checks +): # pylint: disable=too-many-branches,too-many-locals,too-many-locals r"""Validate the hostvar for all hosts within an Ansible inventory. The hostvar are dynamically rendered based on groups. @@ -182,6 +184,7 @@ def ansible(inventory, limit, show_pass, show_checks): # pylint: disable=too-ma inventory (string): The name of the inventory file to validate against limit (string, None): Name of a host to limit the execution to show_pass (bool): Shows validation checks that passed Default to False + show_checks (book): Shows the schema checks each host will be evaluated against Example: $ cd examples/ansible @@ -245,7 +248,10 @@ def ansible(inventory, limit, show_pass, show_checks): # pylint: disable=too-ma hostvars = inv.get_clean_host_vars(host) # Acquire validation settings for the given host - declared_schema_ids, strict, automap = inv.get_schema_validation_settings(host) + schema_validation_settings = inv.get_schema_validation_settings(host) + declared_schema_ids = schema_validation_settings["declared_schema_ids"] + strict = schema_validation_settings["strict"] + automap = schema_validation_settings["automap"] # Validate declared schemas exist smgr.validate_schemas_exist(declared_schema_ids) diff --git a/schema_enforcer/exceptions.py b/schema_enforcer/exceptions.py index 8b95b5c..1665c72 100644 --- a/schema_enforcer/exceptions.py +++ b/schema_enforcer/exceptions.py @@ -13,5 +13,8 @@ """ -class SchemaNotDefinedError(Exception): - """Raised when a schema is declared but not defiled""" \ No newline at end of file +class SchemaNotDefined(Exception): + """Raised when a schema is declared but not defined. + + Args (Exception): Base Exception Object + """ diff --git a/schema_enforcer/schemas/manager.py b/schema_enforcer/schemas/manager.py index c3242c6..0b274e3 100644 --- a/schema_enforcer/schemas/manager.py +++ b/schema_enforcer/schemas/manager.py @@ -5,6 +5,7 @@ from termcolor import colored from schema_enforcer.utils import load_file, find_and_load_file, find_files, dump_data_to_yaml from schema_enforcer.validation import ValidationResult, RESULT_PASS, RESULT_FAIL +from schema_enforcer.exceptions import SchemaNotDefined from schema_enforcer.schemas.jsonschema import JsonSchema @@ -224,10 +225,9 @@ def _get_test_directory(self): def validate_schemas_exist(self, schema_ids): """Validate that each schema ID in a list of schema IDs exists. - + Args: schema_ids (list): A list of schema IDs, each of which should exist as a schema object """ - if not isinstance(schema_ids, list): raise TypeError("schema_ids argument passed into validate_schemas_exist must be of type list") for schema_id in schema_ids: diff --git a/schema_enforcer/validation.py b/schema_enforcer/validation.py index aae0d28..ae51f83 100644 --- a/schema_enforcer/validation.py +++ b/schema_enforcer/validation.py @@ -73,4 +73,7 @@ def print_passed(self): print(colored("PASS", "green") + f" | [{self.instance_type}] {self.instance_location}/{self.instance_name}") if self.instance_type == "HOST": - print(colored("PASS", "green") + f" | [{self.instance_type}] {self.instance_hostname} [SCHEMA ID] {self.schema_id}") + print( + colored("PASS", "green") + + f" | [{self.instance_type}] {self.instance_hostname} [SCHEMA ID] {self.schema_id}" + ) From 0bb924872291d7a74a404fafcfe42599992f9d50 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 23 Nov 2020 12:26:17 -0800 Subject: [PATCH 05/22] Add documentation for ansible command --- docs/ansible_command.md | 284 ++++++++++++++++++++++++++ examples/ansible/group_vars/leaf.yml | 4 - examples/ansible/group_vars/spine.yml | 2 - 3 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 docs/ansible_command.md diff --git a/docs/ansible_command.md b/docs/ansible_command.md new file mode 100644 index 0000000..a1e1ddc --- /dev/null +++ b/docs/ansible_command.md @@ -0,0 +1,284 @@ +# The `ansible` command + +The `ansible` command is used to check ansible inventory for adherence to a schema definition. An example exists in the `examples/ansible` folder. With no flags passed in, schema-enforcer will display a line for each property definition that **fails** schema validation along with contextual information elucidating why a given portion of the ansible inventory failed schema validation, the host for which schema validation failed, and the portion of structured data that is failing validation. If all checks pass, `schema-enforcer` will inform the user that all tests have passed. + +## How the inventory is loaded + +When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory then validated against schema. Take the following example + +```cli +bash $ cd examples/ansible && schema-enforcer ansible +Found 4 hosts in the inventory +FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address +FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +``` + +The `schema-enforcer ansible` command validates adherence to schema on a **per host** basis. In the example above, both `spine1` and `spine2` devices belong to a group called `spine` + +```ini +[nyc:children] +spine +leaf + +[spine] +spine1 +spine2 + +[leaf] +leaf1 +leaf2 +``` + +The property `dns_servers` is defined only at the `spine` group level in ansible and not at the host level. Below is the `spine.yml` group_vars file. + +```yaml +cat group_vars/spine.yaml +--- +dns_servers: + - address: true + - address: "10.2.2.2" +interfaces: + swp1: + role: "uplink" + swp2: + role: "uplink" + +schema_enforcer_schema_ids: + - "schemas/dns_servers" + - "schemas/interfaces" +``` + +Though the invalid property (the boolean true for a DNS address) is defined only once, two validation errors are flagged because two different hosts belong to to the `spine` group. + +## The `--show-checks` flag + +The `--show-checks` flag is used to show which ansible inventory hosts will be validated against which schema definition IDs. + +```cli +Found 4 hosts in the inventory +Ansible Host Schema ID +-------------------------------------------------------------------------------- +leaf1 ['schemas/dns_servers'] +leaf2 ['schemas/dns_servers'] +spine1 ['schemas/dns_servers', 'schemas/interfaces'] +spine2 ['schemas/dns_servers', 'schemas/interfaces'] +``` + +> Note: The ansible inventory hosts can be mapped to schema definition ids in one of a few ways. This is discussed in the Schema Mapping section below + +## The `--show-pass` flag + +The `--show-pass` flag is used to show what schema definition ids each host passes in addition to the schema definition ids each host fails. + +```cli +bash$ schema-enforcer ansible --show-pass +Found 4 hosts in the inventory +FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address +PASS | [HOST] spine1 [SCHEMA ID] schemas/interfaces +FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces +PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers +PASS | [HOST] leaf2 [SCHEMA ID] schemas/dns_servers +``` + +In the above example, the leaf switches are checked for adherence to the `schemas/dns_servers` definition and the spine switches are checked for adherence to two schema ids; the `schemas/dns_servers` schema id and the `schemas/interfaces` schema id. A PASS statement is printed to stdout for each validation that passes and a FAIL statement is printed for each validation that fails. + +## The `--host` flag + +The `--host` flag can be used to limit schema validation to a single ansible inventory host. `-h` can also be used as shorthand for `--host` + +```cli +bash$ schema-enforcer ansible -h spine2 --show-pass +Found 4 hosts in the inventory +FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces +``` + +## Specifying an ansible inventory file to use + +The ansible inventory file which should be used can be specified in one of two ways: + +1) The `--inventory` flag (or `-i`) can be used to pass in the location of an ansible inventory file +2) A `pyproject.toml` file can contain a `[tool.schema_enforcer]` config block setting the `ansible_inventory` paramer. This `pyproject.toml` file must be inside the repository from which the tool is run. + +```toml +bash$ cat pyproject.toml +[tool.schema_enforcer] +ansible_inventory = "inventory.ini" +``` + +If the inventory is set in both ways, the -i flag will take precedence. + +> Note: Dynamic inventory sources can not currently be parsed for schema adherence. + +## Mapping inventory variables to schema definitions + +`schema-enforcer` will check ansible hosts for adherence to defined schema ids in one of two ways. + +1) The `schema_enforcer_schema_ids` ansible inventory variable can be used to declare which schemas a given host/group of hosts should be checked for adherence to. The value of this variable is a list of the schema ids. + +Take for example the `spine` group in our `ansible` exmple. In this example, the schema ids `schemas/dns_servers` and `schemas/interfaces` are declared. + +```yaml +bash$ cat group_vars/spine.yml +--- +dns_servers: + - address: true + - address: "10.2.2.2" +interfaces: + swp1: + role: "uplink" + swp2: + role: "uplink" + +schema_enforcer_schema_ids: + - "schemas/dns_servers" + - "schemas/interfaces" +``` + +The `$id` property in the following schema definition file is what is being declared by spine group var file above. + +```yaml +bash$ cat schema/schemas/interfaces.yml +--- +$schema: "http://json-schema.org/draft-07/schema#" +$id: "schemas/interfaces" +description: "Interfaces configuration schema." +type: "object" +properties: + interfaces: + type: "object" + patternProperties: + ^swp.*$: + properties: + type: + type: "string" + description: + type: "string" + role: + type: "string" +``` + +2) Automatically infer which schema IDs should be used to check for adherence. If no `schema_enforcer_schema_ids` property is declared, the `schema-enforcer ansible` command will automatically infer which ansible hosts should be checked for adherence to which schema definition. It does this by matching the top level property in a schema definition to the top level key in a defined variable. + +The leaf group in the included ansible example does not declare any schemas per the `schema_enforcer_schema_ids` property. + +```yaml +bash$ cat group_vars/leaf.yml +--- +dns_servers: + - address: "10.1.1.1" + - address: "10.2.2.2" +``` + +Yet when schema enforcer is run against one of the leaf hosts, we can see it's host vars are checked for adherence to the dns_servers.yml schema. + +```cli +schema-enforcer ansible -h leaf1 --show-pass +Found 4 hosts in the inventory +PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers +ALL SCHEMA VALIDATION CHECKS PASSED +``` + +This is done because `schema-enforcer` maps the `dns_servers` key in the `group_vars/leaf.yml` to the `dns_servers` top level property in the `schema/schemas/dns.yml` schema definition file. + +```yaml +cat schema/schemas/dns.yml +--- +$schema: "http://json-schema.org/draft-07/schema#" +$id: "schemas/dns_servers" +description: "DNS Server Configuration schema." +type: "object" +properties: + dns_servers: + $ref: "../definitions/arrays/ip.yml#ipv4_hosts" +required: + - "dns_servers" +``` + +> Note: The order listed above is the order in which the options for mapping schema ids to variables occurs. +> Note: Schema ID to host property mapping methods are **mutually exclusive**. This means that if a `schema_definition_schema_ids` variable is declared in an ansible hosts/groups file, automatic mapping of schema IDs to variables will not occur. + +## Advanced Options + +### The `schema_enforcer_automap_default` variable + +The `schema_enforcer_automap_default` variable can be declared in an ansible host or group file. This variable defaults to true if not set. If set to false, the automapping behaviour described above will not occur. For instance, if we change the `schema_enforcer_automap_default` variable for leaf switches to false then re-run schema validation, no checks will be performed because automapping is disabled. + +```yaml +bash$ cat group_vars/leaf.yml +--- +dns_servers: + - address: "10.1.1.1" + - address: "10.2.2.2" + +schema_enforcer_automap_default: false +``` + +```yaml +bash$ schema-enforcer ansible -h leaf1 --show-checks +Found 4 hosts in the inventory +Ansible Host Schema ID +-------------------------------------------------------------------------------- +leaf1 [] +``` + +### The `schema_enforcer_strict` variable + +The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This varaible defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional properties can be specified as variables beyond those that are defined in the schema. Two major caveats apply to using the `schema_enforcer_strict` variable. + +1) If the `schema_enforcer_strict` variable is set to true, the `schema_enforcer_schema_ids` variabe **MUST** be defined as a list of one and only one schema ID. If it is either not defined at all or defined as something other than a list with one element, an error will be printed to the screen and the tool will exit before performing any validations. +2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables defined for the ansible host/group. If an ansible variable not defined in the schema is defined for a given host, schema validation will fail as, when strict mode is run, properties not defined in the schema are not allowed. + +> Note: If either of these conditions are not met, an error message will be printed to stdout and the tool will stop execution before evaluating host variables against schema. + +In the following example, the leaf.yml group vars file has been modified so that all hosts which belong to it are checked for strict enforcement against the `schemas/dns_servers` schema id. + +```yaml +bash$ cat group_vars/leaf.yml +--- +dns_servers: + - address: "10.1.1.1" + - address: "10.2.2.2" + +schema_enforcer_schema_ids: + - schemas/dns_servers + +schema_enforcer_strict: true +``` + +When `schema-enforcer` is run, it shows checks passing as expected + +```cli +schema-enforcer ansible -h leaf1 --show-pass +Found 4 hosts in the inventory +PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers +ALL SCHEMA VALIDATION CHECKS PASSED +``` + +If we do the same thing for the spine switches then run validation, we two validation errors -- one indicating that the `dns_servers` property failed validation because its first address is of type `bool`, and one indicating that `interfaces` is an additional property which falls outside of the declared schema definition (`schemas/dns_servers') and is not allowed. + +```yaml +bash$ cat group_vars/spine.yml +--- +dns_servers: + - address: true + - address: "10.2.2.2" +interfaces: + swp1: + role: "uplink" + swp2: + role: "uplink" + +schema_enforcer_schema_ids: + - "schemas/dns_servers" + +schema_enforcer_strict: true +``` + +```cli +bash$ schema-enforcer ansible -h spine1 --show-pass +Found 4 hosts in the inventory +FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address +FAIL | [ERROR] Additional properties are not allowed ('interfaces' was unexpected) [HOST] spine1 [PROPERTY] +``` diff --git a/examples/ansible/group_vars/leaf.yml b/examples/ansible/group_vars/leaf.yml index a786313..191f440 100644 --- a/examples/ansible/group_vars/leaf.yml +++ b/examples/ansible/group_vars/leaf.yml @@ -2,7 +2,3 @@ dns_servers: - address: "10.1.1.1" - address: "10.2.2.2" - -# schema_enforcer_schema_ids: -# - "schemas/dns_servers" - diff --git a/examples/ansible/group_vars/spine.yml b/examples/ansible/group_vars/spine.yml index ead0c60..5a118db 100644 --- a/examples/ansible/group_vars/spine.yml +++ b/examples/ansible/group_vars/spine.yml @@ -11,5 +11,3 @@ interfaces: schema_enforcer_schema_ids: - "schemas/dns_servers" - "schemas/interfaces" - -schema_enforcer_automap_default: true \ No newline at end of file From 037192913e0614d6f0a77bb04a11e41503a3d865 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 23 Nov 2020 13:33:08 -0800 Subject: [PATCH 06/22] Update documentation --- docs/ansible_command.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index a1e1ddc..23bc230 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -50,7 +50,9 @@ schema_enforcer_schema_ids: Though the invalid property (the boolean true for a DNS address) is defined only once, two validation errors are flagged because two different hosts belong to to the `spine` group. -## The `--show-checks` flag +## Command Arguments + +### The `--show-checks` flag The `--show-checks` flag is used to show which ansible inventory hosts will be validated against which schema definition IDs. @@ -66,7 +68,7 @@ spine2 ['schemas/dns_servers', 'schemas/interfaces'] > Note: The ansible inventory hosts can be mapped to schema definition ids in one of a few ways. This is discussed in the Schema Mapping section below -## The `--show-pass` flag +### The `--show-pass` flag The `--show-pass` flag is used to show what schema definition ids each host passes in addition to the schema definition ids each host fails. @@ -83,7 +85,7 @@ PASS | [HOST] leaf2 [SCHEMA ID] schemas/dns_servers In the above example, the leaf switches are checked for adherence to the `schemas/dns_servers` definition and the spine switches are checked for adherence to two schema ids; the `schemas/dns_servers` schema id and the `schemas/interfaces` schema id. A PASS statement is printed to stdout for each validation that passes and a FAIL statement is printed for each validation that fails. -## The `--host` flag +### The `--host` flag The `--host` flag can be used to limit schema validation to a single ansible inventory host. `-h` can also be used as shorthand for `--host` @@ -94,9 +96,9 @@ FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces ``` -## Specifying an ansible inventory file to use +### The `--inventory` flag -The ansible inventory file which should be used can be specified in one of two ways: +The `--inventory` flag (or `-i`) specifies the inventory file which should be used to determine the ansible inventory. The inventory file can be specified in one of two ways: 1) The `--inventory` flag (or `-i`) can be used to pass in the location of an ansible inventory file 2) A `pyproject.toml` file can contain a `[tool.schema_enforcer]` config block setting the `ansible_inventory` paramer. This `pyproject.toml` file must be inside the repository from which the tool is run. @@ -111,11 +113,16 @@ If the inventory is set in both ways, the -i flag will take precedence. > Note: Dynamic inventory sources can not currently be parsed for schema adherence. -## Mapping inventory variables to schema definitions +## Inventory Variables and Schema Mapping `schema-enforcer` will check ansible hosts for adherence to defined schema ids in one of two ways. -1) The `schema_enforcer_schema_ids` ansible inventory variable can be used to declare which schemas a given host/group of hosts should be checked for adherence to. The value of this variable is a list of the schema ids. +- By using a list of schema ids defined by the `schema_enforcer_schema_ids` command +- By automatically mapping a schema's top level property to ansible variable keys. + +### Using The `schema_enforcer_schema_ids` ansible inventory variable + +This variable can be used to declare which schemas a given host/group of hosts should be checked for adherence to. The value of this variable is a list of the schema ids. Take for example the `spine` group in our `ansible` exmple. In this example, the schema ids `schemas/dns_servers` and `schemas/interfaces` are declared. @@ -159,7 +166,9 @@ properties: type: "string" ``` -2) Automatically infer which schema IDs should be used to check for adherence. If no `schema_enforcer_schema_ids` property is declared, the `schema-enforcer ansible` command will automatically infer which ansible hosts should be checked for adherence to which schema definition. It does this by matching the top level property in a schema definition to the top level key in a defined variable. +### Using the `schema_enforcer_automap_default` ansible inventory variable + +This variable specifies whether or not to use automapping. It defaults to true. When automapping is in use, schema enforcer will automatically map map schema IDs to host variables if the variable's name matches a top level property defined in the schema. This happens by default when no `schema_enforcer_schema_ids` property is declared. The leaf group in the included ansible example does not declare any schemas per the `schema_enforcer_schema_ids` property. @@ -199,10 +208,6 @@ required: > Note: The order listed above is the order in which the options for mapping schema ids to variables occurs. > Note: Schema ID to host property mapping methods are **mutually exclusive**. This means that if a `schema_definition_schema_ids` variable is declared in an ansible hosts/groups file, automatic mapping of schema IDs to variables will not occur. -## Advanced Options - -### The `schema_enforcer_automap_default` variable - The `schema_enforcer_automap_default` variable can be declared in an ansible host or group file. This variable defaults to true if not set. If set to false, the automapping behaviour described above will not occur. For instance, if we change the `schema_enforcer_automap_default` variable for leaf switches to false then re-run schema validation, no checks will be performed because automapping is disabled. ```yaml @@ -223,6 +228,8 @@ Ansible Host Schema ID leaf1 [] ``` +## Advanced Options + ### The `schema_enforcer_strict` variable The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This varaible defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional properties can be specified as variables beyond those that are defined in the schema. Two major caveats apply to using the `schema_enforcer_strict` variable. From 21d210f45dd6f9291bd19cba80747b73d94a6087 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 23 Nov 2020 23:03:59 -0800 Subject: [PATCH 07/22] Updates per peer review --- docs/ansible_command.md | 2 +- schema_enforcer/ansible_inventory.py | 7 ++++--- schema_enforcer/cli.py | 6 +++--- schema_enforcer/validation.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 23bc230..0b81a0e 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -168,7 +168,7 @@ properties: ### Using the `schema_enforcer_automap_default` ansible inventory variable -This variable specifies whether or not to use automapping. It defaults to true. When automapping is in use, schema enforcer will automatically map map schema IDs to host variables if the variable's name matches a top level property defined in the schema. This happens by default when no `schema_enforcer_schema_ids` property is declared. +This variable specifies whether or not to use automapping. It defaults to true. When automapping is in use, schema enforcer will automatically map schema IDs to host variables if the variable's name matches a top level property defined in the schema. This happens by default when no `schema_enforcer_schema_ids` property is declared. The leaf group in the included ansible example does not declare any schemas per the `schema_enforcer_schema_ids` property. diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index b7d3b25..4b91dae 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -104,8 +104,9 @@ def get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap): Args: hostvars (dict): dictionary of cleaned host vars which will be evaluated against schema smgr (schema_enforcer.schemas.manager.SchemaManager): SchemaManager object - declared_schema_ids: list of declared schema IDs inferred from schema_enforcer_schemas variable - automap: + declared_schema_ids (list): A list of declared schema IDs inferred from schema_enforcer_schemas variable + automap (bool): Whether or not to use the `automap` feature to automatically map top level hostvar keys + to top level schema definition properties if no schema ids are declared (list of schema ids is empty) Returns: applicable_schemas (dict): dictionary mapping schema_id to schema obj for all applicable schemas @@ -129,7 +130,7 @@ def get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap): def get_schema_validation_settings(self, host): """Parse Ansible Schema Validation Settings from a host object. - Validate settings to ensure an error is raised in the event an invalid parameter is + Validate settings or ensure an error is raised in the event an invalid parameter is configured in the host file. Args: diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index ef9ef25..55ef56d 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -258,8 +258,8 @@ def ansible( # Acquire schemas applicable to the given host applicable_schemas = inv.get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap) - - for _, schema_obj in applicable_schemas.items(): + # import pdb; pdb.set_trace() + for schema_obj in applicable_schemas.values(): # Combine host attributes into a single data structure matching to properties defined at the top level of the schema definition if not strict: data = dict() @@ -269,7 +269,7 @@ def ansible( # If the schema_enforcer_strict bool is set, hostvars should match a single schema exactly. # Thus, we want to pass the entirety of the cleaned host vars into the validate method rather # than creating a data structure with only the top level vars defined by the schema. - if strict: + else: data = hostvars # Validate host vars against schema diff --git a/schema_enforcer/validation.py b/schema_enforcer/validation.py index ae51f83..75fe487 100644 --- a/schema_enforcer/validation.py +++ b/schema_enforcer/validation.py @@ -59,7 +59,7 @@ def print_failed(self): if self.instance_type == "FILE": msg += f" [{self.instance_type}] {self.instance_location}/{self.instance_name}" - if self.instance_type == "HOST": + elif self.instance_type == "HOST": msg += f" [{self.instance_type}] {self.instance_hostname}" msg += f" [PROPERTY] {':'.join(str(item) for item in self.absolute_path)}" From fa8e627b99590b3a749fd90cf800c846af128512 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Fri, 27 Nov 2020 21:23:36 -0500 Subject: [PATCH 08/22] update doc strings --- .pydocstyle.ini | 3 +-- schema_enforcer/__init__.py | 1 + schema_enforcer/config.py | 4 ++-- schema_enforcer/instances/file.py | 25 ++++++++++----------- schema_enforcer/schemas/jsonschema.py | 20 ++++++++--------- schema_enforcer/schemas/manager.py | 31 ++++++++++++++------------- schema_enforcer/utils.py | 12 +++++------ 7 files changed, 49 insertions(+), 47 deletions(-) diff --git a/.pydocstyle.ini b/.pydocstyle.ini index 45dfcef..ed87bc3 100644 --- a/.pydocstyle.ini +++ b/.pydocstyle.ini @@ -1,5 +1,4 @@ [pydocstyle] convention = google inherit = false -match = (?!__init__).*\.py -match-dir = (?!tests)[^\.].* \ No newline at end of file +match-dir = (?!tests)[^\.].* diff --git a/schema_enforcer/__init__.py b/schema_enforcer/__init__.py index 79738fa..b0dadf3 100644 --- a/schema_enforcer/__init__.py +++ b/schema_enforcer/__init__.py @@ -1,3 +1,4 @@ +"""My chence.""" # pylint: disable=C0114 __version__ = "0.1.0" diff --git a/schema_enforcer/config.py b/schema_enforcer/config.py index af80484..8909241 100644 --- a/schema_enforcer/config.py +++ b/schema_enforcer/config.py @@ -17,7 +17,7 @@ class Settings(BaseSettings): # pylint: disable=too-few-public-methods The type of each setting is defined using Python annotations and is validated when a config file is loaded with Pydantic. - Most input files specific to this project are expected to be located in the same directory + Most input files specific to this project are expected to be located in the same directory. e.g. schema/ - definitions - schemas @@ -61,7 +61,7 @@ def load(config_file_name="pyproject.toml", config_data=None): config_data can be passed in to override the config_file_name. If this is done, a combination of the data specified and the defaults for parameters not specified will be used, and settings in the config file will - be ignored + be ignored. Args: config_file_name (str, optional): Name of the configuration file to load. Defaults to "pyproject.toml". diff --git a/schema_enforcer/instances/file.py b/schema_enforcer/instances/file.py index 347de59..2d2afaa 100644 --- a/schema_enforcer/instances/file.py +++ b/schema_enforcer/instances/file.py @@ -11,10 +11,13 @@ class InstanceFileManager: # pylint: disable=too-few-public-methods """InstanceFileManager.""" - def __init__(self, config): + def _init__(self, config): """Initialize the interface File manager. - The file manager will locate all potential instance files in the search directories + The file manager will locate all potential instance files in the search directories. + + Args: + config (string): The pydantec config object. """ self.instances = [] self.config = config @@ -57,9 +60,8 @@ def __init__(self, root, filename, matches=None): """Initializes InstanceFile object. Args: - root (string): Location of the file on the filesystem - filename (string): Name of the file - matches (string, optional): List of schema IDs that matches with this Instance file. Defaults to None. + filename (string): Name of the file. + matches (list, optional): List of schema IDs that matches with this Instance file. Defaults to None. """ self.data = None self.path = root @@ -79,10 +81,10 @@ def _find_matches_inline(self, content=None): Look for a line with # jsonschema: schema_id,schema_id Args: - content (string, optional): Content of the file to analyze. Default to None + content (string, optional): Content of the file to analyze. Default to None. Returns: - list(string): List of matches found in the file + list(string): List of matches found in the file. """ if not content: content = Path(os.path.join(self.full_path, self.filename)).read_text() @@ -103,22 +105,21 @@ def get_content(self): Content returned can be either dict or list depending on the content of the file Returns: - dict or list: Content of the instance file + dict or list: Content of the instance file. """ return load_file(os.path.join(self.full_path, self.filename)) def validate(self, schema_manager, strict=False): """Validate this instance file with all matching schema in the schema manager. - # TODO need to add something to check if a schema is missing - Args: - schema_manager (SchemaManager): SchemaManager object + schema_manager (SchemaManager): A SchemaManager object. strict (bool, optional): True is the validation should automatically flag unsupported element. Defaults to False. Returns: - iterator: Iterator of ValidationErrors returned by schema.validate + iterator: Iterator of ValidationErrors returned by schema.validate. """ + # TODO need to add something to check if a schema is missing # Create new iterator chain to be able to aggregate multiple iterators errs = itertools.chain() diff --git a/schema_enforcer/schemas/jsonschema.py b/schema_enforcer/schemas/jsonschema.py index 27151af..48e94cd 100644 --- a/schema_enforcer/schemas/jsonschema.py +++ b/schema_enforcer/schemas/jsonschema.py @@ -19,8 +19,8 @@ def __init__(self, schema, filename, root): """Initilize a new JsonSchema object from a dict. Args: - schema (dict): Data representing the schema. Must be jsonschema valid - filename (string): Name of the schema file on the filesystem + schema (dict): Data representing the schema. Must be jsonschema valid. + filename (string): Name of the schema file on the filesystem. root (string): Absolute path to the directory where the schema file is located. """ self.filename = filename @@ -41,7 +41,7 @@ def validate(self, data, strict=False): """Validate a given data with this schema. Args: - data (dict, list): Data to validate against the schema + data (dict, list): Data to validate against the schema. strict (bool, optional): if True the validation will automatically flag additional properties. Defaults to False. Returns: @@ -71,11 +71,11 @@ def validate_to_dict(self, data, strict=False): These are generated with the validate() function in dict() format instead of as a Python Object. Args: - data (dict, list): Data to validate against the schema + data (dict, list): Data to validate against the schema. strict (bool, optional): if True the validation will automatically flag additional properties. Defaults to False. Returns: - list of dictionnary + list of dictionnaries containing the results. """ return [ result.dict(exclude_unset=True, exclude_none=True) for result in self.validate(data=data, strict=strict) @@ -85,7 +85,7 @@ def __get_validator(self): """Return the validator for this schema, create if it doesn't exist already. Returns: - Draft7Validator: Validator for this schema + Draft7Validator: The validator for this schema. """ if self.validator: return self.validator @@ -97,12 +97,12 @@ def __get_validator(self): def __get_strict_validator(self): """Return a strict version of the Validator, create it if it doesn't exist already. - To create a strict version of the schema, this function adds `additionalProperties` to all objects in the schema - TODO Currently the function is only modifying the top level object, need to add that to all objects recursively + To create a strict version of the schema, this function adds `additionalProperties` to all objects in the schema. Returns: - Draft7Validator: Validator for this schema in strict mode + Draft7Validator: Validator for this schema in strict mode. """ + # TODO Currently the function is only modifying the top level object, need to add that to all objects recursively if self.strict_validator: return self.strict_validator @@ -131,7 +131,7 @@ def check_if_valid(self): """Check if the schema definition is valid against JsonSchema draft7. Returns: - List[ValidationResult] + List[ValidationResult]: A list of validation result objects. """ validator = Draft7Validator(v7schema) diff --git a/schema_enforcer/schemas/manager.py b/schema_enforcer/schemas/manager.py index 0b274e3..e197f7d 100644 --- a/schema_enforcer/schemas/manager.py +++ b/schema_enforcer/schemas/manager.py @@ -17,7 +17,7 @@ def __init__(self, config): """Initialize the SchemaManager and search for all schema files in the schema_directories. Args: - config (Config): Instance of Config object returned by jsonschema_testing.config.load() method + config (Config): Instance of Config object returned by jsonschema_testing.config.load() method. """ self.schemas = {} self.config = config @@ -41,14 +41,14 @@ def __init__(self, config): def create_schema_from_file(self, root, filename): # pylint: disable=no-self-use """Create a new JsonSchema object for a given file. - Load the content from disk and resolve all JSONRef within the schema file + Load the content from disk and resolve all JSONRef within the schema file. Args: - root (string): Absolute location of the file in the filesystem - filename (string): Name of the file + root (string): Absolute location of the file in the filesystem. + filename (string): Name of the file. Returns: - JsonSchema: JsonSchema object newly created + JsonSchema: JsonSchema object newly created. """ file_data = load_file(os.path.join(root, filename)) @@ -62,14 +62,14 @@ def iter_schemas(self): """Return an iterator of all schemas in the SchemaManager. Returns: - Iterator: Iterator of all schemas in K,v format (key, value) + Iterator: Iterator of all schemas in K,v format (key, value). """ return self.schemas.items() def print_schemas_list(self): """Print the list of all schemas to the cli. - To avoid very long location string, dynamically replace the current dir with a dot + To avoid very long location string, dynamically replace the current dir with a dot. """ current_dir = os.getcwd() columns = "{:20}{:12}{:30} {:20}" @@ -83,9 +83,9 @@ def test_schemas(self): """Validate all schemas passing tests defined for them. For each schema, 3 set of tests will be potentially executed. - - schema must be Draft7 valid - - Valid tests must pass - - Invalid tests must pass + - schema must be Draft7 valid. + - Valid tests must pass. + - Invalid tests must pass. """ error_exists = False @@ -108,10 +108,10 @@ def test_schema_valid(self, schema_id, strict=False): """Execute all valid tests for a given schema. Args: - schema_id (str): unique identifier of a schema + schema_id (str): The unique identifier of a schema. Returns: - list of ValidationResult + list of ValidationResult. """ schema = self.schemas[schema_id] @@ -148,10 +148,10 @@ def test_schema_invalid(self, schema_id): # pylint: disable=too-many-locals """Execute all invalid tests for a given schema. Args: - schema_id (str): unique identifier of a schema + schema_id (str): The unique identifier of a schema. Returns: - list of ValidationResult + list of ValidationResult. """ schema = self.schemas[schema_id] @@ -226,7 +226,8 @@ def _get_test_directory(self): def validate_schemas_exist(self, schema_ids): """Validate that each schema ID in a list of schema IDs exists. - Args: schema_ids (list): A list of schema IDs, each of which should exist as a schema object + Args: + schema_ids (list): A list of schema IDs, each of which should exist as a schema object. """ if not isinstance(schema_ids, list): raise TypeError("schema_ids argument passed into validate_schemas_exist must be of type list") diff --git a/schema_enforcer/utils.py b/schema_enforcer/utils.py index 8b44aa2..89e9dff 100755 --- a/schema_enforcer/utils.py +++ b/schema_enforcer/utils.py @@ -105,7 +105,7 @@ def get_conversion_filepaths(original_path, original_extension, conversion_path, original_path (str): The path to look for files to convert. original_extension (str): The original file extension of files being converted. conversion_path (str): The root path to place files after conversion. - conversion_extension (str): The file extension to use for files after conversion + conversion_extension (str): The file extension to use for files after conversion. Returns: list: A tuple of paths to the original and the conversion files. @@ -459,15 +459,15 @@ def handle_parse_result(self, ctx, opts, args): """Validate that two mutually exclusive arguments are not provided together. Args: - ctx : context - opts : options - args : arguments + ctx : context. + opts : options. + args : arguments. Raises: - UsageError: if two mutually exclusive arguments are provided + UsageError: If two mutually exclusive arguments are provided. Returns: - ctx, opts, args + ctx, opts, args. """ if self.mutually_exclusive.intersection(opts) and self.name in opts: raise UsageError( From 0a97328a28d348054f3be32dcf5241d4864d1e77 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Fri, 27 Nov 2020 22:22:04 -0500 Subject: [PATCH 09/22] fix tests --- .gitignore | 166 +++++++++++++++++- schema_enforcer/__init__.py | 2 +- schema_enforcer/instances/file.py | 3 +- schema_enforcer/validation.py | 2 +- tasks.py | 8 +- tests/test_ansible_inventory.py | 8 +- tests/test_instances_instance_file.py | 3 +- tests/test_instances_instance_file_manager.py | 5 +- 8 files changed, 179 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index a18c516..5b7677e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Linux +.*.swp + +# Project +jsonschema_testing.egg-info + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -127,4 +133,162 @@ dmypy.json # Pyre type checker .pyre/ -jsonschema_testing.egg-info + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### vscode ### +.vscode/* + +*.code-workspace diff --git a/schema_enforcer/__init__.py b/schema_enforcer/__init__.py index b0dadf3..89aed24 100644 --- a/schema_enforcer/__init__.py +++ b/schema_enforcer/__init__.py @@ -1,4 +1,4 @@ -"""My chence.""" +"""Initialization file for library.""" # pylint: disable=C0114 __version__ = "0.1.0" diff --git a/schema_enforcer/instances/file.py b/schema_enforcer/instances/file.py index 2d2afaa..83e7185 100644 --- a/schema_enforcer/instances/file.py +++ b/schema_enforcer/instances/file.py @@ -11,7 +11,7 @@ class InstanceFileManager: # pylint: disable=too-few-public-methods """InstanceFileManager.""" - def _init__(self, config): + def __init__(self, config): """Initialize the interface File manager. The file manager will locate all potential instance files in the search directories. @@ -60,6 +60,7 @@ def __init__(self, root, filename, matches=None): """Initializes InstanceFile object. Args: + root (string): Absolute path to the directory where the schema file is located. filename (string): Name of the file. matches (list, optional): List of schema IDs that matches with this Instance file. Defaults to None. """ diff --git a/schema_enforcer/validation.py b/schema_enforcer/validation.py index 75fe487..510f4d3 100644 --- a/schema_enforcer/validation.py +++ b/schema_enforcer/validation.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, validator # pylint: disable=no-name-in-module from termcolor import colored -RESULT_PASS = "PASS" +RESULT_PASS = "PASS" # nosec RESULT_FAIL = "FAIL" diff --git a/tasks.py b/tasks.py index 32d780e..e4a34a8 100644 --- a/tasks.py +++ b/tasks.py @@ -226,14 +226,14 @@ def tests(context, name=NAME, python_ver=PYTHON_VER): pytest(context, name, python_ver) print("Running black...") black(context, name, python_ver) - # print("Running flake8...") - # flake8(context, name, python_ver) + print("Running flake8...") + flake8(context, name, python_ver) print("Running pylint...") pylint(context, name, python_ver) print("Running yamllint...") yamllint(context, name, python_ver) print("Running pydocstyle...") pydocstyle(context, name, python_ver) - # print("Running bandit...") - # bandit(context, name, python_ver) + print("Running bandit...") + bandit(context, name, python_ver) print("All tests have passed!") diff --git a/tests/test_ansible_inventory.py b/tests/test_ansible_inventory.py index a793266..5eb4a0b 100644 --- a/tests/test_ansible_inventory.py +++ b/tests/test_ansible_inventory.py @@ -51,12 +51,12 @@ def test_get_hosts_containing_var(ansible_inv): def test_get_host_vars(ansible_inv): expected = { - "dns_servers": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"},], + "dns_servers": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], "group_names": ["ios", "na", "nyc"], "inventory_hostname": "host3", "ntp_servers": [{"address": "10.3.3.3"}], - "os_dns": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"},], - "region_dns": [{"address": "10.1.1.1", "vrf": "mgmt"}, {"address": "10.2.2.2"},], + "os_dns": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], + "region_dns": [{"address": "10.1.1.1", "vrf": "mgmt"}, {"address": "10.2.2.2"}], } filtered_hosts = ansible_inv.get_hosts_containing(var="os_dns") @@ -76,7 +76,7 @@ def test_get_host_vars(ansible_inv): def test_get_clean_host_vars(ansible_inv): expected = { - "dns_servers": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"},], + "dns_servers": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], "os_dns": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], "region_dns": [{"address": "10.1.1.1", "vrf": "mgmt"}, {"address": "10.2.2.2"}], "ntp_servers": [{"address": "10.3.3.3"}], diff --git a/tests/test_instances_instance_file.py b/tests/test_instances_instance_file.py index 33073e8..9b30ec2 100644 --- a/tests/test_instances_instance_file.py +++ b/tests/test_instances_instance_file.py @@ -5,8 +5,7 @@ import pytest from schema_enforcer.schemas.manager import SchemaManager -from schema_enforcer.instances.file import InstanceFileManager, InstanceFile -from schema_enforcer import config +from schema_enforcer.instances.file import InstanceFile from schema_enforcer.validation import ValidationResult from schema_enforcer.config import Settings diff --git a/tests/test_instances_instance_file_manager.py b/tests/test_instances_instance_file_manager.py index c5e9361..0132475 100644 --- a/tests/test_instances_instance_file_manager.py +++ b/tests/test_instances_instance_file_manager.py @@ -7,11 +7,8 @@ import pytest -from schema_enforcer.schemas.manager import SchemaManager -from schema_enforcer.instances.file import InstanceFileManager, InstanceFile -from schema_enforcer import config +from schema_enforcer.instances.file import InstanceFileManager from schema_enforcer.config import Settings -from schema_enforcer.validation import ValidationResult FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_instances") From 83900bd7d891de19886a528d12c8ee7f469cbb4a Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sat, 28 Nov 2020 10:15:54 -0500 Subject: [PATCH 10/22] update ansible example to folder of static and inventory plugin --- ansible.cfg | 2 +- docs/ansible_command.md | 28 +++++++++++-------- .../{inventory.ini => inventory/inventory} | 0 examples/ansible/inventory/simpleplugin.yml | 3 ++ .../ansible/inventory_plugins/simpleplugin.py | 17 +++++++++++ examples/ansible/pyproject.toml | 2 +- 6 files changed, 39 insertions(+), 13 deletions(-) rename examples/ansible/{inventory.ini => inventory/inventory} (100%) create mode 100644 examples/ansible/inventory/simpleplugin.yml create mode 100644 examples/ansible/inventory_plugins/simpleplugin.py diff --git a/ansible.cfg b/ansible.cfg index dd716e2..b290758 100755 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,2 +1,2 @@ [defaults] -inventory = examples/inventory/inventory +inventory = examples/ansible/inventory diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 0b81a0e..98d9081 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -7,12 +7,14 @@ The `ansible` command is used to check ansible inventory for adherence to a sche When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory then validated against schema. Take the following example ```cli -bash $ cd examples/ansible && schema-enforcer ansible -Found 4 hosts in the inventory -FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address -FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +bash $ cd examples/ansible && ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory +Found 6 hosts in the inventory +FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address +FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address ``` +> Note: The `ANSIBLE_INVENTORY_PLUGINS` environment variable is used in this sample (and should be used in all examples described in this file), as the inventory is not relative to the inventory_plugin directory. In actual implementations, standard inventory rules apply. + The `schema-enforcer ansible` command validates adherence to schema on a **per host** basis. In the example above, both `spine1` and `spine2` devices belong to a group called `spine` ```ini @@ -57,11 +59,13 @@ Though the invalid property (the boolean true for a DNS address) is defined only The `--show-checks` flag is used to show which ansible inventory hosts will be validated against which schema definition IDs. ```cli -Found 4 hosts in the inventory +Found 6 hosts in the inventory Ansible Host Schema ID -------------------------------------------------------------------------------- leaf1 ['schemas/dns_servers'] leaf2 ['schemas/dns_servers'] +leaf3 ['schemas/dns_servers'] +leaf4 ['schemas/dns_servers'] spine1 ['schemas/dns_servers', 'schemas/interfaces'] spine2 ['schemas/dns_servers', 'schemas/interfaces'] ``` @@ -73,14 +77,15 @@ spine2 ['schemas/dns_servers', 'schemas/interfaces'] The `--show-pass` flag is used to show what schema definition ids each host passes in addition to the schema definition ids each host fails. ```cli -bash$ schema-enforcer ansible --show-pass -Found 4 hosts in the inventory -FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address +Found 6 hosts in the inventory +FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address PASS | [HOST] spine1 [SCHEMA ID] schemas/interfaces -FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers PASS | [HOST] leaf2 [SCHEMA ID] schemas/dns_servers +PASS | [HOST] leaf3 [SCHEMA ID] schemas/dns_servers +PASS | [HOST] leaf4 [SCHEMA ID] schemas/dns_servers ``` In the above example, the leaf switches are checked for adherence to the `schemas/dns_servers` definition and the spine switches are checked for adherence to two schema ids; the `schemas/dns_servers` schema id and the `schemas/interfaces` schema id. A PASS statement is printed to stdout for each validation that passes and a FAIL statement is printed for each validation that fails. @@ -98,7 +103,8 @@ PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces ### The `--inventory` flag -The `--inventory` flag (or `-i`) specifies the inventory file which should be used to determine the ansible inventory. The inventory file can be specified in one of two ways: +The `--inventory` flag (or `-i`) specifies the inventory file which should be used to determine the ansible inventory. The inventory can reference a static file, a inventory plugin, or folder +containing multiple inventories of either static or inventory plugins can be specified in one of two ways: 1) The `--inventory` flag (or `-i`) can be used to pass in the location of an ansible inventory file 2) A `pyproject.toml` file can contain a `[tool.schema_enforcer]` config block setting the `ansible_inventory` paramer. This `pyproject.toml` file must be inside the repository from which the tool is run. @@ -106,7 +112,7 @@ The `--inventory` flag (or `-i`) specifies the inventory file which should be us ```toml bash$ cat pyproject.toml [tool.schema_enforcer] -ansible_inventory = "inventory.ini" +ansible_inventory = "inventory" ``` If the inventory is set in both ways, the -i flag will take precedence. diff --git a/examples/ansible/inventory.ini b/examples/ansible/inventory/inventory similarity index 100% rename from examples/ansible/inventory.ini rename to examples/ansible/inventory/inventory diff --git a/examples/ansible/inventory/simpleplugin.yml b/examples/ansible/inventory/simpleplugin.yml new file mode 100644 index 0000000..f387558 --- /dev/null +++ b/examples/ansible/inventory/simpleplugin.yml @@ -0,0 +1,3 @@ +--- + +plugin: "simpleplugin" diff --git a/examples/ansible/inventory_plugins/simpleplugin.py b/examples/ansible/inventory_plugins/simpleplugin.py new file mode 100644 index 0000000..a572095 --- /dev/null +++ b/examples/ansible/inventory_plugins/simpleplugin.py @@ -0,0 +1,17 @@ +"""Custom inventory Plugin for testing.""" + +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable + +class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): + + NAME = "simpleplugin" + + def verify_file(self, path): + """Verify file method, return True for this test plugin.""" + return True + + def parse(self, inventory, loader, path, cache=True): + """Parse method, add host from simple list of dictionaries.""" + super(InventoryModule, self).parse(inventory, loader, path, cache) + for device in [{"name": "leaf3", "group": "leaf"}, {"name": "leaf4", "group": "leaf"}]: + self.inventory.add_host(device['name'], group=device['group']) diff --git a/examples/ansible/pyproject.toml b/examples/ansible/pyproject.toml index d318418..26094bb 100644 --- a/examples/ansible/pyproject.toml +++ b/examples/ansible/pyproject.toml @@ -1,2 +1,2 @@ [tool.schema_enforcer] -ansible_inventory = "inventory.ini" \ No newline at end of file +ansible_inventory = "inventory" From 5f2f6f1371f7f6ff60801b7b4d2ba6643c7fc6fc Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sat, 28 Nov 2020 10:24:33 -0500 Subject: [PATCH 11/22] fix tests --- examples/ansible/inventory_plugins/simpleplugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/ansible/inventory_plugins/simpleplugin.py b/examples/ansible/inventory_plugins/simpleplugin.py index a572095..94f7ad8 100644 --- a/examples/ansible/inventory_plugins/simpleplugin.py +++ b/examples/ansible/inventory_plugins/simpleplugin.py @@ -2,16 +2,18 @@ from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable + class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): + """Sample simple inventory plugin.""" NAME = "simpleplugin" def verify_file(self, path): """Verify file method, return True for this test plugin.""" return True - + def parse(self, inventory, loader, path, cache=True): """Parse method, add host from simple list of dictionaries.""" super(InventoryModule, self).parse(inventory, loader, path, cache) for device in [{"name": "leaf3", "group": "leaf"}, {"name": "leaf4", "group": "leaf"}]: - self.inventory.add_host(device['name'], group=device['group']) + self.inventory.add_host(device["name"], group=device["group"]) From a03e39b7fa520c4752014ca469332284e844dd7d Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sat, 28 Nov 2020 21:21:31 -0500 Subject: [PATCH 12/22] remove jsonschema-testing --- .gitignore | 3 --- README.md | 2 +- schema_enforcer/schemas/manager.py | 2 +- tasks.py | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 5b7677e..56c6bad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ # Linux .*.swp -# Project -jsonschema_testing.egg-info - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 12ca340..f708cc4 100755 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ dns_servers: - address: "10.1.1.1" - address: "10.2.2.2" ``` -> Note: The line `# jsonschema: schemas/dns_servers` tells `schema-enforcer` the ID of the schema which the structured data defined in the file should be validated against. More information on how the structured data is mapped to a schema ID to which it should adhere can be found in the [docs/mapping_schemas.md README](https://github.com/networktocode-llc/jsonschema_testing/tree/master/docs/mapping_schemas.md) +> Note: The line `# jsonschema: schemas/dns_servers` tells `schema-enforcer` the ID of the schema which the structured data defined in the file should be validated against. More information on how the structured data is mapped to a schema ID to which it should adhere can be found in the [docs/mapping_schemas.md README](./docs/mapping_schemas.md) The file `schema/schemas/dns.yml` is a schema definition file. It contains a schema definition for ntp servers written in JSONSchema. The data in `chi-beijing-rt1/dns.yml` and `eng-london-rt1/dns.yml` should adhere to the schema defined in this schema definition file. diff --git a/schema_enforcer/schemas/manager.py b/schema_enforcer/schemas/manager.py index e197f7d..d69d120 100644 --- a/schema_enforcer/schemas/manager.py +++ b/schema_enforcer/schemas/manager.py @@ -17,7 +17,7 @@ def __init__(self, config): """Initialize the SchemaManager and search for all schema files in the schema_directories. Args: - config (Config): Instance of Config object returned by jsonschema_testing.config.load() method. + config (Config): Instance of Config object returned by schema_enforcer.config.load() method. """ self.schemas = {} self.config = config diff --git a/tasks.py b/tasks.py index e4a34a8..803157a 100644 --- a/tasks.py +++ b/tasks.py @@ -6,7 +6,7 @@ # Can be set to a separate Python version to be used for launching or building container PYTHON_VER = os.getenv("PYTHON_VER", "3.7") # Name of the docker image/container -NAME = os.getenv("IMAGE_NAME", "jsonschema-testing") +NAME = os.getenv("IMAGE_NAME", "schema-enforcer") # Gather current working directory for Docker commands PWD = os.getcwd() From 017499c9298610760204db2283c52fc98d5d85ae Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sun, 29 Nov 2020 14:57:46 -0800 Subject: [PATCH 13/22] Updates per peer review --- docs/ansible_command.md | 58 ++++++++++++++++++++++------ schema_enforcer/ansible_inventory.py | 4 +- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 0b81a0e..3a6ddcb 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -232,38 +232,74 @@ leaf1 [] ### The `schema_enforcer_strict` variable -The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This varaible defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional properties can be specified as variables beyond those that are defined in the schema. Two major caveats apply to using the `schema_enforcer_strict` variable. +The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This variable defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional host vars can exist beyond those that are defined in the schema. + +From a design pattern perspective, when strict enforcment is used, all host variables are evaulated against a single schema id. This is in contrast to a design patern where a different schema id is defined for each top level host var/property. To this end, when strict enforcement is used, a single schema should be defined with references to schemas for all properties which are defined for a given host. The ids for such schema definitions are better named by role instead of host variable. For instance `schemas/spines` or `schemas/leafs` makes more sense with this design pattern than `schemas/dns_servers`. + +Two major caveats apply to using the `schema_enforcer_strict` variable. 1) If the `schema_enforcer_strict` variable is set to true, the `schema_enforcer_schema_ids` variabe **MUST** be defined as a list of one and only one schema ID. If it is either not defined at all or defined as something other than a list with one element, an error will be printed to the screen and the tool will exit before performing any validations. 2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables defined for the ansible host/group. If an ansible variable not defined in the schema is defined for a given host, schema validation will fail as, when strict mode is run, properties not defined in the schema are not allowed. > Note: If either of these conditions are not met, an error message will be printed to stdout and the tool will stop execution before evaluating host variables against schema. -In the following example, the leaf.yml group vars file has been modified so that all hosts which belong to it are checked for strict enforcement against the `schemas/dns_servers` schema id. +In the following example, the `spine.yml` ansible group has been defined to use strict enforcement in checking against the `schemas/spine` schema ID. ```yaml -bash$ cat group_vars/leaf.yml +bash$ cd examples/ansible2 && cat group_vars/spine.yml --- dns_servers: - address: "10.1.1.1" - address: "10.2.2.2" +interfaces: + swp1: + role: "uplink" + swp2: + role: "uplink" +schema_enforcer_strict: true schema_enforcer_schema_ids: - - schemas/dns_servers + - schemas/spines +``` -schema_enforcer_strict: true +The `schemas/spines` schema definition includes two properties -- dns_servers and interfaces. Both of these properties are required by the schema. + +```yaml +--- +$schema: "http://json-schema.org/draft-07/schema#" +$id: "schemas/spines" +description: "Spine Switches Schema" +type: "object" +properties: + dns_servers: + type: object + $ref: "../definitions/arrays/ip.yml#ipv4_hosts" + interfaces: + type: "object" + patternProperties: + ^swp.*$: + properties: + type: + type: "string" + description: + type: "string" + role: + type: "string" +required: + - dns_servers + - interfaces ``` When `schema-enforcer` is run, it shows checks passing as expected ```cli -schema-enforcer ansible -h leaf1 --show-pass +bash$ schema-enforcer ansible -h spine1 --show-pass Found 4 hosts in the inventory -PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers +PASS | [HOST] spine1 [SCHEMA ID] schemas/spines ALL SCHEMA VALIDATION CHECKS PASSED ``` -If we do the same thing for the spine switches then run validation, we two validation errors -- one indicating that the `dns_servers` property failed validation because its first address is of type `bool`, and one indicating that `interfaces` is an additional property which falls outside of the declared schema definition (`schemas/dns_servers') and is not allowed. +If we add another property to the spine switches group, we see that spine1 fails validation. This is because properties outside of the purview of those defined by the schema are not allowed when `schema_enforcer_strict` is set to true. ```yaml bash$ cat group_vars/spine.yml @@ -276,6 +312,7 @@ interfaces: role: "uplink" swp2: role: "uplink" +bogus_property: true schema_enforcer_schema_ids: - "schemas/dns_servers" @@ -284,8 +321,7 @@ schema_enforcer_strict: true ``` ```cli -bash$ schema-enforcer ansible -h spine1 --show-pass +bash$ schema-enforcer ansible -h spine1 Found 4 hosts in the inventory -FAIL | [ERROR] True is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address -FAIL | [ERROR] Additional properties are not allowed ('interfaces' was unexpected) [HOST] spine1 [PROPERTY] +FAIL | [ERROR] Additional properties are not allowed ('bogus_property' was unexpected) [HOST] spine1 [PROPERTY] ``` diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 4b91dae..34c9590 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -119,7 +119,7 @@ def get_applicable_schemas(hostvars, smgr, declared_schema_ids, automap): applicable_schemas[schema_id] = smgr.schemas[schema_id] # extract applicable schema ID to JsonSchema objects based on host var to top level property mapping. - if not declared_schema_ids and automap: + elif automap: for schema in smgr.schemas.values(): if key in schema.top_level_properties: applicable_schemas[schema.id] = schema @@ -172,7 +172,7 @@ def get_schema_validation_settings(self, host): if strict and not declared_schema_ids: msg = ( f"The 'schema_enforcer_strict' parameter is set for {host.name} but the 'schema_enforcer_schema_ids' parameter does not declare a schema id. " - "The 'schema_enforcer_schemas' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." + "The 'schema_enforcer_schema_ids' parameter MUST be defined as a list declaring only one schema ID if 'schema_enforcer_strict' is set." ) raise ValueError(msg) From bddd2c5f0d77377d022a8fee79e30dcbe5047202 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sun, 29 Nov 2020 18:01:05 -0500 Subject: [PATCH 14/22] ps updates --- ansible.cfg | 2 -- docs/ansible_command.md | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100755 ansible.cfg diff --git a/ansible.cfg b/ansible.cfg deleted file mode 100755 index b290758..0000000 --- a/ansible.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[defaults] -inventory = examples/ansible/inventory diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 98d9081..a6f148d 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -59,6 +59,7 @@ Though the invalid property (the boolean true for a DNS address) is defined only The `--show-checks` flag is used to show which ansible inventory hosts will be validated against which schema definition IDs. ```cli +bash $ ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory --show-checks Found 6 hosts in the inventory Ansible Host Schema ID -------------------------------------------------------------------------------- @@ -77,6 +78,7 @@ spine2 ['schemas/dns_servers', 'schemas/interfaces'] The `--show-pass` flag is used to show what schema definition ids each host passes in addition to the schema definition ids each host fails. ```cli +bash $ ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory --show-pass Found 6 hosts in the inventory FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address PASS | [HOST] spine1 [SCHEMA ID] schemas/interfaces @@ -103,8 +105,9 @@ PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces ### The `--inventory` flag -The `--inventory` flag (or `-i`) specifies the inventory file which should be used to determine the ansible inventory. The inventory can reference a static file, a inventory plugin, or folder -containing multiple inventories of either static or inventory plugins can be specified in one of two ways: +The `--inventory` flag (or `-i`) specifies the inventory file or folder which should be used to construct the ansible inventory. The inventory can +reference a static file, an inventory plugin, or a folder containing multiple inventories. The inventory which should be used can be specified in one of +two ways: 1) The `--inventory` flag (or `-i`) can be used to pass in the location of an ansible inventory file 2) A `pyproject.toml` file can contain a `[tool.schema_enforcer]` config block setting the `ansible_inventory` paramer. This `pyproject.toml` file must be inside the repository from which the tool is run. From b00571b77df28c4d570f3c05c7414d3f8f83daa6 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sun, 29 Nov 2020 15:05:56 -0800 Subject: [PATCH 15/22] Update README.md with reference to ansible command --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 12ca340..9ed1c12 100755 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ python -m pip install schema-enforcer Schema Enforcer requires that two different elements be defined by the user: - Schema Definition Files: These are files which define the schema to which a given set of data should adhere. -- Structured Data Files: These are files which contain data that should adhere to the schema defined in one (or multiple) of the schema definition files +- Structured Data Files: These are files which contain data that should adhere to the schema defined in one (or multiple) of the schema definition files. + +> Note: Data which needs to be validated against a schema definition can come in the form of Structured Data Files or Ansible host vars. In the interest of brevity and simplicity, this README.md contains discussion only of Structured Data Files -- for more information on how to use `schema-enforcer` with ansible host vars, see [the ansible_command README](docs/ansible_command.md) When `schema-enforcer` runs, it assumes directory hierarchy which should be in place from the folder in which the tool is run. From e154323261cab6db4ff7f21a1503c006b2c906e3 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sun, 29 Nov 2020 17:44:47 -0800 Subject: [PATCH 16/22] Update examples --- docs/ansible_command.md | 27 +++++++------------ .../{inventory/inventory => inventory.ini} | 0 examples/ansible/inventory/simpleplugin.yml | 3 --- .../ansible/inventory_plugins/simpleplugin.py | 19 ------------- examples/ansible/pyproject.toml | 2 +- examples/ansible2/group_vars/leaf.yml | 8 ++++++ examples/ansible2/group_vars/nyc.yml | 1 + examples/ansible2/group_vars/spine.yml | 14 ++++++++++ examples/ansible2/inventory.ini | 12 +++++++++ examples/ansible2/pyproject.toml | 2 ++ .../ansible2/schema/definitions/arrays/ip.yml | 11 ++++++++ .../schema/definitions/objects/ip.yml | 26 ++++++++++++++++++ .../schema/definitions/properties/ip.yml | 8 ++++++ examples/ansible2/schema/schemas/leafs.yml | 11 ++++++++ examples/ansible2/schema/schemas/spines.yml | 23 ++++++++++++++++ 15 files changed, 127 insertions(+), 40 deletions(-) rename examples/ansible/{inventory/inventory => inventory.ini} (100%) delete mode 100644 examples/ansible/inventory/simpleplugin.yml delete mode 100644 examples/ansible/inventory_plugins/simpleplugin.py create mode 100644 examples/ansible2/group_vars/leaf.yml create mode 100644 examples/ansible2/group_vars/nyc.yml create mode 100644 examples/ansible2/group_vars/spine.yml create mode 100644 examples/ansible2/inventory.ini create mode 100644 examples/ansible2/pyproject.toml create mode 100755 examples/ansible2/schema/definitions/arrays/ip.yml create mode 100755 examples/ansible2/schema/definitions/objects/ip.yml create mode 100755 examples/ansible2/schema/definitions/properties/ip.yml create mode 100644 examples/ansible2/schema/schemas/leafs.yml create mode 100644 examples/ansible2/schema/schemas/spines.yml diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 8b28d75..c939be1 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -7,14 +7,12 @@ The `ansible` command is used to check ansible inventory for adherence to a sche When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory then validated against schema. Take the following example ```cli -bash $ cd examples/ansible && ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory -Found 6 hosts in the inventory +bash $ cd examples/ansible && schema-enforcer ansible +Found 4 hosts in the inventory FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address ``` -> Note: The `ANSIBLE_INVENTORY_PLUGINS` environment variable is used in this sample (and should be used in all examples described in this file), as the inventory is not relative to the inventory_plugin directory. In actual implementations, standard inventory rules apply. - The `schema-enforcer ansible` command validates adherence to schema on a **per host** basis. In the example above, both `spine1` and `spine2` devices belong to a group called `spine` ```ini @@ -59,14 +57,12 @@ Though the invalid property (the boolean true for a DNS address) is defined only The `--show-checks` flag is used to show which ansible inventory hosts will be validated against which schema definition IDs. ```cli -bash $ ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory --show-checks -Found 6 hosts in the inventory +bash$ schema-enforcer ansible --show-checks +Found 4 hosts in the inventory Ansible Host Schema ID -------------------------------------------------------------------------------- leaf1 ['schemas/dns_servers'] leaf2 ['schemas/dns_servers'] -leaf3 ['schemas/dns_servers'] -leaf4 ['schemas/dns_servers'] spine1 ['schemas/dns_servers', 'schemas/interfaces'] spine2 ['schemas/dns_servers', 'schemas/interfaces'] ``` @@ -78,16 +74,14 @@ spine2 ['schemas/dns_servers', 'schemas/interfaces'] The `--show-pass` flag is used to show what schema definition ids each host passes in addition to the schema definition ids each host fails. ```cli -bash $ ANSIBLE_INVENTORY_PLUGINS=$(pwd inventory_plugins) schema-enforcer ansible -i inventory --show-pass -Found 6 hosts in the inventory +bash$ schema-enforcer ansible --show-pass +Found 4 hosts in the inventory FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address PASS | [HOST] spine1 [SCHEMA ID] schemas/interfaces FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces PASS | [HOST] leaf1 [SCHEMA ID] schemas/dns_servers PASS | [HOST] leaf2 [SCHEMA ID] schemas/dns_servers -PASS | [HOST] leaf3 [SCHEMA ID] schemas/dns_servers -PASS | [HOST] leaf4 [SCHEMA ID] schemas/dns_servers ``` In the above example, the leaf switches are checked for adherence to the `schemas/dns_servers` definition and the spine switches are checked for adherence to two schema ids; the `schemas/dns_servers` schema id and the `schemas/interfaces` schema id. A PASS statement is printed to stdout for each validation that passes and a FAIL statement is printed for each validation that fails. @@ -97,16 +91,15 @@ In the above example, the leaf switches are checked for adherence to the `schema The `--host` flag can be used to limit schema validation to a single ansible inventory host. `-h` can also be used as shorthand for `--host` ```cli -bash$ schema-enforcer ansible -h spine2 --show-pass +bash$ schema-enforcer ansible -h spine2 --show-pass Found 4 hosts in the inventory -FAIL | [ERROR] True is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address +FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address PASS | [HOST] spine2 [SCHEMA ID] schemas/interfaces ``` ### The `--inventory` flag -The `--inventory` flag (or `-i`) specifies the inventory file or folder which should be used to construct the ansible inventory. The inventory can -reference a static file, an inventory plugin, or a folder containing multiple inventories. The inventory which should be used can be specified in one of +The `--inventory` flag (or `-i`) specifies the inventory file or folder which should be used to construct the ansible inventory. The inventory can reference a static file, an inventory plugin, or a folder containing multiple inventories. The inventory which should be used can be specified in one of two ways: 1) The `--inventory` flag (or `-i`) can be used to pass in the location of an ansible inventory file @@ -120,7 +113,7 @@ ansible_inventory = "inventory" If the inventory is set in both ways, the -i flag will take precedence. -> Note: Dynamic inventory sources can not currently be parsed for schema adherence. +> Note: Dynamic inventory sources can be parsed for schema adherence by using ansible built-in environment variables. An `ansible.cfg` file is not currently ingested as part of ansible inventory instantiation by `schema-enforcer` and thus can not declare settings. ## Inventory Variables and Schema Mapping diff --git a/examples/ansible/inventory/inventory b/examples/ansible/inventory.ini similarity index 100% rename from examples/ansible/inventory/inventory rename to examples/ansible/inventory.ini diff --git a/examples/ansible/inventory/simpleplugin.yml b/examples/ansible/inventory/simpleplugin.yml deleted file mode 100644 index f387558..0000000 --- a/examples/ansible/inventory/simpleplugin.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- - -plugin: "simpleplugin" diff --git a/examples/ansible/inventory_plugins/simpleplugin.py b/examples/ansible/inventory_plugins/simpleplugin.py deleted file mode 100644 index 94f7ad8..0000000 --- a/examples/ansible/inventory_plugins/simpleplugin.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Custom inventory Plugin for testing.""" - -from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable - - -class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): - """Sample simple inventory plugin.""" - - NAME = "simpleplugin" - - def verify_file(self, path): - """Verify file method, return True for this test plugin.""" - return True - - def parse(self, inventory, loader, path, cache=True): - """Parse method, add host from simple list of dictionaries.""" - super(InventoryModule, self).parse(inventory, loader, path, cache) - for device in [{"name": "leaf3", "group": "leaf"}, {"name": "leaf4", "group": "leaf"}]: - self.inventory.add_host(device["name"], group=device["group"]) diff --git a/examples/ansible/pyproject.toml b/examples/ansible/pyproject.toml index 26094bb..18157c4 100644 --- a/examples/ansible/pyproject.toml +++ b/examples/ansible/pyproject.toml @@ -1,2 +1,2 @@ [tool.schema_enforcer] -ansible_inventory = "inventory" +ansible_inventory = "inventory.ini" diff --git a/examples/ansible2/group_vars/leaf.yml b/examples/ansible2/group_vars/leaf.yml new file mode 100644 index 0000000..41431cd --- /dev/null +++ b/examples/ansible2/group_vars/leaf.yml @@ -0,0 +1,8 @@ +--- +dns_servers: + - address: "10.1.1.1" + - address: "10.2.2.2" + +schema_enforcer_strict: true +schema_enforcer_schema_ids: + - schemas/leafs diff --git a/examples/ansible2/group_vars/nyc.yml b/examples/ansible2/group_vars/nyc.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/examples/ansible2/group_vars/nyc.yml @@ -0,0 +1 @@ +--- diff --git a/examples/ansible2/group_vars/spine.yml b/examples/ansible2/group_vars/spine.yml new file mode 100644 index 0000000..312ee8b --- /dev/null +++ b/examples/ansible2/group_vars/spine.yml @@ -0,0 +1,14 @@ +--- +dns_servers: + - address: "10.1.1.1" + - address: "10.2.2.2" +interfaces: + swp1: + role: "uplink" + swp2: + role: "uplink" +bogus_property: true + +schema_enforcer_strict: true +schema_enforcer_schema_ids: + - schemas/spines diff --git a/examples/ansible2/inventory.ini b/examples/ansible2/inventory.ini new file mode 100644 index 0000000..9112915 --- /dev/null +++ b/examples/ansible2/inventory.ini @@ -0,0 +1,12 @@ + +[nyc:children] +spine +leaf + +[spine] +spine1 +spine2 + +[leaf] +leaf1 +leaf2 \ No newline at end of file diff --git a/examples/ansible2/pyproject.toml b/examples/ansible2/pyproject.toml new file mode 100644 index 0000000..d318418 --- /dev/null +++ b/examples/ansible2/pyproject.toml @@ -0,0 +1,2 @@ +[tool.schema_enforcer] +ansible_inventory = "inventory.ini" \ No newline at end of file diff --git a/examples/ansible2/schema/definitions/arrays/ip.yml b/examples/ansible2/schema/definitions/arrays/ip.yml new file mode 100755 index 0000000..0d22782 --- /dev/null +++ b/examples/ansible2/schema/definitions/arrays/ip.yml @@ -0,0 +1,11 @@ +--- +ipv4_networks: + type: "array" + items: + $ref: "../objects/ip.yml#ipv4_network" + uniqueItems: true +ipv4_hosts: + type: "array" + items: + $ref: "../objects/ip.yml#ipv4_host" + uniqueItems: true diff --git a/examples/ansible2/schema/definitions/objects/ip.yml b/examples/ansible2/schema/definitions/objects/ip.yml new file mode 100755 index 0000000..a8b38fe --- /dev/null +++ b/examples/ansible2/schema/definitions/objects/ip.yml @@ -0,0 +1,26 @@ +--- +ipv4_network: + type: "object" + properties: + name: + type: "string" + network: + $ref: "../properties/ip.yml#ipv4_address" + mask: + $ref: "../properties/ip.yml#ipv4_cidr" + vrf: + type: "string" + required: + - "network" + - "mask" +ipv4_host: + type: "object" + properties: + name: + type: "string" + address: + $ref: "../properties/ip.yml#ipv4_address" + vrf: + type: "string" + required: + - "address" diff --git a/examples/ansible2/schema/definitions/properties/ip.yml b/examples/ansible2/schema/definitions/properties/ip.yml new file mode 100755 index 0000000..8f0f830 --- /dev/null +++ b/examples/ansible2/schema/definitions/properties/ip.yml @@ -0,0 +1,8 @@ +--- +ipv4_address: + type: "string" + format: "ipv4" +ipv4_cidr: + type: "number" + minimum: 0 + maximum: 32 diff --git a/examples/ansible2/schema/schemas/leafs.yml b/examples/ansible2/schema/schemas/leafs.yml new file mode 100644 index 0000000..9112e4b --- /dev/null +++ b/examples/ansible2/schema/schemas/leafs.yml @@ -0,0 +1,11 @@ +--- +$schema: "http://json-schema.org/draft-07/schema#" +$id: "schemas/leafs" +description: "Leaf Switches Schema" +type: "object" +properties: + dns_servers: + type: object + $ref: "../definitions/arrays/ip.yml#ipv4_hosts" +required: + - dns_servers diff --git a/examples/ansible2/schema/schemas/spines.yml b/examples/ansible2/schema/schemas/spines.yml new file mode 100644 index 0000000..de090d8 --- /dev/null +++ b/examples/ansible2/schema/schemas/spines.yml @@ -0,0 +1,23 @@ +--- +$schema: "http://json-schema.org/draft-07/schema#" +$id: "schemas/spines" +description: "Spine Switches Schema" +type: "object" +properties: + dns_servers: + type: object + $ref: "../definitions/arrays/ip.yml#ipv4_hosts" + interfaces: + type: "object" + patternProperties: + ^swp.*$: + properties: + type: + type: "string" + description: + type: "string" + role: + type: "string" +required: + - dns_servers + - interfaces \ No newline at end of file From 8d1fb96ae5b0aed3dec9dfba1061f4f7946f7672 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sun, 29 Nov 2020 18:02:21 -0800 Subject: [PATCH 17/22] Update files to pass YAMLLint --- examples/ansible2/group_vars/leaf.yml | 2 +- examples/ansible2/group_vars/spine.yml | 2 +- examples/ansible2/schema/schemas/leafs.yml | 4 ++-- examples/ansible2/schema/schemas/spines.yml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/ansible2/group_vars/leaf.yml b/examples/ansible2/group_vars/leaf.yml index 41431cd..847e087 100644 --- a/examples/ansible2/group_vars/leaf.yml +++ b/examples/ansible2/group_vars/leaf.yml @@ -5,4 +5,4 @@ dns_servers: schema_enforcer_strict: true schema_enforcer_schema_ids: - - schemas/leafs + - "schemas/leafs" diff --git a/examples/ansible2/group_vars/spine.yml b/examples/ansible2/group_vars/spine.yml index 312ee8b..ba3d4bf 100644 --- a/examples/ansible2/group_vars/spine.yml +++ b/examples/ansible2/group_vars/spine.yml @@ -11,4 +11,4 @@ bogus_property: true schema_enforcer_strict: true schema_enforcer_schema_ids: - - schemas/spines + - "schemas/spines" diff --git a/examples/ansible2/schema/schemas/leafs.yml b/examples/ansible2/schema/schemas/leafs.yml index 9112e4b..20161c0 100644 --- a/examples/ansible2/schema/schemas/leafs.yml +++ b/examples/ansible2/schema/schemas/leafs.yml @@ -5,7 +5,7 @@ description: "Leaf Switches Schema" type: "object" properties: dns_servers: - type: object + type: "object" $ref: "../definitions/arrays/ip.yml#ipv4_hosts" required: - - dns_servers + - "dns_servers" diff --git a/examples/ansible2/schema/schemas/spines.yml b/examples/ansible2/schema/schemas/spines.yml index de090d8..dbba992 100644 --- a/examples/ansible2/schema/schemas/spines.yml +++ b/examples/ansible2/schema/schemas/spines.yml @@ -5,7 +5,7 @@ description: "Spine Switches Schema" type: "object" properties: dns_servers: - type: object + type: "object" $ref: "../definitions/arrays/ip.yml#ipv4_hosts" interfaces: type: "object" @@ -19,5 +19,5 @@ properties: role: type: "string" required: - - dns_servers - - interfaces \ No newline at end of file + - "dns_servers" + - "interfaces" From 08137c27752a23fba764cb1d73d39062d9e64eed Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Sun, 29 Nov 2020 18:02:37 -0800 Subject: [PATCH 18/22] Update doc strings --- poetry.lock | 89 ++++++++++++++-------------- schema_enforcer/ansible_inventory.py | 8 +-- schema_enforcer/cli.py | 44 +++++++------- 3 files changed, 68 insertions(+), 73 deletions(-) diff --git a/poetry.lock b/poetry.lock index 24d23d2..4660f9a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -112,7 +112,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.3" +version = "1.14.4" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -221,18 +221,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "2.0.0" +version = "3.1.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"] [[package]] name = "invoke" @@ -329,7 +329,7 @@ python-versions = ">=3.5" [[package]] name = "packaging" -version = "20.4" +version = "20.7" description = "Core utilities for Python packages" category = "main" optional = false @@ -337,7 +337,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" -six = "*" [[package]] name = "pathspec" @@ -706,42 +705,40 @@ certifi = [ {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, ] cffi = [ - {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, - {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, - {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, - {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, - {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, - {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, - {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, - {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, - {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, - {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, - {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, - {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, - {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, - {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, - {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, - {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, - {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, - {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, - {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, + {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, + {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, + {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, + {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, + {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, + {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, + {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, + {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, + {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, + {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, + {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, + {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, + {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, + {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, + {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, + {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, + {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, + {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, + {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -831,8 +828,8 @@ idna = [ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] importlib-metadata = [ - {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, - {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, + {file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"}, + {file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"}, ] invoke = [ {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, @@ -922,8 +919,8 @@ more-itertools = [ {file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"}, ] packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, + {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"}, + {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"}, ] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 34c9590..849f58b 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -27,7 +27,7 @@ def __init__(self, inventory=None, extra_vars=None): def get_hosts_containing(self, var=None): """Gets hosts that have a value for ``var``. - If ``var`` is None, then all hosts in inventory will be returned. + If ``var`` is None, then all hosts in the inventory will be returned. Args: var (str): The variable to use to restrict hosts. @@ -47,7 +47,7 @@ def get_host_vars(self, host): """Retrieves Jinja2 rendered variables for ``host``. Args: - host (ansible.inventory.host.Host): The host to retrieve variable data. + host (ansible.inventory.host.Host): The host to retrieve variable data from. Returns: dict: The variables defined by the ``host`` in Ansible Inventory. @@ -60,10 +60,10 @@ def get_clean_host_vars(self, host): """Return clean hostvars for a given host, cleaned up of all keys inserted by Templar. Args: - host (ansible.inventory.host.Host): The host to retrieve variable data. + host (ansible.inventory.host.Host): The host to retrieve variable data from. Returns: - dict: clean hostvar + dict: clean hostvars """ keys_cleanup = [ "inventory_file", diff --git a/schema_enforcer/cli.py b/schema_enforcer/cli.py index 55ef56d..6f012cd 100644 --- a/schema_enforcer/cli.py +++ b/schema_enforcer/cli.py @@ -173,18 +173,21 @@ def schema(check, generate_invalid, list_schemas): # noqa: D417 def ansible( inventory, limit, show_pass, show_checks ): # pylint: disable=too-many-branches,too-many-locals,too-many-locals - r"""Validate the hostvar for all hosts within an Ansible inventory. - - The hostvar are dynamically rendered based on groups. - For each host, if a variable `jsonschema_mapping` is defined, it will be used - to determine which schemas should be use to validate each key. + r"""Validate the hostvars for all hosts within an Ansible inventory. + + The hostvars are dynamically rendered based on groups to which each host belongs. + For each host, if a variable `schema_enforcer_schema_ids` is defined, it will be used + to determine which schemas should be use to validate each key. If this variable is + not defined, the hostvars top level keys will be automatically mapped to a schema + definition's top level properties to automatically infer which schema should be used + to validate which hostvar. \f Args: - inventory (string): The name of the inventory file to validate against - limit (string, None): Name of a host to limit the execution to - show_pass (bool): Shows validation checks that passed Default to False - show_checks (book): Shows the schema checks each host will be evaluated against + inventory (string): The name of the file used to construct an ansible inventory. + limit (string, None): Name of a host to limit the execution to. + show_pass (bool): Shows validation checks that pass. Defaults to False. + show_checks (bool): Shows the schema ids each host will be evaluated against. Example: $ cd examples/ansible @@ -194,22 +197,17 @@ def ansible( drwxr-xr-x 4 damien staff 128B Jul 25 16:37 host_vars -rw-r--r-- 1 damien staff 69B Jul 25 16:37 inventory.ini drwxr-xr-x 4 damien staff 128B Jul 25 16:37 schema - $ test-schema ansible -i inventory.ini - Found 4 hosts in the ansible inventory - FAIL | [ERROR] 12 is not of type 'string' [HOST] leaf1 [PROPERTY] dns_servers:0:address [SCHEMA] schemas/dns_servers - FAIL | [ERROR] 12 is not of type 'string' [HOST] leaf2 [PROPERTY] dns_servers:0:address [SCHEMA] schemas/dns_servers - $ test-schema ansible -i inventory.ini -h leaf1 - Found 4 hosts in the ansible inventory - FAIL | [ERROR] 12 is not of type 'string' [HOST] leaf1 [PROPERTY] dns_servers:0:address [SCHEMA] schemas/dns_servers - $ test-schema ansible -i inventory.ini -h spine1 --show-pass - WARNING | Could not find pyproject.toml in the current working directory. - WARNING | Script is being executed from CWD: /Users/damien/projects/schema_validator/examples/ansible - WARNING | Using built-in defaults for [tool.schema_validator] - WARNING | [tool.schema_validator.data_file_to_schema_ids_mapping] is not defined, instances must be tagged to apply schemas to instances + $ schema-enforcer ansible -i inventory.ini + Found 4 hosts in the inventory + FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address + FAIL | [ERROR] False is not of type 'string' [HOST] spine2 [PROPERTY] dns_servers:0:address + $ schema-enforcer ansible -i inventory.ini -h leaf1 Found 4 hosts in the inventory - PASS | [HOST] spine1 | [VAR] dns_servers | [SCHEMA] schemas/dns_servers - PASS | [HOST] spine1 | [VAR] interfaces | [SCHEMA] schemas/interfaces ALL SCHEMA VALIDATION CHECKS PASSED + $ schema-enforcer ansible -i inventory.ini -h spine1 --show-pass + Found 4 hosts in the inventory + FAIL | [ERROR] False is not of type 'string' [HOST] spine1 [PROPERTY] dns_servers:0:address + PASS | [HOST] spine1 [SCHEMA ID] schemas/interfaces """ if inventory: config.load(config_data={"ansible_inventory": inventory}) From da448cfc76bf6706b8cefd525f95733d2ba8f91b Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 30 Nov 2020 09:02:17 -0800 Subject: [PATCH 19/22] Fix typos per peer review --- docs/ansible_command.md | 2 +- schema_enforcer/ansible_inventory.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index c939be1..f312131 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -236,7 +236,7 @@ leaf1 [] The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This variable defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional host vars can exist beyond those that are defined in the schema. -From a design pattern perspective, when strict enforcment is used, all host variables are evaulated against a single schema id. This is in contrast to a design patern where a different schema id is defined for each top level host var/property. To this end, when strict enforcement is used, a single schema should be defined with references to schemas for all properties which are defined for a given host. The ids for such schema definitions are better named by role instead of host variable. For instance `schemas/spines` or `schemas/leafs` makes more sense with this design pattern than `schemas/dns_servers`. +From a design pattern perspective, when strict enforcment is used, all host variables are evaluated against a single schema id. This is in contrast to a design patern where a different schema id is defined for each top level host var/property. To this end, when strict enforcement is used, a single schema should be defined with references to schemas for all properties which are defined for a given host. The ids for such schema definitions are better named by role instead of host variable. For instance `schemas/spines` or `schemas/leafs` makes more sense with this design pattern than `schemas/dns_servers`. Two major caveats apply to using the `schema_enforcer_strict` variable. diff --git a/schema_enforcer/ansible_inventory.py b/schema_enforcer/ansible_inventory.py index 849f58b..643ec5e 100644 --- a/schema_enforcer/ansible_inventory.py +++ b/schema_enforcer/ansible_inventory.py @@ -137,7 +137,7 @@ def get_schema_validation_settings(self, host): host (AnsibleInventory.host): Ansible Inventory Host Object Raises: - TypeError: Raised when one of the scehma configuration parameters is of the wrong type + TypeError: Raised when one of the schema configuration parameters is of the wrong type ValueError: Raised when one of the schema configuration parameters is incorrectly configured Returns: From c123f0708eb0d1099c4daec820aeb4996c9e7e0b Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 30 Nov 2020 14:39:05 -0800 Subject: [PATCH 20/22] Update ansible readme per peer review --- docs/ansible_command.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index f312131..837fb57 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -4,7 +4,7 @@ The `ansible` command is used to check ansible inventory for adherence to a sche ## How the inventory is loaded -When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory then validated against schema. Take the following example +When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory into a single data structure per host, then this data structure is validated against all applicable schemas. Take the following example ```cli bash $ cd examples/ansible && schema-enforcer ansible @@ -120,7 +120,7 @@ If the inventory is set in both ways, the -i flag will take precedence. `schema-enforcer` will check ansible hosts for adherence to defined schema ids in one of two ways. - By using a list of schema ids defined by the `schema_enforcer_schema_ids` command -- By automatically mapping a schema's top level property to ansible variable keys. +- By automatically mapping a schema's top level properties to ansible variable keys. ### Using The `schema_enforcer_schema_ids` ansible inventory variable @@ -234,14 +234,14 @@ leaf1 [] ### The `schema_enforcer_strict` variable -The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This variable defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. This means that no additional host vars can exist beyond those that are defined in the schema. +The `schema_enforcer_strict` variable can be declared in an ansible host or group file. This setting defaults to false if not set. If set to true, the `schema-enforcer` tool checks for `strict` adherence to schema. Just like in normal operation, each host's variables are extracted from the ansible inventory into a single data structure. Unlike normal operation, only a single schema can be defined, and no additional host vars can exist in this data structure beyond those that are defined in that single schema. -From a design pattern perspective, when strict enforcment is used, all host variables are evaluated against a single schema id. This is in contrast to a design patern where a different schema id is defined for each top level host var/property. To this end, when strict enforcement is used, a single schema should be defined with references to schemas for all properties which are defined for a given host. The ids for such schema definitions are better named by role instead of host variable. For instance `schemas/spines` or `schemas/leafs` makes more sense with this design pattern than `schemas/dns_servers`. +From a design pattern perspective, because all host variables are evaluated against a single schema id when strict mode is used, the ids for schema definitions are better named by role instead of host variable. For instance `schemas/spines` or `schemas/leafs` makes more sense with this design pattern than `schemas/dns_servers` and/or `schemas/ntp`. Two major caveats apply to using the `schema_enforcer_strict` variable. 1) If the `schema_enforcer_strict` variable is set to true, the `schema_enforcer_schema_ids` variabe **MUST** be defined as a list of one and only one schema ID. If it is either not defined at all or defined as something other than a list with one element, an error will be printed to the screen and the tool will exit before performing any validations. -2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables defined for the ansible host/group. If an ansible variable not defined in the schema is defined for a given host, schema validation will fail as, when strict mode is run, properties not defined in the schema are not allowed. +2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables that exists when the inventory is rendered for a host. If an ansible variable not defined in the schema id defined for a given host, schema validation will fail. This happens because properties not defined in the schema are not allowed when strict mode is run. > Note: If either of these conditions are not met, an error message will be printed to stdout and the tool will stop execution before evaluating host variables against schema. From 5b529b81e1e1ae0e9002637d7c02eec392f92524 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 30 Nov 2020 14:46:01 -0800 Subject: [PATCH 21/22] Fix typos in readme.md --- docs/ansible_command.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 837fb57..722e752 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -4,7 +4,7 @@ The `ansible` command is used to check ansible inventory for adherence to a sche ## How the inventory is loaded -When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory into a single data structure per host, then this data structure is validated against all applicable schemas. Take the following example +When the `schema-enforcer ansible` command is run, an ansible inventory is constructed. Each host's properties are extracted from the ansible inventory into a single data structure per host, then this data structure is validated against all applicable schemas. For instance, take a look at the following example: ```cli bash $ cd examples/ansible && schema-enforcer ansible @@ -241,7 +241,7 @@ From a design pattern perspective, because all host variables are evaluated agai Two major caveats apply to using the `schema_enforcer_strict` variable. 1) If the `schema_enforcer_strict` variable is set to true, the `schema_enforcer_schema_ids` variabe **MUST** be defined as a list of one and only one schema ID. If it is either not defined at all or defined as something other than a list with one element, an error will be printed to the screen and the tool will exit before performing any validations. -2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables that exists when the inventory is rendered for a host. If an ansible variable not defined in the schema id defined for a given host, schema validation will fail. This happens because properties not defined in the schema are not allowed when strict mode is run. +2) The schema ID referenced by `schema_enforcer_schema_ids` **MUST** include all variables that exists when the inventory is rendered for a host. If an ansible variable is not defined as a property in the schema id defined for a given host, schema validation will fail. This happens because properties not defined in the schema are not allowed when strict mode is run. > Note: If either of these conditions are not met, an error message will be printed to stdout and the tool will stop execution before evaluating host variables against schema. From 3d51c5b0e14dec77b847071621103c925b7ecd38 Mon Sep 17 00:00:00 2001 From: Phillip Simonds Date: Mon, 30 Nov 2020 15:02:00 -0800 Subject: [PATCH 22/22] Update documentation per peer review --- docs/ansible_command.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/ansible_command.md b/docs/ansible_command.md index 722e752..f42533e 100644 --- a/docs/ansible_command.md +++ b/docs/ansible_command.md @@ -1,6 +1,13 @@ # The `ansible` command -The `ansible` command is used to check ansible inventory for adherence to a schema definition. An example exists in the `examples/ansible` folder. With no flags passed in, schema-enforcer will display a line for each property definition that **fails** schema validation along with contextual information elucidating why a given portion of the ansible inventory failed schema validation, the host for which schema validation failed, and the portion of structured data that is failing validation. If all checks pass, `schema-enforcer` will inform the user that all tests have passed. +The `ansible` command is used to check ansible inventory for adherence to a schema definition. An example exists in the `examples/ansible` folder. With no flags passed in, schema-enforcer will: + +- display a line for each property definition that **fails** schema validation +- provide contextual information elucidating why a given portion of the ansible inventory failed schema validation +- display the host for which schema validation failed +- enumerate the portion of structured data that is failing validation. + +If all checks pass, `schema-enforcer` will inform the user that all tests have passed. ## How the inventory is loaded