├── .bandit.yml ├── .dockerignore ├── .flake8 ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .pydocstyle.ini ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── ansible_command.md ├── assets │ └── images │ │ └── schema_list.png ├── configuration.md ├── custom_validators.md ├── mapping_data_files_to_schemas.md ├── schema_command.md └── validate_command.md ├── examples ├── ansible │ ├── group_vars │ │ ├── leaf.yml │ │ ├── nyc.yml │ │ └── spine.yml │ ├── host_vars │ │ ├── spine1.yml │ │ └── spine2.yml │ ├── inventory.ini │ ├── pyproject.toml │ └── schema │ │ ├── definitions │ │ ├── arrays │ │ │ └── ip.yml │ │ ├── objects │ │ │ └── ip.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── dns.yml │ │ └── interfaces.yml ├── ansible2 │ ├── group_vars │ │ ├── leaf.yml │ │ ├── nyc.yml │ │ └── spine.yml │ ├── inventory.ini │ ├── pyproject.toml │ └── schema │ │ ├── definitions │ │ ├── arrays │ │ │ └── ip.yml │ │ ├── objects │ │ │ └── ip.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── leafs.yml │ │ └── spines.yml ├── ansible3 │ ├── host_vars │ │ ├── az_phx_pe01 │ │ │ └── base.yml │ │ └── az_phx_pe02 │ │ │ └── base.yml │ ├── inventory.yml │ ├── pyproject.toml │ └── validators │ │ └── check_interfaces.py ├── example1 │ ├── chi-beijing-rt1 │ │ ├── dns.yml │ │ └── syslog.yml │ ├── eng-london-rt1 │ │ ├── dns.yml │ │ └── ntp.yml │ └── schema │ │ └── schemas │ │ ├── dns.yml │ │ ├── ntp.yml │ │ └── syslog.yml ├── example2 │ ├── README.md │ ├── hostvars │ │ ├── chi-beijing-rt1 │ │ │ ├── dns │ │ │ │ ├── v1 │ │ │ │ │ └── dns.yml │ │ │ │ └── v2 │ │ │ │ │ └── dns.yml │ │ │ └── syslog.yml │ │ ├── eng-london-rt1 │ │ │ ├── dns_v1.yml │ │ │ └── ntp.yml │ │ └── ger-berlin-rt1 │ │ │ └── dns_v2.yml │ ├── pyproject.toml │ └── schema │ │ ├── definitions │ │ ├── arrays │ │ │ ├── ip.yml │ │ │ └── ip_v2.yml │ │ ├── objects │ │ │ ├── ip.yml │ │ │ └── ip_v2.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── dns │ │ ├── v1.yml │ │ └── v2.yml │ │ ├── ntp.yml │ │ └── syslog.yml └── example3 │ ├── hostvars │ ├── chi-beijing-rt1 │ │ ├── dns.yml │ │ └── syslog.yml │ ├── eng-london-rt1 │ │ ├── dns.yml │ │ └── ntp.yml │ ├── fail-tests │ │ ├── dns.yml │ │ └── ntp.yml │ ├── ger-berlin-rt1 │ │ └── dns.yml │ ├── mex-mxc-rt1 │ │ └── hostvars.yml │ ├── usa-lax-rt1 │ │ ├── dns.yml │ │ └── syslog.yml │ └── usa-nyc-rt1 │ │ ├── dns.yml │ │ └── syslog.yml │ ├── pyproject.toml │ └── schema │ ├── definitions │ ├── arrays │ │ └── ip.yml │ ├── objects │ │ └── ip.yml │ └── properties │ │ └── ip.yml │ ├── schemas │ ├── dns.yml │ ├── ntp.yml │ └── syslog.yml │ └── tests │ ├── dns_servers │ ├── invalid │ │ ├── invalid_format │ │ │ ├── data.yml │ │ │ └── results.yml │ │ ├── invalid_ip │ │ │ ├── data.yml │ │ │ └── results.yml │ │ └── missing_required │ │ │ ├── data.yml │ │ │ └── results.yml │ └── valid │ │ ├── full_implementation.json │ │ └── partial_implementation.yml │ ├── ntp │ ├── invalid │ │ ├── invalid_format │ │ │ ├── data.yml │ │ │ └── results.yml │ │ ├── invalid_ip │ │ │ ├── data.yml │ │ │ └── results.yml │ │ └── missing_required │ │ │ ├── data.yml │ │ │ └── results.yml │ └── valid │ │ ├── full_implementation.json │ │ └── partial_implementation.json │ └── syslog_servers │ ├── invalid │ ├── invalid_format │ │ ├── data.yml │ │ └── results.yml │ ├── invalid_ip │ │ ├── data.yml │ │ └── results.yml │ └── missing_required │ │ ├── data.yml │ │ └── results.yml │ └── valid │ ├── full_implementation.json │ └── partial_implementation.json ├── poetry.lock ├── pyproject.toml ├── schema_enforcer ├── __init__.py ├── ansible_inventory.py ├── cli.py ├── config.py ├── exceptions.py ├── instances │ └── file.py ├── schemas │ ├── draft7_schema.json │ ├── jsonschema.py │ ├── manager.py │ └── validator.py ├── utils.py └── validation.py ├── tasks.py └── tests ├── __init__.py ├── conftest.py ├── fixtures ├── test_config │ ├── pyproject.toml │ ├── pyproject2.toml │ └── pyproject_invalid_attr.toml ├── test_instances │ ├── hostvars │ │ ├── chi-beijing-rt1 │ │ │ ├── dns.yml │ │ │ └── syslog.yml │ │ └── eng-london-rt1 │ │ │ ├── dns.yaml │ │ │ └── ntp.yaml │ ├── pyproject.toml │ └── schema │ │ ├── definitions │ │ ├── arrays │ │ │ └── ip.yml │ │ ├── objects │ │ │ └── ip.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── dns.yml │ │ ├── ntp.yml │ │ └── syslog.yml ├── test_jsonschema │ ├── hostvars │ │ ├── can-vancouver-rt1 │ │ │ └── dns.yml │ │ ├── chi-beijing-rt1 │ │ │ └── dns.yml │ │ ├── eng-london-rt1 │ │ │ └── dns.yml │ │ └── spa-madrid-rt1 │ │ │ ├── dns.yml │ │ │ ├── incorrect_date_format.yml │ │ │ ├── incorrect_datetime_format.yml │ │ │ ├── incorrect_email_format.yml │ │ │ ├── incorrect_hostname_format.yml │ │ │ ├── incorrect_ipv4_format.yml │ │ │ ├── incorrect_ipv6_format.yml │ │ │ ├── incorrect_jsonptr_format.yml │ │ │ ├── incorrect_regex_format.yml │ │ │ ├── incorrect_time_format.yml │ │ │ └── incorrect_uri_format.yml │ └── schema │ │ ├── definitions │ │ ├── arrays │ │ │ └── ip.yml │ │ ├── objects │ │ │ └── ip.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── dns.yml │ │ ├── incorrect_date_format.yml │ │ ├── incorrect_datetime_format.yml │ │ ├── incorrect_email_format.yml │ │ ├── incorrect_hostname_format.yml │ │ ├── incorrect_ipv4_format.yml │ │ ├── incorrect_ipv6_format.yml │ │ ├── incorrect_jsonptr_format.yml │ │ ├── incorrect_regex_format.yml │ │ ├── incorrect_time_format.yml │ │ ├── incorrect_uri_format.yml │ │ └── invalid.yml ├── test_manager │ ├── dump │ │ ├── all.txt │ │ └── byid.txt │ ├── invalid │ │ ├── pyproject.toml │ │ └── schema │ │ │ └── schemas │ │ │ └── invalid.yml │ └── invalid_generate │ │ └── schema │ │ ├── schemas │ │ └── test.yml │ │ └── tests │ │ └── test │ │ ├── invalid │ │ ├── invalid_type1 │ │ │ ├── data.json │ │ │ ├── exp_results.yml │ │ │ └── results.yml │ │ └── invalid_type2 │ │ │ ├── data.json │ │ │ ├── exp_results.yml │ │ │ └── results.yml │ │ └── valid │ │ └── test.json ├── test_validators │ ├── inventory │ │ ├── host_vars │ │ │ ├── az_phx_pe01 │ │ │ │ └── base.yml │ │ │ ├── az_phx_pe02 │ │ │ │ └── base.yml │ │ │ └── co_den_p01 │ │ │ │ └── base.yml │ │ └── inventory.yml │ └── validators │ │ ├── check_hostname.py │ │ ├── check_interfaces.py │ │ ├── check_interfaces_ipv4.py │ │ └── check_peers.py └── test_validators_pydantic │ ├── __init__.py │ ├── inventory │ ├── host_vars │ │ ├── az_phx_pe01 │ │ │ ├── base.yml │ │ │ └── dns.yml │ │ ├── az_phx_pe02 │ │ │ └── base.yml │ │ └── co_den_p01 │ │ │ ├── base.yml │ │ │ └── dns.yml │ └── inventory.yml │ ├── inventory_fail │ ├── host_vars │ │ ├── az_phx_pe01 │ │ │ ├── base.yml │ │ │ └── dns.yml │ │ ├── az_phx_pe02 │ │ │ └── base.yml │ │ └── co_den_p01 │ │ │ ├── base.yml │ │ │ └── dns.yml │ └── inventory.yml │ └── pydantic_validators │ ├── __init__.py │ └── models │ ├── __init__.py │ ├── dns.py │ ├── hostname.py │ └── interfaces.py ├── mocks ├── dns │ ├── invalid │ │ ├── invalid_format.json │ │ ├── invalid_format.yml │ │ ├── invalid_ip.json │ │ ├── invalid_ip.yml │ │ ├── missing_required.json │ │ └── missing_required.yml │ └── valid │ │ ├── full_implementation.json │ │ └── partial_implementation.json ├── inventory │ ├── group_vars │ │ ├── all.yml │ │ ├── emea.yml │ │ ├── ios.yml │ │ └── na.yml │ └── hosts ├── ntp │ ├── invalid │ │ ├── invalid_format.json │ │ ├── invalid_format.yml │ │ ├── invalid_ip.json │ │ ├── invalid_ip.yml │ │ ├── missing_required.json │ │ └── missing_required.yml │ └── valid │ │ ├── full_implementation.json │ │ └── partial_implementation.json ├── schema │ ├── json │ │ ├── definitions │ │ │ ├── arrays │ │ │ │ └── ip.json │ │ │ ├── objects │ │ │ │ └── ip.json │ │ │ └── properties │ │ │ │ └── ip.json │ │ ├── full_schemas │ │ │ └── ntp.json │ │ └── schemas │ │ │ ├── dns.json │ │ │ └── ntp.json │ └── yaml │ │ ├── definitions │ │ ├── arrays │ │ │ └── ip.yml │ │ ├── objects │ │ │ └── ip.yml │ │ └── properties │ │ │ └── ip.yml │ │ └── schemas │ │ ├── dns.yml │ │ └── ntp.yml ├── syslog │ ├── invalid │ │ ├── invalid_format.json │ │ ├── invalid_format.yml │ │ ├── invalid_ip.json │ │ ├── invalid_ip.yml │ │ ├── missing_required.json │ │ └── missing_required.yml │ └── valid │ │ ├── full_implementation.json │ │ └── partial_implementation.json └── utils │ ├── formatted.json │ ├── formatted.yml │ ├── host1 │ ├── dns.yml │ └── ntp.yml │ ├── host2 │ ├── dns.yml │ └── ntp.yml │ ├── host3 │ ├── dns.yml │ └── ntp.yml │ ├── host4 │ ├── dns.yml │ └── ntp.yml │ └── ntp_schema.json ├── test_ansible_inventory.py ├── test_cli_ansible_exists.py ├── test_cli_ansible_not_exists.py ├── test_config_settings.py ├── test_instances_instance_file.py ├── test_instances_instance_file_manager.py ├── test_jsonschema.py ├── test_schemas_pydantic_validators.py ├── test_schemas_schema_manager.py ├── test_schemas_validator.py ├── test_utils.py └── test_validator.py /.bandit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | skips: [] 3 | # No need to check for security issues in the test scripts! 4 | exclude_dirs: 5 | - "./tests/" 6 | - "./.venv/" 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | **/*.pyo 3 | **/*.log 4 | .git/ 5 | .gitignore 6 | Dockerfile 7 | docker-compose.yml 8 | .env 9 | docs/_build -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # E501: Line length is enforced by Black, so flake8 doesn't need to check it 3 | # W503: Black disagrees with this rule, as does PEP 8; Black wins 4 | ignore = E501, W503 5 | exclude = .venv -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | # This is a comment. 3 | # Each line is a file pattern followed by one or more owners. 4 | # See: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners 5 | 6 | # These owners will be the default owners for everything in the repo. 7 | # Unless a later match takes precedence, these will be requested for 8 | # review when someone opens a pull request. Once approved, PR creators 9 | # are encouraged to merge their own PRs. 10 | * @chadell @glennmatthews 11 | 12 | # Order is important; the last matching pattern takes the most 13 | # precedence. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report a reproducible bug in the current release of schema-enforcer 4 | --- 5 | 6 | ### Environment 7 | * Python version: 8 | * schema-enforcer version: 9 | 10 | 11 | ### Expected Behavior 12 | 13 | 14 | 15 | ### Observed Behavior 16 | 17 | 21 | ### Steps to Reproduce 22 | 1. 23 | 2. 24 | 3. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Propose a new feature or enhancement 4 | 5 | --- 6 | 7 | ### Environment 8 | * Python version: 9 | * schema-enforcer version: 10 | 11 | 14 | ### Proposed Functionality 15 | 16 | 21 | ### Use Case 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## New Pull Request 2 | 3 | Have you: 4 | - [ ] Written a unit test? 5 | - [ ] Updated the README if necessary? 6 | - [ ] Updated any configuration settings? 7 | 8 | ## Change Notes 9 | 10 | ## Justification 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | ### macOS ### 132 | # General 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | # Thumbnails 138 | ._* 139 | 140 | # Files that might appear in the root of a volume 141 | .DocumentRevisions-V100 142 | .fseventsd 143 | .Spotlight-V100 144 | .TemporaryItems 145 | .Trashes 146 | .VolumeIcon.icns 147 | .com.apple.timemachine.donotpresent 148 | 149 | # Directories potentially created on remote AFP share 150 | .AppleDB 151 | .AppleDesktop 152 | Network Trash Folder 153 | Temporary Items 154 | .apdisk 155 | 156 | ### Windows ### 157 | # Windows thumbnail cache files 158 | Thumbs.db 159 | Thumbs.db:encryptable 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Dump file 164 | *.stackdump 165 | 166 | # Folder config file 167 | [Dd]esktop.ini 168 | 169 | # Recycle Bin used on file shares 170 | $RECYCLE.BIN/ 171 | 172 | # Windows Installer files 173 | *.cab 174 | *.msi 175 | *.msix 176 | *.msm 177 | *.msp 178 | 179 | # Windows shortcuts 180 | *.lnk 181 | 182 | ### PyCharm ### 183 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 184 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 185 | 186 | # User-specific stuff 187 | .idea/**/workspace.xml 188 | .idea/**/tasks.xml 189 | .idea/**/usage.statistics.xml 190 | .idea/**/dictionaries 191 | .idea/**/shelf 192 | 193 | # Generated files 194 | .idea/**/contentModel.xml 195 | 196 | # Sensitive or high-churn files 197 | .idea/**/dataSources/ 198 | .idea/**/dataSources.ids 199 | .idea/**/dataSources.local.xml 200 | .idea/**/sqlDataSources.xml 201 | .idea/**/dynamic.xml 202 | .idea/**/uiDesigner.xml 203 | .idea/**/dbnavigator.xml 204 | 205 | # Gradle 206 | .idea/**/gradle.xml 207 | .idea/**/libraries 208 | 209 | # Gradle and Maven with auto-import 210 | # When using Gradle or Maven with auto-import, you should exclude module files, 211 | # since they will be recreated, and may cause churn. Uncomment if using 212 | # auto-import. 213 | # .idea/artifacts 214 | # .idea/compiler.xml 215 | # .idea/jarRepositories.xml 216 | # .idea/modules.xml 217 | # .idea/*.iml 218 | # .idea/modules 219 | # *.iml 220 | # *.ipr 221 | 222 | # CMake 223 | cmake-build-*/ 224 | 225 | # Mongo Explorer plugin 226 | .idea/**/mongoSettings.xml 227 | 228 | # File-based project format 229 | *.iws 230 | 231 | # IntelliJ 232 | out/ 233 | 234 | # mpeltonen/sbt-idea plugin 235 | .idea_modules/ 236 | 237 | # JIRA plugin 238 | atlassian-ide-plugin.xml 239 | 240 | # Cursive Clojure plugin 241 | .idea/replstate.xml 242 | 243 | # Crashlytics plugin (for Android Studio and IntelliJ) 244 | com_crashlytics_export_strings.xml 245 | crashlytics.properties 246 | crashlytics-build.properties 247 | fabric.properties 248 | 249 | # Editor-based Rest Client 250 | .idea/httpRequests 251 | 252 | # Android studio 3.1+ serialized cache file 253 | .idea/caches/build_file_checksums.ser 254 | 255 | ### PyCharm Patch ### 256 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 257 | 258 | # *.iml 259 | # modules.xml 260 | # .idea/misc.xml 261 | # *.ipr 262 | 263 | # Sonarlint plugin 264 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 265 | .idea/**/sonarlint/ 266 | 267 | # SonarQube Plugin 268 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 269 | .idea/**/sonarIssues.xml 270 | 271 | # Markdown Navigator plugin 272 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 273 | .idea/**/markdown-navigator.xml 274 | .idea/**/markdown-navigator-enh.xml 275 | .idea/**/markdown-navigator/ 276 | 277 | # Cache file creation bug 278 | # See https://youtrack.jetbrains.com/issue/JBR-2257 279 | .idea/$CACHE_FILE$ 280 | 281 | # CodeStream plugin 282 | # https://plugins.jetbrains.com/plugin/12206-codestream 283 | .idea/codestream.xml 284 | 285 | ### vscode ### 286 | .vscode/* 287 | 288 | *.code-workspace 289 | -------------------------------------------------------------------------------- /.pydocstyle.ini: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | convention = google 3 | inherit = false 4 | match = (?!__init__).*\.py 5 | match-dir = (?!tests)[^\.].* -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "default" 3 | rules: 4 | comments: "enable" 5 | empty-values: "enable" 6 | indentation: 7 | indent-sequences: "consistent" 8 | line-length: "disable" 9 | quoted-strings: 10 | quote-type: "double" 11 | ignore: | 12 | .venv/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.3.0 - 2024-02-13 4 | 5 | - #161 Migrate Schema enforcer to use pydanticv2 6 | 7 | ## v1.2.2 8 | 9 | - #156 Add support for jsonschema 4.18 10 | - Remove support for python version 3.7 11 | 12 | ## v1.2.1 13 | 14 | ### Changes 15 | 16 | - #152 Update requirement for rich to `>=9.5` 17 | 18 | ## v1.2.0 - 2023-06-05 19 | 20 | ### Adds 21 | 22 | - Support for versions of jsonschema >= 4.6 23 | 24 | ### Removes 25 | 26 | - Support for versions of jsonschema < 4.6. See #141 for details. 27 | 28 | ## v1.1.5 - 2022-07-27 29 | 30 | ### Changes 31 | 32 | - Fixes #141 - Can not install schema-enforcer in environments which require a version of jsonschema < 4.6 33 | 34 | ## v1.1.4 - 2022-07-13 35 | 36 | ### Adds 37 | 38 | - Add format_nongpl extra to jsonschema install. This ensures draft7 format checkers validate format adherence as expected while also ensuring GPL-Licenced transitive dependencies are not installed. 39 | 40 | ### Changes 41 | 42 | - Update jsonschema schema version dependency so that versions in the 4.x train are supported. 43 | 44 | ### Removes 45 | 46 | - Automatic support for `iri` and `iri-reference` format checkers. This was removed because these format checkers require the `rfc3987` library, which is licensed under GPL. If you require these checkers, you can manually install `rfc3987` or install this package as `jsonschema[rfc3987]`. 47 | 48 | ## v1.1.3 - 2022-05-31 49 | 50 | ### Changes 51 | 52 | - jinja2 version dependency specification modified such that versions in the 3.x release are supported 53 | 54 | ## v1.1.2 - 2022-01-10 55 | 56 | ### Changes 57 | 58 | - Update dependencies 59 | - Switch from slim to full python docker base image 60 | 61 | ## v1.1.1 - 2021-12-23 62 | 63 | ### Changes 64 | 65 | - Minor updates to documentation 66 | - Update CI build environment to use github actions instead of Travis CI 67 | - Update version of ruamel from 0.16 to 0.17 68 | 69 | ## v1.1.0 - 2021-05-25 70 | 71 | ### Adds 72 | 73 | - [Custom Validators](docs/custom_validators.md) 74 | - [Automatic mapping of schemas to data files](docs/mapping_data_files_to_schemas.md) 75 | - Automatic implementation of draft7 format checker to support [IPv4 and IPv6 format declarations](https://json-schema.org/understanding-json-schema/reference/string.html#id12) in a JSON Schema definition [#94](https://github.com/networktocode/schema-enforcer/issues/94) 76 | 77 | ### Changes 78 | 79 | - Removes Ansible as a mandatory dependency [#90](https://github.com/networktocode/schema-enforcer/issues/90) 80 | - `docs/mapping_schemas.md` renamed to `docs/mapping_data_files_to_schemas.md` 81 | - Simplifies the invoke tasks used for development 82 | - Schema enforcer now exits if an invalid schema is found while loading schemas [#99](https://github.com/networktocode/schema-enforcer/issues/99) 83 | 84 | ## v1.0.0 - 2021-01-26 85 | 86 | Schema Enforcer Initial Release 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | To contribute, follow this workflow. 4 | 5 | 1. Open an issue 6 | 2. Get approval from one of the codeowners before working on the issue 7 | 3. If working on the issue, assign the issue to yourself 8 | 4. Open a PR into integration 9 | 5. Get peer review and approval to merge from one of the codeowners 10 | 6. Once approval has been gained, merge the PR into integration 11 | 7. Once the PR is merged, delete the branch 12 | 8. One of the codeowners will enumerate the features added per the contributer's PR when a tagged release is merged into master 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VER 2 | 3 | FROM python:${PYTHON_VER} as base 4 | 5 | RUN pip install --upgrade pip && \ 6 | pip install poetry 7 | 8 | WORKDIR /local 9 | # Poetry fails install without README.md being copied. 10 | COPY pyproject.toml poetry.lock README.md /local/ 11 | COPY schema_enforcer /local/schema_enforcer 12 | 13 | RUN poetry config virtualenvs.create false \ 14 | && poetry install --no-interaction --no-ansi 15 | 16 | # ----------------------------------------------------------------------------- 17 | # Defines stage with ansible installed 18 | # ----------------------------------------------------------------------------- 19 | FROM base as with_ansible 20 | ARG ANSIBLE_PACKAGE=ansible-core 21 | ARG ANSIBLE_VER=2.11.7 22 | RUN pip install $ANSIBLE_PACKAGE==$ANSIBLE_VER 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2021 Network to Code 3 | Network to Code, LLC 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. -------------------------------------------------------------------------------- /docs/assets/images/schema_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/docs/assets/images/schema_list.png -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Various settings can be configured in [TOML format](https://toml.io/en/) by use of a pyproject.toml file in the folder from which the tool is run. A set of intuitive default configuration values exist. If a pyproject.toml file is defined, it will override the defaults for settings it declares, and leave the defaults in place for settings it does not declare. 4 | 5 | ## Customizing Project Config 6 | 7 | The CLI tool uses a configuration section beginning with `tool.schema_enforcer` in a `pyproject.toml` file to configure settings. There are examples of the configuration file in `examples/example2/pyproject.toml` and `examples/example3/pyproject.toml` folders, which work with the files inside of the `examples/example2/` and `examples/example3/` directories/subdirectories (respectively). 8 | 9 | ### Default Configuration Settings 10 | 11 | The following parameters can be specified within the pyproject.toml file used to configure the `schema enforcer` tool. The below text snippet lists the default for each of these configuration parameters. If a pyproject.toml file defines a subset of the available parameters, this susbset defined will override the defaults. Any parameter not defined in the pyproject.toml file will fall back to it's default value (as listed below). 12 | 13 | ```toml 14 | [tool.schema_enforcer] 15 | 16 | # Main Directory Names 17 | main_directory = "schema" 18 | definition_directory = "definitions" 19 | schema_directory = "schemas" 20 | test_directory = "tests" 21 | 22 | # Settings specific to the schema files 23 | schema_file_exclude_filenames = [] 24 | 25 | # settings specific to search and identify all instance file to validate 26 | data_file_search_directories = ["./"] 27 | data_file_extensions = [".json", ".yaml", ".yml"] 28 | data_file_exclude_filenames = [".yamllint.yml", ".travis.yml"] 29 | data_file_automap = true 30 | 31 | [tool.schema_enforcer.schema_mapping] 32 | ``` 33 | 34 | ### Overriding the Default Configuration 35 | 36 | The table below enumerates each individual setting, it's expected type, it's default, and a description. 37 | 38 | | Configuration Setting | Type | Default | description | 39 | |---|---|---|---| 40 | | main_directory | string | "schema" | The directory in which to start searching for schema and definition files | 41 | | definition_directory | string | "definitions" | The directory in which to search for schema definition references. These definitions are can be referenced by the schema files in the "schema_directory". This directory should be nested in the "main_directory" | 42 | | schema_directory | string | "schemas" | The directory in which to search for schemas. This directory should be nested in the "main_directory" | 43 | | test_directory | string | "tests" | The directory in which to search for valid and invalid unit tests for schemas | 44 | | validator_directory | string | "validators" | The directory in which schema-enforcer searches for custom validators | 45 | | schema_file_extensions | list | [".json", ".yaml", ".yml"] | The extensions to use when searching for schema definition files | 46 | | schema_file_exclude_filenames | list | [] | The list of filenames to exclude when searching for schema files in the `schema_directory` directory | 47 | | data_file_search_directories | list | ["./"] The paths at which to start searching for files with structured data in them to validate against defined schemas. This path is relative to the directory in which `schema-enforcer` is executed. 48 | | data_file_extensions | list | [".json", ".yaml", ".yml"] | The extensions to use when searching for structured data files | 49 | | data_file_exclude_filenames | list | [".yamllint.yml", ".travis.yml"] | The list of filenames to exclude when searching for structured data files | 50 | | data_file_automap | bool | true | Whether or not to map top level keys in a data file to the top level properties defined in a schema | 51 | | ansible_inventory | str | None | The ansible inventory file to use when building an inventory of hosts against which to check for schema adherence | 52 | | schema_mapping | dict | {} | A mapping of structured data file names (keys) to lists of schema IDs (values) against which the data file should be checked for adherence | -------------------------------------------------------------------------------- /docs/validate_command.md: -------------------------------------------------------------------------------- 1 | # The `validate` command 2 | 3 | The `schema-enforcer validate` command is used to check structured data files for adherence to schema definitions. Inside of examples/example3 exists a basic hierarchy of directories. With no flags passed in, this tool will display a line per each property definition that **fails** schema validation along with contextual information elucidating why a given portion of the structured data failed schema validation, the file in which the structured data failing validation is defined, 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. 4 | 5 | ```cli 6 | bash$ cd examples/example3 && schema-enforcer validate 7 | FAIL | [ERROR] 123 is not of type 'string' [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] ntp_servers:1:vrf 8 | FAIL | [ERROR] Additional properties are not allowed ('test_extra_property' was unexpected) [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] 9 | ``` 10 | 11 | In addition to printing these messages, `schema-enforcer` *intentionally exits with an error code of 1*. This is done so that the tool can be used in a pipeline or a script and fail the pipeline/script so that further execution is not performed if schema validations do not pass. As an example, if some tool is consuming YAML data you may want to make sure that YAML data is schema valid before passing it into the tool to ensure downstream failures do not occur because the data it's consuming is not a valid input. 12 | 13 | If multiple schema validation errors occur in the same file, all errors will be printed to stdout on their own line. This was done in the spirit of a tool like pylint, which informs the user of all errors for a given file so that the user can correct them before re-running the tool. 14 | 15 | The default behaviour of the `schema-enforcer validate` command can be modified by passing in one of a few flags. 16 | 17 | #### The `--show-checks` flag 18 | 19 | The `--show-checks` flag is used to show which structured data files will be validated against which schema definition IDs. 20 | 21 | ```cli 22 | bash$ cd examples/example3 && schema-enforcer validate --show-checks 23 | Structured Data File Schema ID 24 | -------------------------------------------------------------------------------- 25 | ./hostvars/chi-beijing-rt1/dns.yml ['schemas/dns_servers'] 26 | ./hostvars/chi-beijing-rt1/syslog.yml ['schemas/syslog_servers'] 27 | ./hostvars/eng-london-rt1/dns.yml ['schemas/dns_servers'] 28 | ./hostvars/eng-london-rt1/ntp.yml ['schemas/ntp'] 29 | ./hostvars/fail-tests/dns.yml ['schemas/dns_servers'] 30 | ./hostvars/fail-tests/ntp.yml ['schemas/ntp'] 31 | ./hostvars/ger-berlin-rt1/dns.yml ['schemas/dns_servers'] 32 | ./hostvars/mex-mxc-rt1/dns.yml ['schemas/dns_servers'] 33 | ./hostvars/mex-mxc-rt1/syslog.yml ['schemas/syslog_servers'] 34 | ./hostvars/usa-lax-rt1/dns.yml ['schemas/dns_servers'] 35 | ./hostvars/usa-lax-rt1/syslog.yml ['schemas/syslog_servers'] 36 | ./hostvars/usa-nyc-rt1/dns.yml ['schemas/dns_servers'] 37 | ./hostvars/usa-nyc-rt1/syslog.yml ['schemas/syslog_servers'] 38 | ./inventory/group_vars/all.yml [] 39 | ./inventory/group_vars/apac.yml [] 40 | ./inventory/group_vars/emea.yml [] 41 | ./inventory/group_vars/lax.yml [] 42 | ./inventory/group_vars/na.yml [] 43 | ./inventory/group_vars/nyc.yml [] 44 | ``` 45 | 46 | > The structured data file can be mapped to schema definitions in one of a few ways. See the [README in docs/mapping_schemas.md](./mapping_schemas.md) for more information. The [README.md in examples/example2](../examples/example2) also contains detailed examples of schema mappings. 47 | 48 | #### The `--show-pass` flag 49 | 50 | By default, only portinos of data which fail schema validation are printed to stdout. If you would like to see files which pass schema validation as well as those that fail, you can pass the `--show-pass` flag into `schema-enforcer`. 51 | 52 | ```cli 53 | bash$ cd examples/example3 && schema-enforcer validate --show-pass 54 | PASS [FILE] ./hostvars/eng-london-rt1/ntp.yml 55 | PASS [FILE] ./hostvars/eng-london-rt1/dns.yml 56 | PASS [FILE] ./hostvars/chi-beijing-rt1/syslog.yml 57 | PASS [FILE] ./hostvars/chi-beijing-rt1/dns.yml 58 | PASS [FILE] ./hostvars/usa-lax-rt1/syslog.yml 59 | PASS [FILE] ./hostvars/usa-lax-rt1/dns.yml 60 | PASS [FILE] ./hostvars/ger-berlin-rt1/dns.yml 61 | PASS [FILE] ./hostvars/usa-nyc-rt1/syslog.yml 62 | PASS [FILE] ./hostvars/usa-nyc-rt1/dns.yml 63 | PASS [FILE] ./hostvars/mex-mxc-rt1/syslog.yml 64 | PASS [FILE] ./hostvars/mex-mxc-rt1/dns.yml 65 | FAIL | [ERROR] 123 is not of type 'string' [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] ntp_servers:1:vrf 66 | FAIL | [ERROR] Additional properties are not allowed ('test_extra_property' was unexpected) [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] 67 | PASS [FILE] ./hostvars/fail-tests/dns.yml 68 | ``` 69 | 70 | #### The `--strict` flag 71 | 72 | By default, schema validations defined by JSONSchema are done in a "non-strict" manner. In effect, this means that extra properties are allowed at every level of a schema definition unless the `additionalProperties` key is explicitly set to false for the JSONSchema property. Running the validate command with the `--strict` flag ensures that, if not explicitly set to allowed, additionalProperties are disallowed and structued data files with additional properties will fail schema validation. 73 | 74 | ```cli 75 | bash$ cd examples/example3 && schema-enforcer validate --strict 76 | FAIL | [ERROR] 123 is not of type 'string' [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] ntp_servers:1:vrf 77 | FAIL | [ERROR] Additional properties are not allowed ('test_extra_item_property' was unexpected) [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] ntp_servers:1 78 | FAIL | [ERROR] Additional properties are not allowed ('test_extra_property' was unexpected) [FILE] ./hostvars/fail-tests/ntp.yml [PROPERTY] 79 | FAIL | [ERROR] Additional properties are not allowed ('test_extra_property' was unexpected) [FILE] ./hostvars/fail-tests/dns.yml [PROPERTY] dns_servers:1 80 | ``` 81 | 82 | > Note: The schema definition `additionalProperties` attribute is part of JSONSchema standard definitions. More information on how to construct these definitions can be found [here](https://json-schema.org/understanding-json-schema/reference/object.html) -------------------------------------------------------------------------------- /examples/ansible/group_vars/leaf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | - address: "10.2.2.2" 5 | -------------------------------------------------------------------------------- /examples/ansible/group_vars/nyc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /examples/ansible/group_vars/spine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: false 4 | - address: "10.2.2.2" 5 | interfaces: 6 | swp1: 7 | role: "uplink" 8 | swp2: 9 | role: "uplink" 10 | 11 | schema_enforcer_schema_ids: 12 | - "schemas/dns_servers" 13 | - "schemas/interfaces" 14 | -------------------------------------------------------------------------------- /examples/ansible/host_vars/spine1.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/examples/ansible/host_vars/spine1.yml -------------------------------------------------------------------------------- /examples/ansible/host_vars/spine2.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/examples/ansible/host_vars/spine2.yml -------------------------------------------------------------------------------- /examples/ansible/inventory.ini: -------------------------------------------------------------------------------- 1 | 2 | [nyc:children] 3 | spine 4 | leaf 5 | 6 | [spine] 7 | spine1 8 | spine2 9 | 10 | [leaf] 11 | leaf1 12 | leaf2 -------------------------------------------------------------------------------- /examples/ansible/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | ansible_inventory = "inventory.ini" 3 | -------------------------------------------------------------------------------- /examples/ansible/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /examples/ansible/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /examples/ansible/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /examples/ansible/schema/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /examples/ansible/schema/schemas/interfaces.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/interfaces" 4 | description: "Interfaces configuration schema." 5 | type: "object" 6 | properties: 7 | interfaces: 8 | type: "object" 9 | patternProperties: 10 | ^swp.*$: 11 | properties: 12 | type: 13 | type: "string" 14 | description: 15 | type: "string" 16 | role: 17 | type: "string" 18 | -------------------------------------------------------------------------------- /examples/ansible2/group_vars/leaf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | - address: "10.2.2.2" 5 | 6 | schema_enforcer_strict: true 7 | schema_enforcer_schema_ids: 8 | - "schemas/leafs" 9 | magic_vars_to_evaluate: ["inventory_hostname"] 10 | -------------------------------------------------------------------------------- /examples/ansible2/group_vars/nyc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /examples/ansible2/group_vars/spine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | - address: "10.2.2.2" 5 | interfaces: 6 | swp1: 7 | role: "uplink" 8 | swp2: 9 | role: "uplink" 10 | bogus_property: true 11 | 12 | schema_enforcer_strict: true 13 | schema_enforcer_schema_ids: 14 | - "schemas/spines" 15 | magic_vars_to_evaluate: ["inventory_hostname"] 16 | -------------------------------------------------------------------------------- /examples/ansible2/inventory.ini: -------------------------------------------------------------------------------- 1 | 2 | [nyc:children] 3 | spine 4 | leaf 5 | 6 | [spine] 7 | spine1 8 | spine2 9 | 10 | [leaf] 11 | leaf1 12 | leaf2 -------------------------------------------------------------------------------- /examples/ansible2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | ansible_inventory = "inventory.ini" -------------------------------------------------------------------------------- /examples/ansible2/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /examples/ansible2/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /examples/ansible2/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /examples/ansible2/schema/schemas/leafs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/leafs" 4 | description: "Leaf Switches Schema" 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "object" 9 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 10 | inventory_hostname: 11 | type: "string" 12 | required: 13 | - "dns_servers" 14 | - "inventory_hostname" 15 | -------------------------------------------------------------------------------- /examples/ansible2/schema/schemas/spines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/spines" 4 | description: "Spine Switches Schema" 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "object" 9 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 10 | interfaces: 11 | type: "object" 12 | patternProperties: 13 | ^swp.*$: 14 | properties: 15 | type: 16 | type: "string" 17 | description: 18 | type: "string" 19 | role: 20 | type: "string" 21 | inventory_hostname: 22 | type: "string" 23 | required: 24 | - "dns_servers" 25 | - "interfaces" 26 | - "inventory_hostname" 27 | -------------------------------------------------------------------------------- /examples/ansible3/host_vars/az_phx_pe01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe01" 3 | pair_rtr: "az-phx-pe02" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.1" 8 | Loopback0: 9 | ipv4: "192.168.1.1" 10 | ipv6: "2001:db8:1::1" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.1" 13 | ipv6: "2001:db8::" 14 | peer: "az-phx-pe02" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.37" 19 | ipv6: "2001:db8::12" 20 | peer: "co-den-p01" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "core" 23 | -------------------------------------------------------------------------------- /examples/ansible3/host_vars/az_phx_pe02/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe02" 3 | pair_rtr: "az-phx-pe01" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.2" 8 | Loopback0: 9 | ipv4: "192.168.1.2" 10 | ipv6: "2001:db8:1::2" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.2" 13 | ipv6: "2001:db8::1" 14 | peer: "az-phx-pe01" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.41" 19 | ipv6: "2001:db8::14" 20 | peer: "co-den-p02" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "access" 23 | -------------------------------------------------------------------------------- /examples/ansible3/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | vars: 4 | ansible_network_os: "iosxr" 5 | ansible_user: "cisco" 6 | ansible_password: "cisco" 7 | ansible_connection: "netconf" 8 | ansible_netconf_ssh_config: true 9 | children: 10 | pe_rtrs: 11 | hosts: 12 | az_phx_pe01: 13 | ansible_host: "172.16.1.1" 14 | az_phx_pe02: 15 | ansible_host: "172.16.1.2" 16 | -------------------------------------------------------------------------------- /examples/ansible3/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | ansible_inventory = "inventory.yml" -------------------------------------------------------------------------------- /examples/ansible3/validators/check_interfaces.py: -------------------------------------------------------------------------------- 1 | """Example validator plugin.""" 2 | from schema_enforcer.schemas.validator import JmesPathModelValidation 3 | 4 | 5 | class CheckInterface(JmesPathModelValidation): # pylint: disable=too-few-public-methods 6 | """Check that each device has more than one core uplink.""" 7 | 8 | top_level_properties = ["interfaces"] 9 | id = "CheckInterface" # pylint: disable=invalid-name 10 | left = "interfaces.*[@.type=='core'][] | length([?@])" 11 | right = 2 12 | operator = "gte" 13 | error = "Less than two core interfaces" 14 | -------------------------------------------------------------------------------- /examples/example1/chi-beijing-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/dns_servers 2 | --- 3 | dns_servers: 4 | - address: true 5 | - address: "10.2.2.2" 6 | -------------------------------------------------------------------------------- /examples/example1/chi-beijing-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/syslog_servers 2 | --- 3 | syslog_servers: 4 | - address: "10.3.3.3" 5 | -------------------------------------------------------------------------------- /examples/example1/eng-london-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/dns_servers 2 | --- 3 | dns_servers: 4 | - address: "10.6.6.6" 5 | - address: "10.7.7.7" 6 | -------------------------------------------------------------------------------- /examples/example1/eng-london-rt1/ntp.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/ntp 2 | --- 3 | ntp_servers: 4 | - address: "10.6.6.6" 5 | name: "ntp1" 6 | - address: "10.7.7.7" 7 | name: "ntp1" 8 | ntp_authentication: false 9 | ntp_logging: true 10 | -------------------------------------------------------------------------------- /examples/example1/schema/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | name: 13 | type: "string" 14 | address: 15 | type: "string" 16 | format: "ipv4" 17 | vrf: 18 | type: "string" 19 | required: 20 | - "address" 21 | uniqueItems: true 22 | required: 23 | - "dns_servers" 24 | -------------------------------------------------------------------------------- /examples/example1/schema/schemas/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/ntp" 4 | description: "NTP Configuration schema." 5 | type: "object" 6 | properties: 7 | ntp_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | name: 13 | type: "string" 14 | address: 15 | type: "string" 16 | format: "ipv4" 17 | vrf: 18 | type: "string" 19 | required: 20 | - "address" 21 | uniqueItems: true 22 | ntp_authentication: 23 | type: "boolean" 24 | ntp_logging: 25 | type: "boolean" 26 | additionalProperties: false 27 | required: 28 | - "ntp_servers" 29 | -------------------------------------------------------------------------------- /examples/example1/schema/schemas/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/syslog_servers" 4 | description: "Syslog Server Configuration schema." 5 | type: "object" 6 | properties: 7 | syslog_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | name: 13 | type: "string" 14 | address: 15 | type: "string" 16 | format: "ipv4" 17 | vrf: 18 | type: "string" 19 | required: 20 | - "address" 21 | uniqueItems: true 22 | required: 23 | - "syslog_servers" 24 | -------------------------------------------------------------------------------- /examples/example2/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This README.md describes behaviour not yet implemented. As such, it has been commented out and will be modified when the behaviour is implemented. 4 | 5 | 144 | -------------------------------------------------------------------------------- /examples/example2/hostvars/chi-beijing-rt1/dns/v1/dns.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/dns_servers 2 | --- 3 | dns_servers: 4 | - address: "10.1.1.1" 5 | - address: "10.2.2.2" 6 | -------------------------------------------------------------------------------- /examples/example2/hostvars/chi-beijing-rt1/dns/v2/dns.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/dns_servers_v2 2 | --- 3 | dns_servers: 4 | - host: "10.1.1.1" 5 | - host: "10.2.2.2" 6 | -------------------------------------------------------------------------------- /examples/example2/hostvars/chi-beijing-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - address: "10.3.3.3" 4 | -------------------------------------------------------------------------------- /examples/example2/hostvars/eng-london-rt1/dns_v1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /examples/example2/hostvars/eng-london-rt1/ntp.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/ntp 2 | --- 3 | ntp_servers: 4 | - address: "10.6.6.6" 5 | name: "ntp1" 6 | - address: "10.7.7.7" 7 | name: "ntp1" 8 | ntp_authentication: false 9 | ntp_logging: true 10 | -------------------------------------------------------------------------------- /examples/example2/hostvars/ger-berlin-rt1/dns_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - host: "10.6.6.6" 4 | - host: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /examples/example2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer.schema_mapping] 2 | # Map structured data filename to schema IDs 3 | 'dns_v1.yml' = ['schemas/dns_servers'] 4 | 'dns_v2.yml' = ['schemas/dns_servers_v2'] 5 | 'syslog.yml' = ["schemas/syslog_servers"] 6 | -------------------------------------------------------------------------------- /examples/example2/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /examples/example2/schema/definitions/arrays/ip_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip_v2.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip_v2.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /examples/example2/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /examples/example2/schema/definitions/objects/ip_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | host: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "host" 27 | -------------------------------------------------------------------------------- /examples/example2/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /examples/example2/schema/schemas/dns/v1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /examples/example2/schema/schemas/dns/v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers_v2" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../../definitions/arrays/ip_v2.yml#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /examples/example2/schema/schemas/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/ntp" 4 | description: "NTP Configuration schema." 5 | type: "object" 6 | properties: 7 | ntp_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | ntp_authentication: 10 | type: "boolean" 11 | ntp_logging: 12 | type: "boolean" 13 | additionalProperties: false 14 | required: 15 | - "ntp_servers" 16 | something: "extra" 17 | -------------------------------------------------------------------------------- /examples/example2/schema/schemas/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/syslog_servers" 4 | description: "Syslog Server Configuration schema." 5 | type: "object" 6 | properties: 7 | syslog_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "syslog_servers" 11 | -------------------------------------------------------------------------------- /examples/example3/hostvars/chi-beijing-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | - address: "10.2.2.2" 5 | -------------------------------------------------------------------------------- /examples/example3/hostvars/chi-beijing-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - address: "10.3.3.3" 4 | -------------------------------------------------------------------------------- /examples/example3/hostvars/eng-london-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /examples/example3/hostvars/eng-london-rt1/ntp.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/ntp 2 | # Future: , http://networktocode.com/schemas/core/ntp 3 | --- 4 | ntp_servers: 5 | - address: "10.6.6.6" 6 | name: "ntp1" 7 | - address: "10.7.7.7" 8 | name: "ntp1" 9 | ntp_authentication: false 10 | ntp_logging: true 11 | -------------------------------------------------------------------------------- /examples/example3/hostvars/fail-tests/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | test_extra_property: "Will fail --strict testing" 6 | -------------------------------------------------------------------------------- /examples/example3/hostvars/fail-tests/ntp.yml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/ntp 2 | # Future: , http://networktocode.com/schemas/core/ntp 3 | --- 4 | ntp_servers: 5 | - address: "10.6.6.6" 6 | name: "ntp1" 7 | - address: "10.7.7.7" 8 | name: "ntp1" 9 | vrf: 123 10 | test_extra_item_property: "This should trigger when --strict is used" 11 | ntp_authentication: false 12 | ntp_logging: true 13 | test_extra_property: "This extra property will trigger when --strict is used" 14 | -------------------------------------------------------------------------------- /examples/example3/hostvars/ger-berlin-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /examples/example3/hostvars/mex-mxc-rt1/hostvars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.12.12.12" 4 | - address: "10.13.13.13" 5 | syslog_servers: 6 | - address: "10.14.14.14" 7 | -------------------------------------------------------------------------------- /examples/example3/hostvars/usa-lax-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.9.9.9" 4 | - address: "10.10.10.10" 5 | -------------------------------------------------------------------------------- /examples/example3/hostvars/usa-lax-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - address: "10.11.11.11" 4 | -------------------------------------------------------------------------------- /examples/example3/hostvars/usa-nyc-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.14.14.14" 4 | - address: "10.15.15.15" 5 | -------------------------------------------------------------------------------- /examples/example3/hostvars/usa-nyc-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - address: "10.8.8.8" 4 | -------------------------------------------------------------------------------- /examples/example3/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | schema_file_exclude_filenames = [] 3 | 4 | definition_directory = "definitions" 5 | schema_directory = "schemas" 6 | 7 | data_file_exclude_filenames = ['.yamllint.yml', '.travis.yml'] 8 | 9 | [tool.schema_enforcer.schema_mapping] 10 | # Map structured data filename to list of schema id which should be used to validate adherence to schema 11 | 'dns.yml' = ['schemas/dns_servers'] 12 | 'syslog.yml' = ["schemas/syslog_servers"] -------------------------------------------------------------------------------- /examples/example3/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /examples/example3/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /examples/example3/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | pattern: ^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$ # yamllint disable-line rule:quoted-strings 6 | ipv4_cidr: 7 | type: "number" 8 | minimum: 0 9 | maximum: 32 10 | -------------------------------------------------------------------------------- /examples/example3/schema/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /examples/example3/schema/schemas/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/ntp" 4 | description: "NTP Configuration schema." 5 | type: "object" 6 | properties: 7 | ntp_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | ntp_authentication: 10 | type: "boolean" 11 | ntp_logging: 12 | type: "boolean" 13 | additionalProperties: false 14 | required: 15 | - "ntp_servers" 16 | something: "extra" 17 | -------------------------------------------------------------------------------- /examples/example3/schema/schemas/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/syslog_servers" 4 | description: "Syslog Server Configuration schema." 5 | type: "object" 6 | properties: 7 | syslog_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "syslog_servers" 11 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/invalid_format/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - "10.1.1.1" 4 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/invalid_format/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/dns_servers" 5 | absolute_path: 6 | - "dns_servers" 7 | - "0" 8 | message: "'10.1.1.1' is not of type 'object'" 9 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/invalid_ip/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - name: "ntp-east" 4 | address: "10.1.1.1000" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/invalid_ip/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/dns_servers" 5 | absolute_path: 6 | - "dns_servers" 7 | - "0" 8 | - "address" 9 | message: "'10.1.1.1000' does not match '^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 10 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 11 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$'" 12 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/missing_required/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_server: 3 | - name: "ntp-east" 4 | address: "10.1.1.1" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/invalid/missing_required/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/dns_servers" 5 | absolute_path: [] 6 | message: "'dns_servers' is a required property" 7 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/dns_servers/valid/partial_implementation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - name: "ntp-east" 4 | address: "10.1.1.1" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/invalid_format/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - "10.1.1.1" 4 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/invalid_format/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/ntp" 5 | absolute_path: 6 | - "ntp_servers" 7 | - "0" 8 | message: "'10.1.1.1' is not of type 'object'" 9 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/invalid_ip/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - name: "ntp-east" 4 | address: "10.1.1.1000" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/invalid_ip/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/ntp" 5 | absolute_path: 6 | - "ntp_servers" 7 | - "0" 8 | - "address" 9 | message: "'10.1.1.1000' does not match '^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 10 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 11 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$'" 12 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/missing_required/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_server: 3 | - name: "ntp-east" 4 | address: "10.1.1.1" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/invalid/missing_required/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/ntp" 5 | absolute_path: [] 6 | message: "Additional properties are not allowed ('ntp_server' was unexpected)" 7 | - result: "FAIL" 8 | schema_id: "schemas/ntp" 9 | absolute_path: [] 10 | message: "'ntp_servers' is a required property" 11 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/ntp/valid/partial_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/invalid_format/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - "10.1.1.1" 4 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/invalid_format/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/syslog_servers" 5 | absolute_path: 6 | - "syslog_servers" 7 | - "0" 8 | message: "'10.1.1.1' is not of type 'object'" 9 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/invalid_ip/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - name: "ntp-east" 4 | address: "10.1.1.1" 5 | - name: "ntp-west" 6 | address: "10.1.1.1000" 7 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/invalid_ip/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/syslog_servers" 5 | absolute_path: 6 | - "syslog_servers" 7 | - "1" 8 | - "address" 9 | message: "'10.1.1.1000' does not match '^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 10 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\ 11 | \\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$'" 12 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/missing_required/data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_server: 3 | - name: "ntp-east" 4 | address: "10.1.1.1" 5 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/invalid/missing_required/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/syslog_servers" 5 | absolute_path: [] 6 | message: "'syslog_servers' is a required property" 7 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/example3/schema/tests/syslog_servers/valid/partial_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "schema-enforcer" 3 | version = "1.3.0" 4 | description = "Tool/Framework for testing structured data against schema definitions" 5 | authors = ["Network to Code, LLC "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/networktocode/schema-enforcer" 9 | repository = "https://github.com/networktocode/schema-enforcer" 10 | include = [ 11 | "CHANGELOG.md", 12 | "LICENSE", 13 | "README.md", 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.8" 18 | click = "^7.1 || ^8.0" 19 | termcolor = "^1.1" 20 | toml = "^0.10" 21 | "ruamel.yaml" = "^0.16 || ^0.17" 22 | jinja2 = ">=2.11" 23 | jsonref = "^0.2" 24 | pydantic = "^2.0" 25 | rich = ">=9.5" 26 | jsonpointer = "^2.1" 27 | jmespath = "^0.10" 28 | ansible = { version = "^2.10.0", optional = true } 29 | ansible-base = { version = "^2.10.0", optional = true } 30 | jsonschema = {version = "^4.6", extras = ["format-nongpl"]} 31 | pydantic-settings = "^2.1" 32 | 33 | [tool.poetry.extras] 34 | ansible = ["ansible"] 35 | ansible-base = ["ansible-base"] 36 | 37 | [tool.poetry.dev-dependencies] 38 | pytest = "*" 39 | requests_mock = "*" 40 | pyyaml = "*" 41 | black = "*" 42 | pylint = "*" 43 | pydocstyle = "*" 44 | yamllint = "*" 45 | bandit = "*" 46 | invoke = "*" 47 | toml = "*" 48 | flake8 = "*" 49 | 50 | [tool.poetry.scripts] 51 | schema-enforcer = "schema_enforcer.cli:main" 52 | [tool.black] 53 | line-length = 120 54 | include = '\.pyi?$' 55 | exclude = ''' 56 | /( 57 | \.git 58 | | \.tox 59 | | \.venv 60 | | env/ 61 | | _build 62 | | build 63 | | dist 64 | )/ 65 | ''' 66 | 67 | [tool.pylint.master] 68 | ignore=".venv" 69 | 70 | [tool.pylint.basic] 71 | # No docstrings required for private methods (pylint default) or for test_ functions. 72 | no-docstring-rgx="^(_|test_)" 73 | 74 | [tool.pylint.messages_control] 75 | # Line length is enforced by Black, so pylint doesn't need to check it. 76 | # Pylint and Black disagree about how to format multi-line arrays; Black wins. 77 | disable = """, 78 | line-too-long, 79 | """ 80 | 81 | [tool.pylint.miscellaneous] 82 | # Don't flag TODO as a failure, let us commit with things that still need to be done in the code 83 | notes = """, 84 | FIXME, 85 | XXX, 86 | """ 87 | 88 | [tool.pylint.SIMILARITIES] 89 | min-similarity-lines = 15 90 | 91 | [tool.pytest.ini_options] 92 | testpaths = [ 93 | "tests" 94 | ] 95 | addopts = "-vv --doctest-modules" 96 | 97 | [build-system] 98 | requires = ["poetry>=0.12"] 99 | build-backend = "poetry.masonry.api" 100 | -------------------------------------------------------------------------------- /schema_enforcer/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialization file for library.""" 2 | # pylint: disable=C0114 3 | 4 | __version__ = "1.1.3" 5 | -------------------------------------------------------------------------------- /schema_enforcer/config.py: -------------------------------------------------------------------------------- 1 | """Tests config Settings class.""" 2 | import os 3 | import os.path 4 | import sys 5 | from pathlib import Path 6 | from typing import Dict, List, Optional 7 | from typing_extensions import Annotated 8 | 9 | import toml 10 | from pydantic import Field, ValidationError 11 | from pydantic_settings import BaseSettings, SettingsConfigDict 12 | 13 | SETTINGS = None 14 | 15 | 16 | class Settings(BaseSettings): # pylint: disable=too-few-public-methods 17 | """Main Settings Class for the project. 18 | 19 | The type of each setting is defined using Python annotations 20 | and is validated when a config file is loaded with Pydantic. 21 | 22 | Most input files specific to this project are expected to be located in the same directory. e.g. 23 | schema/ 24 | - definitions 25 | - schemas 26 | """ 27 | 28 | model_config = SettingsConfigDict(populate_by_name=True, env_prefix="jsonschema_") 29 | 30 | # Main directory names 31 | main_directory: str = Field("schema", alias="jsonschema_directory") 32 | definition_directory: str = "definitions" 33 | schema_directory: str = "schemas" 34 | validator_directory: str = "validators" 35 | pydantic_validators: Optional[List[Annotated[str, Field(pattern="^.*:.*$")]]] = Field(default_factory=list) 36 | test_directory: str = "tests" 37 | 38 | # Settings specific to the schema files 39 | schema_file_extensions: List[str] = [ 40 | ".json", 41 | ".yaml", 42 | ".yml", 43 | ] # Do we still need that ? 44 | schema_file_exclude_filenames: List[str] = [] 45 | 46 | # settings specific to search and identify all instance file to validate 47 | data_file_search_directories: List[str] = ["./"] 48 | data_file_extensions: List[str] = [".json", ".yaml", ".yml"] 49 | data_file_exclude_filenames: List[str] = [".yamllint.yml", ".travis.yml"] 50 | data_file_automap: bool = True 51 | 52 | ansible_inventory: Optional[str] = None 53 | schema_mapping: Dict = {} 54 | 55 | 56 | def load(config_file_name="pyproject.toml", config_data=None): 57 | """Load configuration. 58 | 59 | Configuration is loaded from a file in pyproject.toml format that contains the settings, 60 | or from a dictionary of those settings passed in as "config_data" 61 | 62 | The settings for this app are expected to be in [tool.json_schema_testing] in TOML 63 | if nothing is found in the config file or if the config file do not exist, the default values will be used. 64 | 65 | config_data can be passed in to override the config_file_name. If this is done, a combination of the data 66 | specified and the defaults for parameters not specified will be used, and settings in the config file will 67 | be ignored. 68 | 69 | Args: 70 | config_file_name (str, optional): Name of the configuration file to load. Defaults to "pyproject.toml". 71 | config_data (dict, optional): dict to load as the config file instead of reading the file. Defaults to None. 72 | """ 73 | global SETTINGS # pylint: disable=global-statement 74 | 75 | if config_data: 76 | SETTINGS = Settings(**config_data) 77 | return 78 | if os.path.exists(config_file_name): 79 | config_string = Path(config_file_name).read_text(encoding="utf-8") 80 | config_tmp = toml.loads(config_string) 81 | 82 | if "tool" in config_tmp and "schema_enforcer" in config_tmp.get("tool", {}): 83 | SETTINGS = Settings(**config_tmp["tool"]["schema_enforcer"]) 84 | return 85 | 86 | SETTINGS = Settings() 87 | 88 | 89 | def load_and_exit(config_file_name="pyproject.toml", config_data=None): 90 | """Calls load, but wraps it in a try except block. 91 | 92 | This is done to handle a ValidationErorr which is raised when settings are specified but invalid. 93 | In such cases, a message is printed to the screen indicating the settings which don't pass validation. 94 | 95 | Args: 96 | config_file_name (str, optional): [description]. Defaults to "pyproject.toml". 97 | config_data (dict, optional): [description]. Defaults to None. 98 | """ 99 | try: 100 | load(config_file_name=config_file_name, config_data=config_data) 101 | except ValidationError as err: 102 | print(f"Configuration not valid, found {len(err.errors())} error(s)") 103 | for error in err.errors(): 104 | print(f" {'/'.join(error['loc'])} | {error['msg']} ({error['type']})") 105 | sys.exit(1) 106 | -------------------------------------------------------------------------------- /schema_enforcer/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes used in Schema Enforcer. 2 | 3 | Copyright (c) 2020 Network To Code, LLC 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | 16 | class SchemaNotDefined(Exception): 17 | """Raised when a schema is declared but not defined. 18 | 19 | Args (Exception): Base Exception Object 20 | """ 21 | 22 | 23 | class InvalidJSONSchema(Exception): 24 | """Raised when a JSONschema file is invalid. 25 | 26 | Args (Exception): Base Exception Object 27 | """ 28 | 29 | def __init__(self, schema): 30 | """Provide instance variables when invalid schema is detected.""" 31 | super().__init__(schema) 32 | self.schema = schema 33 | 34 | def __str__(self): 35 | """Generate error string including validation errors.""" 36 | errors = [result.message for result in self.schema.check_if_valid() if not result.passed()] 37 | message = f"Invalid JSONschema file: {self.schema.filename} - {errors}" 38 | return message 39 | -------------------------------------------------------------------------------- /schema_enforcer/instances/file.py: -------------------------------------------------------------------------------- 1 | """InstanceFile and InstanceFileManager.""" 2 | import os 3 | import re 4 | import itertools 5 | from pathlib import Path 6 | from ruamel.yaml.comments import CommentedMap 7 | from schema_enforcer.utils import find_files, load_file 8 | 9 | SCHEMA_TAG = "jsonschema" 10 | 11 | 12 | class InstanceFileManager: # pylint: disable=too-few-public-methods 13 | """InstanceFileManager.""" 14 | 15 | def __init__(self, config): 16 | """Initialize the interface File manager. 17 | 18 | The file manager will locate all potential instance files in the search directories. 19 | 20 | Args: 21 | config (pydantic.BaseSettings): The Pydantec settings object. 22 | """ 23 | self.instances = [] 24 | self.config = config 25 | 26 | # Find all instance files 27 | instance_files = find_files( 28 | file_extensions=config.data_file_extensions, 29 | search_directories=config.data_file_search_directories, 30 | excluded_filenames=config.data_file_exclude_filenames, 31 | excluded_directories=[config.main_directory], 32 | return_dir=True, 33 | ) 34 | 35 | # For each instance file, check if there is a static mapping defined in the config 36 | # Create the InstanceFile object and save it 37 | for root, filename in instance_files: 38 | matches = set() 39 | if filename in config.schema_mapping: 40 | matches.update(config.schema_mapping[filename]) 41 | 42 | instance = InstanceFile(root=root, filename=filename, matches=matches) 43 | self.instances.append(instance) 44 | 45 | def add_matches_by_property_automap(self, schema_manager): 46 | """Adds schema_ids to matches by automapping top level schema properties to top level keys in instance data. 47 | 48 | Args: 49 | schema_manager (schema_enforcer.schemas.manager.SchemaManager): Schema manager oject 50 | """ 51 | for instance in self.instances: 52 | instance.add_matches_by_property_automap(schema_manager) 53 | 54 | def print_schema_mapping(self): 55 | """Print in CLI the matches for all instance files.""" 56 | print("{:50} Schema ID".format("Structured Data File")) # pylint: disable=consider-using-f-string 57 | print("-" * 80) 58 | print_strings = [] 59 | for instance in self.instances: 60 | filepath = f"{instance.path}/{instance.filename}" 61 | print_strings.append(f"{filepath:50} {sorted(instance.matches)}") 62 | print("\n".join(sorted(print_strings))) 63 | 64 | 65 | class InstanceFile: 66 | """Class to manage an instance file.""" 67 | 68 | def __init__(self, root, filename, matches=None): 69 | """Initializes InstanceFile object. 70 | 71 | Args: 72 | root (string): Absolute path to the directory where the schema file is located. 73 | filename (string): Name of the file. 74 | matches (set, optional): Set of schema IDs that matches with this Instance file. Defaults to None. 75 | """ 76 | self.data = None 77 | self.path = root 78 | self.full_path = os.path.realpath(root) 79 | self.filename = filename 80 | 81 | # Internal vars for caching data 82 | self._top_level_properties = set() 83 | 84 | if matches: 85 | self.matches = matches 86 | else: 87 | self.matches = set() 88 | 89 | self._add_matches_by_decorator() 90 | 91 | @property 92 | def top_level_properties(self): 93 | """Return a list of top level properties in the structured data defined by the data pulled from _get_content. 94 | 95 | Returns: 96 | set: Set of the strings of top level properties defined by the data file 97 | """ 98 | if not self._top_level_properties: 99 | content = self._get_content() 100 | # TODO: Investigate and see if we should be checking this on initialization if the file doesn't exists or is empty. 101 | if not content: 102 | return self._top_level_properties 103 | 104 | if isinstance(content, CommentedMap) or hasattr(content, "keys"): 105 | self._top_level_properties = set(content.keys()) 106 | elif isinstance(content, str): 107 | self._top_level_properties = set([content]) 108 | elif isinstance(content, list): 109 | properties = set() 110 | for m in content: 111 | if isinstance(m, dict) or hasattr(m, "keys"): 112 | properties.update(m.keys()) 113 | else: 114 | properties.add(m) 115 | self._top_level_properties = properties 116 | else: 117 | self._top_level_properties = set(content) 118 | 119 | return self._top_level_properties 120 | 121 | def _add_matches_by_decorator(self, content=None): 122 | """Add matches which declare schema IDs they should adhere to using a decorator comment. 123 | 124 | If a line of the form # jsonschema: , is defined in the data file, the 125 | schema IDs will be added to the list of schema IDs the data will be checked for adherence to. 126 | 127 | Args: 128 | content (string, optional): Content of the file to analyze. Default to None. 129 | 130 | Returns: 131 | set(string): Set of matches (strings of schema_ids) found in the file. 132 | """ 133 | if not content: 134 | content = self._get_content(structured=False) 135 | 136 | matches = set() 137 | 138 | if SCHEMA_TAG in content: 139 | line_regexp = r"^#.*{0}:\s*(.*)$".format(SCHEMA_TAG) # pylint: disable=consider-using-f-string 140 | match = re.match(line_regexp, content, re.MULTILINE) 141 | if match: 142 | matches = {x.strip() for x in match.group(1).split(",")} 143 | 144 | self.matches.update(matches) 145 | 146 | def _get_content(self, structured=True): 147 | """Returns the content of the instance file. 148 | 149 | Args: 150 | structured (bool): Return structured data if true. If false returns the string representation of the data 151 | stored in the instance file. Defaults to True. 152 | 153 | Returns: 154 | dict, list, or str: File Contents. Dict or list if structured is set to True. Otherwise returns a string. 155 | """ 156 | file_location = os.path.join(self.full_path, self.filename) 157 | 158 | if not structured: 159 | return Path(file_location).read_text(encoding="utf-8") 160 | 161 | return load_file(file_location) 162 | 163 | def add_matches_by_property_automap(self, schema_manager): 164 | """Adds schema_ids to self.matches by automapping top level schema properties to top level keys in instance data. 165 | 166 | Args: 167 | schema_manager (schema_enforcer.schemas.manager.SchemaManager): Schema manager oject 168 | """ 169 | matches = set() 170 | 171 | for schema_id, schema_obj in schema_manager.iter_schemas(): 172 | if schema_obj.top_level_properties.intersection(self.top_level_properties): 173 | matches.add(schema_id) 174 | 175 | self.matches.update(matches) 176 | 177 | def validate(self, schema_manager, strict=False): 178 | """Validate this instance file with all matching schema in the schema manager. 179 | 180 | Args: 181 | schema_manager (SchemaManager): A SchemaManager object. 182 | strict (bool, optional): True is the validation should automatically flag unsupported element. Defaults to False. 183 | 184 | Returns: 185 | iterator: Iterator of ValidationErrors returned by schema.validate. 186 | """ 187 | # TODO need to add something to check if a schema is missing 188 | # Create new iterator chain to be able to aggregate multiple iterators 189 | errs = itertools.chain() 190 | 191 | # Go over all schemas and skip any schema not present in the matches 192 | for schema_id, schema in schema_manager.iter_schemas(): 193 | if schema_id not in self.matches: 194 | continue 195 | schema.validate(self._get_content(), strict) 196 | results = schema.get_results() 197 | errs = itertools.chain(errs, results) 198 | schema.clear_results() 199 | 200 | return errs 201 | -------------------------------------------------------------------------------- /schema_enforcer/schemas/draft7_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://json-schema.org/draft-07/schema#", 4 | "title": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "nonNegativeInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "nonNegativeIntegerDefault0": { 16 | "allOf": [ 17 | { "$ref": "#/definitions/nonNegativeInteger" }, 18 | { "default": 0 } 19 | ] 20 | }, 21 | "simpleTypes": { 22 | "enum": [ 23 | "array", 24 | "boolean", 25 | "integer", 26 | "null", 27 | "number", 28 | "object", 29 | "string" 30 | ] 31 | }, 32 | "stringArray": { 33 | "type": "array", 34 | "items": { "type": "string" }, 35 | "uniqueItems": true, 36 | "default": [] 37 | } 38 | }, 39 | "type": ["object", "boolean"], 40 | "properties": { 41 | "$id": { 42 | "type": "string", 43 | "format": "uri-reference" 44 | }, 45 | "$schema": { 46 | "type": "string", 47 | "format": "uri" 48 | }, 49 | "$ref": { 50 | "type": "string", 51 | "format": "uri-reference" 52 | }, 53 | "$comment": { 54 | "type": "string" 55 | }, 56 | "title": { 57 | "type": "string" 58 | }, 59 | "description": { 60 | "type": "string" 61 | }, 62 | "default": true, 63 | "readOnly": { 64 | "type": "boolean", 65 | "default": false 66 | }, 67 | "writeOnly": { 68 | "type": "boolean", 69 | "default": false 70 | }, 71 | "examples": { 72 | "type": "array", 73 | "items": true 74 | }, 75 | "multipleOf": { 76 | "type": "number", 77 | "exclusiveMinimum": 0 78 | }, 79 | "maximum": { 80 | "type": "number" 81 | }, 82 | "exclusiveMaximum": { 83 | "type": "number" 84 | }, 85 | "minimum": { 86 | "type": "number" 87 | }, 88 | "exclusiveMinimum": { 89 | "type": "number" 90 | }, 91 | "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, 92 | "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 93 | "pattern": { 94 | "type": "string", 95 | "format": "regex" 96 | }, 97 | "additionalItems": { "$ref": "#" }, 98 | "items": { 99 | "anyOf": [ 100 | { "$ref": "#" }, 101 | { "$ref": "#/definitions/schemaArray" } 102 | ], 103 | "default": true 104 | }, 105 | "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, 106 | "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 107 | "uniqueItems": { 108 | "type": "boolean", 109 | "default": false 110 | }, 111 | "contains": { "$ref": "#" }, 112 | "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, 113 | "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 114 | "required": { "$ref": "#/definitions/stringArray" }, 115 | "additionalProperties": { "$ref": "#" }, 116 | "definitions": { 117 | "type": "object", 118 | "additionalProperties": { "$ref": "#" }, 119 | "default": {} 120 | }, 121 | "properties": { 122 | "type": "object", 123 | "additionalProperties": { "$ref": "#" }, 124 | "default": {} 125 | }, 126 | "patternProperties": { 127 | "type": "object", 128 | "additionalProperties": { "$ref": "#" }, 129 | "propertyNames": { "format": "regex" }, 130 | "default": {} 131 | }, 132 | "dependencies": { 133 | "type": "object", 134 | "additionalProperties": { 135 | "anyOf": [ 136 | { "$ref": "#" }, 137 | { "$ref": "#/definitions/stringArray" } 138 | ] 139 | } 140 | }, 141 | "propertyNames": { "$ref": "#" }, 142 | "const": true, 143 | "enum": { 144 | "type": "array", 145 | "items": true, 146 | "minItems": 1, 147 | "uniqueItems": true 148 | }, 149 | "type": { 150 | "anyOf": [ 151 | { "$ref": "#/definitions/simpleTypes" }, 152 | { 153 | "type": "array", 154 | "items": { "$ref": "#/definitions/simpleTypes" }, 155 | "minItems": 1, 156 | "uniqueItems": true 157 | } 158 | ] 159 | }, 160 | "format": { "type": "string" }, 161 | "contentMediaType": { "type": "string" }, 162 | "contentEncoding": { "type": "string" }, 163 | "if": { "$ref": "#" }, 164 | "then": { "$ref": "#" }, 165 | "else": { "$ref": "#" }, 166 | "allOf": { "$ref": "#/definitions/schemaArray" }, 167 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 168 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 169 | "not": { "$ref": "#" } 170 | }, 171 | "default": true 172 | } 173 | -------------------------------------------------------------------------------- /schema_enforcer/schemas/jsonschema.py: -------------------------------------------------------------------------------- 1 | """class to manage jsonschema type schema.""" 2 | import copy 3 | import json 4 | import os 5 | from functools import cached_property 6 | 7 | from jsonschema import Draft7Validator # pylint: disable=import-self 8 | from schema_enforcer.schemas.validator import BaseValidation 9 | from schema_enforcer.validation import ValidationResult, RESULT_FAIL, RESULT_PASS 10 | 11 | 12 | class JsonSchema(BaseValidation): # pylint: disable=too-many-instance-attributes 13 | """class to manage jsonschema type schemas.""" 14 | 15 | schematype = "jsonchema" 16 | 17 | def __init__(self, schema, filename, root): 18 | """Initilize a new JsonSchema object from a dict. 19 | 20 | Args: 21 | schema (dict): Data representing the schema. Must be jsonschema valid. 22 | filename (string): Name of the schema file on the filesystem. 23 | root (string): Absolute path to the directory where the schema file is located. 24 | """ 25 | super().__init__() 26 | self.filename = filename 27 | self.root = root 28 | self.data = schema 29 | self.id = self.data.get("$id") # pylint: disable=invalid-name 30 | self.top_level_properties = set(self.data.get("properties")) 31 | self.validator = None 32 | self.strict_validator = None 33 | self.format_checker = Draft7Validator.FORMAT_CHECKER 34 | 35 | @cached_property 36 | def v7_schema(self): 37 | """Draft7 Schema.""" 38 | local_dirname = os.path.dirname(os.path.abspath(__file__)) 39 | with open(os.path.join(local_dirname, "draft7_schema.json"), encoding="utf-8") as fhd: 40 | v7_schema = json.loads(fhd.read()) 41 | 42 | return v7_schema 43 | 44 | def get_id(self): 45 | """Return the unique ID of the schema.""" 46 | return self.id 47 | 48 | def validate(self, data, strict=False): 49 | """Validate a given data with this schema. 50 | 51 | Args: 52 | data (dict, list): Data to validate against the schema. 53 | strict (bool, optional): if True the validation will automatically flag additional properties. Defaults to False. 54 | 55 | Returns: 56 | Iterator: Iterator of ValidationResult 57 | """ 58 | if strict: 59 | validator = self.__get_strict_validator() 60 | else: 61 | validator = self.__get_validator() 62 | 63 | has_error = False 64 | for err in validator.iter_errors(data): 65 | has_error = True 66 | self.add_validation_error(err.message, absolute_path=list(err.absolute_path)) 67 | 68 | if not has_error: 69 | self.add_validation_pass() 70 | return self.get_results() 71 | 72 | def validate_to_dict(self, data, strict=False): 73 | """Return a list of ValidationResult objects. 74 | 75 | These are generated with the validate() function in dict() format instead of as a Python Object. 76 | 77 | Args: 78 | data (dict, list): Data to validate against the schema. 79 | strict (bool, optional): if True the validation will automatically flag additional properties. Defaults to False. 80 | 81 | Returns: 82 | list of dictionnaries containing the results. 83 | """ 84 | return [ 85 | result.model_dump(exclude_unset=True, exclude_none=True) 86 | for result in self.validate(data=data, strict=strict) 87 | ] 88 | 89 | def __get_validator(self): 90 | """Return the validator for this schema, create if it doesn't exist already. 91 | 92 | Returns: 93 | Draft7Validator: The validator for this schema. 94 | """ 95 | if self.validator: 96 | return self.validator 97 | 98 | self.validator = Draft7Validator(self.data, format_checker=self.format_checker) 99 | 100 | return self.validator 101 | 102 | def __get_strict_validator(self): 103 | """Return a strict version of the Validator, create it if it doesn't exist already. 104 | 105 | To create a strict version of the schema, this function adds `additionalProperties` to all objects in the schema. 106 | 107 | Returns: 108 | Draft7Validator: Validator for this schema in strict mode. 109 | """ 110 | # TODO Currently the function is only modifying the top level object, need to add that to all objects recursively 111 | if self.strict_validator: 112 | return self.strict_validator 113 | 114 | # Create a copy if the schema first and modify it to insert `additionalProperties` 115 | schema = copy.deepcopy(self.data) 116 | 117 | if schema.get("additionalProperties", False) is not False: 118 | print(f"{schema['$id']}: Overriding existing additionalProperties: {schema['additionalProperties']}") 119 | 120 | schema["additionalProperties"] = False 121 | 122 | # TODO This should be recursive, e.g. all sub-objects, currently it only goes one level deep, look in jsonschema for utilitiies 123 | for prop_name, prop in schema.get("properties", {}).items(): 124 | items = prop.get("items", {}) 125 | if items.get("type") == "object": 126 | if items.get("additionalProperties", False) is not False: 127 | print( 128 | f"{schema['$id']}: Overriding item {prop_name}.additionalProperties: {items['additionalProperties']}" 129 | ) 130 | items["additionalProperties"] = False 131 | 132 | self.strict_validator = Draft7Validator(schema, format_checker=self.format_checker) 133 | return self.strict_validator 134 | 135 | def check_if_valid(self): 136 | """Check if the schema definition is valid against JsonSchema draft7. 137 | 138 | Returns: 139 | List[ValidationResult]: A list of validation result objects. 140 | """ 141 | validator = Draft7Validator(self.v7_schema, format_checker=self.format_checker) 142 | 143 | results = [] 144 | has_error = False 145 | for err in validator.iter_errors(self.data): 146 | has_error = True 147 | 148 | results.append( 149 | ValidationResult( 150 | schema_id=self.id, 151 | result=RESULT_FAIL, 152 | message=err.message, 153 | absolute_path=list(err.absolute_path), 154 | instance_type="SCHEMA", 155 | instance_name=self.id, 156 | instance_location="", 157 | ) 158 | ) 159 | 160 | if not has_error: 161 | results.append( 162 | ValidationResult( 163 | schema_id=self.id, 164 | result=RESULT_PASS, 165 | instance_type="SCHEMA", 166 | instance_name=self.id, 167 | instance_location="", 168 | ) 169 | ) 170 | 171 | return results 172 | -------------------------------------------------------------------------------- /schema_enforcer/schemas/validator.py: -------------------------------------------------------------------------------- 1 | """Classes for custom validator plugins.""" 2 | 3 | # pylint: disable=no-member, too-few-public-methods 4 | # See PEP585 (https://www.python.org/dev/peps/pep-0585/) 5 | from __future__ import annotations 6 | from typing import List, Union 7 | import pkgutil 8 | import importlib 9 | import inspect 10 | import jmespath 11 | from pydantic import BaseModel, ValidationError 12 | from schema_enforcer.validation import ValidationResult 13 | 14 | 15 | class BaseValidation: 16 | """Base class for Validation classes.""" 17 | 18 | def __init__(self): 19 | """Base init for all validation classes.""" 20 | self._results: list[ValidationResult] = [] 21 | 22 | def add_validation_error(self, message: str, **kwargs): 23 | """Add validator error to results. 24 | 25 | Args: 26 | message (str): error message 27 | kwargs (optional): additional arguments to add to ValidationResult when required 28 | """ 29 | self._results.append(ValidationResult(result="FAIL", schema_id=self.id, message=message, **kwargs)) 30 | 31 | def add_validation_pass(self, **kwargs): 32 | """Add validator pass to results. 33 | 34 | Args: 35 | kwargs (optional): additional arguments to add to ValidationResult when required 36 | """ 37 | self._results.append(ValidationResult(result="PASS", schema_id=self.id, **kwargs)) 38 | 39 | def get_results(self) -> list[ValidationResult]: 40 | """Return all validation results for this validator.""" 41 | if not self._results: 42 | self._results.append(ValidationResult(result="PASS", schema_id=self.id)) 43 | 44 | return self._results 45 | 46 | def clear_results(self): 47 | """Reset results for validator instance.""" 48 | self._results = [] 49 | 50 | def validate(self, data: dict, strict: bool): 51 | """Required function for custom validator. 52 | 53 | Args: 54 | data (dict): variables to be validated by validator 55 | strict (bool): true when --strict cli option is used to request strict validation (if provided) 56 | 57 | Returns: 58 | None 59 | 60 | Use add_validation_error and add_validation_pass to report results. 61 | """ 62 | raise NotImplementedError 63 | 64 | 65 | class JmesPathModelValidation(BaseValidation): 66 | """Base class for JmesPathModelValidation classes.""" 67 | 68 | def validate(self, data: dict, strict: bool): # pylint: disable=W0613 69 | """Validate data using custom jmespath validator plugin.""" 70 | operators = { 71 | "gt": lambda r, v: int(r) > int(v), 72 | "gte": lambda r, v: int(r) >= int(v), 73 | "eq": lambda r, v: r == v, 74 | "lt": lambda r, v: int(r) < int(v), 75 | "lte": lambda r, v: int(r) <= int(v), 76 | "contains": lambda r, v: v in r, 77 | } 78 | lhs = jmespath.search(self.left, data) 79 | valid = True 80 | if lhs: 81 | # Check rhs for compiled jmespath expression 82 | if isinstance(self.right, jmespath.parser.ParsedResult): 83 | rhs = self.right.search(data) 84 | else: 85 | rhs = self.right 86 | valid = operators[self.operator](lhs, rhs) 87 | if not valid: 88 | self.add_validation_error(self.error) 89 | 90 | 91 | class PydanticValidation(BaseValidation): 92 | """Basic wrapper for Pydantic models to be used as validators.""" 93 | 94 | model: BaseModel 95 | 96 | def validate(self, data: dict, strict: bool = False): 97 | """Validate data against Pydantic model. 98 | 99 | Args: 100 | data (dict): variables to be validated by validator 101 | strict (bool): true when --strict cli option is used to request strict validation (if provided) 102 | 103 | Returns: 104 | None 105 | 106 | Use add_validation_error and add_validation_pass to report results. 107 | """ 108 | try: 109 | self.model.model_validate(data, strict=strict) 110 | self.add_validation_pass() 111 | except ValidationError as err: 112 | self.add_validation_error(str(err)) 113 | 114 | 115 | def is_validator(obj) -> bool: 116 | """Returns True if the object is a BaseValidation or JmesPathModelValidation subclass.""" 117 | try: 118 | return (issubclass(obj, BaseValidation) or issubclass(obj, BaseModel)) and obj not in ( 119 | BaseModel, 120 | BaseValidation, 121 | JmesPathModelValidation, 122 | ) 123 | except TypeError: 124 | return False 125 | 126 | 127 | def pydantic_validation_factory(orig_model) -> PydanticValidation: 128 | """Create a PydanticValidation instance from a Pydantic model.""" 129 | return type( 130 | orig_model.__name__, 131 | (PydanticValidation,), 132 | { 133 | "id": f"{orig_model.id}", 134 | "top_level_properties": set([property for property in orig_model.model_fields]), 135 | "model": orig_model, 136 | }, 137 | ) 138 | 139 | 140 | def load_pydantic_validators( 141 | model_packages: list, 142 | ) -> dict[str, PydanticValidation]: 143 | """Load all validator plugins from validator_packages.""" 144 | validators = {} 145 | for package in model_packages: 146 | module_name, attr = package.split(":") 147 | try: 148 | module = importlib.import_module(module_name) 149 | except ModuleNotFoundError: 150 | print(f"Unable to load the validator {package}, the module ({module_name}) does not exist.") 151 | continue 152 | 153 | manager = getattr(module, attr, None) 154 | if not manager: 155 | print(f"Unable to load the validator {package}, the module or attribute ({attr}) does not exist.") 156 | continue 157 | 158 | for model in manager.models: 159 | model.id = f"{manager.prefix}/{model.__name__}" if manager.prefix else model.__name__ 160 | cls = pydantic_validation_factory(model) 161 | 162 | if cls.id in validators: 163 | print(f"Unable to load the validator {cls.id}, there is already a validator with the same name.") 164 | continue 165 | 166 | validators[cls.id] = cls() 167 | 168 | return validators 169 | 170 | 171 | def load_validators_path( 172 | validators_path: str, 173 | ) -> dict[str, Union[BaseValidation, PydanticValidation]]: 174 | """Load all validators from local path.""" 175 | validators = {} 176 | for importer, module_name, _ in pkgutil.iter_modules([validators_path]): 177 | module = importer.find_module(module_name).load_module(module_name) 178 | for name, cls in inspect.getmembers(module, is_validator): 179 | # Default to class name if id doesn't exist 180 | if not hasattr(cls, "id"): 181 | cls.id = name 182 | 183 | if issubclass(cls, BaseModel) and not issubclass(cls, PydanticValidation): 184 | # Save original pydantic model that will be used for validation 185 | cls = pydantic_validation_factory(cls) 186 | 187 | if cls.id in validators: 188 | print( 189 | f"Unable to load the validator {cls.id}, there is already a validator with the same name ({name})." 190 | ) 191 | continue 192 | 193 | validators[cls.id] = cls() 194 | 195 | return validators 196 | 197 | 198 | def load_validators( 199 | validators_path: str, pydantic_validators: List[str] = None 200 | ) -> dict[str, Union[BaseValidation, PydanticValidation]]: 201 | """Load all validator plugins from validator_path.""" 202 | if not pydantic_validators: 203 | pydantic_validators = [] 204 | validators = load_validators_path(validators_path) 205 | validators.update(load_pydantic_validators(pydantic_validators)) 206 | return validators 207 | -------------------------------------------------------------------------------- /schema_enforcer/validation.py: -------------------------------------------------------------------------------- 1 | """Validation related classes.""" 2 | from typing import List, Optional, Any 3 | from pydantic import BaseModel, ConfigDict, field_validator # pylint: disable=no-name-in-module 4 | from termcolor import colored 5 | 6 | RESULT_PASS = "PASS" # nosec 7 | RESULT_FAIL = "FAIL" 8 | 9 | 10 | class ValidationResult(BaseModel): 11 | """ValidationResult object. 12 | 13 | This object is meant to store the result of a given test along with some contextual 14 | information about the test itself. 15 | """ 16 | 17 | # Added to allow coercion of numbers to strings as this doesn't appear to be a default in v2 18 | model_config = ConfigDict(coerce_numbers_to_str=True) 19 | 20 | result: str 21 | schema_id: str 22 | instance_name: Optional[str] = None 23 | instance_location: Optional[str] = None 24 | instance_type: Optional[str] = None 25 | instance_hostname: Optional[str] = None 26 | source: Any = None 27 | strict: bool = False 28 | 29 | # if failed 30 | absolute_path: Optional[List[str]] = [] 31 | message: Optional[str] = None 32 | 33 | # TODO: I believe we can change result to be an Enum and accomplish the same result with less code. 34 | @field_validator("result") 35 | def result_must_be_pass_or_fail(cls, var): # pylint: disable=no-self-argument 36 | """Validate that result either PASS or FAIL.""" 37 | if var.upper() not in [RESULT_PASS, RESULT_FAIL]: 38 | raise ValueError("must be either PASS or FAIL") 39 | return var.upper() 40 | 41 | def passed(self): 42 | """Return True or False to indicate if the test has passed. 43 | 44 | Returns 45 | Bool: indicate if the test passed or failed 46 | """ 47 | if self.result == RESULT_PASS: 48 | return True 49 | 50 | return False 51 | 52 | def print(self): 53 | """Print the result of the test in CLI.""" 54 | if self.passed(): 55 | self.print_passed() 56 | else: 57 | self.print_failed() 58 | 59 | def print_failed(self): 60 | """Print the result of the test to CLI when the test failed.""" 61 | # Construct the message dynamically based on the instance_type 62 | msg = f"{colored('FAIL', 'red')} |" 63 | if self.instance_type == "FILE": 64 | msg += f" [{self.instance_type}] {self.instance_location}/{self.instance_name}" 65 | 66 | elif self.instance_type == "HOST": 67 | msg += f" [{self.instance_type}] {self.instance_hostname}" 68 | 69 | if self.schema_id: 70 | msg += f" [SCHEMA ID] {self.schema_id}" 71 | 72 | if self.absolute_path: 73 | msg += f" [PROPERTY] {':'.join(str(item) for item in self.absolute_path)}" 74 | 75 | if self.message: 76 | msg += f"\n | [ERROR] {self.message}" 77 | 78 | # print the msg 79 | print(msg) 80 | 81 | def print_passed(self): 82 | """Print the result of the test to CLI when the test passed.""" 83 | if self.instance_type == "FILE": 84 | print(colored("PASS", "green") + f" | [{self.instance_type}] {self.instance_location}/{self.instance_name}") 85 | 86 | if self.instance_type == "HOST": 87 | print( 88 | colored("PASS", "green") 89 | + f" | [{self.instance_type}] {self.instance_hostname} [SCHEMA ID] {self.schema_id}" 90 | ) 91 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """conftest file for pytest""" 2 | import glob 3 | import os 4 | from schema_enforcer.utils import load_file 5 | from schema_enforcer.schemas.jsonschema import JsonSchema 6 | 7 | 8 | FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_jsonschema") 9 | FORMAT_CHECK_ERROR_MESSAGE_MAPPING = { 10 | "incorrect_regex_format": "'[' is not a 'regex'", 11 | "incorrect_date_format": "'2021-111-28' is not a 'date'", 12 | "incorrect_hostname_format": "'ntc@ntc.com' is not a 'hostname'", 13 | "incorrect_uri_format": "'sftp//' is not a 'uri'", 14 | "incorrect_jsonptr_format": "'fakejsonptr' is not a 'json-pointer'", 15 | "incorrect_email_format": "'networktocode.code.com' is not a 'email'", 16 | "incorrect_ipv4_format": "'10.1.1.300' is not a 'ipv4'", 17 | "incorrect_ipv6_format": "'2001:00000:3238:DFE1:63:0000:0000:FEFB' is not a 'ipv6'", 18 | "incorrect_time_format": "'20:20:33333+00:00' is not a 'time'", 19 | "incorrect_datetime_format": "'January 29th 2021' is not a 'date-time'", 20 | } 21 | 22 | 23 | def pytest_generate_tests(metafunc): 24 | """Pytest_generate_tests prehook""" 25 | if metafunc.function.__name__ == "test_format_checkers": 26 | schema_files = glob.glob(f"{FIXTURES_DIR}/schema/schemas/incorrect_*.yml") 27 | schema_instances = [] 28 | for schema_file in schema_files: 29 | schema_instance = JsonSchema( 30 | schema=load_file(schema_file), 31 | filename=os.path.basename(schema_file), 32 | root=os.path.join(FIXTURES_DIR, "schema", "schemas"), 33 | ) 34 | schema_instances.append(schema_instance) 35 | 36 | data_files = glob.glob(f"{FIXTURES_DIR}/hostvars/spa-madrid-rt1/incorrect_*.yml") 37 | data_instances = [] 38 | for data_file in data_files: 39 | data = load_file(data_file) 40 | data_instances.append(data) 41 | 42 | metafunc.parametrize( 43 | "schema_instance,data_instance, expected_error_message", 44 | [ 45 | ( 46 | schema_instances[i], 47 | data_instances[i], 48 | FORMAT_CHECK_ERROR_MESSAGE_MAPPING.get(os.path.basename(schema_files[i])[:-4]), 49 | ) 50 | for i in range(0, len(schema_instances)) 51 | ], 52 | ids=[os.path.basename(schema_files[i])[:-4] for i in range(0, len(schema_instances))], 53 | ) 54 | -------------------------------------------------------------------------------- /tests/fixtures/test_config/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | 3 | main_directory = "schema1" 4 | definition_directory = "definitions1" 5 | schema_directory = "schemas1" 6 | test_directory = "tests1" 7 | schema_file_extensions = [".json1", ".yaml1", ".yml1"] 8 | schema_file_exclude_filenames = ["happy_file.yml1"] 9 | data_file_search_directories = ["./instance_test/"] 10 | data_file_extensions = [".json1", ".yaml1", ".yml1"] 11 | data_file_exclude_filenames = [".yamllint.yml1", ".travis.yml1"] 12 | ansible_inventory = "inventory.inv" 13 | 14 | [tool.schema_enforcer.schema_mapping] 15 | 16 | 'dns.yml' = ['schemas/dns_servers'] 17 | 'syslog.yml' = ["schemas/syslog_servers"] 18 | -------------------------------------------------------------------------------- /tests/fixtures/test_config/pyproject2.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | 3 | main_directory = "schema1" 4 | definition_directory = "definitions1" -------------------------------------------------------------------------------- /tests/fixtures/test_config/pyproject_invalid_attr.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | 3 | happy_variable = "fun_variable" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/hostvars/chi-beijing-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | - address: "10.2.2.2" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/hostvars/chi-beijing-rt1/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | syslog_servers: 3 | - address: "10.3.3.3" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/hostvars/eng-london-rt1/dns.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | fun_extr_attribute: "super_fun_when_not_trying_strict" 6 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/hostvars/eng-london-rt1/ntp.yaml: -------------------------------------------------------------------------------- 1 | # jsonschema: schemas/ntp 2 | # Future: , http://networktocode.com/schemas/core/ntp 3 | --- 4 | ntp_servers: 5 | - address: "10.6.6.6" 6 | name: "ntp1" 7 | - address: "10.7.7.7" 8 | name: "ntp1" 9 | ntp_authentication: false 10 | ntp_logging: true 11 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | schema_file_exclude_filenames = [] 3 | 4 | definition_directory = "definitions" 5 | schema_directory = "schemas" 6 | 7 | [tool.schema_enforcer.schema_mapping] 8 | # Map instance filename to schema filename 9 | 'dns.yml' = ['schemas/dns_servers'] 10 | # 'syslog.yml' = ["schemas/syslog_servers"] -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/schemas/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/ntp" 4 | description: "NTP Configuration schema." 5 | type: "object" 6 | properties: 7 | ntp_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | ntp_authentication: 10 | type: "boolean" 11 | ntp_logging: 12 | type: "boolean" 13 | additionalProperties: false 14 | required: 15 | - "ntp_servers" 16 | something: "extra" 17 | -------------------------------------------------------------------------------- /tests/fixtures/test_instances/schema/schemas/syslog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/syslog_servers" 4 | description: "Syslog Server Configuration schema." 5 | type: "object" 6 | properties: 7 | syslog_servers: 8 | $ref: "../definitions/arrays/ip.yml#ipv4_hosts" 9 | required: 10 | - "syslog_servers" 11 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/can-vancouver-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: true 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/chi-beijing-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/eng-london-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.6.6.6" 4 | - address: "10.7.7.7" 5 | fun_extr_attribute: "super_fun_when_not_trying_strict" 6 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.300" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_date_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - last_rebooted: "2021-111-28" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_datetime_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - last_rebooted: "January 29th 2021" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_email_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - contact_email: "networktocode.code.com" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_hostname_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - hostname: "ntc@ntc.com" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_ipv4_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.300" 4 | - address: "10.7.7.7" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_ipv6_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "2001:00000:3238:DFE1:63:0000:0000:FEFB" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_jsonptr_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - json_ptr: "fakejsonptr" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_regex_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - site_prefix: "[" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_time_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - uptime: "20:20:33333+00:00" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/hostvars/spa-madrid-rt1/incorrect_uri_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - uri: "sftp//" 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.yml#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.yml#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.yml#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.yml#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.yml#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | name: 13 | type: "string" 14 | address: 15 | type: "string" 16 | format: "ipv4" 17 | vrf: 18 | type: "string" 19 | required: 20 | - "address" 21 | uniqueItems: true 22 | required: 23 | - "dns_servers" 24 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_date_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_date_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | last_rebooted: 13 | type: "string" 14 | format: "date" 15 | required: 16 | - "last_rebooted" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_datetime_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_datetime_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | last_rebooted: 13 | type: "string" 14 | format: "date-time" 15 | required: 16 | - "last_rebooted" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_email_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_email_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | contact_email: 13 | type: "string" 14 | format: "email" 15 | required: 16 | - "contact_email" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_hostname_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_hostname_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | hostname: 13 | type: "string" 14 | format: "hostname" 15 | required: 16 | - "hostname" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_ipv4_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_ipv4_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | name: 13 | type: "string" 14 | address: 15 | type: "string" 16 | format: "ipv4" 17 | vrf: 18 | type: "string" 19 | required: 20 | - "address" 21 | uniqueItems: true 22 | required: 23 | - "dns_servers" 24 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_ipv6_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_ipv6_format" 4 | description: "DNS Schema to test ipv6 formatter." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | address: 13 | type: "string" 14 | format: "ipv6" 15 | required: 16 | - "address" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_jsonptr_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_jsonptr_format" 4 | description: "DNS Schema to test ipv6 formatter." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | json_ptr: 13 | type: "string" 14 | format: "json-pointer" 15 | required: 16 | - "json_ptr" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_regex_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_regex_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | site_prefix: 13 | type: "string" 14 | format: "regex" 15 | required: 16 | - "site_prefix" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_time_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_time_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | uptime: 13 | type: "string" 14 | format: "time" 15 | required: 16 | - "uptime" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/incorrect_uri_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/incorrect_uri_format" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | type: "array" 9 | items: 10 | type: "object" 11 | properties: 12 | uri: 13 | type: "string" 14 | format: "uri" 15 | required: 16 | - "uri" 17 | uniqueItems: true 18 | required: 19 | - "dns_servers" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_jsonschema/schema/schemas/invalid.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/checks" 4 | description: "Schema Checks" 5 | type: "object" 6 | properties: 7 | type: "integer" 8 | items: 9 | type: "object" 10 | properties: 11 | name: "bla bla bla bla" 12 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/dump/all.txt: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/dns_servers", 4 | "description": "DNS Server Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "dns_servers": { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | }, 15 | "address": { 16 | "type": "string", 17 | "format": "ipv4" 18 | }, 19 | "vrf": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "address" 25 | ] 26 | }, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": [ 31 | "dns_servers" 32 | ] 33 | } 34 | { 35 | "$schema": "http://json-schema.org/draft-07/schema#", 36 | "$id": "schemas/ntp", 37 | "description": "NTP Configuration schema.", 38 | "type": "object", 39 | "properties": { 40 | "ntp_servers": { 41 | "type": "array", 42 | "items": { 43 | "type": "object", 44 | "properties": { 45 | "name": { 46 | "type": "string" 47 | }, 48 | "address": { 49 | "type": "string", 50 | "format": "ipv4" 51 | }, 52 | "vrf": { 53 | "type": "string" 54 | } 55 | }, 56 | "required": [ 57 | "address" 58 | ] 59 | }, 60 | "uniqueItems": true 61 | }, 62 | "ntp_authentication": { 63 | "type": "boolean" 64 | }, 65 | "ntp_logging": { 66 | "type": "boolean" 67 | } 68 | }, 69 | "additionalProperties": false, 70 | "required": [ 71 | "ntp_servers" 72 | ], 73 | "something": "extra" 74 | } 75 | { 76 | "$schema": "http://json-schema.org/draft-07/schema#", 77 | "$id": "schemas/syslog_servers", 78 | "description": "Syslog Server Configuration schema.", 79 | "type": "object", 80 | "properties": { 81 | "syslog_servers": { 82 | "type": "array", 83 | "items": { 84 | "type": "object", 85 | "properties": { 86 | "name": { 87 | "type": "string" 88 | }, 89 | "address": { 90 | "type": "string", 91 | "format": "ipv4" 92 | }, 93 | "vrf": { 94 | "type": "string" 95 | } 96 | }, 97 | "required": [ 98 | "address" 99 | ] 100 | }, 101 | "uniqueItems": true 102 | } 103 | }, 104 | "required": [ 105 | "syslog_servers" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/dump/byid.txt: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/dns_servers", 4 | "description": "DNS Server Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "dns_servers": { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | }, 15 | "address": { 16 | "type": "string", 17 | "format": "ipv4" 18 | }, 19 | "vrf": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "address" 25 | ] 26 | }, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": [ 31 | "dns_servers" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.schema_enforcer] 2 | schema_file_exclude_filenames = [] 3 | 4 | schema_directory = "schemas" 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid/schema/schemas/invalid.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/checks" 4 | description: "Schema Checks" 5 | type: "object" 6 | properties: 7 | type: "integer" 8 | items: 9 | type: "object" 10 | properties: 11 | name: "bla bla bla bla" 12 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/schemas/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/test" 4 | description: "NFTables Firewall Configuration schema." 5 | type: "object" 6 | properties: 7 | firewall: 8 | type: "object" 9 | uniqueItems: true 10 | additionalProperties: false 11 | required: 12 | - "rule" 13 | - "variables" 14 | properties: 15 | rule: 16 | type: "object" 17 | properties: 18 | bool: 19 | type: "boolean" 20 | Text: 21 | type: "string" 22 | dict: 23 | type: "object" 24 | variables: 25 | type: "object" 26 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type1/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "firewall": { 3 | "rule": { 4 | "bool": "true", 5 | "Text": "text", 6 | "dict": {} 7 | }, 8 | "variables": { 9 | "Text": "text", 10 | "array": [] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type1/exp_results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/test" 5 | absolute_path: 6 | - "firewall" 7 | - "rule" 8 | - "bool" 9 | message: "'true' is not of type 'boolean'" 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type1/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/test" 5 | absolute_path: 6 | - "firewall" 7 | - "rule" 8 | - "bool" 9 | message: "'true' is not of type 'boolean'" 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type2/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "firewall": { 3 | "rule": { 4 | "bool": true, 5 | "Text": 123, 6 | "dict": {} 7 | }, 8 | "variables": { 9 | "Text": "text", 10 | "array": {} 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type2/exp_results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/test" 5 | absolute_path: 6 | - "firewall" 7 | - "rule" 8 | - "Text" 9 | message: "123 is not of type 'string'" 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/invalid/invalid_type2/results.yml: -------------------------------------------------------------------------------- 1 | --- 2 | results: 3 | - result: "FAIL" 4 | schema_id: "schemas/test" 5 | absolute_path: 6 | - "firewall" 7 | - "rule" 8 | - "Text" 9 | message: "123 is not of type 'string'" 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_manager/invalid_generate/schema/tests/test/valid/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "firewall": { 3 | "rule": { 4 | "bool": true, 5 | "Text": "text", 6 | "dict": {} 7 | }, 8 | "variables": { 9 | "Text": "text", 10 | "array": [] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /tests/fixtures/test_validators/inventory/host_vars/az_phx_pe01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe01" 3 | pair_rtr: "az-phx-pe02" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.1" 8 | Loopback0: 9 | ipv4: "192.168.1.1" 10 | ipv6: "2001:db8:1::1" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.1" 13 | ipv6: "2001:db8::" 14 | peer: "az-phx-pe02" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.37" 19 | ipv6: "2001:db8::12" 20 | peer: "co-den-p01" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "core" 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/inventory/host_vars/az_phx_pe02/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe02" 3 | pair_rtr: "az-phx-pe01" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.2" 8 | Loopback0: 9 | ipv4: "192.168.1.2" 10 | ipv6: "2001:db8:1::2" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.2" 13 | ipv6: "2001:db8::1" 14 | peer: "az-phx-pe01" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.41" 19 | ipv6: "2001:db8::14" 20 | peer: "co-den-p02" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "access" 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/inventory/host_vars/co_den_p01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "co-den-p01" 3 | pair_rtr: "co-den-p02" 4 | interfaces: 5 | MgmtEth0/0/CPU0/0: 6 | ipv4: "172.16.1.5" 7 | Loopback0: 8 | ipv4: "192.168.1.5" 9 | ipv6: "2001:db8:1::5" 10 | GigabitEthernet0/0/0/2: 11 | ipv4: "10.1.0.38" 12 | ipv6: "2001:db8::13" 13 | peer: "ut-slc-pe01" 14 | peer_int: "GigabitEthernet0/0/0/2" 15 | GigabitEthernet0/0/0/3: 16 | ipv6: "2001:db8::16" 17 | peer: "ut-slc-pe01" 18 | peer_int: "GigabitEthernet0/0/0/1" 19 | type: "core" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/inventory/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | vars: 4 | ansible_network_os: "iosxr" 5 | ansible_user: "cisco" 6 | ansible_password: "cisco" 7 | ansible_connection: "netconf" 8 | ansible_netconf_ssh_config: true 9 | children: 10 | pe_rtrs: 11 | hosts: 12 | az_phx_pe01: 13 | ansible_host: "172.16.1.1" 14 | az_phx_pe02: 15 | ansible_host: "172.16.1.2" 16 | p_rtrs: 17 | hosts: 18 | co_den_p01: 19 | ansible_host: "172.16.1.3" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/validators/check_hostname.py: -------------------------------------------------------------------------------- 1 | """Validate hostname is valid.""" 2 | from pydantic import BaseModel 3 | 4 | 5 | class CheckHostname(BaseModel): 6 | """Validate hostname is valid.""" 7 | 8 | hostname: str 9 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/validators/check_interfaces.py: -------------------------------------------------------------------------------- 1 | """Test validator for JmesPathModelValidation class""" 2 | from schema_enforcer.schemas.validator import JmesPathModelValidation 3 | 4 | 5 | class CheckInterface(JmesPathModelValidation): # pylint: disable=too-few-public-methods 6 | """Test validator for JmesPathModelValidation class""" 7 | 8 | top_level_properties = {"interfaces"} 9 | id = "CheckInterface" # pylint: disable=invalid-name 10 | left = "interfaces.*[@.type=='core'][] | length([?@])" 11 | right = 2 12 | operator = "gte" 13 | error = "Less than two core interfaces" 14 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/validators/check_interfaces_ipv4.py: -------------------------------------------------------------------------------- 1 | """Test validator for JmesPathModelValidation class""" 2 | import jmespath 3 | from schema_enforcer.schemas.validator import JmesPathModelValidation 4 | 5 | 6 | class CheckInterfaceIPv4(JmesPathModelValidation): # pylint: disable=too-few-public-methods 7 | """Test validator for JmesPathModelValidation class""" 8 | 9 | top_level_properties = {"interfaces"} 10 | id = "CheckInterfaceIPv4" # pylint: disable=invalid-name 11 | left = "interfaces.*[@.type=='core'][] | length([?@])" 12 | right = jmespath.compile("interfaces.* | length([?@.type=='core'][].ipv4)") 13 | operator = "eq" 14 | error = "All core interfaces do not have IPv4 addresses" 15 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators/validators/check_peers.py: -------------------------------------------------------------------------------- 1 | """Test validator for ModelValidation class""" 2 | from schema_enforcer.schemas.validator import BaseValidation 3 | 4 | 5 | def ansible_hostname(hostname: str): 6 | """Convert hostname to ansible format""" 7 | return hostname.replace("-", "_") 8 | 9 | 10 | def normal_hostname(hostname: str): 11 | """Convert ansible hostname to normal format""" 12 | return hostname.replace("_", "-") 13 | 14 | 15 | class CheckPeers(BaseValidation): # pylint: disable=too-few-public-methods 16 | """ 17 | Validate that peer and peer_int are defined properly on both sides of a connection 18 | 19 | Requires full Ansible host_vars as data which is currently unsupported in schema-enforcer 20 | """ 21 | 22 | id = "CheckPeers" 23 | top_level_properties = set() 24 | 25 | def validate(self, data: dict, strict: bool): 26 | for host in data: 27 | for interface, int_cfg in data[host]["interfaces"].items(): 28 | if "peer" not in int_cfg: 29 | continue 30 | peer = int_cfg["peer"] 31 | if "peer_int" not in int_cfg: 32 | self.add_validation_error("Peer interface is not defined") 33 | continue 34 | peer_int = int_cfg["peer_int"] 35 | peer = ansible_hostname(peer) 36 | if peer not in data: 37 | continue 38 | peer_match = data[peer]["interfaces"][peer_int]["peer"] == normal_hostname(host) 39 | peer_int_match = data[peer]["interfaces"][peer_int]["peer_int"] == interface 40 | if peer_match and peer_int_match: 41 | self.add_validation_pass() 42 | else: 43 | self.add_validation_error("Peer information does not match.") 44 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/tests/fixtures/test_validators_pydantic/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/host_vars/az_phx_pe01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe01" 3 | pair_rtr: "az-phx-pe02" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.1" 8 | Loopback0: 9 | ipv4: "192.168.1.1" 10 | ipv6: "2001:db8:1::1" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.1" 13 | ipv6: "2001:db8::" 14 | peer: "az-phx-pe02" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.37" 19 | ipv6: "2001:db8::12" 20 | peer: "co-den-p01" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "core" 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/host_vars/az_phx_pe01/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # jsonschema: pydantic/Dns 3 | dns_servers: 4 | - "8.8.8.8" 5 | - "1.1.1.1" 6 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/host_vars/az_phx_pe02/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe02" 3 | pair_rtr: "az-phx-pe01" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.2" 8 | Loopback0: 9 | ipv4: "192.168.1.2" 10 | ipv6: "2001:db8:1::2" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.2" 13 | ipv6: "2001:db8::1" 14 | peer: "az-phx-pe01" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.41" 19 | ipv6: "2001:db8::14" 20 | peer: "co-den-p02" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "access" 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/host_vars/co_den_p01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "co-den-p01" 3 | pair_rtr: "co-den-p02" 4 | interfaces: 5 | MgmtEth0/0/CPU0/0: 6 | ipv4: "172.16.1.5" 7 | Loopback0: 8 | ipv4: "192.168.1.5" 9 | ipv6: "2001:db8:1::5" 10 | GigabitEthernet0/0/0/2: 11 | ipv4: "10.1.0.38" 12 | ipv6: "2001:db8::13" 13 | peer: "ut-slc-pe01" 14 | peer_int: "GigabitEthernet0/0/0/2" 15 | GigabitEthernet0/0/0/3: 16 | ipv6: "2001:db8::16" 17 | peer: "ut-slc-pe01" 18 | peer_int: "GigabitEthernet0/0/0/1" 19 | type: "core" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/host_vars/co_den_p01/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # jsonschema: pydantic/Dns 3 | dns_servers: 4 | - "8.8.8.8" 5 | - "1.1.1.1" 6 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | vars: 4 | ansible_network_os: "iosxr" 5 | ansible_user: "cisco" 6 | ansible_password: "cisco" 7 | ansible_connection: "netconf" 8 | ansible_netconf_ssh_config: true 9 | children: 10 | pe_rtrs: 11 | hosts: 12 | az_phx_pe01: 13 | ansible_host: "172.16.1.1" 14 | az_phx_pe02: 15 | ansible_host: "172.16.1.2" 16 | p_rtrs: 17 | hosts: 18 | co_den_p01: 19 | ansible_host: "172.16.1.3" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/host_vars/az_phx_pe01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phx-pe01" 3 | pair_rtr: "az-phx-pe02" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.1" 8 | Loopback0: 9 | ipv4: "192.168.1.1" 10 | ipv6: "2001:db8:1::1" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.1" 13 | ipv6: "2001:db8::" 14 | peer: "az-phx-pe02" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.37" 19 | ipv6: "2001:db8::12" 20 | peer: "co-den-p01" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "core" 23 | GigabitEthernet0/0/0/2: 24 | ipv4: "300.123.178.41" 25 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/host_vars/az_phx_pe01/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # jsonschema: pydantic/Dns 3 | dns_servers: 4 | - "8.8.8.8" 5 | - "1.1.1.1" 6 | - "dns.google" 7 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/host_vars/az_phx_pe02/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "az-phoenix-pe02" 3 | pair_rtr: "az-phx-pe01" 4 | upstreams: [] 5 | interfaces: 6 | MgmtEth0/0/CPU0/0: 7 | ipv4: "172.16.1.2" 8 | Loopback0: 9 | ipv4: "192.168.1.2" 10 | ipv6: "2001:db8:1::2" 11 | GigabitEthernet0/0/0/0: 12 | ipv4: "10.1.0.2" 13 | ipv6: "2001:db8::1" 14 | peer: "az-phx-pe01" 15 | peer_int: "GigabitEthernet0/0/0/0" 16 | type: "core" 17 | GigabitEthernet0/0/0/1: 18 | ipv4: "10.1.0.41" 19 | ipv6: "2001:db8::14" 20 | peer: "co-den-p02" 21 | peer_int: "GigabitEthernet0/0/0/2" 22 | type: "access" 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/host_vars/co_den_p01/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: "co-denver-p01" 3 | pair_rtr: "co-den-p02" 4 | interfaces: 5 | MgmtEth0/0/CPU0/0: 6 | ipv4: "172.16.1.5" 7 | Loopback0: 8 | ipv4: "192.168.1.5" 9 | ipv6: "2001:db8:1::5" 10 | GigabitEthernet0/0/0/2: 11 | ipv4: "10.1.0.38" 12 | ipv6: "2001:db8::13" 13 | peer: "ut-slc-pe01" 14 | peer_int: "GigabitEthernet0/0/0/2" 15 | GigabitEthernet0/0/0/3: 16 | ipv6: "2001:db8::16" 17 | peer: "ut-slc-pe01" 18 | peer_int: "GigabitEthernet0/0/0/1" 19 | type: "core" 20 | GigabitEthernet0/0/0/4: 21 | ipv6: "2001:db8:16::yo" 22 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/host_vars/co_den_p01/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # jsonschema: pydantic/Dns 3 | dns_servers: 4 | - "8.8.8.8" 5 | - "1.1.1.1" 6 | - "2001:db8:1::1" 7 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/inventory_fail/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | vars: 4 | ansible_network_os: "iosxr" 5 | ansible_user: "cisco" 6 | ansible_password: "cisco" 7 | ansible_connection: "netconf" 8 | ansible_netconf_ssh_config: true 9 | children: 10 | pe_rtrs: 11 | hosts: 12 | az_phx_pe01: 13 | ansible_host: "172.16.1.1" 14 | az_phx_pe02: 15 | ansible_host: "172.16.1.2" 16 | p_rtrs: 17 | hosts: 18 | co_den_p01: 19 | ansible_host: "172.16.1.3" 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/pydantic_validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/schema-enforcer/d8ac84bbdc8a59bcc5a29bb8a748f5a56cf1056d/tests/fixtures/test_validators_pydantic/pydantic_validators/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/pydantic_validators/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .dns import Dns 2 | from .hostname import Hostname 3 | from .interfaces import Interfaces # , Interface, InterfaceTypes 4 | 5 | from schema_enforcer.schemas.manager import PydanticManager 6 | 7 | 8 | manager1 = PydanticManager(models=[Hostname, Interfaces]) 9 | manager2 = PydanticManager(prefix="pydantic", models=[Hostname, Interfaces, Dns]) 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/pydantic_validators/models/dns.py: -------------------------------------------------------------------------------- 1 | """Validate DNS servers is valid.""" 2 | 3 | from typing import List 4 | from pydantic import BaseModel, Field 5 | from pydantic.networks import IPvAnyAddress 6 | 7 | 8 | class Dns(BaseModel): 9 | """Validate DNS is valid.""" 10 | 11 | dns_servers: List[IPvAnyAddress] = Field(description="DNS servers") 12 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/pydantic_validators/models/hostname.py: -------------------------------------------------------------------------------- 1 | """Validate hostname is valid.""" 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class Hostname(BaseModel): 7 | """Validate hostname is valid.""" 8 | 9 | hostname: str = Field(pattern="^[a-z]{2}-[a-z]{3}-[a-z]{1,2}[0-9]{2}$") 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_validators_pydantic/pydantic_validators/models/interfaces.py: -------------------------------------------------------------------------------- 1 | """Validate interfaces are valid.""" 2 | 3 | from enum import Enum 4 | from typing import Dict, Optional 5 | from ipaddress import IPv4Address, IPv6Address 6 | from pydantic import BaseModel 7 | 8 | 9 | class InterfaceTypes(str, Enum): 10 | """Interface types.""" 11 | 12 | access = "access" 13 | core = "core" 14 | 15 | 16 | class Interface(BaseModel): 17 | ipv4: Optional[IPv4Address] = None 18 | ipv6: Optional[IPv6Address] = None 19 | peer: Optional[str] = None 20 | peer_int: Optional[str] = None 21 | type: Optional[InterfaceTypes] = None 22 | 23 | 24 | class Interfaces(BaseModel): 25 | """Validate interfaces are valid.""" 26 | 27 | interfaces: Dict[str, Interface] 28 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/invalid_format.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_servers": [ 3 | "10.1.1.1" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/invalid_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1' is not of type 'object'" 3 | schema_path: "deque(['properties', 'dns_servers', 'items', 'type'])" 4 | validator: "type" 5 | validator_value: "object" 6 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/invalid_ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1000" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/invalid_ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1000' is not a 'ipv4'" 3 | schema_path: "deque(['properties', 'dns_servers', 'items', 'properties', 'address',\ 4 | \ 'format'])" 5 | validator: "format" 6 | validator_value: "ipv4" 7 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/missing_required.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_server": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/dns/invalid/missing_required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'dns_servers' is a required property" 3 | schema_path: "deque(['required'])" 4 | validator: "required" 5 | validator_value: "['dns_servers']" 6 | -------------------------------------------------------------------------------- /tests/mocks/dns/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/mocks/dns/valid/partial_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "dns_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/inventory/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This tests that inventory resolves Jinja2 variables 3 | dns_servers: "{{ os_dns | default(region_dns) }}" 4 | -------------------------------------------------------------------------------- /tests/mocks/inventory/group_vars/emea.yml: -------------------------------------------------------------------------------- 1 | --- 2 | region_dns: 3 | - address: "10.4.4.4" 4 | vrf: "mgmt" 5 | - address: "10.5.5.5" 6 | 7 | ntp_servers: 8 | - address: "10.6.6.6" 9 | -------------------------------------------------------------------------------- /tests/mocks/inventory/group_vars/ios.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_dns: 3 | - address: "10.7.7.7" 4 | vrf: "mgmt" 5 | - address: "10.8.8.8" 6 | -------------------------------------------------------------------------------- /tests/mocks/inventory/group_vars/na.yml: -------------------------------------------------------------------------------- 1 | --- 2 | region_dns: 3 | - address: "10.1.1.1" 4 | vrf: "mgmt" 5 | - address: "10.2.2.2" 6 | 7 | ntp_servers: 8 | - address: "10.3.3.3" 9 | -------------------------------------------------------------------------------- /tests/mocks/inventory/hosts: -------------------------------------------------------------------------------- 1 | [ios] 2 | host3 3 | 4 | [eos] 5 | host4 6 | 7 | [na:children] 8 | nyc 9 | 10 | [emea:children] 11 | lon 12 | 13 | [nyc] 14 | host3 15 | 16 | [lon] 17 | host4 18 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/invalid_format.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | "10.1.1.1" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/invalid_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1' is not of type 'object'" 3 | schema_path: "deque(['properties', 'ntp_servers', 'items', 'type'])" 4 | validator: "type" 5 | validator_value: "object" 6 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/invalid_ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1000" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/invalid_ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1000' is not a 'ipv4'" 3 | schema_path: "deque(['properties', 'ntp_servers', 'items', 'properties', 'address',\ 4 | \ 'format'])" 5 | validator: "format" 6 | validator_value: "ipv4" 7 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/missing_required.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_server": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/ntp/invalid/missing_required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'ntp_servers' is a required property" 3 | schema_path: "deque(['required'])" 4 | validator: "required" 5 | validator_value: "['ntp_servers']" 6 | -------------------------------------------------------------------------------- /tests/mocks/ntp/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ], 13 | "authentication": false, 14 | "logging": true 15 | } 16 | -------------------------------------------------------------------------------- /tests/mocks/ntp/valid/partial_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ntp_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/definitions/arrays/ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_networks": { 3 | "type": "array", 4 | "items": { 5 | "$ref": "../objects/ip.json#ipv4_network" 6 | }, 7 | "uniqueItems": true 8 | }, 9 | "ipv4_hosts": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "../objects/ip.json#ipv4_host" 13 | }, 14 | "uniqueItems": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/definitions/objects/ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_network": { 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string" 7 | }, 8 | "network": { 9 | "$ref": "../properties/ip.json#ipv4_address" 10 | }, 11 | "mask": { 12 | "$ref": "../properties/ip.json#ipv4_cidr" 13 | }, 14 | "vrf": { 15 | "type": "string" 16 | } 17 | }, 18 | "required": [ 19 | "network", 20 | "mask" 21 | ] 22 | }, 23 | "ipv4_host": { 24 | "type": "object", 25 | "properties": { 26 | "name": { 27 | "type": "string" 28 | }, 29 | "address": { 30 | "$ref": "../properties/ip.json#ipv4_address" 31 | }, 32 | "vrf": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "address" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/definitions/properties/ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4_address": { 3 | "type": "string", 4 | "format": "ipv4" 5 | }, 6 | "ipv4_cidr": { 7 | "type": "number", 8 | "minimum": 0, 9 | "maximum": 32 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/full_schemas/ntp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/ntp", 4 | "description": "NTP Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "ntp_servers": { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | }, 15 | "address": { 16 | "type": "string", 17 | "format": "ipv4" 18 | }, 19 | "vrf": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "address" 25 | ] 26 | }, 27 | "uniqueItems": true 28 | }, 29 | "ntp_authentication": { 30 | "type": "boolean" 31 | }, 32 | "ntp_logging": { 33 | "type": "boolean" 34 | } 35 | }, 36 | "required": [ 37 | "ntp_servers" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/schemas/dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/dns_servers", 4 | "description": "DNS Server Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "dns_servers": { 8 | "$ref": "../definitions/arrays/ip.json#ipv4_hosts" 9 | } 10 | }, 11 | "required": [ 12 | "dns_servers" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/mocks/schema/json/schemas/ntp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/ntp", 4 | "description": "NTP Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "ntp_servers": { 8 | "$ref": "../definitions/arrays/ip.json#ipv4_hosts" 9 | }, 10 | "ntp_authentication": { 11 | "type": "boolean" 12 | }, 13 | "ntp_logging": { 14 | "type": "boolean" 15 | } 16 | }, 17 | "required": [ 18 | "ntp_servers" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/mocks/schema/yaml/definitions/arrays/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_networks: 3 | type: "array" 4 | items: 5 | $ref: "../objects/ip.json#ipv4_network" 6 | uniqueItems: true 7 | ipv4_hosts: 8 | type: "array" 9 | items: 10 | $ref: "../objects/ip.json#ipv4_host" 11 | uniqueItems: true 12 | -------------------------------------------------------------------------------- /tests/mocks/schema/yaml/definitions/objects/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_network: 3 | type: "object" 4 | properties: 5 | name: 6 | type: "string" 7 | network: 8 | $ref: "../properties/ip.json#ipv4_address" 9 | mask: 10 | $ref: "../properties/ip.json#ipv4_cidr" 11 | vrf: 12 | type: "string" 13 | required: 14 | - "network" 15 | - "mask" 16 | ipv4_host: 17 | type: "object" 18 | properties: 19 | name: 20 | type: "string" 21 | address: 22 | $ref: "../properties/ip.json#ipv4_address" 23 | vrf: 24 | type: "string" 25 | required: 26 | - "address" 27 | -------------------------------------------------------------------------------- /tests/mocks/schema/yaml/definitions/properties/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv4_address: 3 | type: "string" 4 | format: "ipv4" 5 | ipv4_cidr: 6 | type: "number" 7 | minimum: 0 8 | maximum: 32 9 | -------------------------------------------------------------------------------- /tests/mocks/schema/yaml/schemas/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/dns_servers" 4 | description: "DNS Server Configuration schema." 5 | type: "object" 6 | properties: 7 | dns_servers: 8 | $ref: "../definitions/arrays/ip.json#ipv4_hosts" 9 | required: 10 | - "dns_servers" 11 | -------------------------------------------------------------------------------- /tests/mocks/schema/yaml/schemas/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | $schema: "http://json-schema.org/draft-07/schema#" 3 | $id: "schemas/ntp" 4 | description: "NTP Configuration schema." 5 | type: "object" 6 | properties: 7 | ntp_servers: 8 | $ref: "../definitions/arrays/ip.json#ipv4_hosts" 9 | ntp_authentication: 10 | type: "boolean" 11 | ntp_logging: 12 | type: "boolean" 13 | required: 14 | - "ntp_servers" 15 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/invalid_format.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | "10.1.1.1" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/invalid_format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1' is not of type 'object'" 3 | schema_path: "deque(['properties', 'syslog_servers', 'items', 'type'])" 4 | validator: "type" 5 | validator_value: "object" 6 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/invalid_ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.1.1.1000" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/invalid_ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'10.1.1.1000' is not a 'ipv4'" 3 | schema_path: "deque(['properties', 'syslog_servers', 'items', 'properties', 'address',\ 4 | \ 'format'])" 5 | validator: "format" 6 | validator_value: "ipv4" 7 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/missing_required.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_server": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/syslog/invalid/missing_required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | message: "'syslog_servers' is a required property" 3 | schema_path: "deque(['required'])" 4 | validator: "required" 5 | validator_value: "['syslog_servers']" 6 | -------------------------------------------------------------------------------- /tests/mocks/syslog/valid/full_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | }, 7 | { 8 | "name": "ntp-west", 9 | "address": "10.2.1.1", 10 | "vrf": "mgmt" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/mocks/syslog/valid/partial_implementation.json: -------------------------------------------------------------------------------- 1 | { 2 | "syslog_servers": [ 3 | { 4 | "name": "ntp-east", 5 | "address": "10.1.1.1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/utils/formatted.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | "list_of_strings": [ 4 | "one", 5 | "two" 6 | ], 7 | "list_of_lists": [ 8 | [ 9 | 1, 10 | 2 11 | ], 12 | [ 13 | 3, 14 | 4 15 | ] 16 | ], 17 | "list_of_dicts": [ 18 | { 19 | "one": 1, 20 | "two": 2 21 | }, 22 | { 23 | "one": "1", 24 | "two": "2" 25 | } 26 | ], 27 | "nested": { 28 | "data": [ 29 | "one", 30 | "two" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/mocks/utils/formatted.yml: -------------------------------------------------------------------------------- 1 | --- 2 | key: "value" 3 | list_of_strings: 4 | - "one" 5 | - "two" 6 | # yamllint disable rule:hyphens 7 | list_of_lists: 8 | - - 1 9 | - 2 10 | - - 3 11 | - 4 12 | # yamllint enable rule:hyphens 13 | list_of_dicts: 14 | - one: 1 15 | two: 2 16 | - one: "1" 17 | two: "2" 18 | nested: 19 | data: 20 | - "one" 21 | - "two" 22 | -------------------------------------------------------------------------------- /tests/mocks/utils/host1/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.1.1.1" 4 | vrf: "mgmt" 5 | -------------------------------------------------------------------------------- /tests/mocks/utils/host1/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - address: "10.1.1.1" 4 | vrf: "mgmt" 5 | ntp_authentication: true 6 | -------------------------------------------------------------------------------- /tests/mocks/utils/host2/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.2.1.1" 4 | vrf: "mgmt" 5 | -------------------------------------------------------------------------------- /tests/mocks/utils/host2/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - address: "10.2.1.1" 4 | vrf: "mgmt" 5 | -------------------------------------------------------------------------------- /tests/mocks/utils/host3/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.7.7.7" 4 | vrf: "mgmt" 5 | - address: "10.8.8.8" 6 | -------------------------------------------------------------------------------- /tests/mocks/utils/host3/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - address: "10.3.3.3" 4 | -------------------------------------------------------------------------------- /tests/mocks/utils/host4/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dns_servers: 3 | - address: "10.4.4.4" 4 | vrf: "mgmt" 5 | - address: "10.5.5.5" 6 | -------------------------------------------------------------------------------- /tests/mocks/utils/host4/ntp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ntp_servers: 3 | - address: "10.6.6.6" 4 | -------------------------------------------------------------------------------- /tests/mocks/utils/ntp_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "schemas/ntp", 4 | "description": "NTP Configuration schema.", 5 | "type": "object", 6 | "properties": { 7 | "ntp_servers": { 8 | "$ref": "../definitions/arrays/ip.json#ipv4_hosts" 9 | }, 10 | "ntp_authentication": { 11 | "type": "boolean" 12 | }, 13 | "ntp_logging": { 14 | "type": "boolean" 15 | } 16 | }, 17 | "required": [ 18 | "ntp_servers" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/test_ansible_inventory.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | """Unit Tests for ansible_inventory.py""" 3 | 4 | import pytest 5 | 6 | from schema_enforcer.ansible_inventory import AnsibleInventory 7 | 8 | 9 | INVENTORY_DIR = "tests/mocks/inventory" 10 | 11 | 12 | @pytest.fixture(scope="module") 13 | def ansible_inv(): 14 | """Ansible inventory fixture.""" 15 | return AnsibleInventory(INVENTORY_DIR) 16 | 17 | 18 | def test_init_hosts(ansible_inv): 19 | """Test initialization of hosts.""" 20 | expected = {"host3", "host4"} 21 | actual = set(ansible_inv.inv_mgr.hosts) 22 | assert actual == expected 23 | 24 | 25 | def test_init_groups(ansible_inv): 26 | """Test initialization of groups.""" 27 | expected = { 28 | "ios": ["host3"], 29 | "eos": ["host4"], 30 | "na": ["host3"], 31 | "emea": ["host4"], 32 | "nyc": ["host3"], 33 | "lon": ["host4"], 34 | } 35 | ansible_vars = ansible_inv.var_mgr.get_vars() 36 | actual = ansible_vars["groups"] 37 | actual.pop("all") 38 | actual.pop("ungrouped") 39 | assert actual == expected 40 | 41 | 42 | def test_get_hosts_containing_no_var(ansible_inv): 43 | expected = ["host3", "host4"] 44 | all_hosts = ansible_inv.get_hosts_containing() 45 | actual = [host.name for host in all_hosts] 46 | assert actual == expected, str(dir(actual[0])) 47 | 48 | 49 | def test_get_hosts_containing_var(ansible_inv): 50 | expected = ["host3"] 51 | filtered_hosts = ansible_inv.get_hosts_containing(var="os_dns") 52 | actual = [host.name for host in filtered_hosts] 53 | assert actual == expected 54 | 55 | 56 | def test_get_host_vars(ansible_inv): 57 | expected = { 58 | "dns_servers": [ 59 | {"address": "10.7.7.7", "vrf": "mgmt"}, 60 | {"address": "10.8.8.8"}, 61 | ], 62 | "group_names": ["ios", "na", "nyc"], 63 | "inventory_hostname": "host3", 64 | "ntp_servers": [{"address": "10.3.3.3"}], 65 | "os_dns": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], 66 | "region_dns": [{"address": "10.1.1.1", "vrf": "mgmt"}, {"address": "10.2.2.2"}], 67 | } 68 | 69 | filtered_hosts = ansible_inv.get_hosts_containing(var="os_dns") 70 | host3 = [host for host in filtered_hosts if host.name == "host3"][0] 71 | host3_vars = ansible_inv.get_host_vars(host3) 72 | interesting_keys = [ 73 | "dns_servers", 74 | "group_names", 75 | "inventory_hostname", 76 | "ntp_servers", 77 | "os_dns", 78 | "region_dns", 79 | ] 80 | actual = {key: host3_vars[key] for key in interesting_keys} 81 | assert actual == expected 82 | 83 | 84 | def test_get_clean_host_vars(ansible_inv): 85 | expected = { 86 | "dns_servers": [ 87 | {"address": "10.7.7.7", "vrf": "mgmt"}, 88 | {"address": "10.8.8.8"}, 89 | ], 90 | "os_dns": [{"address": "10.7.7.7", "vrf": "mgmt"}, {"address": "10.8.8.8"}], 91 | "region_dns": [{"address": "10.1.1.1", "vrf": "mgmt"}, {"address": "10.2.2.2"}], 92 | "ntp_servers": [{"address": "10.3.3.3"}], 93 | } 94 | host3 = ansible_inv.inv_mgr.get_host("host3") 95 | host3_cleaned_vars = ansible_inv.get_clean_host_vars(host3) 96 | assert expected == host3_cleaned_vars 97 | 98 | # Test setting magic_vars_to_evaluate 99 | host3.set_variable("magic_vars_to_evaluate", ["inventory_hostname"]) 100 | expected["inventory_hostname"] = host3.name 101 | host3_cleaned_vars = ansible_inv.get_clean_host_vars(host3) 102 | assert expected == host3_cleaned_vars 103 | 104 | # Test invalid magic_vars_to_evaluate setting 105 | host3.set_variable("magic_vars_to_evaluate", "inventory_hostname") 106 | with pytest.raises(TypeError): 107 | host3_cleaned_vars = ansible_inv.get_clean_host_vars(host3) 108 | -------------------------------------------------------------------------------- /tests/test_cli_ansible_exists.py: -------------------------------------------------------------------------------- 1 | """Unit tests for cli.py when ansible is installed""" 2 | from click.testing import CliRunner 3 | 4 | from schema_enforcer import cli 5 | 6 | 7 | def test_ansible_import_when_exists(): 8 | """Tests ansible command exits when ansible is installed on the host system but message indicates the exit is because 'No schemas were loaded'.""" 9 | runner = CliRunner() 10 | raised_error = runner.invoke(cli.ansible, ["--show-checks"]) 11 | # For whatever reason, the raised error does not exactly match SystemExit(1). The diff output by pylint shows no 12 | # differences between the objects name or type, so the assertion converts to string before matching as this 13 | # effectively accomplishes the same thing. 14 | assert str(raised_error.exception) == str(SystemExit(1)) 15 | assert raised_error.exit_code == 1 16 | assert raised_error.output == "\x1b[31m ERROR |\x1b[0m No schemas were loaded\n" 17 | -------------------------------------------------------------------------------- /tests/test_cli_ansible_not_exists.py: -------------------------------------------------------------------------------- 1 | """Unit tests for cli.py ansible when ansible is not installed""" 2 | from click.testing import CliRunner 3 | 4 | from schema_enforcer import cli 5 | 6 | 7 | def test_ansible_import_when_not_exists(): 8 | """Tests ansible command exits when ansible is not installed on the host system and message indicates the exit is because the ansible command is not found.""" 9 | runner = CliRunner() 10 | raised_error = runner.invoke(cli.ansible, ["--show-checks"]) 11 | # For whatever reason, the raised error does not exactly match SystemExit(1). The diff output by pylint shows no 12 | # differences between the objects name or type, so the assertion converts to string before matching as this 13 | # effectively accomplishes the same thing. 14 | assert str(raised_error.exception) == str(SystemExit(1)) 15 | assert raised_error.exit_code == 1 16 | assert ( 17 | raised_error.output 18 | == "\x1b[31m ERROR |\x1b[0m ansible package not found, you can run the command 'pip install schema-enforcer[ansible]' to install the latest schema-enforcer sanctioned version.\n" 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_config_settings.py: -------------------------------------------------------------------------------- 1 | """ Test Setting Configuration Parameters""" 2 | from unittest import mock 3 | import os 4 | 5 | import pytest 6 | from schema_enforcer import config 7 | 8 | FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_config") 9 | 10 | 11 | def test_load_default(): 12 | """ 13 | Test load of default config 14 | """ 15 | config.load() 16 | 17 | assert config.SETTINGS.main_directory == "schema" 18 | 19 | 20 | def test_load_custom(): 21 | """ 22 | Test load from configuration file 23 | """ 24 | # Load config file using fixture of config file 25 | config_file_name = FIXTURES_DIR + "/pyproject.toml" 26 | config.load(config_file_name=config_file_name) 27 | 28 | assert config.SETTINGS.main_directory == "schema1" 29 | 30 | 31 | def test_load_data(): 32 | """ 33 | Test load from python data structure 34 | """ 35 | data = { 36 | "main_directory": "schema2", 37 | } 38 | config.load(config_data=data) 39 | 40 | assert config.SETTINGS.main_directory == "schema2" 41 | 42 | 43 | def test_load_mixed(): 44 | """ 45 | Test config load when config_file_name, data, and defaults are all used 46 | """ 47 | config_file_name = FIXTURES_DIR + "/pyproject2.toml" 48 | data = {"main_directory": "fake_dir"} 49 | 50 | config.load(config_file_name=config_file_name, config_data=data) 51 | 52 | # Assert main_directory inhered from data passed in 53 | assert config.SETTINGS.main_directory == "fake_dir" 54 | 55 | # Assert definitions_directory inhered from default, and not from file 56 | assert config.SETTINGS.definition_directory == "definitions" 57 | 58 | 59 | def test_load_and_exit_invalid_data(): 60 | """ 61 | Test config load raises proper error when config file contains invalid attributes 62 | """ 63 | config_file_name = FIXTURES_DIR + "/pyproject_invalid_attr.toml" 64 | with pytest.raises(SystemExit): 65 | config.load_and_exit(config_file_name=config_file_name) 66 | 67 | 68 | def test_load_environment_vars(): 69 | """ 70 | Test load from environment variables 71 | """ 72 | 73 | # WWrite test to mock os.environ to test pydantic BaseSettings 74 | with mock.patch.dict( 75 | os.environ, 76 | { 77 | "jsonschema_directory": "schema_env", 78 | "jsonschema_definition_directory": "definitions_env", 79 | }, 80 | ): 81 | config.load() 82 | 83 | assert config.SETTINGS.main_directory == "schema_env" 84 | assert config.SETTINGS.definition_directory == "definitions_env" 85 | -------------------------------------------------------------------------------- /tests/test_instances_instance_file.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | """ Tests instances.py InstanceFile class""" 3 | 4 | import os 5 | 6 | import pytest 7 | 8 | from schema_enforcer.schemas.manager import SchemaManager 9 | from schema_enforcer.instances.file import InstanceFile, InstanceFileManager 10 | from schema_enforcer.validation import ValidationResult 11 | from schema_enforcer.config import Settings 12 | 13 | FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_instances") 14 | 15 | CONFIG_DATA = { 16 | "main_directory": os.path.join(FIXTURES_DIR, "schema"), 17 | "data_file_search_directories": [os.path.join(FIXTURES_DIR, "hostvars")], 18 | "schema_mapping": {"dns.yml": ["schemas/dns_servers"]}, 19 | } 20 | 21 | 22 | @pytest.fixture 23 | def if_w_extended_matches(): 24 | """ 25 | InstanceFile class with extended matches defined as a `# jsonschema:` decorator in the 26 | instance file. 27 | """ 28 | if_instance = InstanceFile(root=os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1"), filename="ntp.yaml") 29 | 30 | return if_instance 31 | 32 | 33 | @pytest.fixture 34 | def if_w_matches(): 35 | """ 36 | InstanceFile class with matches passed in 37 | """ 38 | if_instance = InstanceFile( 39 | root=os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1"), 40 | filename="dns.yaml", 41 | matches={"schemas/dns_servers"}, 42 | ) 43 | 44 | return if_instance 45 | 46 | 47 | @pytest.fixture 48 | def if_wo_matches(): 49 | """ 50 | InstanceFile class without matches passed in and without extended matches denoted in a `# jsonschema` 51 | decorator in the instance file. 52 | """ 53 | if_instance = InstanceFile(root=os.path.join(FIXTURES_DIR, "hostvars", "chi-beijing-rt1"), filename="syslog.yml") 54 | 55 | return if_instance 56 | 57 | 58 | @pytest.fixture 59 | def schema_manager(): 60 | """ 61 | Instantiated SchemaManager class 62 | 63 | Returns: 64 | SchemaManager 65 | """ 66 | schema_manager = SchemaManager(config=Settings(**CONFIG_DATA)) 67 | 68 | return schema_manager 69 | 70 | 71 | @pytest.fixture 72 | def ifm(): 73 | """Instance of InstanceFileManager.""" 74 | ifm = InstanceFileManager(config=Settings(**CONFIG_DATA)) 75 | return ifm 76 | 77 | 78 | def test_init(if_wo_matches, if_w_matches, if_w_extended_matches): 79 | """ 80 | Tests initialization of InstanceFile object 81 | 82 | Args: 83 | if_w_matches (InstanceFile): Initialized InstanceFile pytest fixture 84 | if_wo_matches (InstanceFile): Initialized InstanceFile pytest fixture 85 | if_w_extended_matches (InstanceFile): Initizlized InstanceFile pytest fixture 86 | """ 87 | assert if_wo_matches.matches == set() 88 | assert not if_wo_matches.data 89 | assert if_wo_matches.path == os.path.join(FIXTURES_DIR, "hostvars", "chi-beijing-rt1") 90 | assert if_wo_matches.filename == "syslog.yml" 91 | 92 | assert if_w_matches.matches == { 93 | "schemas/dns_servers", 94 | } 95 | assert not if_w_matches.data 96 | assert if_w_matches.path == os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1") 97 | assert if_w_matches.filename == "dns.yaml" 98 | 99 | assert if_w_extended_matches.matches == { 100 | "schemas/ntp", 101 | } 102 | assert not if_w_extended_matches.data 103 | assert if_w_extended_matches.path == os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1") 104 | assert if_w_extended_matches.filename == "ntp.yaml" 105 | 106 | 107 | def test_get_content(if_w_matches): 108 | """ 109 | Tests get_content method of InstanceFile object 110 | 111 | Args: 112 | if_w_matches (InstanceFile): Initialized InstanceFile pytest fixture 113 | """ 114 | content = if_w_matches._get_content() # pylint: disable=protected-access 115 | assert content["dns_servers"][0]["address"] == "10.6.6.6" 116 | assert content["dns_servers"][1]["address"] == "10.7.7.7" 117 | 118 | raw_content = if_w_matches._get_content(structured=False) # pylint: disable=protected-access 119 | with open(os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1", "dns.yaml"), "r", encoding="utf-8") as fhd: 120 | assert raw_content == fhd.read() 121 | 122 | 123 | def test_validate(if_w_matches, schema_manager): 124 | """ 125 | Tests validate method of InstanceFile object 126 | 127 | Args: 128 | if_w_matches (InstanceFile): Initialized InstanceFile pytest fixture 129 | schema_manager (SchemaManager): Initialized SchemaManager object, needed to run "validate" method. 130 | """ 131 | errs = list(if_w_matches.validate(schema_manager=schema_manager)) 132 | strict_errs = list(if_w_matches.validate(schema_manager=schema_manager, strict=True)) 133 | 134 | assert len(errs) == 1 135 | assert isinstance(errs[0], ValidationResult) 136 | assert errs[0].result == "PASS" 137 | assert not errs[0].message 138 | 139 | assert len(strict_errs) == 1 140 | assert isinstance(strict_errs[0], ValidationResult) 141 | assert strict_errs[0].result == "FAIL" 142 | assert strict_errs[0].message == "Additional properties are not allowed ('fun_extr_attribute' was unexpected)" 143 | 144 | 145 | def test_add_matches_by_property_automap(if_wo_matches, schema_manager): 146 | """Tests add_matches_by_property_automap method of InstanceFile class.""" 147 | assert not if_wo_matches.matches 148 | assert if_wo_matches.top_level_properties == {"syslog_servers"} 149 | assert if_wo_matches._top_level_properties == {"syslog_servers"} # pylint: disable=protected-access 150 | if_wo_matches.add_matches_by_property_automap(schema_manager) 151 | assert if_wo_matches.matches == set(["schemas/syslog_servers"]) 152 | -------------------------------------------------------------------------------- /tests/test_instances_instance_file_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests instances.py InstanceFileManager class 3 | """ 4 | # pylint: disable=redefined-outer-name,unnecessary-comprehension 5 | 6 | import os 7 | 8 | import pytest 9 | 10 | from schema_enforcer.instances.file import InstanceFileManager 11 | from schema_enforcer.config import Settings 12 | 13 | FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_instances") 14 | 15 | CONFIG_DATA = { 16 | "main_directory": os.path.join(FIXTURES_DIR, "schema"), 17 | "data_file_search_directories": [os.path.join(FIXTURES_DIR, "hostvars")], 18 | "schema_mapping": {"dns.yml": ["schemas/dns_servers"]}, 19 | } 20 | 21 | 22 | @pytest.fixture 23 | def ifm(): 24 | """ 25 | Instantiate an InstanceFileManager Class for use in tests. 26 | 27 | Returns: 28 | InstanceFileManager: Instantiated InstanceFileManager class 29 | """ 30 | instance_file_manager = InstanceFileManager(Settings(**CONFIG_DATA)) 31 | 32 | return instance_file_manager 33 | 34 | 35 | def test_init(ifm): 36 | """ 37 | Tests initialization of InstanceFileManager object 38 | """ 39 | assert len(ifm.instances) == 4 40 | 41 | 42 | def test_print_instances_schema_mapping(ifm, capsys): 43 | """ 44 | Tests print_instances_schema_mapping func of InstanceFileManager object 45 | """ 46 | print_string = ( 47 | "Structured Data File Schema ID\n" 48 | "--------------------------------------------------------------------------------\n" 49 | "/local/tests/fixtures/test_instances/hostvars/chi-beijing-rt1/dns.yml ['schemas/dns_servers']\n" 50 | "/local/tests/fixtures/test_instances/hostvars/chi-beijing-rt1/syslog.yml []\n" 51 | "/local/tests/fixtures/test_instances/hostvars/eng-london-rt1/dns.yaml []\n" 52 | "/local/tests/fixtures/test_instances/hostvars/eng-london-rt1/ntp.yaml ['schemas/ntp']\n" 53 | ) 54 | ifm.print_schema_mapping() 55 | captured = capsys.readouterr() 56 | captured_stdout = captured[0] 57 | assert captured_stdout == print_string 58 | -------------------------------------------------------------------------------- /tests/test_jsonschema.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | """Tests to validate functions defined in jsonschema.py""" 3 | import os 4 | import pytest 5 | 6 | from schema_enforcer.schemas.jsonschema import JsonSchema 7 | from schema_enforcer.validation import RESULT_PASS, RESULT_FAIL 8 | from schema_enforcer.utils import load_file 9 | 10 | FIXTURES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_jsonschema") 11 | LOADED_SCHEMA_DATA = load_file(os.path.join(FIXTURES_DIR, "schema", "schemas", "dns.yml")) 12 | LOADED_INSTANCE_DATA = load_file(os.path.join(FIXTURES_DIR, "hostvars", "chi-beijing-rt1", "dns.yml")) 13 | 14 | 15 | @pytest.fixture 16 | def schema_instance(): 17 | """JSONSchema schema instance.""" 18 | schema_instance = JsonSchema( 19 | schema=LOADED_SCHEMA_DATA, 20 | filename="dns.yml", 21 | root=os.path.join(FIXTURES_DIR, "schema", "schemas"), 22 | ) 23 | return schema_instance 24 | 25 | 26 | @pytest.fixture 27 | def valid_instance_data(): 28 | """Valid instance data loaded from YAML file.""" 29 | return load_file(os.path.join(FIXTURES_DIR, "hostvars", "chi-beijing-rt1", "dns.yml")) 30 | 31 | 32 | @pytest.fixture 33 | def invalid_instance_data(): 34 | """Invalid instance data loaded from YAML file.""" 35 | return load_file(os.path.join(FIXTURES_DIR, "hostvars", "can-vancouver-rt1", "dns.yml")) 36 | 37 | 38 | @pytest.fixture 39 | def strict_invalid_instance_data(): 40 | """Invalid instance data when strict mode is used. Loaded from YAML file.""" 41 | return load_file(os.path.join(FIXTURES_DIR, "hostvars", "eng-london-rt1", "dns.yml")) 42 | 43 | 44 | class TestJsonSchema: 45 | """Tests methods relating to schema_enforcer.schemas.jsonschema.JsonSchema Class""" 46 | 47 | @staticmethod 48 | def test_init(schema_instance): 49 | """Tests __init__() magic method of JsonSchema class. 50 | 51 | Args: 52 | schema_instance (JsonSchema): Instance of JsonSchema class 53 | """ 54 | assert schema_instance.filename == "dns.yml" 55 | assert schema_instance.root == os.path.join(FIXTURES_DIR, "schema", "schemas") 56 | assert schema_instance.data == LOADED_SCHEMA_DATA 57 | assert schema_instance.id == LOADED_SCHEMA_DATA.get("$id") # pylint: disable=invalid-name 58 | 59 | @staticmethod 60 | def test_get_id(schema_instance): 61 | """Tests git_id() method of JsonSchema class. 62 | 63 | Args: 64 | schema_instance (JsonSchema): Instance of JsonSchema class 65 | """ 66 | assert schema_instance.get_id() == "schemas/dns_servers" 67 | 68 | @staticmethod 69 | def test_validate(schema_instance, valid_instance_data, invalid_instance_data, strict_invalid_instance_data): 70 | """Tests validate method of JsonSchema class 71 | 72 | Args: 73 | schema_instance (JsonSchema): Instance of JsonSchema class 74 | """ 75 | schema_instance.validate(data=valid_instance_data) 76 | validation_results = schema_instance.get_results() 77 | assert len(validation_results) == 1 78 | assert validation_results[0].schema_id == LOADED_SCHEMA_DATA.get("$id") 79 | assert validation_results[0].result == RESULT_PASS 80 | assert validation_results[0].message is None 81 | schema_instance.clear_results() 82 | 83 | schema_instance.validate(data=invalid_instance_data) 84 | validation_results = schema_instance.get_results() 85 | assert len(validation_results) == 1 86 | assert validation_results[0].schema_id == LOADED_SCHEMA_DATA.get("$id") 87 | assert validation_results[0].result == RESULT_FAIL 88 | assert validation_results[0].message == "True is not of type 'string'" 89 | assert validation_results[0].absolute_path == ["dns_servers", "0", "address"] 90 | schema_instance.clear_results() 91 | 92 | schema_instance.validate(data=strict_invalid_instance_data, strict=False) 93 | validation_results = schema_instance.get_results() 94 | assert validation_results[0].result == RESULT_PASS 95 | schema_instance.clear_results() 96 | 97 | schema_instance.validate(data=strict_invalid_instance_data, strict=True) 98 | validation_results = schema_instance.get_results() 99 | assert validation_results[0].result == RESULT_FAIL 100 | assert ( 101 | validation_results[0].message 102 | == "Additional properties are not allowed ('fun_extr_attribute' was unexpected)" 103 | ) 104 | schema_instance.clear_results() 105 | 106 | @staticmethod 107 | def test_format_checkers(schema_instance, data_instance, expected_error_message): 108 | """Test format checkers""" 109 | validation_results = list(schema_instance.validate(data=data_instance)) 110 | assert validation_results[0].result == RESULT_FAIL 111 | assert validation_results[0].message == expected_error_message 112 | 113 | @staticmethod 114 | def test_validate_to_dict(schema_instance, valid_instance_data): 115 | """Tests validate_to_dict method of JsonSchema class 116 | 117 | Args: 118 | schema_instance (JsonSchema): Instance of JsonSchema class 119 | """ 120 | validation_results_dicts = schema_instance.validate_to_dict(data=valid_instance_data) 121 | assert isinstance(validation_results_dicts, list) 122 | assert isinstance(validation_results_dicts[0], dict) 123 | assert "result" in validation_results_dicts[0] 124 | assert validation_results_dicts[0]["result"] == RESULT_PASS 125 | 126 | @staticmethod 127 | def test_get_validator(): 128 | pass 129 | 130 | @staticmethod 131 | def test_get_strict_validator(): 132 | pass 133 | 134 | @staticmethod 135 | def test_check_if_valid(): 136 | schema_data = load_file(os.path.join(FIXTURES_DIR, "schema", "schemas", "invalid.yml")) 137 | schema_instance = JsonSchema( 138 | schema=schema_data, 139 | filename="invalid.yml", 140 | root=os.path.join(FIXTURES_DIR, "schema", "schemas"), 141 | ) 142 | results = schema_instance.check_if_valid() 143 | for result in results: 144 | assert not result.passed() 145 | 146 | # def test_get_id(): 147 | -------------------------------------------------------------------------------- /tests/test_schemas_schema_manager.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | """ Test manager.py SchemaManager class """ 3 | import os 4 | import pytest 5 | from schema_enforcer.schemas.manager import SchemaManager 6 | from schema_enforcer.config import Settings 7 | from schema_enforcer.exceptions import InvalidJSONSchema 8 | 9 | FIXTURE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures") 10 | 11 | CONFIG = { 12 | "main_directory": os.path.join(FIXTURE_DIR, "test_instances", "schema"), 13 | "data_file_search_directories": [os.path.join(FIXTURE_DIR, "hostvars")], 14 | "schema_mapping": {"dns.yml": ["schemas/dns_servers"]}, 15 | } 16 | 17 | 18 | @pytest.fixture 19 | def schema_manager(): 20 | """ 21 | Instantiated SchemaManager class 22 | 23 | Returns: 24 | SchemaManager 25 | """ 26 | schema_manager = SchemaManager(config=Settings(**CONFIG)) 27 | 28 | return schema_manager 29 | 30 | 31 | @pytest.mark.skip(reason="inconsistent result in GHA, need to revisit") 32 | @pytest.mark.parametrize("schema_id, result_file", [(None, "all.txt"), ("schemas/dns_servers", "byid.txt")]) 33 | def test_dump(capsys, schema_manager, schema_id, result_file): 34 | """Test validates schema dump for multiple parameters.""" 35 | 36 | test_file = os.path.join(FIXTURE_DIR, "test_manager", "dump", result_file) 37 | with open(test_file, encoding="utf-8") as res_file: 38 | expected = res_file.read() 39 | schema_manager.dump_schema(schema_id) 40 | captured = capsys.readouterr() 41 | assert captured.out == expected 42 | 43 | 44 | def test_invalid(): 45 | """Test validates that SchemaManager reports an error when an invalid schema is loaded.""" 46 | config = { 47 | "main_directory": os.path.join(FIXTURE_DIR, "test_manager", "invalid", "schema"), 48 | "data_file_search_directories": [os.path.join(FIXTURE_DIR, "hostvars")], 49 | "schema_mapping": {"dns.yml": ["schemas/dns_servers"]}, 50 | } 51 | with pytest.raises(InvalidJSONSchema) as e: # pylint: disable=invalid-name 52 | schema_manager = SchemaManager(config=Settings(**config)) # noqa pylint: disable=unused-variable 53 | expected_error = """Invalid JSONschema file: invalid.yml - ["'bla bla bla bla' is not of type 'object', 'boolean'", "'integer' is not of type 'object', 'boolean'"]""" 54 | assert expected_error in str(e) 55 | 56 | 57 | def test_generate_invalid(capsys): 58 | """Test validates that generate_invalid_test_expected generates the correct data.""" 59 | config = { 60 | "main_directory": os.path.join(FIXTURE_DIR, "test_manager", "invalid_generate", "schema"), 61 | } 62 | schema_id = "schemas/test" 63 | 64 | schema_manager = SchemaManager(config=Settings(**config)) 65 | schema_manager.generate_invalid_tests_expected(schema_id) 66 | 67 | invalid_dir = os.path.join(config["main_directory"], "tests", "test", "invalid") 68 | invalid_tests = ["invalid_type1", "invalid_type2"] 69 | 70 | for test in invalid_tests: 71 | test_dir = os.path.join(invalid_dir, test) 72 | with open(os.path.join(test_dir, "exp_results.yml"), encoding="utf-8") as exp_file: 73 | expected = exp_file.read() 74 | with open(os.path.join(test_dir, "results.yml"), encoding="utf-8") as gen_file: 75 | generated = gen_file.read() 76 | assert expected == generated 77 | 78 | test_schema = schema_manager.schemas.get(schema_id) 79 | # Clear results as these would not be carried over into subsequent run of --check 80 | test_schema.clear_results() 81 | # Ignore earlier output 82 | capsys.readouterr() 83 | schema_manager.test_schemas() 84 | captured = capsys.readouterr() 85 | assert "ALL SCHEMAS ARE VALID" in captured.out 86 | -------------------------------------------------------------------------------- /tests/test_schemas_validator.py: -------------------------------------------------------------------------------- 1 | """Tests for validator plugin support.""" 2 | # pylint: disable=redefined-outer-name 3 | import os 4 | import pytest 5 | from schema_enforcer.ansible_inventory import AnsibleInventory 6 | import schema_enforcer.schemas.validator as v 7 | 8 | FIXTURE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixtures", "test_validators") 9 | 10 | 11 | @pytest.fixture 12 | def inventory(): 13 | """Fixture for Ansible inventory used in tests.""" 14 | inventory_dir = os.path.join(FIXTURE_DIR, "inventory") 15 | 16 | inventory = AnsibleInventory(inventory_dir) 17 | return inventory 18 | 19 | 20 | @pytest.fixture 21 | def host_vars(inventory): 22 | """Fixture for providing Ansible host_vars as a consolidated dict.""" 23 | hosts = inventory.get_hosts_containing() 24 | host_vars = {} 25 | for host in hosts: 26 | hostname = host.get_vars()["inventory_hostname"] 27 | host_vars[hostname] = inventory.get_host_vars(host) 28 | return host_vars 29 | 30 | 31 | @pytest.fixture(scope="session") 32 | def validators(): 33 | """Test that validator files are loaded and appended to base class validator list.""" 34 | validator_path = os.path.join(FIXTURE_DIR, "validators") 35 | return v.load_validators(validator_path) 36 | 37 | 38 | def test_validator_load(validators): 39 | """Test that validators are loaded and appended to base class validator list.""" 40 | assert len(validators) == 4 41 | assert "CheckInterfaceIPv4" in validators 42 | assert "CheckInterface" in validators 43 | assert "CheckPeers" in validators 44 | assert "CheckHostname" in validators 45 | 46 | 47 | def test_jmespathvalidation_pass(host_vars, validators): 48 | """ 49 | Validator: "interfaces.*[@.type=='core'][] | length([?@])" gte 2 50 | Test expected to pass for az_phx_pe01 with two core interfaces: 51 | interfaces: 52 | GigabitEthernet0/0/0/0: 53 | type: "core" 54 | GigabitEthernet0/0/0/1: 55 | type: "core" 56 | """ 57 | validator = validators["CheckInterface"] 58 | validator.validate(host_vars["az_phx_pe01"], False) 59 | result = validator.get_results() 60 | assert result[0].passed() 61 | validator.clear_results() 62 | 63 | 64 | def test_jmespathvalidation_fail(host_vars, validators): 65 | """ 66 | Validator: "interfaces.*[@.type=='core'][] | length([?@])" gte 2 67 | Test expected to fail for az_phx_pe02 with one core interface: 68 | interfaces: 69 | GigabitEthernet0/0/0/0: 70 | type: "core" 71 | GigabitEthernet0/0/0/1: 72 | type: "access" 73 | """ 74 | validator = validators["CheckInterface"] 75 | validator.validate(host_vars["az_phx_pe02"], False) 76 | result = validator.get_results() 77 | assert not result[0].passed() 78 | validator.clear_results() 79 | 80 | 81 | def test_jmespathvalidation_with_compile_pass(host_vars, validators): 82 | """ 83 | Validator: "interfaces.*[@.type=='core'][] | length([?@])" eq jmespath.compile("interfaces.* | length([?@.type=='core'][].ipv4)") 84 | Test expected to pass for az_phx_pe01 where all core interfaces have IPv4 addresses: 85 | GigabitEthernet0/0/0/0: 86 | ipv4: "10.1.0.1" 87 | ipv6: "2001:db8::" 88 | peer: "az-phx-pe02" 89 | peer_int: "GigabitEthernet0/0/0/0" 90 | type: "core" 91 | GigabitEthernet0/0/0/1: 92 | ipv4: "10.1.0.37" 93 | ipv6: "2001:db8::12" 94 | peer: "co-den-p01" 95 | peer_int: "GigabitEthernet0/0/0/2" 96 | type: "core" 97 | """ 98 | validator = validators["CheckInterfaceIPv4"] 99 | validator.validate(host_vars["az_phx_pe01"], False) 100 | result = validator.get_results() 101 | assert result[0].passed() 102 | validator.clear_results() 103 | 104 | 105 | def test_jmespathvalidation_with_compile_fail(host_vars, validators): 106 | """ 107 | Validator: "interfaces.*[@.type=='core'][] | length([?@])" eq jmespath.compile("interfaces.* | length([?@.type=='core'][].ipv4)") 108 | Test expected to fail for co_den_p01 where core interface is missing an IPv4 addresses: 109 | GigabitEthernet0/0/0/3: 110 | ipv6: "2001:db8::16" 111 | peer: "ut-slc-pe01" 112 | peer_int: "GigabitEthernet0/0/0/1" 113 | type: "core" 114 | """ 115 | validator = validators["CheckInterfaceIPv4"] 116 | validator.validate(host_vars["co_den_p01"], False) 117 | result = validator.get_results() 118 | assert not result[0].passed() 119 | validator.clear_results() 120 | 121 | 122 | def test_modelvalidation_pass(host_vars, validators): 123 | """ 124 | Validator: Checks that peer and peer_int match between peers 125 | Test expected to pass for az_phx_pe01/az_phx_pe02: 126 | 127 | az_phx_pe01: 128 | GigabitEthernet0/0/0/0: 129 | peer: "az-phx-pe02" 130 | peer_int: "GigabitEthernet0/0/0/0" 131 | 132 | az_phx_pe02: 133 | GigabitEthernet0/0/0/0: 134 | peer: "az-phx-pe01" 135 | peer_int: "GigabitEthernet0/0/0/0" 136 | """ 137 | validator = validators["CheckPeers"] 138 | validator.validate(host_vars, False) 139 | result = validator.get_results() 140 | assert result[0].passed() 141 | assert result[2].passed() 142 | validator.clear_results() 143 | 144 | 145 | def test_modelvalidation_fail(host_vars, validators): 146 | """ 147 | Validator: Checks that peer and peer_int match between peers 148 | 149 | Test expected to fail for az_phx_pe01/co_den_p01: 150 | 151 | az_phx_pe01: 152 | GigabitEthernet0/0/0/1: 153 | peer: "co-den-p01" 154 | peer_int: "GigabitEthernet0/0/0/2" 155 | 156 | co_den_p01: 157 | GigabitEthernet0/0/0/2: 158 | peer: ut-slc-pe01 159 | peer_int: GigabitEthernet0/0/0/2 160 | """ 161 | validator = validators["CheckPeers"] 162 | validator.validate(host_vars, False) 163 | result = validator.get_results() 164 | assert not result[1].passed() 165 | 166 | 167 | def test_validator_hostname_pydantic_pass(host_vars, validators): 168 | """Test the we can validate a hostname using a pydantic model.""" 169 | validator = validators["CheckHostname"] 170 | validator.validate({"hostname": host_vars["az_phx_pe01"]["hostname"]}) 171 | results = validator.get_results() 172 | for result in results: 173 | assert result.passed(), result 174 | validator.clear_results() 175 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Tests to validate functions defined in utils.py""" 2 | 3 | import os 4 | import json 5 | import shutil 6 | 7 | from schema_enforcer import utils 8 | 9 | # fmt: off 10 | TEST_DATA = { 11 | 'key': 'value', 12 | "list_of_strings": ["one", "two"], 13 | "list_of_lists": [[1, 2], [3, 4]], 14 | "list_of_dicts": [ 15 | {"one": 1, "two": 2}, 16 | {"one": "1", "two": "2"}, 17 | ], 18 | "nested": { 19 | "data": ["one", "two"], 20 | }, 21 | } 22 | # fmt: on 23 | 24 | 25 | ANSIBLE_HOST_VARIABLES = { 26 | "host1": { 27 | "ntp_servers": [{"address": "10.1.1.1", "vrf": "mgmt"}], 28 | "ntp_authentication": True, 29 | "dns_servers": [{"address": "10.1.1.1", "vrf": "mgmt"}], 30 | "syslog_servers": [{"address": "10.1.1.1."}], 31 | }, 32 | "host2": { 33 | "ntp_servers": [{"address": "10.2.1.1", "vrf": "mgmt"}], 34 | "dns_servers": [{"address": "10.2.1.1", "vrf": "mgmt"}], 35 | }, 36 | } 37 | 38 | 39 | def remove_comments_from_yaml_string(yaml_string): 40 | """Strip comments from yaml data which is in string representation. 41 | 42 | Args: 43 | yaml_string (str): yaml data as a string 44 | 45 | Returns: 46 | yaml_string (str): yaml data as a string 47 | """ 48 | yaml_payload_list = yaml_string.split("\n") 49 | for item in yaml_payload_list: 50 | if item.startswith("#"): 51 | yaml_payload_list.remove(item) 52 | yaml_string = "\n".join(yaml_payload_list) 53 | 54 | return yaml_string 55 | 56 | 57 | def test_get_path_and_filename(): 58 | path, filename = utils.get_path_and_filename("json/schemas/ntp.json") 59 | assert path == "json/schemas" 60 | assert filename == "ntp" 61 | 62 | 63 | def test_ensure_yaml_output_format(): 64 | data_formatted = utils.ensure_strings_have_quotes_mapping(TEST_DATA) 65 | yaml_path = "tests/mocks/utils/.formatted.yml" 66 | with open(yaml_path, "w", encoding="utf-8") as fileh: 67 | utils.YAML_HANDLER.dump(data_formatted, fileh) 68 | 69 | with open(yaml_path, encoding="utf-8") as fileh: 70 | actual = fileh.read() 71 | 72 | with open("tests/mocks/utils/formatted.yml", encoding="utf-8") as fileh: 73 | mock = fileh.read() 74 | 75 | mock = remove_comments_from_yaml_string(mock) 76 | 77 | assert actual == mock 78 | os.remove(yaml_path) 79 | assert not os.path.isfile(yaml_path) 80 | 81 | 82 | def test_get_conversion_filepaths(): 83 | yaml_path = "tests/mocks/schema/yaml" 84 | json_path = yaml_path.replace("yaml", "json") 85 | actual = utils.get_conversion_filepaths(yaml_path, "yml", json_path, "json") 86 | expected_defs = [ 87 | ( 88 | f"{yaml_path}/definitions/{subdir}/ip.yml", 89 | f"{json_path}/definitions/{subdir}/ip.json", 90 | ) 91 | for subdir in ("arrays", "objects", "properties") 92 | ] 93 | expected_schemas = [ 94 | (f"{yaml_path}/schemas/{schema}.yml", f"{json_path}/schemas/{schema}.json") for schema in ("dns", "ntp") 95 | ] 96 | mock = set(expected_defs + expected_schemas) 97 | # the results in actual are unordered, so test just ensures contents are the same 98 | assert not mock.difference(actual) 99 | 100 | 101 | def test_load_schema_from_json_file(): 102 | schema_root_dir = os.path.realpath("tests/mocks/schema/json") 103 | schema_filepath = f"{schema_root_dir}/schemas/ntp.json" 104 | validator = utils.load_schema_from_json_file(schema_root_dir, schema_filepath) 105 | with open("tests/mocks/ntp/valid/full_implementation.json", encoding="utf-8") as fileh: 106 | # testing validation tests that the RefResolver works as expected 107 | validator.validate(json.load(fileh)) 108 | 109 | 110 | def test_dump_data_to_yaml(): 111 | test_file = "tests/mocks/utils/.test_data.yml" 112 | if os.path.isfile(test_file): 113 | os.remove(test_file) 114 | 115 | assert not os.path.isfile(test_file) 116 | utils.dump_data_to_yaml(TEST_DATA, test_file) 117 | with open(test_file, encoding="utf-8") as fileh: 118 | actual = fileh.read() 119 | with open("tests/mocks/utils/formatted.yml", encoding="utf-8") as fileh: 120 | mock = fileh.read() 121 | 122 | mock = remove_comments_from_yaml_string(mock) 123 | 124 | assert actual == mock 125 | os.remove(test_file) 126 | assert not os.path.isfile(test_file) 127 | 128 | 129 | def test_dump_data_json(): 130 | test_file = "tests/mocks/utils/.test_data.json" 131 | assert not os.path.isfile(test_file) 132 | utils.dump_data_to_json(TEST_DATA, test_file) 133 | with open(test_file, encoding="utf-8") as fileh: 134 | actual = fileh.read() 135 | with open("tests/mocks/utils/formatted.json", encoding="utf-8") as fileh: 136 | mock = fileh.read() 137 | assert actual == mock 138 | os.remove(test_file) 139 | assert not os.path.isfile(test_file) 140 | 141 | 142 | def test_get_schema_properties(): 143 | schema_files = [f"tests/mocks/schema/json/schemas/{schema}.json" for schema in ("dns", "ntp")] 144 | actual = utils.get_schema_properties(schema_files) 145 | mock = { 146 | "dns": ["dns_servers"], 147 | "ntp": ["ntp_servers", "ntp_authentication", "ntp_logging"], 148 | } 149 | assert actual == mock 150 | 151 | 152 | def test_dump_schema_vars(): 153 | output_dir = "tests/mocks/utils/hostvar" 154 | assert not os.path.isdir(output_dir) 155 | schema_properties = { 156 | "dns": ["dns_servers"], 157 | "ntp": ["ntp_servers", "ntp_authentication", "ntp_logging"], 158 | } 159 | host_variables = ANSIBLE_HOST_VARIABLES["host1"] 160 | utils.dump_schema_vars(output_dir, schema_properties, host_variables) 161 | for file in ("dns.yml", "ntp.yml"): 162 | with open(f"{output_dir}/{file}", encoding="utf-8") as fileh: 163 | actual = fileh.read() 164 | with open(f"tests/mocks/utils/host1/{file}", encoding="utf-8") as fileh: 165 | mock = fileh.read() 166 | 167 | assert actual == mock 168 | 169 | shutil.rmtree(output_dir) 170 | assert not os.path.isdir(output_dir) 171 | -------------------------------------------------------------------------------- /tests/test_validator.py: -------------------------------------------------------------------------------- 1 | """Test validator functions.""" 2 | import pytest 3 | from schema_enforcer.schemas.validator import ( 4 | BaseModel, 5 | BaseValidation, 6 | is_validator, 7 | JmesPathModelValidation, 8 | PydanticValidation, 9 | pydantic_validation_factory, 10 | ) 11 | 12 | 13 | def test_is_validator_true(): 14 | """ 15 | Test if the is_validator function returns True for custom and Pydantic validators. 16 | """ 17 | 18 | class CustomValidation(BaseValidation): 19 | """Custom model for testing.""" 20 | 21 | def validate(self, data: dict, strict: bool = False): 22 | """Implement abstract method for testing.""" 23 | 24 | assert is_validator(CustomValidation) 25 | assert is_validator(PydanticValidation) 26 | 27 | 28 | @pytest.mark.parametrize("model", [BaseModel, BaseValidation, JmesPathModelValidation, int, str, dict, list]) 29 | def test_is_validator_false(model): 30 | """ 31 | Test case to verify that the function is_validator returns False for various non validator types. 32 | """ 33 | 34 | assert not is_validator(model) 35 | 36 | 37 | def test_pydantic_validation_factory(): 38 | """Test the pydantic factory provides the correct type and properties.""" 39 | 40 | class TestModel(BaseModel): # pylint: disable=too-few-public-methods 41 | """Custom model for testing.""" 42 | 43 | field1: str = None 44 | field2: str = None 45 | 46 | # Add id as it's required further on and added when loading pydantic sub models. 47 | TestModel.id = TestModel.__name__ 48 | validation = pydantic_validation_factory(TestModel) 49 | 50 | assert validation.__name__ == "TestModel" 51 | assert issubclass(validation, PydanticValidation) 52 | assert issubclass(validation, BaseValidation) 53 | assert validation.id == "TestModel" 54 | assert validation.top_level_properties == {"field1", "field2"} 55 | --------------------------------------------------------------------------------