├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .yamllint ├── ANNOUNCE.md ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action_plugins ├── cli.py ├── command_parser.py ├── textfsm_parser.py ├── validate_role_spec.py └── verify_dependent_role_version.py ├── bindep.txt ├── changelogs ├── config.yaml └── fragments │ ├── v0-initial-release.yaml │ ├── v251-bugfixes.yaml │ ├── v251-docs.yaml │ ├── v251-terminology-changes.yaml │ ├── v252-bugfixes.yaml │ ├── v252-filter-plugins.yaml │ ├── v252-lookup-plugins.yaml │ ├── v252-minorchanges.yaml │ ├── v252-tasks.yaml │ ├── v253-minorchanges.yaml │ ├── v253-removed-features.yaml │ ├── v254-bugfixes.yaml │ ├── v254-minorchanges.yaml │ ├── v260-initial-release.yaml │ ├── v261-bugfixes.yaml │ ├── v261-docs.yaml │ ├── v262-bugfixes.yaml │ ├── v262-filter-plugins.yaml │ ├── v262-lookup-plugins.yaml │ ├── v262-modules.yaml │ ├── v263-bugfixes.yaml │ ├── v263-minorchanges.yaml │ ├── v264-bugfixes.yaml │ ├── v264-docs.yaml │ ├── v264-removed-features.yaml │ ├── v265-bugfixes.yaml │ ├── v266-minor-changes.yaml │ ├── v266-removed-features.yaml │ ├── v270-bugfixes.yaml │ ├── v270-initial-release.yaml │ ├── v271-minorchanges.yaml │ ├── v272-minorchanges.yaml │ ├── v273-deprecated-features.yaml │ ├── v273-minor-changes.yaml │ ├── v274-bugfixes.yaml │ └── v275-bugfixes.yaml ├── defaults └── main.yml ├── docs ├── directives │ ├── parser_directives.md │ └── template_directives.md ├── plugins │ ├── filter_plugins.md │ └── verify_dependent_role_version.md ├── tasks │ └── cli.md ├── tests │ └── test_guide.md └── user_guide │ ├── README.md │ ├── command_parser.md │ └── textfsm_parser.md ├── filter_plugins └── network_engine.py ├── includes └── init.yaml ├── lib └── network_engine │ ├── __init__.py │ ├── plugins │ ├── __init__.py │ ├── parser │ │ ├── __init__.py │ │ └── pattern_match.py │ └── template │ │ ├── __init__.py │ │ ├── json_template.py │ │ └── normal.py │ └── utils.py ├── library ├── command_parser.py ├── net_facts.py └── textfsm_parser.py ├── lookup_plugins ├── __init__.py ├── config_template.py ├── json_template.py ├── netcfg_diff.py └── network_template.py ├── meta └── main.yml ├── requirements.txt ├── tasks ├── cli.yaml └── main.yml ├── test-requirements.txt ├── tests ├── ansible.cfg ├── command_parser │ ├── command_parser │ │ ├── defaults │ │ │ └── main.yaml │ │ ├── output │ │ │ └── ios │ │ │ │ ├── show_interfaces.txt │ │ │ │ └── show_version.txt │ │ ├── parser_templates │ │ │ └── ios │ │ │ │ ├── show_interfaces.yaml │ │ │ │ ├── show_interfaces_expand.yaml │ │ │ │ ├── show_version.yaml │ │ │ │ └── show_version_expand.yaml │ │ └── tasks │ │ │ ├── ios.yaml │ │ │ └── main.yaml │ └── test.yml ├── config_template │ ├── config_template │ │ ├── tasks │ │ │ ├── config_template.yaml │ │ │ └── main.yaml │ │ └── templates │ │ │ ├── fail.j2 │ │ │ └── pass.j2 │ └── test.yml ├── interface_range │ ├── interface_range │ │ └── tasks │ │ │ ├── interface_range.yaml │ │ │ └── main.yaml │ └── test.yml ├── interface_split │ ├── interface_split │ │ └── tasks │ │ │ ├── interface_split.yaml │ │ │ └── main.yaml │ └── test.yml ├── inventory ├── json_template │ ├── json_template │ │ ├── defaults │ │ │ └── main.yaml │ │ ├── tasks │ │ │ ├── json_lookup.yaml │ │ │ └── main.yaml │ │ └── templates │ │ │ └── config.json │ └── test.yml ├── netcfg_diff │ ├── netcfg_diff │ │ ├── defaults │ │ │ └── main.yaml │ │ ├── files │ │ │ └── ios │ │ │ │ ├── have.txt │ │ │ │ └── want.txt │ │ └── tasks │ │ │ ├── ios.yaml │ │ │ └── main.yaml │ └── test.yml ├── test.yml ├── textfsm_parser │ ├── test.yml │ └── textfsm_parser │ │ ├── defaults │ │ └── main.yaml │ │ ├── output │ │ └── ios │ │ │ ├── show_interfaces.txt │ │ │ └── show_version.txt │ │ ├── parser_templates │ │ └── ios │ │ │ ├── show_interfaces │ │ │ └── show_version │ │ └── tasks │ │ ├── ios.yaml │ │ └── main.yaml ├── to_lines │ ├── test.yml │ └── to_lines │ │ └── tasks │ │ ├── main.yaml │ │ └── to_lines.yaml ├── validate_role_spec │ ├── test.yml │ └── validate_role_spec │ │ ├── meta │ │ ├── failedtest.yaml │ │ └── test.yaml │ │ └── tasks │ │ ├── main.yaml │ │ ├── validate_role_spec.yaml │ │ └── validate_role_spec_failed.yaml ├── vlan_compress │ ├── test.yml │ └── vlan_compress │ │ └── tasks │ │ ├── main.yaml │ │ └── vlan_compress.yaml └── vlan_expand │ ├── test.yml │ └── vlan_expand │ └── tasks │ ├── main.yaml │ └── vlan_expand.yaml ├── tox.ini └── vars └── main.yml /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # ISSUE TYPE 6 | 7 | 8 | 9 | - Bug Report 10 | - Feature Idea 11 | - Documentation Report 12 | 13 | # ANSIBLE VERSION 14 | 15 | ``` 16 | ansible --version 17 | 18 | ansible-galaxy list | grep ansible.network 19 | ``` 20 | 21 | # Network OS 22 | 23 | - Operating System (inc version) of machine running Ansible 24 | - Network device make/model, including version 25 | 26 | # SUMMARY 27 | 28 | 29 | 30 | # STEPS TO REPRODUCE 31 | 32 | 35 | 36 | ```yaml 37 | 38 | ``` 39 | 40 | 41 | 42 | # EXPECTED RESULTS 43 | 44 | 45 | 46 | # ACTUAL RESULTS 47 | 48 | 50 | 51 | ``` 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.swp 3 | *.swo 4 | *.bak 5 | .tox 6 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | ignore: | 4 | .git/ 5 | .tox/ 6 | 7 | rules: 8 | braces: 9 | max-spaces-inside: 1 10 | level: error 11 | brackets: 12 | max-spaces-inside: 1 13 | level: error 14 | line-length: disable 15 | -------------------------------------------------------------------------------- /ANNOUNCE.md: -------------------------------------------------------------------------------- 1 | Ansible Network: Network Engine Release 2 | ---------------------------------------- 3 | 4 | The Ansible Network team is pleased to announce that the initial release of the Network Engine Ansible role is now available in Ansible Galaxy! 5 | 6 | What is an Ansible Role? 7 | ---------------------------------- 8 | An Ansible Role is a collection of related tasks, methods, plugins, and modules in a standard format. You can use Roles in tasks or playbooks. 9 | 10 | What does the Network Engine Role do? 11 | ---------------------------------- 12 | The Network Engine Role provides the fundamental building blocks for a data-model-driven approach to automated network management. Network Engine: 13 | 14 | - extracts data about your network devices 15 | - returns the data as Ansible facts in a JSON data structure, ready to be added to your inventory host facts and/or consumed by Ansible tasks and templates 16 | - works on any network platform 17 | 18 | With the Network Engine role, and other Roles built around it, you can normalize your Ansible facts across your entire network. 19 | 20 | How do I get it? 21 | ---------------------------------- 22 | Via Ansible Galaxy using the following Linux command: 23 | 24 | `ansible-galaxy install ansible-network.network-engine` 25 | 26 | How do I use it? 27 | ---------------------------------- 28 | See the [User Guide](https://github.com/ansible-network/network-engine/blob/devel/docs/user_guide/README.md) for details and examples. 29 | 30 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Ansible Network network-engine 3 | ============================== 4 | 5 | .. _Ansible Network network-engine_v2.7.5: 6 | 7 | v2.7.5 8 | ====== 9 | 10 | .. _Ansible Network network-engine_v2.7.5_Bugfixes: 11 | 12 | Bugfixes 13 | -------- 14 | 15 | - Fix src_path to src command_parser `network-engine#230 `_. 16 | 17 | 18 | .. _Ansible Network network-engine_v2.7.4: 19 | 20 | v2.7.4 21 | ====== 22 | 23 | .. _Ansible Network network-engine_v2.7.4_Bugfixes: 24 | 25 | Bugfixes 26 | -------- 27 | 28 | - Fail validate_role_spec plugin if argument_spec is undefined `network-engine#221 `_. 29 | 30 | - Fix relative path failure in command_parser when template is not present in playbook directory `network-engine#222 `_. 31 | 32 | 33 | .. _Ansible Network network-engine_v2.7.3: 34 | 35 | v2.7.3 36 | ====== 37 | 38 | .. _Ansible Network network-engine_v2.7.3_Minor Changes: 39 | 40 | Minor Changes 41 | ------------- 42 | 43 | - Add verify_depedent_role_version action plugin `network-engine#214 `_. 44 | 45 | 46 | .. _Ansible Network network-engine_v2.7.3_Deprecated Features: 47 | 48 | Deprecated Features 49 | ------------------- 50 | 51 | - Deprecate lookup plugin network_template `network-engine#215 `_. 52 | 53 | 54 | .. _Ansible Network network-engine_v2.7.2: 55 | 56 | v2.7.2 57 | ====== 58 | 59 | .. _Ansible Network network-engine_v2.7.2_Minor Changes: 60 | 61 | Minor Changes 62 | ------------- 63 | 64 | - Add support for nested spec validation in validate_role_spec `network-engine#198 `_. 65 | 66 | 67 | .. _Ansible Network network-engine_v2.7.1: 68 | 69 | v2.7.1 70 | ====== 71 | 72 | .. _Ansible Network network-engine_v2.7.1_Minor Changes: 73 | 74 | Minor Changes 75 | ------------- 76 | 77 | - Add name option for textfsm to create facts to key `network-engine#202 `_. 78 | 79 | - Document name option for textfsm in cli plugin and update cli task `network-engine#205 `_. 80 | 81 | 82 | .. _Ansible Network network-engine_v2.7.0: 83 | 84 | v2.7.0 85 | ====== 86 | 87 | .. _Ansible Network network-engine_v2.7.0_Major Changes: 88 | 89 | Major Changes 90 | ------------- 91 | 92 | - Initial release of 2.7.0 ``network-engine`` Ansible role that is supported with Ansible 2.7.0 93 | 94 | 95 | .. _Ansible Network network-engine_v2.7.0_Bugfixes: 96 | 97 | Bugfixes 98 | -------- 99 | 100 | - Fix native type conversion in json_template `network-engine#154 `_. 101 | 102 | - Fix templating repeat_for `network-engine#190 `_. 103 | 104 | - Add missing boilerplate for net_facts module `network-engine#194 `_. 105 | 106 | 107 | .. _Ansible Network network-engine_v2.6.6: 108 | 109 | v2.6.6 110 | ====== 111 | 112 | .. _Ansible Network network-engine_v2.6.6_Minor Changes: 113 | 114 | Minor Changes 115 | ------------- 116 | 117 | - Capability to filter AnsibleModule kwargs `network-engine#149 `_. 118 | 119 | 120 | .. _Ansible Network network-engine_v2.6.6_Removed Features (previously deprecated): 121 | 122 | Removed Features (previously deprecated) 123 | ---------------------------------------- 124 | 125 | - Remove deprecated module ``cli_get`` 126 | 127 | 128 | .. _Ansible Network network-engine_v2.6.5: 129 | 130 | v2.6.5 131 | ====== 132 | 133 | .. _Ansible Network network-engine_v2.6.5_Bugfixes: 134 | 135 | Bugfixes 136 | -------- 137 | 138 | - Remove GenericLinux from supported platforms `network-engine#145 `_. 139 | 140 | 141 | .. _Ansible Network network-engine_v2.6.4: 142 | 143 | v2.6.4 144 | ====== 145 | 146 | .. _Ansible Network network-engine_v2.6.4_Removed Features (previously deprecated): 147 | 148 | Removed Features (previously deprecated) 149 | ---------------------------------------- 150 | 151 | - Remove deprecated module ``text_parser``. 152 | 153 | - Remove deprecated module ``textfsm``. 154 | 155 | 156 | .. _Ansible Network network-engine_v2.6.4_Bugfixes: 157 | 158 | Bugfixes 159 | -------- 160 | 161 | - Fix repeat_for in json_template `network-engine#139 `_. 162 | 163 | 164 | .. _Ansible Network network-engine_v2.6.4_Documentation Updates: 165 | 166 | Documentation Updates 167 | --------------------- 168 | 169 | - Removes unnecessary details from README `network-engine#126 `_. 170 | 171 | 172 | .. _Ansible Network network-engine_v2.6.3: 173 | 174 | v2.6.3 175 | ====== 176 | 177 | .. _Ansible Network network-engine_v2.6.3_Minor Changes: 178 | 179 | Minor Changes 180 | ------------- 181 | 182 | - Makes parser directive extend templatable `network-engine#132 `_. 183 | 184 | 185 | .. _Ansible Network network-engine_v2.6.3_Bugfixes: 186 | 187 | Bugfixes 188 | -------- 189 | 190 | - Task to fail if ansible_min_version isn't met `network-engine#130 `_. 191 | 192 | 193 | .. _Ansible Network network-engine_v2.6.2: 194 | 195 | v2.6.2 196 | ====== 197 | 198 | .. _Ansible Network network-engine_v2.6.2_New Lookup Plugins: 199 | 200 | New Lookup Plugins 201 | ------------------ 202 | 203 | - NEW ``config_template`` lookup plugin 204 | 205 | - NEW ``yang_json2xml`` lookup plugin 206 | 207 | 208 | .. _Ansible Network network-engine_v2.6.2_New Filter Plugins: 209 | 210 | New Filter Plugins 211 | ------------------ 212 | 213 | - NEW ``to_lines`` filter plugin 214 | 215 | 216 | .. _Ansible Network network-engine_v2.6.2_New Modules: 217 | 218 | New Modules 219 | ----------- 220 | 221 | - NEW ``validate_role_spec`` handle validating facts required by the role 222 | 223 | 224 | .. _Ansible Network network-engine_v2.6.2_Bugfixes: 225 | 226 | Bugfixes 227 | -------- 228 | 229 | - Fix role path test dependency `network-engine#121 `_. 230 | 231 | 232 | .. _Ansible Network network-engine_v2.6.1: 233 | 234 | v2.6.1 235 | ====== 236 | 237 | .. _Ansible Network network-engine_v2.6.1_Documentation Updates: 238 | 239 | Documentation Updates 240 | --------------------- 241 | 242 | - The argument to end a block of text when searching with match_greedy was missing `network-engine#116 `_. 243 | 244 | 245 | .. _Ansible Network network-engine_v2.6.0: 246 | 247 | v2.6.0 248 | ====== 249 | 250 | .. _Ansible Network network-engine_v2.6.0_Major Changes: 251 | 252 | Major Changes 253 | ------------- 254 | 255 | - Initial release of 2.6.0 ``network-engine`` Ansible role that is supported with Ansible 2.6.0 256 | 257 | 258 | .. _Ansible Network network-engine_v2.5.4: 259 | 260 | v2.5.4 261 | ====== 262 | 263 | .. _Ansible Network network-engine_v2.5.4_Minor Changes: 264 | 265 | Minor Changes 266 | ------------- 267 | 268 | - Add parsers to search path `network-engine#89 `_. 269 | 270 | - Fix export_as templating vars `network-engine#104 `_. 271 | 272 | 273 | .. _Ansible Network network-engine_v2.5.4_Bugfixes: 274 | 275 | Bugfixes 276 | -------- 277 | 278 | - Fix cli task parser undefined issue when only command is used `network-engine#103 `_. 279 | 280 | - Fix an issue with using the extend directive with a loop `network-engine#105 `_. 281 | 282 | - Fixes bug when loading a dir of parsers `network-engine#113 `_. 283 | 284 | 285 | .. _Ansible Network network-engine_v2.5.3: 286 | 287 | v2.5.3 288 | ====== 289 | 290 | .. _Ansible Network network-engine_v2.5.3_Minor Changes: 291 | 292 | Minor Changes 293 | ------------- 294 | 295 | - Templating the regex sent to the parser to allow us to use ansible variables in the regex string `network-engine#97 `_. 296 | 297 | 298 | .. _Ansible Network network-engine_v2.5.3_Removed Features (previously deprecated): 299 | 300 | Removed Features (previously deprecated) 301 | ---------------------------------------- 302 | 303 | - Move yang2spec lookup to feature branch, till the right location for this plugin is identified `network-engine#100 `_. 304 | 305 | 306 | .. _Ansible Network network-engine_v2.5.2: 307 | 308 | v2.5.2 309 | ====== 310 | 311 | .. _Ansible Network network-engine_v2.5.2_Minor Changes: 312 | 313 | Minor Changes 314 | ------------- 315 | 316 | - Add new directives extend `network-engine#91 `_. 317 | 318 | - Adds conditional support to nested template objects `network-engine#55 `_. 319 | 320 | 321 | .. _Ansible Network network-engine_v2.5.2_New Lookup Plugins: 322 | 323 | New Lookup Plugins 324 | ------------------ 325 | 326 | - New lookup plugin ``json_template`` 327 | 328 | - New lookup plugin ``network_template`` 329 | 330 | - New lookup plugin ``yang2spec`` 331 | 332 | - New lookup plugin ``netcfg_diff`` 333 | 334 | 335 | .. _Ansible Network network-engine_v2.5.2_New Filter Plugins: 336 | 337 | New Filter Plugins 338 | ------------------ 339 | 340 | - New filter plugin ``interface_range`` 341 | 342 | - New filter plugin ``interface_split`` 343 | 344 | - New filter plugin ``vlan_compress`` 345 | 346 | - New filter plugin ``vlan_expand`` 347 | 348 | 349 | .. _Ansible Network network-engine_v2.5.2_New Tasks: 350 | 351 | New Tasks 352 | --------- 353 | 354 | - New task ``cli`` 355 | 356 | 357 | .. _Ansible Network network-engine_v2.5.2_Bugfixes: 358 | 359 | Bugfixes 360 | -------- 361 | 362 | - Fix AnsibleFilterError, deprecations, and unused imports `network-engine#82 `_. 363 | 364 | 365 | .. _Ansible Network network-engine_v2.5.1: 366 | 367 | v2.5.1 368 | ====== 369 | 370 | .. _Ansible Network network-engine_v2.5.1_Deprecated Features: 371 | 372 | Deprecated Features 373 | ------------------- 374 | 375 | - Module ``text_parser`` renamed to ``command_parser``; original name deprecated; legacy use supported; will be removed in 2.6.0. 376 | 377 | - Module ``textfsm`` renamed to ``textfsm_parser``; original name deprecated; legacy use supported; will be removed in 2.6.0. 378 | 379 | 380 | .. _Ansible Network network-engine_v2.5.1_New Modules: 381 | 382 | New Modules 383 | ----------- 384 | 385 | - New module ``command_parser`` (renamed from ``text_parser``) 386 | 387 | - New module ``textfsm_parser`` (renamed from ``textfsm``) 388 | 389 | 390 | .. _Ansible Network network-engine_v2.5.1_Bugfixes: 391 | 392 | Bugfixes 393 | -------- 394 | 395 | - Fix ``command_parser`` Absolute path with tilde in src should work `network-engine#58 `_ 396 | 397 | - Fix content mush only accepts string type `network-engine#72 `_ 398 | 399 | - Fix StringIO to work with Python3 in addition to Python2 `network-engine#53 `_ 400 | 401 | 402 | .. _Ansible Network network-engine_v2.5.1_Documentation Updates: 403 | 404 | Documentation Updates 405 | --------------------- 406 | 407 | - User Guide `docs/user_guide `_. 408 | 409 | 410 | .. _Ansible Network network-engine_v2.5.0: 411 | 412 | v2.5.0 413 | ====== 414 | 415 | .. _Ansible Network network-engine_v2.5.0_Major Changes: 416 | 417 | Major Changes 418 | ------------- 419 | 420 | - Initial release of the ``network-engine`` Ansible role. 421 | 422 | - This role provides the foundation for building network roles by providing modules and plugins that are common to all Ansible Network roles. All of the artifacts in this role can be used independent of the platform that is being managed. 423 | 424 | 425 | .. _Ansible Network network-engine_v2.5.0_New Modules: 426 | 427 | New Modules 428 | ----------- 429 | 430 | - NEW ``text_parser`` Parses ASCII text into JSON facts using text_parser engine and YAML-formatted input. Provides a rules-based text parser that is closely modeled after the Ansible playbook language. This parser will iterate over the rules and parse the output of structured ASCII text into a JSON data structure that can be added to the inventory host facts. 431 | 432 | - NEW ``textfsm`` Parses ASCII text into JSON facts using textfsm engine and Google TextFSM-formatted input. Provides textfsm rules-based templates to parse data from text. The template acting as parser will iterate of the rules and parse the output of structured ASCII text into a JSON data structure that can be added to the inventory host facts. 433 | 434 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developer Guide 2 | 3 | This role is developed and maintained by the Ansible Network Working Group. 4 | Contributions to this role are welcomed. This document will provide individuals 5 | with information about how to contribute to the further development of this 6 | role. 7 | 8 | ## Contributing 9 | 10 | There are many ways you can contribute to this role. Adding new artifacts such 11 | as modules and plugins, testing and/or reviewing and updating documentation. 12 | 13 | ### Adding support for a new platform 14 | 15 | To add support for a new platform to this role, there are a couple of things 16 | that need to be done. 17 | 18 | 1) Create the module for the platform specific implementation in Ansible. The 19 | module can be contributed directly to Ansible core, distributed through Ansible 20 | Galaxy or added to this role. 21 | 22 | 2) (Optional) If adding the module code directly to this role, add the module 23 | to `library/` 24 | 25 | 3) (Optional) If the new platform module is distributed through another Galaxy 26 | role, please update [README](README.md) Dependencies section to include the 27 | name of the Galaxy role that includes the module. 28 | 29 | 4) Once the module has been created, the add a new task in `tasks/` for the 30 | specific platform to be supported. Use any of the existing platform 31 | implementations as a guide. 32 | 33 | 5) (Optional) If a configuration parameter is not supported, then the 34 | implementation in tasks should detect that and provide a warning message. 35 | 36 | 6) Update the `meta/main.yaml` file to add the newly provided platform to 37 | the `platforms` meta data. 38 | 39 | ### Adding platform specific arguments 40 | 41 | Sometimes there is a need to add platform specific arguments to a role for use 42 | by a platform specific module. This can be accomplished by adding the adding 43 | the arguments under a platform specific key. 44 | 45 | Note: It is the responsibility of the task writer to handle the implementation 46 | of the platform specific arguments. 47 | 48 | Here is an example that implements a platform specific argument: 49 | 50 | ```yaml 51 | tasks: 52 | - name: configure network device resource 53 | include_role: 54 | name: net_system 55 | vars: 56 | resource: 57 | foo: bar 58 | ios: 59 | foo: baz 60 | ``` 61 | 62 | ### Adding documentation for a platform specific implementation 63 | 64 | While not required, there are times when providing implementation nodes are 65 | advantageous to instructing the playbook writer how to implement platform 66 | specific arguments. In order to provide platform specific documentation, 67 | create a file in the docs directory using GitHub Markdown. The file name 68 | should be the platform name. 69 | 70 | For instance, let's assume we want to create implementation nodes for a 71 | fictitious platform call `foo`. Create a new file `docs/foo.md` and 72 | then add a link to [README](README.md) pointing to `docs/foo.md` in the `PLATFORM 73 | NOTES` section. 74 | 75 | # Note 76 | 77 | The release cadence for the network-engine role is two weeks and it will be 78 | released on every second Tuesday at 12:00 PM (GMT) from the date of prior release. 79 | For the PR to be available in the upcoming release it should be in a mergeable state 80 | that is CI is passing and all review comments fixed at least two days prior to scheduled date 81 | of release. 82 | 83 | ## Bug Reporting 84 | 85 | If you have found a bug in the with the current role please open a [GitHub 86 | issue](../../issues) 87 | 88 | ## Contact 89 | 90 | * [#ansible-network IRC channel](https://webchat.freenode.net/?channels=ansible-network) on Freenode.net 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # network-engine 2 | 3 | [![network-engine Ansible Galaxy Role](https://img.shields.io/ansible/role/25206.svg)](https://galaxy.ansible.com/ansible-network/network-engine/) 4 | 5 | This role provides the foundation for building network roles by providing 6 | modules and plugins that are common to all Ansible Network roles. Typically 7 | this role should not be directly invoked in a playbook. 8 | 9 | To install this role: `ansible-galaxy install ansible-network.network-engine` 10 | 11 | To see the version of this role you currently have installed: `ansible-galaxy info ansible-network.network-engine` 12 | 13 | To ensure you have the latest version available: `ansible-galaxy install -f ansible-network.network-engine` 14 | 15 | To find other roles maintained by the Ansible Network team, see our [Galaxy Profile](https://galaxy.ansible.com/ansible-network/). 16 | 17 | Any open bugs and/or feature requests are tracked in [GitHub issues](https://github.com/ansible-network/network-engine/issues). 18 | 19 | Interested in contributing to this role? Check out [CONTRIBUTING](https://github.com/ansible-network/network-engine/blob/devel/CONTRIBUTING.md) before submitting a pull request. 20 | 21 | 22 | ## Functions 23 | 24 | This section provides a list of the available functions that are included in 25 | this role. Any of the provided functions can be implemented in Ansible 26 | playbooks directly. To use a particular function, please see the `docs` link 27 | associated with the function. 28 | 29 | * `cli` [[source]](https://github.com/ansible-network/network-engine/blob/devel/tasks/cli.yaml) [[docs]](https://github.com/ansible-network/network-engine/blob/devel/docs/tasks/cli.md). 30 | 31 | ## Developer Guide 32 | 33 | - [How to use](https://github.com/ansible-network/network-engine/blob/devel/docs/user_guide/README.md) 34 | - [Parser Directives](https://github.com/ansible-network/network-engine/blob/devel/docs/directives/parser_directives.md) 35 | - [Filter Plugins](https://github.com/ansible-network/network-engine/blob/devel/docs/plugins/filter_plugins.md) 36 | - [How to test](https://github.com/ansible-network/network-engine/blob/devel/docs/tests/test_guide.md) 37 | 38 | 39 | ## License 40 | 41 | GPLv3 42 | 43 | ## Author Information 44 | 45 | Ansible Network Community (ansible-network) 46 | -------------------------------------------------------------------------------- /action_plugins/cli.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | ANSIBLE_METADATA = {'metadata_version': '1.1', 11 | 'status': ['preview'], 12 | 'supported_by': 'network'} 13 | 14 | DOCUMENTATION = """ 15 | --- 16 | module: cli 17 | author: Peter Sprygada (@privateip) 18 | short_description: Runs the specific command and returns the output 19 | description: 20 | - The command specified in C(command) will be executed on the remote 21 | device and its output will be returned to the module. This module 22 | requires that the device is supported using the C(network_cli) 23 | connection plugin and has a valid C(cliconf) plugin to work correctly. 24 | version_added: "2.5" 25 | options: 26 | command: 27 | description: 28 | - The command to be executed on the remote node. The value for this 29 | argument will be passed unchanged to the network device and the 30 | output returned. 31 | required: yes 32 | default: null 33 | parser: 34 | description: 35 | - The parser file to pass the output from the command through to 36 | generate Ansible facts. If this argument is specified, the output 37 | from the command will be parsed based on the rules in the 38 | specified parser. 39 | default: null 40 | engine: 41 | description: 42 | - Defines the engine to use when parsing the output. This argument 43 | accepts one of two valid values, C(command_parser) or C(textfsm_parser). 44 | default: command_parser 45 | choices: 46 | - command_parser 47 | - textfsm_parser 48 | name: 49 | description: 50 | - The C(name) argument is used to define the top-level fact name to 51 | hold the output of textfsm_engine parser. If this argument is not provided, 52 | the output from parsing will not be exported. Note that this argument is 53 | only considered when C(engine) is C(textfsm_parser). 54 | default: null 55 | """ 56 | 57 | EXAMPLES = """ 58 | - name: return show version 59 | cli: 60 | command: show version 61 | 62 | - name: return parsed command output 63 | cli: 64 | command: show version 65 | parser: parser_templates/show_version.yaml 66 | 67 | - name: parse with textfsm_parser engine 68 | cli: 69 | command: show version 70 | parser: parser_templates/show_version 71 | engine: textfsm_parser 72 | name: system_facts 73 | """ 74 | 75 | RETURN = """ 76 | stdout: 77 | description: returns the output from the command 78 | returned: always 79 | type: dict 80 | json: 81 | description: the output converted from json to a hash 82 | returned: always 83 | type: dict 84 | """ 85 | 86 | import json 87 | 88 | from ansible.plugins.action import ActionBase 89 | from ansible.module_utils.connection import Connection, ConnectionError 90 | from ansible.module_utils._text import to_text 91 | from ansible.errors import AnsibleError 92 | from ansible.utils.display import Display 93 | 94 | display = Display() 95 | 96 | 97 | class ActionModule(ActionBase): 98 | 99 | def run(self, tmp=None, task_vars=None): 100 | ''' handler for cli operations ''' 101 | 102 | if task_vars is None: 103 | task_vars = dict() 104 | 105 | result = super(ActionModule, self).run(tmp, task_vars) 106 | del tmp # tmp no longer has any effect 107 | 108 | try: 109 | command = self._task.args['command'] 110 | parser = self._task.args.get('parser') 111 | engine = self._task.args.get('engine', 'command_parser') 112 | if engine == 'textfsm_parser': 113 | name = self._task.args.get('name') 114 | elif engine == 'command_parser' and self._task.args.get('name'): 115 | display.warning('name is unnecessary when using command_parser and will be ignored') 116 | del self._task.args['name'] 117 | except KeyError as exc: 118 | raise AnsibleError(to_text(exc)) 119 | 120 | socket_path = getattr(self._connection, 'socket_path') or task_vars.get('ansible_socket') 121 | connection = Connection(socket_path) 122 | 123 | # make command a required argument 124 | if not command: 125 | raise AnsibleError('missing required argument `command`') 126 | 127 | try: 128 | output = connection.get(command) 129 | except ConnectionError as exc: 130 | raise AnsibleError(to_text(exc)) 131 | 132 | result['stdout'] = output 133 | 134 | # try to convert the cli output to native json 135 | try: 136 | json_data = json.loads(output) 137 | except Exception: 138 | json_data = None 139 | 140 | result['json'] = json_data 141 | 142 | if parser: 143 | if engine not in ('command_parser', 'textfsm_parser'): 144 | raise AnsibleError('missing or invalid value for argument engine') 145 | 146 | new_task = self._task.copy() 147 | new_task.args = { 148 | 'file': parser, 149 | 'content': (json_data or output) 150 | } 151 | if engine == 'textfsm_parser': 152 | new_task.args.update({'name': name}) 153 | 154 | kwargs = { 155 | 'task': new_task, 156 | 'connection': self._connection, 157 | 'play_context': self._play_context, 158 | 'loader': self._loader, 159 | 'templar': self._templar, 160 | 'shared_loader_obj': self._shared_loader_obj 161 | } 162 | 163 | task_parser = self._shared_loader_obj.action_loader.get(engine, **kwargs) 164 | result.update(task_parser.run(task_vars=task_vars)) 165 | 166 | self._remove_tmp_path(self._connection._shell.tmpdir) 167 | 168 | # this is needed so the strategy plugin can identify the connection as 169 | # a persistent connection and track it, otherwise the connection will 170 | # not be closed at the end of the play 171 | socket_path = getattr(self._connection, 'socket_path') or task_vars.get('ansible_socket') 172 | self._task.args['_ansible_socket'] = socket_path 173 | 174 | return result 175 | -------------------------------------------------------------------------------- /action_plugins/textfsm_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (c) 2018, Ansible by Red Hat, inc 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | # 6 | # You should have received a copy of the GNU General Public License 7 | # along with Ansible. If not, see . 8 | # 9 | from __future__ import (absolute_import, division, print_function) 10 | __metaclass__ = type 11 | 12 | from ansible.module_utils.six import StringIO, string_types 13 | 14 | from ansible.plugins.action import ActionBase 15 | from ansible.errors import AnsibleError 16 | 17 | try: 18 | import textfsm 19 | HAS_TEXTFSM = True 20 | except ImportError: 21 | HAS_TEXTFSM = False 22 | 23 | 24 | class ActionModule(ActionBase): 25 | 26 | def run(self, tmp=None, task_vars=None): 27 | ''' handler for textfsm action ''' 28 | 29 | if task_vars is None: 30 | task_vars = dict() 31 | 32 | result = super(ActionModule, self).run(tmp, task_vars) 33 | del tmp # tmp no longer has any effect 34 | 35 | try: 36 | if not HAS_TEXTFSM: 37 | raise AnsibleError('textfsm_parser engine requires the TextFSM library to be installed') 38 | 39 | try: 40 | filename = self._task.args.get('file') 41 | src = self._task.args.get('src') 42 | content = self._task.args['content'] 43 | name = self._task.args.get('name') 44 | except KeyError as exc: 45 | raise AnsibleError('missing required argument: %s' % exc) 46 | 47 | if src and filename: 48 | raise AnsibleError('`src` and `file` are mutually exclusive arguments') 49 | 50 | if not isinstance(content, string_types): 51 | return {'failed': True, 'msg': '`content` must be of type str, got %s' % type(content)} 52 | 53 | if filename: 54 | tmpl = open(filename) 55 | else: 56 | tmpl = StringIO() 57 | tmpl.write(src.strip()) 58 | tmpl.seek(0) 59 | 60 | try: 61 | re_table = textfsm.TextFSM(tmpl) 62 | fsm_results = re_table.ParseText(content) 63 | 64 | except Exception as exc: 65 | raise AnsibleError(str(exc)) 66 | 67 | final_facts = [] 68 | for item in fsm_results: 69 | facts = {} 70 | facts.update(dict(zip(re_table.header, item))) 71 | final_facts.append(facts) 72 | 73 | if name: 74 | result['ansible_facts'] = {name: final_facts} 75 | else: 76 | result['ansible_facts'] = {} 77 | 78 | finally: 79 | self._remove_tmp_path(self._connection._shell.tmpdir) 80 | 81 | return result 82 | -------------------------------------------------------------------------------- /action_plugins/validate_role_spec.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | ANSIBLE_METADATA = {'metadata_version': '1.1', 11 | 'status': ['preview'], 12 | 'supported_by': 'network'} 13 | 14 | DOCUMENTATION = """ 15 | --- 16 | module: validate_role_spec 17 | author: Peter Sprygada (@privateip 18 | short_description: Validate required arguments are set from facts 19 | description: 20 | - This module will accept an external argument spec file that will be used to 21 | validate arguments have been configured and set properly in order to allow 22 | the role to proceed. This validate specification file provides the 23 | equivalent of the Ansible module argument spec. 24 | version_added: "2.7" 25 | options: 26 | spec: 27 | description: 28 | - Relative or absolute path to the arugment specification file to use to 29 | validate arguments are properly set for role execution. 30 | required: yes 31 | """ 32 | 33 | EXAMPLES = """ 34 | - name: use spec file for role validation 35 | validate_role_spec: 36 | spec: args.yaml 37 | """ 38 | 39 | RETURN = """ 40 | """ 41 | import os 42 | import json 43 | 44 | from ansible.plugins.action import ActionBase 45 | from ansible.module_utils._text import to_text, to_bytes 46 | from ansible.module_utils.six import iteritems, string_types 47 | from ansible.module_utils import basic 48 | from ansible.errors import AnsibleModuleError 49 | from ansible.utils.display import Display 50 | 51 | display = Display() 52 | 53 | 54 | class ActionModule(ActionBase): 55 | 56 | VALID_MODULE_KWARGS = ( 57 | 'argument_spec', 'mutually_exclusive', 'required_if', 58 | 'required_one_of', 'required_together' 59 | ) 60 | 61 | def run(self, tmp=None, task_vars=None): 62 | ''' handler for cli operations ''' 63 | 64 | if task_vars is None: 65 | task_vars = dict() 66 | 67 | result = super(ActionModule, self).run(tmp, task_vars) 68 | del tmp # tmp no longer has any effect 69 | 70 | try: 71 | spec = self._task.args['spec'] 72 | except KeyError as exc: 73 | raise AnsibleModuleError(to_text(exc)) 74 | 75 | if not spec: 76 | raise AnsibleModuleError('missing required argument: spec') 77 | 78 | spec_fp = os.path.join(task_vars['role_path'], 'meta/%s' % spec) 79 | display.vvv('using role spec %s' % spec_fp) 80 | spec = self._loader.load_from_file(spec_fp) 81 | 82 | if 'argument_spec' not in spec: 83 | return {'failed': True, 'msg': 'missing required field in specification file: argument_spec'} 84 | 85 | argument_spec = spec['argument_spec'] 86 | 87 | args = {} 88 | self._handle_options(task_vars, args, argument_spec) 89 | 90 | basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': args})) 91 | basic.AnsibleModule.fail_json = self.fail_json 92 | 93 | spec = dict([(k, v) for k, v in iteritems(spec) if k in self.VALID_MODULE_KWARGS]) 94 | validated_spec = basic.AnsibleModule(**spec) 95 | 96 | result['role_params'] = validated_spec.params 97 | result['changed'] = False 98 | self._remove_tmp_path(self._connection._shell.tmpdir) 99 | 100 | return result 101 | 102 | def fail_json(self, msg): 103 | raise AnsibleModuleError(msg) 104 | 105 | def _handle_options(self, task_vars, args, spec): 106 | for key, attrs in iteritems(spec): 107 | if attrs is None: 108 | spec[key] = {'type': 'str'} 109 | elif isinstance(attrs, dict): 110 | suboptions_spec = attrs.get('options') 111 | if suboptions_spec: 112 | args[key] = dict() 113 | self._handle_options(task_vars, args[key], suboptions_spec) 114 | if key in task_vars: 115 | if isinstance(task_vars[key], string_types): 116 | value = self._templar.do_template(task_vars[key]) 117 | if value: 118 | args[key] = value 119 | else: 120 | args[key] = task_vars[key] 121 | elif attrs: 122 | if 'aliases' in attrs: 123 | for item in attrs['aliases']: 124 | if item in task_vars: 125 | args[key] = self._templar.do_template(task_vars[item]) 126 | else: 127 | args[key] = None 128 | -------------------------------------------------------------------------------- /action_plugins/verify_dependent_role_version.py: -------------------------------------------------------------------------------- 1 | # (c) 2019, Ansible Inc, 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | from __future__ import (absolute_import, division, print_function) 18 | __metaclass__ = type 19 | 20 | import os 21 | import yaml 22 | import copy 23 | import re 24 | 25 | from ansible import constants as C 26 | from ansible.module_utils._text import to_text 27 | from ansible.playbook.role.requirement import RoleRequirement 28 | from ansible.plugins.action import ActionBase 29 | from ansible.utils.display import Display 30 | 31 | 32 | display = Display() 33 | 34 | 35 | class ActionModule(ActionBase): 36 | 37 | def run(self, tmp=None, task_vars=None): 38 | result = super(ActionModule, self).run(task_vars=task_vars) 39 | self.META_MAIN = os.path.join('meta', 'main.yml') 40 | self.META_INSTALL = os.path.join('meta', '.galaxy_install_info') 41 | 42 | try: 43 | role_path = self._task.args.get('role_path') 44 | role_root_dir = os.path.split(role_path)[0] 45 | except KeyError as exc: 46 | return {'failed': True, 'msg': 'missing required argument: %s' % exc} 47 | 48 | # Get dependancy version dict if not encoded in meta 49 | depends_dict = self._task.args.get('depends_map') 50 | 51 | try: 52 | self._depends = self._get_role_dependencies(role_path) 53 | # check if we know min_version for each dependant role 54 | # from meta file or through user input to this plugin 55 | (rc, msg) = self._check_depends(self._depends, depends_dict) 56 | if not rc: 57 | result['failed'] = True 58 | result['msg'] = msg 59 | return result 60 | default_roles_path = copy.copy(C.DEFAULT_ROLES_PATH) 61 | default_roles_path.append(role_root_dir) 62 | (rc, msg) = self._find_dependant_role_version( 63 | self._depends, default_roles_path) 64 | 65 | if rc == 'Error': 66 | result['failed'] = True 67 | result['msg'] = msg 68 | elif rc == 'Warning': 69 | result['changed'] = True 70 | result['Warning'] = True 71 | result['msg'] = msg 72 | elif rc == 'Success': 73 | result['changed'] = False 74 | result['msg'] = msg 75 | except Exception as exc: 76 | result['failed'] = True 77 | result['msg'] = ('Exception received : %s' % exc) 78 | 79 | return result 80 | 81 | def _get_role_dependencies(self, role_path): 82 | role_dependencies = [] 83 | dep_info = None 84 | meta_path = os.path.join(role_path, self.META_MAIN) 85 | if os.path.isfile(meta_path): 86 | try: 87 | f = open(meta_path, 'r') 88 | metadata = yaml.safe_load(f) 89 | role_dependencies = metadata.get('dependencies') or [] 90 | except (OSError, IOError): 91 | display.vvv("Unable to load metadata for %s" % role_path) 92 | return False 93 | finally: 94 | f.close() 95 | if role_dependencies: 96 | for dep in role_dependencies: 97 | dep_req = RoleRequirement() 98 | dep_info = dep_req.role_yaml_parse(dep) 99 | 100 | return dep_info 101 | 102 | def _find_dependant_role_version(self, dep_role, search_role_path): 103 | found = False 104 | dep_role_list = [] 105 | if isinstance(dep_role, dict): 106 | # single role dependancy 107 | dep_role_list.append(dep_role) 108 | else: 109 | dep_role_list = dep_role 110 | 111 | # First preferrence is to find role in defined C.default_roles_path 112 | for roles in dep_role_list: 113 | for r_path in search_role_path: 114 | dep_role_path = os.path.join(r_path, roles['name']) 115 | if os.path.exists(dep_role_path): 116 | found = True 117 | install_ver = self._get_role_version(dep_role_path) 118 | if install_ver == 'unknown': 119 | msg = "WARNING! : role: %s installed version is unknown " \ 120 | "please check version if you downloded it from scm" % roles['name'] 121 | return ("Warning", msg) 122 | if install_ver < roles['version']: 123 | msg = "Error! : role: %s installed version :%s is less than " \ 124 | "required version: %s" % (roles['name'], 125 | install_ver, roles['version']) 126 | return ("Error", msg) 127 | if not found: 128 | msg = "role : %s is not installed in role search path: %s" \ 129 | % (roles['name'], search_role_path) 130 | return ("Error", msg) 131 | 132 | return ("Success", 'Success: All dependent roles meet min version requirements') 133 | 134 | def _check_depends(self, depends, depends_dict): 135 | depends_list = [] 136 | if isinstance(depends, dict): 137 | # single role dependancy 138 | depends_list.append(depends) 139 | else: 140 | depends_list = depends 141 | for dep in depends_list: 142 | if dep['version'] and depends_dict is None: 143 | # Nothing to be done. Use version from meta 144 | return (True, '') 145 | if dep['version'] is None and depends_dict is None: 146 | msg = "could not find min version from meta for dependent role : %s" \ 147 | " you can pass this info as depends_map arg e.g." \ 148 | "depends_map: - name: %s \n version: 2.6.5" \ 149 | % (dep['name'], dep['name']) 150 | return (False, msg) 151 | # Galaxy might return empty string when meta does not have version 152 | # specified 153 | if dep['version'] == '' and depends_dict is None: 154 | msg = "could not find min version from meta for dependent role : %s" \ 155 | " you can pass this info as depends_map arg e.g." \ 156 | "depends_map: - name: %s \n version: 2.6.5" \ 157 | % (dep['name'], dep['name']) 158 | return (False, msg) 159 | for in_depends in depends_dict: 160 | if in_depends['name'] == dep['name']: 161 | if in_depends['version'] is None: 162 | msg = 'min_version for role_name: %s is Unknown' % dep['name'] 163 | return (False, msg) 164 | else: 165 | ver = to_text(in_depends['version']) 166 | # if version is defined without 'v<>' add 'v' for 167 | # compliance with galaxy versioning 168 | galaxy_compliant_ver = re.sub(r'^(\d+\..*)', r'v\1', ver) 169 | dep['version'] = galaxy_compliant_ver 170 | return (True, '') 171 | 172 | def _get_role_version(self, role_path): 173 | version = "unknown" 174 | install_info = None 175 | info_path = os.path.join(role_path, self.META_INSTALL) 176 | if os.path.isfile(info_path): 177 | try: 178 | f = open(info_path, 'r') 179 | install_info = yaml.safe_load(f) 180 | except (OSError, IOError): 181 | display.vvv( 182 | "Unable to load galaxy install info for %s" % role_path) 183 | return "unknown" 184 | finally: 185 | f.close() 186 | if install_info: 187 | version = install_info.get("version", None) 188 | return version 189 | -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # This is a cross-platform list tracking distribution packages needed by tests; 2 | # see http://docs.openstack.org/infra/bindep/ for additional information. 3 | 4 | gcc-c++ [test platform:rpm] 5 | python3-devel [test !platform:centos-7 platform:rpm] 6 | python3 [test !platform:centos-7 platform:rpm] 7 | python36 [test !platform:centos-7 !platform:fedora-28] 8 | -------------------------------------------------------------------------------- /changelogs/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # FIXME GUNDALOW: This file will need work once we have defined the Git Tag & branch strategy 3 | # Currently we've just copied ansible/ansible's changelogs/config.yaml 4 | release_tag_re: '(v(?:[\d.ab\-]|rc)+)' 5 | pre_release_tag_re: '(?P(?:[ab]|rc)+\d*)$' 6 | notesdir: fragments 7 | prelude_section_name: release_summary 8 | sections: 9 | - ['major_changes', 'Major Changes'] 10 | - ['minor_changes', 'Minor Changes'] 11 | - ['deprecated_features', 'Deprecated Features'] 12 | - ['removed_features', 'Removed Features (previously deprecated)'] 13 | - ['new_lookup_plugins', 'New Lookup Plugins'] 14 | - ['new_callback_plugins', 'New Callback Plugins'] 15 | - ['new_connection_plugins', 'New Connection Plugins'] 16 | - ['new_test_plugins', 'New Test Plugins'] 17 | - ['new_filter_plugins', 'New Filter Plugins'] 18 | - ['new_modules', 'New Modules'] 19 | - ['new_tasks', 'New Tasks'] 20 | - ['bugfixes', 'Bugfixes'] 21 | - ['known_issues', 'Known Issues'] 22 | - ['docs', 'Documentation Updates'] 23 | -------------------------------------------------------------------------------- /changelogs/fragments/v0-initial-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | major_changes: 3 | - Initial release of the ``network-engine`` Ansible role. 4 | - This role provides the foundation for building network roles by providing modules and plugins that are common to all Ansible Network roles. All of the artifacts in this role can be used independent of the platform that is being managed. 5 | 6 | new_modules: 7 | - NEW ``text_parser`` Parses ASCII text into JSON facts using text_parser engine and YAML-formatted input. 8 | Provides a rules-based text parser that is closely modeled after the Ansible 9 | playbook language. This parser will iterate over the rules and parse the 10 | output of structured ASCII text into a JSON data structure that can be 11 | added to the inventory host facts. 12 | - NEW ``textfsm`` Parses ASCII text into JSON facts using textfsm engine and Google TextFSM-formatted input. 13 | Provides textfsm rules-based templates to parse data from text. The template acting as parser will iterate of the rules and parse the output of structured ASCII text into a JSON data structure that can be added to the inventory host facts. 14 | -------------------------------------------------------------------------------- /changelogs/fragments/v251-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix ``command_parser`` Absolute path with tilde in src should work `network-engine#58 `_ 4 | - Fix content mush only accepts string type `network-engine#72 `_ 5 | - Fix StringIO to work with Python3 in addition to Python2 `network-engine#53 `_ 6 | -------------------------------------------------------------------------------- /changelogs/fragments/v251-docs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | docs: 3 | - User Guide `docs/user_guide `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v251-terminology-changes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_modules: 3 | - New module ``command_parser`` (renamed from ``text_parser``) 4 | - New module ``textfsm_parser`` (renamed from ``textfsm``) 5 | 6 | deprecated_features: 7 | - Module ``text_parser`` renamed to ``command_parser``; original name deprecated; legacy use supported; will be removed in 2.6.0. 8 | - Module ``textfsm`` renamed to ``textfsm_parser``; original name deprecated; legacy use supported; will be removed in 2.6.0. 9 | -------------------------------------------------------------------------------- /changelogs/fragments/v252-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix AnsibleFilterError, deprecations, and unused imports `network-engine#82 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v252-filter-plugins.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_filter_plugins: 3 | - New filter plugin ``interface_range`` 4 | - New filter plugin ``interface_split`` 5 | - New filter plugin ``vlan_compress`` 6 | - New filter plugin ``vlan_expand`` 7 | -------------------------------------------------------------------------------- /changelogs/fragments/v252-lookup-plugins.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_lookup_plugins: 3 | - New lookup plugin ``json_template`` 4 | - New lookup plugin ``network_template`` 5 | - New lookup plugin ``yang2spec`` 6 | - New lookup plugin ``netcfg_diff`` 7 | -------------------------------------------------------------------------------- /changelogs/fragments/v252-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Add new directives extend `network-engine#91 `_. 4 | - Adds conditional support to nested template objects `network-engine#55 `_. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v252-tasks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_tasks: 3 | - New task ``cli`` 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v253-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Templating the regex sent to the parser to allow us to use ansible variables in the regex string `network-engine#97 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v253-removed-features.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | removed_features: 3 | - Move yang2spec lookup to feature branch, till the right location for this plugin is identified `network-engine#100 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v254-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix cli task parser undefined issue when only command is used `network-engine#103 `_. 4 | - Fix an issue with using the extend directive with a loop `network-engine#105 `_. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v254-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Add parsers to search path `network-engine#89 `_. 4 | - Fix export_as templating vars `network-engine#104 `_. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v260-initial-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | major_changes: 3 | - Initial release of 2.6.0 ``network-engine`` Ansible role that is supported with Ansible 2.6.0 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v261-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fixes bug when loading a dir of parsers `network-engine#113 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v261-docs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | docs: 3 | - The argument to end a block of text when searching with match_greedy was missing `network-engine#116 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v262-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix role path test dependency `network-engine#121 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v262-filter-plugins.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_filter_plugins: 3 | - NEW ``to_lines`` filter plugin 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v262-lookup-plugins.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_lookup_plugins: 3 | - NEW ``config_template`` lookup plugin 4 | - NEW ``yang_json2xml`` lookup plugin 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v262-modules.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | new_modules: 3 | - NEW ``validate_role_spec`` handle validating facts required by the role 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v263-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Task to fail if ansible_min_version isn't met `network-engine#130 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v263-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Makes parser directive extend templatable `network-engine#132 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v264-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix repeat_for in json_template `network-engine#139 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v264-docs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | docs: 3 | - Removes unnecessary details from README `network-engine#126 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v264-removed-features.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | removed_features: 3 | - Remove deprecated module ``text_parser``. 4 | - Remove deprecated module ``textfsm``. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v265-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Remove GenericLinux from supported platforms `network-engine#145 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v266-minor-changes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Capability to filter AnsibleModule kwargs `network-engine#149 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v266-removed-features.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | removed_features: 3 | - Remove deprecated module ``cli_get`` 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v270-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix native type conversion in json_template `network-engine#154 `_. 4 | - Fix templating repeat_for `network-engine#190 `_. 5 | - Add missing boilerplate for net_facts module `network-engine#194 `_. 6 | -------------------------------------------------------------------------------- /changelogs/fragments/v270-initial-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | major_changes: 3 | - Initial release of 2.7.0 ``network-engine`` Ansible role that is supported with Ansible 2.7.0 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v271-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Add support for nested spec validation in validate_role_spec `network-engine#198 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v272-minorchanges.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Add name option for textfsm to create facts to key `network-engine#202 `_. 4 | - Document name option for textfsm in cli plugin and update cli task `network-engine#205 `_. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v273-deprecated-features.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | deprecated_features: 3 | - Deprecate lookup plugin network_template `network-engine#215 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v273-minor-changes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - Add verify_depedent_role_version action plugin `network-engine#214 `_. 4 | -------------------------------------------------------------------------------- /changelogs/fragments/v274-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fail validate_role_spec plugin if argument_spec is undefined `network-engine#221 `_. 4 | - Fix relative path failure in command_parser when template is not present in playbook directory `network-engine#222 `_. 5 | -------------------------------------------------------------------------------- /changelogs/fragments/v275-bugfixes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - Fix src_path to src command_parser `network-engine#230 `_. 4 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for network-engine 3 | # 4 | network_engine_path: "{{ role_path }}" 5 | network_engine_lib_path: "{{ network_engine_path }}/lib" 6 | 7 | # defaults used in tasks/cli.yaml 8 | # 9 | # command to run on network device 10 | network_engine_command: "{{ command | default(None) }}" 11 | 12 | # the path to parser file 13 | network_engine_parser: "{{ parser | default(None) }}" 14 | 15 | # engine to use for parsing output to JSON facts 16 | network_engine_engine: "{{ engine | default('command_parser') }}" 17 | 18 | # name to display facts for textfsm_parser 19 | network_engine_name: "{{ name | default(None) }}" 20 | -------------------------------------------------------------------------------- /docs/directives/parser_directives.md: -------------------------------------------------------------------------------- 1 | # CLI Parser Directives 2 | 3 | The `command_parser` module is a module that can be used to parse the results of 4 | text strings into Ansible facts. The primary motivation for developing the 5 | `command_parser` module is to convert structured ASCII text output (such as 6 | the stdout returned from network devices) into JSON data structures suitable to be 7 | used as host facts. 8 | 9 | The parser template file format is loosely based on the Ansible playbook directives 10 | language. It uses the Ansible directive language to ease the transition from 11 | writing playbooks to writing parser templates. However, parser templates developed using this 12 | module are not written directly into the playbook, but are a separate file 13 | called from playbooks. This is done for a variety of reasons but most notably 14 | to keep separation between the parsing logic and playbook execution. 15 | 16 | The `command_parser` works based on a set of directives that perform actions 17 | on structured data with the end result being a valid JSON structure that can be 18 | returned to the Ansible facts system. 19 | 20 | ## Parser language 21 | 22 | The parser template format uses YAML formatting, providing an ordered list of directives 23 | to be performed on the content (provided by the module argument). The overall 24 | general structure of a directive is as follows: 25 | 26 | ```yaml 27 | - name: some description name of the task to be performed 28 | directive: 29 | argument: value 30 | argument_option: value 31 | argument: value 32 | directive_option: value 33 | directive_option: value 34 | ``` 35 | 36 | The `command_parser` currently supports the following top-level directives: 37 | 38 | * `pattern_match` 39 | * `pattern_group` 40 | * `json_template` 41 | * `export_facts` 42 | 43 | In addition to the directives, the following common directive options are 44 | currently supported: 45 | 46 | * `name` 47 | * `block` 48 | * `loop` 49 | * `loop_control` 50 | 51 | * `loop_var` 52 | 53 | * `when` 54 | * `register` 55 | * `export` 56 | * `export_as` 57 | * `extend` 58 | 59 | Any of the directive options are accepted but in some cases, the option may 60 | provide no operation. For instance, when using the `export_facts` 61 | directive, the options `register`, `export` and `export_as` are all 62 | ignored. The module should provide warnings when an option is ignored. 63 | 64 | The following sections provide more details about how to use the parser 65 | directives to parse text into JSON structure. 66 | 67 | ## Directive Options 68 | 69 | This section provides details on the various options that are available to be 70 | configured on any directive. 71 | 72 | ### `name` 73 | 74 | All entries in the parser template many contain a `name` directive. The 75 | `name` directive can be used to provide an arbitrary description as to the 76 | purpose of the parser items. The use of `name` is optional for all 77 | directives. 78 | 79 | The default value for `name` is `null`. 80 | 81 | ### `register` 82 | 83 | Use the `register` option to register the results of a directive operation 84 | temporarily into the variable name you specify 85 | so you can retrieve it later in your parser template. You use `register` in 86 | a parser template just as you would in an Ansible playbook. 87 | 88 | Variables created with `register` alone are not available outside of the parser context. 89 | Any values registered are only available within the scope of the parser activities. 90 | If you want to provide values back to the playbook, you must also define the [export](#export) option. 91 | 92 | Typically you will use `register` alone for parsing each individual part of the 93 | command output, then amalgamate them into a single variable at the end of the parser template, 94 | register that variable and set `export: yes` on it. 95 | 96 | The default value for `register` is `null`. 97 | 98 | 99 | 100 | ### `export` 101 | 102 | Use the `export` option to export any value back to the calling task as an 103 | Ansible fact. The `export` option accepts a boolean value that defines if 104 | the registered fact should be exported to the calling task in the playbook (or 105 | role) scope. To export the value, simply set `export` to True. 106 | 107 | Note this option requires the `register` value to be set in some cases and will 108 | produce a warning message if the `register` option is not provided. 109 | 110 | The default value for `export` is `False`. 111 | 112 | ### `export_as` 113 | 114 | Use the `export_as` option to export a value back to the calling task as an 115 | Ansible fact in a specific format. The `export_as` option defines the structure of the exported data. 116 | Accepted values for `export_as`: 117 | 118 | * `dict` 119 | * `hash` 120 | * `object` 121 | * `list` 122 | * `elements` that defines the structure 123 | 124 | **Note** this option requires the `register` value to be set and `export: True`. 125 | Variables can also be used with `export_as`. 126 | How to use variable with `export_as` is as follows: 127 | 128 | Variable should be defined in vars or defaults or in playbook. 129 | ```yaml 130 | vars: 131 | export_type: "list" 132 | ``` 133 | 134 | Parser file needs to have the variable set to `export_as`. 135 | ``` 136 | export_as: "{{ export_type }}" 137 | ``` 138 | 139 | ### `extend` 140 | 141 | Use the `extend` option to extend a current fact hierarchy with the new 142 | registered fact. This will case the facts to be merged and returned as a 143 | single tree. If the fact doesn't previously exist, this will create the entire 144 | structure. 145 | 146 | The default value for `extend` is `null`. 147 | 148 | ### loop 149 | 150 | Use the `loop` option to loop over a directive in order to process values. 151 | With the `loop` option, the parser will iterate over the directive and 152 | provide each of the values provided by the loop content to the directive for 153 | processing. 154 | 155 | Access to the individual items is the same as it would be for Ansible 156 | playbooks. When iterating over a list of items, you can access the individual 157 | item using the `{{ item }}` variable. When looping over a hash, you can 158 | access `{{ item.key }}` and `{{ item.value }}`. 159 | 160 | ### `loop_control` 161 | 162 | Use the `loop_control` option to specify the name of the variable to be 163 | used for the loop instead of default loop variable `item`. 164 | When looping over a hash, you can access `{{ foo.key }}` and `{{ foo.value }}` where `foo` 165 | is `loop_var`. 166 | The general structure of `loop_control` is as follows: 167 | 168 | ```yaml 169 | - name: User defined variable 170 | pattern_match: 171 | regex: "^(\\S+)" 172 | content: "{{ foo }}" 173 | loop: "{{ context }}" 174 | loop_control: 175 | loop_var: foo 176 | 177 | ``` 178 | 179 | ### `when` 180 | 181 | Use the `when` option to place a condition on the directive to 182 | decided if it is executed or not. The `when` option operates the same as 183 | it would in an Ansible playbook. 184 | 185 | For example, if you only want to perform the match statement 186 | when the value of `ansible_network_os` is set to `ios`, you can apply 187 | the `when` conditional like this: 188 | 189 | ```yaml 190 | - name: conditionally matched var 191 | pattern_match: 192 | regex: "hostname (.+)" 193 | when: ansible_network_os == 'ios' 194 | ``` 195 | 196 | ## Directives 197 | 198 | The directives perform actions on the content using regular expressions to 199 | extract various values. Each directive provides some additional arguments that 200 | can be used to perform its operation. 201 | 202 | ### `pattern_match` 203 | 204 | Use the `pattern_match` directive to extract one or more values from 205 | the structured ASCII text based on regular expressions. 206 | 207 | The following arguments are supported for this directive: 208 | 209 | * `regex` 210 | * `content` 211 | * `match_all` 212 | * `match_greedy` 213 | * `match_until` : Sets a ending boundary for `match_greedy`. 214 | 215 | The `regex` argument templates the value given to it so variables and filters can be used. 216 | Example : 217 | ```yaml 218 | - name: Use a variable and a filter 219 | pattern_match: 220 | regex: "{{ inventory_hostname | lower }} (.+)" 221 | ``` 222 | 223 | ### `pattern_group` 224 | 225 | Use the `pattern_group` directive to group multiple 226 | `pattern_match` results together. 227 | 228 | The following arguments are supported for this directive: 229 | 230 | * `json_template` 231 | * `set_vars` 232 | * `export_facts` 233 | 234 | ### `json_template` 235 | 236 | Use the `json_template` directive to create a JSON data structure based on a 237 | template. This directive will allow you to template out a multi-level JSON 238 | blob. 239 | 240 | The following arguments are supported for this directive: 241 | 242 | * `template` 243 | 244 | 245 | **Note** 246 | Native jinja2 datatype (eg. 'int', 'float' etc.) rendering is supported with Ansible version >= 2.7 247 | and jinja2 library version >= 2.10. To enable native jinja2 config add below configuration in active 248 | ansible configuration file. 249 | ``` 250 | [defaults] 251 | jinja2_native= True 252 | ``` 253 | 254 | Usage example: 255 | ```yaml 256 | - set_fact: 257 | count: "1" 258 | 259 | - name: print count 260 | debug: 261 | msg: "{{ count|int }}" 262 | ``` 263 | 264 | With jinja2_native configuration enabled the output of above example task will have 265 | ``` 266 | "msg": 1 267 | ``` 268 | 269 | and with jinja2_native configuration disabled (default) output of above example task will have 270 | ``` 271 | "msg": "1" 272 | ``` 273 | 274 | ### `set_vars` 275 | 276 | Use the `set_vars` directive to set variables to the values like key / value pairs 277 | and return a dictionary. 278 | 279 | ### `export_facts` 280 | 281 | Use the `export_facts` directive to take an arbitrary set of key / value pairs 282 | and expose (return) them back to the playbook global namespace. Any key / 283 | value pairs that are provided in this directive become available on the host. 284 | -------------------------------------------------------------------------------- /docs/directives/template_directives.md: -------------------------------------------------------------------------------- 1 | # CLI Template Directives 2 | 3 | **Note** `network_template` lookup plugin is deprecated in v2.7.3 and will be removed 4 | in version v2.7.7 i.e, four releases from the deprecation version. 5 | 6 | The `network_template` module supports a number of keyword based objectives that 7 | handle how to process the template. Templates are broken up into a series 8 | of blocks that process lines. Blocks are logical groups that have a common 9 | set of properties in common. 10 | 11 | Blocks can also include other template files and are processed in the same 12 | manner as lines. See includes below for a description on how to use the 13 | include directive. 14 | 15 | The template module works by processing the lines directives in sequential 16 | order. The module will attempt to template each line in the lines directive 17 | and, if successful, add the line to the final output. Values used for 18 | variable substitution come from the host facts. If the line could not 19 | be successfully templated, the line is skipped and a warning message is 20 | displayed that the line could not be templated. 21 | 22 | There are additional directives that can be combined to support looping over 23 | lists and hashes as well as applying conditional statements to blocks, lines 24 | and includes. 25 | 26 | ## `name` 27 | 28 | Entries in the template may contain a `name` field. The `name` field 29 | is used to provide a description of the entry. It is also used to provide 30 | feedback when processing the template to indicate when an entry is 31 | skipped or fails. 32 | 33 | ## `lines` 34 | 35 | The `lines` directive provides an ordered list of statements to attempt 36 | to template. Each entry in the `lines` directive will be evaluated for 37 | variable substitution. If the entry can be successfully templated, then the 38 | output will be added to the final set of entries. If the entry cannot be 39 | successfully templated, then the entry is ignored (skipped) and a warning 40 | message is provided. If the entry in the `lines` directive contains 41 | only static text (no variables), then the line will always be processed. 42 | 43 | The `lines` directive also supports standard Jinja2 filters as well as any 44 | Ansible specific Jinja2 filters. For example, lets assume we want to add a 45 | default value if a more specific value was not assigned by a fact. 46 | 47 | ```yaml 48 | - name: render the system hostname 49 | lines: 50 | - "hostname {{ hostname | default(inventory_hostname_short }}" 51 | ``` 52 | 53 | ## `block` 54 | 55 | A group of `lines` directives can be combined into a `block` 56 | directive. These `block` directives are used to apply a common set of 57 | values to one or more `lines` or `includes` entries. 58 | 59 | For instance, a `block` directive that contains one or more `lines` 60 | entries could be use the same set of `loop` values or have a 61 | common `when` conditional statement applied to them. 62 | 63 | ## `include` 64 | 65 | Sometimes it is advantageous to break up templates into separate files and 66 | combine them. The `include` directive will instruct the current template 67 | to load another template file and process it. 68 | 69 | The `include` directive also supports variable substitution for the 70 | provided file name and can be processed with the `loop` and `when` 71 | directives. 72 | 73 | ## `when` 74 | 75 | The `when` directive allows for conditional statements to be applied to 76 | a set of `lines`, a `block` and/or the `include` directive. The 77 | `when` statement is evaluated prior to processing the statements and if 78 | the condition is true, the statements will attempt to be templated. If the 79 | statement is false, the statements are skipped and a message returned. 80 | 81 | ## `loop` 82 | 83 | Depending on the input facts, sometimes it is necessary to iterate over a 84 | set of statements. The `loop` directive allows the same set of statements 85 | to be processed in such a manner. The `loop` directive takes, as input, 86 | the name of a fact that is either a list or a hash and iterates over the 87 | statements for each entry. 88 | 89 | When the provided fact is a list of items, the value will be assigned to a 90 | variable called `item` and can be referenced by the statements. 91 | 92 | When the provided fact is a hash of items, the hash key will be assigned to 93 | the `item.key` variable and the hash value will be assigned to the 94 | `item.value` variable. Both can then be referenced by the statements. 95 | 96 | ## `loop_control` 97 | 98 | The `loop_control` directive allows the template to configure aspects 99 | related to how loops are process. This directive provides a set of suboptions 100 | to configure how loops are processed. 101 | 102 | ### `loop_var` 103 | 104 | The `loop_var` directive allows the template to override the default 105 | variable name `item`. This is useful when handling nested loops such 106 | that both inner and outer loops values can be accessed. 107 | 108 | When setting the `loop_var` to some string, the string will replace 109 | `item` as the variable name used to access the values. 110 | 111 | For example, lets assume instead of using item, we want to use a different 112 | variable name such as entry: 113 | 114 | ```yaml 115 | - name: render entries 116 | lines: 117 | - "hostname {{ entry.hostname }}" 118 | - "domain-name {{ entry.domain_name }}" 119 | loop: "{{ system }}" 120 | loop_control: 121 | loop_var: entry 122 | ``` 123 | 124 | ## `join` 125 | 126 | When building template statements that include optional values for 127 | configuration, the `join` directive can be useful. The `join` 128 | directive instructs the template to combine the templated lines together 129 | into a single string to insert into the configuration. 130 | 131 | For example, lets assume there is a need to add the following statement to 132 | the configuration: 133 | 134 | ``` 135 | ip domain-name ansible.com vrf management 136 | ip domain-name redhat.com 137 | ``` 138 | 139 | To support templating the above lines, the facts might include the domain-name 140 | and the vrf name values. Here is the example facts: 141 | 142 | ```yaml 143 | --- 144 | system: 145 | - domain_name: ansible.com 146 | vrf: management 147 | - domain_name redhat.com 148 | ``` 149 | 150 | And the template statement would be the following: 151 | 152 | ```yaml 153 | - name: render domain-name 154 | lines: 155 | - "ip domain-name {{ item.domain_name }}" 156 | - "vrf {{ item.vrf }}" 157 | loop: "{{ system }}" 158 | join: yes 159 | ``` 160 | 161 | When this entry is processed, the first iteration will successfully template 162 | both lines and add `ip domain-name ansible.com vrf management` to the 163 | output. 164 | 165 | When the second entry is processed, the first line will be successfully 166 | templated but since there is no management key, the second line will return a 167 | null value. The final line added to the configuration will be ` ip 168 | domain-name redhat.com`. 169 | 170 | If the `join` directive had been omitted, then the final set of 171 | configuration statements would be as follows: 172 | 173 | ``` 174 | ip domain-name ansible.com 175 | vrf management 176 | ip domain-name redhat.com 177 | ``` 178 | 179 | ## `join_delimiter` 180 | 181 | When the `join` delimiter is used, the templated values are combined into a 182 | single string that is added to the final output. The lines are joined using a 183 | space. The delimiting character used when processing the `join` can be 184 | modified using `join_delimiter` directive. 185 | 186 | Here is an example of using the this directive. Take the following entry: 187 | 188 | ```yaml 189 | - name: render domain-name 190 | lines: 191 | - "ip domain-name {{ item.domain_name }}" 192 | - "vrf {{ item.vrf }}" 193 | loop: "{{ system }}" 194 | join: yes 195 | join_delimiter: , 196 | ``` 197 | 198 | When the preceding statements are processed, the final output would be 199 | (assuming all variables are provided): 200 | 201 | ``` 202 | ip domain-name ansible.com,vrf management 203 | ip domain-name redhat.com 204 | ``` 205 | 206 | ## `indent` 207 | 208 | The `indent` directive is used to add one or more leading spaces to the 209 | final templated statement. It can be used to recreated a structured 210 | configuration file. 211 | 212 | Take the following template entry as an example: 213 | 214 | ```yaml 215 | - name: render the interface context 216 | lines: "interface Ethernet0/1" 217 | 218 | - name: render the interface configuration 219 | lines: 220 | - "ip address 192.168.10.1/24" 221 | - "no shutdown" 222 | - "description this is an example" 223 | indent: 3 224 | 225 | - name: render the interface context 226 | lines: "!" 227 | ``` 228 | 229 | Then the statements above are processed, the output will look like the 230 | following: 231 | 232 | ``` 233 | interface Ethernet0/1 234 | ip address 192.168.10.1/24 235 | no shutdown 236 | description this is an example 237 | ! 238 | ``` 239 | 240 | ## `required` 241 | 242 | The `required` directive specifies that all of the statements must be 243 | templated otherwise a failure is generated. Essentially it is a way to 244 | make certain that the variables are defined. 245 | 246 | For example, take the following: 247 | 248 | ```yaml 249 | - name: render router ospf context 250 | lines: 251 | - "router ospf {{ process_id }}" 252 | required: yes 253 | ``` 254 | 255 | When the above is processed, if the variable `process_id` is not present, 256 | then the statement cannot be templated. Since the `required` directive 257 | is set to true, the statement will cause the template to generate a failure 258 | message. 259 | 260 | ## `missing_key` 261 | 262 | By default, when statements are processed and a variable is undefined, the 263 | module will simply display a warning message to the screen. In some cases, it 264 | is desired to either suppress warning messages on a missing key or to force the 265 | module to fail on a missing key. 266 | 267 | To change the default behaviour, use the `missing_key` directive. This 268 | directive accepts one of three choices: 269 | 270 | * `ignore` 271 | * `warn` (default) 272 | * `fail` 273 | 274 | The value of this directive will instruct the template how to handle any 275 | condition where the desired variable is undefined. 276 | 277 | -------------------------------------------------------------------------------- /docs/plugins/filter_plugins.md: -------------------------------------------------------------------------------- 1 | # network_engine filter plugins 2 | 3 | The [filter_plugins/network_engine code](https://github.com/ansible-network/network-engine/blob/devel/library/filter_plugins/network_engine.py) 4 | offers four options for managing multiple interfaces and vlans. 5 | 6 | ## interface_split 7 | 8 | The `interface_split` plugin splits an interface and returns its parts: 9 | 10 | {{ 'Ethernet1' | interface_split }} returns '1' as index and 'Ethernet' as name 11 | 12 | {{ 'Ethernet1' | interface_split('name') }} returns 'Ethernet' 13 | 14 | {{ 'Ethernet1' | interface_split('index') }} returns '1' 15 | 16 | [interface_split tests](https://github.com/ansible-network/network-engine/blob/devel/tests/interface_split/interface_split/tasks/interface_split.yaml) 17 | 18 | ## interface_range 19 | 20 | The `interface_range` plugin expands an interface range and returns a list of the interfaces within that range: 21 | 22 | {{ 'Ethernet1-3' | interface_range }} returns ['Ethernet1', 'Ethernet2', 'Ethernet3'] 23 | 24 | {{ 'Ethernet1,3-4,5' | interface_range }} returns ['Ethernet1', 'Ethernet3', 'Ethernet4', 'Ethernet5'] 25 | 26 | {{ 'Ethernet1/3-5,8' | interface_range }} returns ['Ethernet1/3', 'Ethernet1/4', 'Ethernet1/5', 'Ethernet1/8'] 27 | 28 | [interface_range tests](https://github.com/ansible-network/network-engine/blob/devel/tests/interface_range/interface_range/tasks/interface_range.yaml) 29 | 30 | ## vlan_compress 31 | 32 | The `vlan_compress` plugin compresses a list of vlans into a range: 33 | 34 | {{ 'vlan1,2,3,4,5' | vlan_compress }} returns ['1-5'] 35 | 36 | {{ 'vlan1,2,4,5' | vlan_compress }} returns ['1-2,4-5'] 37 | 38 | {{ 'vlan1,2,3,5' | vlan_compress }} returns ['1-3,5'] 39 | 40 | [vlan_compress tests](https://github.com/ansible-network/network-engine/blob/devel/tests/vlan_compress/vlan_compress/tasks/vlan_compress.yaml) 41 | 42 | ## vlan_expand 43 | 44 | The `vlan_expand` plugin expands a vlan range and returns a list of the vlans within that range: 45 | 46 | {{ 'vlan1,3-5,7' | vlan_expand }} returns [1,3,4,5,7] 47 | 48 | {{ 'vlan1-5' | vlan_expand }} returns [1,2,3,4,5] 49 | 50 | [vlan_expand tests](https://github.com/ansible-network/network-engine/blob/devel/tests/vlan_expand/vlan_expand/tasks/vlan_expand.yaml) 51 | -------------------------------------------------------------------------------- /docs/plugins/verify_dependent_role_version.md: -------------------------------------------------------------------------------- 1 | # Plugin verify_dependent_role_version 2 | 3 | 4 | The `verify_dependent_role_version` plugin checks for required minimum version of dependant roles. 5 | The plugin works only inside a role. It verifies the required minimum version of all roles are 6 | installed as defined under dependancies in meta/main.yml of the role. 7 | 8 | ## How to Use 9 | 10 | meta/main.yml 11 | 12 | ```yaml 13 | dependencies: 14 | - src: ansible-network.network-engine 15 | version: v2.7.2 16 | ``` 17 | 18 | tasks/main.yml 19 | 20 | ```yaml 21 | - name: Validate we have required minimum version of dependent roles installed 22 | verify_dependent_role_version: 23 | role_path: "{{ role_path }}" 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/tasks/cli.md: -------------------------------------------------------------------------------- 1 | # Task cli 2 | The ```cli``` task provides an implementation for running CLI commands on 3 | network devices that is platform agnostic. The ```cli``` task accepts a 4 | command and will attempt to execute that command on the remote device returning 5 | the command ouput. 6 | 7 | If the ```parser``` argument is provided, the output from the command will be 8 | passed through the parser and returned as JSON facts using the ```engine``` 9 | argument. 10 | 11 | 12 | ## Requirements 13 | The following is the list of requirements for using the this task: 14 | 15 | * Ansible 2.5 or later 16 | * Connection ```network_cli``` 17 | * ansible_network_os 18 | 19 | ## Arguments 20 | The following are the list of required and optional arguments supported by this 21 | task. 22 | 23 | ### command 24 | This argument specifies the command to be executed on the remote device. The 25 | ```command``` argument is a required value. 26 | 27 | ### parser 28 | This argument specifies the location of the parser to pass the output from the command to 29 | in order to generate JSON data. The ```parser``` argument is an optional value, but required 30 | when ```engine``` is used. 31 | 32 | ### engine 33 | The ```engine``` argument is used to define which parsing engine to use when parsing the output 34 | of the CLI commands. This argument uses the file specified to ```parser``` for parsing output to 35 | JSON facts. This argument requires ```parser``` argument to be specified. 36 | 37 | This action currently supports two different parsers: 38 | 39 | * ```command_parser``` 40 | * ```textfsm_parser``` 41 | 42 | The default value is ```command_parser```. 43 | 44 | ## How to use 45 | This section describes how to use the ```cli``` task in a playbook. 46 | 47 | 48 | The following example runs CLI command on the network node. 49 | ```yaml 50 | 51 | --- 52 | - hosts: ios01 53 | connection: network_cli 54 | 55 | tasks: 56 | - name: run cli command with cli task 57 | import_role: 58 | name: ansible-network.network-engine 59 | tasks_from: cli 60 | vars: 61 | ansible_network_os: ios 62 | command: show version 63 | 64 | ``` 65 | 66 | When run with verbose mode, the output returned is as follows: 67 | 68 | ``` 69 | 70 | ok: [ios01] => { 71 | "changed": false, 72 | "json": null, 73 | "stdout": "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2016 by Cisco Systems, Inc.\nCompiled Tue 22-Mar-16 16:19 by prod_rel_team\n\n\nROM: Bootstrap program is IOSv\n\nan-ios-01 uptime is 19 weeks, 5 days, 19 hours, 14 minutes\nSystem returned to ROM by reload\nSystem image file is \"flash0:/vios-adventerprisek9-m\"\nLast reload reason: Unknown reason\n\n\n\nThis product contains cryptographic features and is subject to United\nStates and local country laws governing import, export, transfer and\nuse. Delivery of Cisco cryptographic products does not imply\nthird-party authority to import, export, distribute or use encryption.\nImporters, exporters, distributors and users are responsible for\ncompliance with U.S. and local country laws. By using this product you\nagree to comply with applicable laws and regulations. If you are unable\nto comply with U.S. and local laws, return this product immediately.\n\nA summary of U.S. laws governing Cisco cryptographic products may be found at:\nhttp://www.cisco.com/wwl/export/crypto/tool/stqrg.html\n\nIf you require further assistance please contact us by sending email to\nexport@cisco.com.\n\nCisco IOSv (revision 1.0) with with 460033K/62464K bytes of memory.\nProcessor board ID 92O0KON393UV5P77JRKZ5\n4 Gigabit Ethernet interfaces\nDRAM configuration is 72 bits wide with parity disabled.\n256K bytes of non-volatile configuration memory.\n2097152K bytes of ATA System CompactFlash 0 (Read/Write)\n0K bytes of ATA CompactFlash 1 (Read/Write)\n0K bytes of ATA CompactFlash 2 (Read/Write)\n10080K bytes of ATA CompactFlash 3 (Read/Write)\n\n\n\nConfiguration register is 0x0" 74 | } 75 | 76 | ``` 77 | 78 | The following example runs cli command and parse output to JSON facts. 79 | ```yaml 80 | 81 | --- 82 | - hosts: ios01 83 | connection: network_cli 84 | 85 | tasks: 86 | - name: run cli command and parse output to JSON facts 87 | import_role: 88 | name: ansible-network.network-engine 89 | tasks_from: cli 90 | vars: 91 | ansible_network_os: ios 92 | command: show version 93 | parser: parser_templates/ios/show_version.yaml 94 | engine: command_parser 95 | 96 | ``` 97 | 98 | When run with verbose mode, the output returned is as follows: 99 | 100 | ``` 101 | 102 | ok: [ios01] => { 103 | "ansible_facts": { 104 | "system_facts": { 105 | "image_file": "\"flash0:/vios-adventerprisek9-m\"", 106 | "memory": { 107 | "free": "62464K", 108 | "total": "460033K" 109 | }, 110 | "model": "IOSv", 111 | "uptime": "19 weeks, 5 days, 19 hours, 34 minutes", 112 | "version": "15.6(2)T" 113 | } 114 | }, 115 | "changed": false, 116 | "included": [ 117 | "parser_templates/ios/show_version.yaml" 118 | ], 119 | "json": null, 120 | "stdout": "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2016 by Cisco Systems, Inc.\nCompiled Tue 22-Mar-16 16:19 by prod_rel_team\n\n\nROM: Bootstrap program is IOSv\n\nan-ios-01 uptime is 19 weeks, 5 days, 19 hours, 34 minutes\nSystem returned to ROM by reload\nSystem image file is \"flash0:/vios-adventerprisek9-m\"\nLast reload reason: Unknown reason\n\n\n\nThis product contains cryptographic features and is subject to United\nStates and local country laws governing import, export, transfer and\nuse. Delivery of Cisco cryptographic products does not imply\nthird-party authority to import, export, distribute or use encryption.\nImporters, exporters, distributors and users are responsible for\ncompliance with U.S. and local country laws. By using this product you\nagree to comply with applicable laws and regulations. If you are unable\nto comply with U.S. and local laws, return this product immediately.\n\nA summary of U.S. laws governing Cisco cryptographic products may be found at:\nhttp://www.cisco.com/wwl/export/crypto/tool/stqrg.html\n\nIf you require further assistance please contact us by sending email to\nexport@cisco.com.\n\nCisco IOSv (revision 1.0) with with 460033K/62464K bytes of memory.\nProcessor board ID 92O0KON393UV5P77JRKZ5\n4 Gigabit Ethernet interfaces\nDRAM configuration is 72 bits wide with parity disabled.\n256K bytes of non-volatile configuration memory.\n2097152K bytes of ATA System CompactFlash 0 (Read/Write)\n0K bytes of ATA CompactFlash 1 (Read/Write)\n0K bytes of ATA CompactFlash 2 (Read/Write)\n10080K bytes of ATA CompactFlash 3 (Read/Write)\n\n\n\nConfiguration register is 0x0" 121 | } 122 | 123 | ``` 124 | 125 | To know how to write a parser for ```command_parser``` or ```textfsm_parser``` engine, please follow the user guide [here](https://github.com/ansible-network/network-engine/blob/devel/docs/user_guide/README.md). 126 | -------------------------------------------------------------------------------- /docs/tests/test_guide.md: -------------------------------------------------------------------------------- 1 | # Test Guide 2 | 3 | The tests in network-engine are role based where the entry point is `tests/test.yml`. 4 | The tests for `textfsm_parser` and `command_parser` are run against `localhost`. 5 | 6 | ## How to run tests locally 7 | 8 | ``` 9 | cd tests/ 10 | ansible-playbook -i inventory test.yml 11 | ``` 12 | 13 | ## Role Structure 14 | 15 | ``` 16 | role_name 17 | ├── defaults 18 | │   └── main.yaml 19 | ├── meta 20 | │   └── main.yaml 21 | ├── output 22 | │   └── platform_name 23 | │   ├── show_interfaces.txt 24 | │   └── show_version.txt 25 | ├── parser_templates 26 | │   └── platform_name 27 | │   ├── show_interfaces.yaml 28 | │   └── show_version.yaml 29 | └── tasks 30 | ├── platform_name.yaml 31 | └── main.yaml 32 | ``` 33 | 34 | If you add any new Role for test, make sure to include the role in `test.yml`: 35 | 36 | ```yaml 37 | 38 | roles: 39 | - command_parser 40 | - textfsm_parser 41 | - $role_name 42 | ``` 43 | 44 | ## Add new platforms tests to an existing roles 45 | 46 | Create directory with the `platform_name` in `output` and `parser_templates` directories 47 | which will contain output and parser files of the platform. 48 | 49 | Add corresponding playbook with the `platform_name` in `tasks/$platform_name.yaml` 50 | and add an entry in `tasks/main.yaml`: 51 | 52 | ```yaml 53 | - name: platform_name command_parser test 54 | import_tasks: platform_name.yaml 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/user_guide/README.md: -------------------------------------------------------------------------------- 1 | Using the Network Engine Role 2 | ---------------------------------- 3 | 4 | The Network Engine role is supported as a dependency of other Roles. The Network Engine Role extracts data about your network devices as Ansible facts in a JSON data structure, ready to be added to your inventory host facts and/or consumed by Ansible tasks and templates. You define the data elements you want to extract from each network OS command in parser templates, using either YAML or Google TextFSM syntax. The matching rules may be different on each network platform, but by defining the same variable names for the output on all platforms, you can normalize similar data across platforms. That's how the Network Engine Role supports truly platform-agnostic network automation. 5 | 6 | The Network Engine role can also be used directly, though direct usage is not supported with your Red Hat subscription. 7 | 8 | The initial release of the Network Engine role includes two parser modules: 9 | * [command_parser](https://github.com/ansible-network/network-engine/blob/devel/docs/user_guide/command_parser.md) accepts YAML input, uses an internally maintained, loosely defined parsing language based on Ansible playbook directives 10 | * [textfsm_parser](https://github.com/ansible-network/network-engine/blob/devel/docs/user_guide/textfsm_parser.md) accepts Google TextFSM input, uses Google TextFSM parsing language 11 | 12 | Both modules iterate over the data definitions in your parser templates, parse command output from your network devices (structured ASCII text) to find matches, and then convert the matches into Ansible facts in a JSON data structure. 13 | 14 | The task ```cli``` provided by the role, can also be directly implemented in your playbook. The documentation can be found here [tasks/cli](https://github.com/ansible-network/network-engine/blob/devel/docs/tasks/cli.md). 15 | 16 | To manage multiple interfaces and vlans, the Network Engine role also offers [filter_plugins](https://github.com/ansible-network/network-engine/blob/devel/docs/plugins/filter_plugins.md) that turn lists of Interfaces or VLANs into ranges and vice versa. 17 | 18 | Modules: 19 | -------- 20 | - `command_parser` 21 | - `textfsm_parser` 22 | - `net_facts` 23 | 24 | To use the Network Engine Role: 25 | ---------------------------------------- 26 | 1. Install the role from Ansible Galaxy 27 | `ansible-galaxy install ansible-network.network-engine` will copy the Network Engine role to `~/.ansible/roles/`. 28 | 1. Select the parser engine you prefer 29 | For YAML formatting, use `command_parser`; for TextFSM formatting, use `textfsm_parser`. The parser docs include 30 | examples of how to define your data and create your tasks. 31 | 1. Define the data you want to extract (or use a pre-existing parser template) 32 | See the parser_template sections of the command_parser and textfsm_parser docs for examples. 33 | 1. Create a playbook to extract the data you've defined 34 | See the Playbook sections of the command_parser and textfsm_parser docs for examples. 35 | 1. Run the playbook with `ansible-playbook -i /path/to/your/inventory -u my_user -k /path/to/your/playbook` 36 | 1. Consume the JSON-formatted Ansible facts about your device(s) in inventory, templates, and tasks. 37 | 38 | Additional Resources 39 | ------------------------------------- 40 | 41 | * [README](https://galaxy.ansible.com/ansible-network/network-engine/#readme) 42 | * [command_parser tests](https://github.com/ansible-network/network-engine/tree/devel/tests/command_parser) 43 | * [textfsm_parser tests](https://github.com/ansible-network/network-engine/tree/devel/tests/textfsm_parser) 44 | * [Full changelog diff](https://github.com/ansible-network/network-engine/blob/devel/CHANGELOG.rst) 45 | 46 | Contributing and Reporting Feedback 47 | ------------------------------------- 48 | [Review issues](https://github.com/ansible-network/network-engine/issues) 49 | -------------------------------------------------------------------------------- /docs/user_guide/command_parser.md: -------------------------------------------------------------------------------- 1 | # command_parser 2 | 3 | The [command_parser](https://github.com/ansible-network/network-engine/blob/devel/library/command_parser.py) 4 | module is closely modeled after the Ansible playbook language. 5 | This module iterates over matching rules defined in YAML format, extracts data from structured ASCII text based on those rules, 6 | and returns Ansible facts in a JSON data structure that can be added to the inventory host facts and/or consumed by Ansible tasks and templates. 7 | 8 | The `command_parser` module requires two inputs: 9 | - the output of commands run on the network device, passed to the `content` parameter 10 | - the parser template that defines the rules for parsing the output, passed to either the `file` or the `dir` parameter 11 | 12 | ## Parameters 13 | 14 | ### content 15 | 16 | The `content` parameter for `command_parser` must point to the ASCII text output of commands run on network devices. The text output can be in a variable or in a file. 17 | 18 | 19 | ### file 20 | 21 | The `file` parameter for `command_parser` must point to a parser template that contains a rule for each data field you want to extract from your network devices. 22 | 23 | Parser templates for the `command_parser` module in the Network Engine role use YAML notation. 24 | 25 | 26 | ### dir 27 | 28 | Points to a directory containing parser templates. Use this parameter instead of `file` if your playbook uses multiple parser templates. 29 | 30 | ## Sample Parser Templates 31 | 32 | Parser templates for the `command_parser` module in the Network Engine role use YAML syntax. 33 | To write a parser template, follow the [parser_directives documentation](docs/directives/parser_directives.md). 34 | 35 | Here are two sample YAML parser templates: 36 | 37 | `parser_templates/ios/show_interfaces.yaml` 38 | ```yaml 39 | 40 | --- 41 | - name: parser meta data 42 | parser_metadata: 43 | version: 1.0 44 | command: show interface 45 | network_os: ios 46 | 47 | - name: match sections 48 | pattern_match: 49 | regex: "^(\\S+) is up," 50 | match_all: yes 51 | match_greedy: yes 52 | register: section 53 | 54 | - name: match interface values 55 | pattern_group: 56 | - name: match name 57 | pattern_match: 58 | regex: "^(\\S+)" 59 | content: "{{ item }}" 60 | register: name 61 | 62 | - name: match hardware 63 | pattern_match: 64 | regex: "Hardware is (\\S+)," 65 | content: "{{ item }}" 66 | register: type 67 | 68 | - name: match mtu 69 | pattern_match: 70 | regex: "MTU (\\d+)" 71 | content: "{{ item }}" 72 | register: mtu 73 | 74 | - name: match description 75 | pattern_match: 76 | regex: "Description: (.*)" 77 | content: "{{ item }}" 78 | register: description 79 | loop: "{{ section }}" 80 | register: interfaces 81 | 82 | - name: generate json data structure 83 | json_template: 84 | template: 85 | - key: "{{ item.name.matches.0 }}" 86 | object: 87 | - key: config 88 | object: 89 | - key: name 90 | value: "{{ item.name.matches.0 }}" 91 | - key: type 92 | value: "{{ item.type.matches.0 }}" 93 | - key: mtu 94 | value: "{{ item.mtu.matches.0 }}" 95 | - key: description 96 | value: "{{ item.description.matches.0 }}" 97 | loop: "{{ interfaces }}" 98 | export: yes 99 | register: interface_facts 100 | 101 | ``` 102 | 103 | `parser_templates/ios/show_version.yaml` 104 | 105 | ```yaml 106 | 107 | --- 108 | - name: parser meta data 109 | parser_metadata: 110 | version: 1.0 111 | command: show version 112 | network_os: ios 113 | 114 | - name: match version 115 | pattern_match: 116 | regex: "Version (\\S+)," 117 | register: version 118 | 119 | - name: match model 120 | pattern_match: 121 | regex: "^Cisco (.+) \\(revision" 122 | register: model 123 | 124 | - name: match image 125 | pattern_match: 126 | regex: "^System image file is (\\S+)" 127 | register: image 128 | 129 | - name: match uptime 130 | pattern_match: 131 | regex: "uptime is (.+)" 132 | register: uptime 133 | 134 | - name: match total memory 135 | pattern_match: 136 | regex: "with (\\S+)/(\\w*) bytes of memory" 137 | register: total_mem 138 | 139 | - name: match free memory 140 | pattern_match: 141 | regex: "with \\w*/(\\S+) bytes of memory" 142 | register: free_mem 143 | 144 | - name: export system facts to playbook 145 | set_vars: 146 | model: "{{ model.matches.0 }}" 147 | image_file: "{{ image.matches.0 }}" 148 | uptime: "{{ uptime.matches.0 }}" 149 | version: "{{ version.matches.0 }}" 150 | memory: 151 | total: "{{ total_mem.matches.0 }}" 152 | free: "{{ free_mem.matches.0 }}" 153 | export: yes 154 | register: system_facts 155 | 156 | ``` 157 | 158 | ## Sample Playbooks 159 | 160 | To extract the data defined in your parser template, create a playbook that includes the Network Engine role and references the `content` and `file` (or `dir`) parameters of the `command_parser` module. 161 | 162 | Each example playbook below runs a show command, imports the Network Engine role, extracts data from the text output of the command by matching it against the rules defined 163 | in your parser template, and stores the results in a variable. To view the content of that final variable, make sure `export: yes` is set in your parser template, and run your playbook in `verbose` mode: `ansible-playbook -vvv`. 164 | 165 | Make sure the `hosts` definition in the playbook matches a host group in your inventory - in these examples, the playbook expects a group called `ios`. 166 | 167 | The first example parses the output of the `show interfaces` command on IOS and creates facts from that output: 168 | 169 | ```yaml 170 | 171 | --- 172 | 173 | # ~/my-playbooks/gather-interface-info.yml 174 | 175 | - hosts: ios 176 | connection: network_cli 177 | 178 | tasks: 179 | - name: Collect interface information from device 180 | ios_command: 181 | commands: 182 | - show interfaces 183 | register: ios_interface_output 184 | 185 | - name: import the network-engine role 186 | import_role: 187 | name: ansible-network.network-engine 188 | 189 | - name: Generate interface facts as JSON 190 | command_parser: 191 | file: "parser_templates/ios/show_interfaces.yaml" 192 | content: "{{ ios_interface_output.stdout.0 }}" 193 | 194 | ``` 195 | 196 | The second example parses the output of the `show version` command on IOS and creates facts from that output: 197 | 198 | ```yaml 199 | 200 | --- 201 | 202 | # ~/my-playbooks/gather-version-info.yml 203 | 204 | - hosts: ios 205 | connection: network_cli 206 | 207 | tasks: 208 | - name: Collect version information from device 209 | ios_command: 210 | commands: 211 | - show version 212 | register: ios_version_output 213 | 214 | - name: import the network-engine role 215 | import_role: 216 | name: ansible-network.network-engine 217 | 218 | - name: Generate version facts as JSON 219 | command_parser: 220 | file: "parser_templates/ios/show_version.yaml" 221 | content: "{{ ios_version_output.stdout.0 }}" 222 | 223 | ``` 224 | -------------------------------------------------------------------------------- /docs/user_guide/textfsm_parser.md: -------------------------------------------------------------------------------- 1 | # textfsm_parser 2 | 3 | The [textfsm_parser](https://github.com/ansible-network/network-engine/blob/devel/library/textfsm_parser.py) 4 | module is based on [Google TextFSM](https://github.com/google/textfsm/wiki/TextFSM) definitions. 5 | This module iterates over matching rules defined in TextFSM format, extracts data from structured ASCII text based on those rules, 6 | and returns Ansible facts in a JSON data structure that can be added to inventory host facts and/or consumed by Ansible tasks and templates. 7 | 8 | The `textfsm_parser` module requires two inputs: 9 | - the output of commands run on the network device, passed to the `content` parameter 10 | - the parser template that defines the rules for parsing the output, passed to either the `file` or the `src` parameter 11 | 12 | ## content 13 | 14 | The `content` parameter for `textfsm_parser` must point to the ASCII text output of commands run on network devices. The text output can be in a variable or in a file. 15 | 16 | ## file 17 | 18 | The `file` parameter for `textfsm_parser` must point to a parser template that contains a TextFSM rule for each data field you want to extract from your network devices. 19 | 20 | Parser templates for the `textfsm_parser` module in the Network Engine role use TextFSM notation. 21 | 22 | ### name 23 | 24 | The `name` parameter for `textfsm_parser` names the variable in which Ansible will store the JSON data structure. If name is not set, the JSON facts from parsing will not be displayed/exported. 25 | 26 | ### src 27 | 28 | The `src` parameter for `textfsm_parser` loads your parser template from an external source, usually a URL. 29 | 30 | ## Sample Parser Templates 31 | 32 | Here is a sample TextFSM parser template: 33 | 34 | `parser_templates/ios/show_interfaces` 35 | ``` 36 | 37 | Value Required name (\S+) 38 | Value type ([\w ]+) 39 | Value description (.*) 40 | Value mtu (\d+) 41 | 42 | Start 43 | ^${name} is up 44 | ^\s+Hardware is ${type} -> Continue 45 | ^\s+Description: ${description} 46 | ^\s+MTU ${mtu} bytes, -> Record 47 | 48 | ``` 49 | 50 | ## Sample Playbooks 51 | 52 | To extract the data defined in your parser template, create a playbook that includes the Network Engine role and references the `content` and `file` parameters of the `command_parser` module. 53 | 54 | The example playbook below runs a show command, imports the Network Engine role, extracts data from the text output of the command by matching it against the rules defined 55 | in your parser template, and stores the results in a variable. To view the content of that final variable, add it to the `name` parameter as shown in the example and run the playbook in `verbose` mode: `ansible-playbook -v`. 56 | 57 | Make sure the `hosts` definition in the playbook matches a host group in your inventory - in these examples, the playbook expects a group called `ios`. 58 | 59 | The example below parses the output of the `show interfaces` command on IOS and creates facts from that output: 60 | 61 | ```yaml 62 | 63 | --- 64 | 65 | # ~/my-playbooks/textfsm-gather-interface-info.yml 66 | 67 | - hosts: ios 68 | connection: network_cli 69 | 70 | tasks: 71 | - name: Collect interface information from device 72 | ios_command: 73 | commands: "show interfaces" 74 | register: ios_interface_output 75 | 76 | - name: Generate interface facts as JSON 77 | textfsm_parser: 78 | file: "parser_templates/ios/show_interfaces" 79 | content: "{{ ios_interface_output.stdout.0 }}" 80 | name: interface_facts 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /filter_plugins/network_engine.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Ansible Project 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | import re 9 | 10 | from ansible.module_utils.six import string_types 11 | from ansible.errors import AnsibleFilterError 12 | 13 | 14 | def interface_split(interface, key=None): 15 | match = re.match(r'([A-Za-z\-]*)(.+)', interface) 16 | if not match: 17 | raise AnsibleFilterError('unable to parse interface %s' % interface) 18 | obj = {'name': match.group(1), 'index': match.group(2)} 19 | if key: 20 | return obj[key] 21 | else: 22 | return obj 23 | 24 | 25 | def interface_range(interface): 26 | if not isinstance(interface, string_types): 27 | raise AnsibleFilterError('value must be of type string, got %s' % type(interface)) 28 | 29 | parts = interface.rpartition('/') 30 | if parts[1]: 31 | prefix = '%s/' % parts[0] 32 | index = parts[2] 33 | else: 34 | match = re.match(r'([A-Za-z]*)(.+)', interface) 35 | if not match: 36 | raise AnsibleFilterError('unable to parse interface %s' % interface) 37 | prefix = match.group(1) 38 | index = match.group(2) 39 | 40 | indicies = list() 41 | 42 | for item in index.split(','): 43 | tokens = item.split('-') 44 | 45 | if len(tokens) == 1: 46 | indicies.append(tokens[0]) 47 | 48 | elif len(tokens) == 2: 49 | start, end = tokens 50 | for i in range(int(start), int(end) + 1): 51 | indicies.append(i) 52 | i += 1 53 | 54 | return ['%s%s' % (prefix, index) for index in indicies] 55 | 56 | 57 | def _gen_ranges(vlan): 58 | s = e = None 59 | for i in sorted(vlan): 60 | if s is None: 61 | s = e = i 62 | elif i == e or i == e + 1: 63 | e = i 64 | else: 65 | yield (s, e) 66 | s = e = i 67 | if s is not None: 68 | yield (s, e) 69 | 70 | 71 | def vlan_compress(vlan): 72 | if not isinstance(vlan, list): 73 | raise AnsibleFilterError('value must be of type list, got %s' % type(vlan)) 74 | 75 | return (','.join(['%d' % s if s == e else '%d-%d' % (s, e) for (s, e) in _gen_ranges(vlan)])) 76 | 77 | 78 | def vlan_expand(vlan): 79 | if not isinstance(vlan, string_types): 80 | raise AnsibleFilterError('value must be of type string, got %s' % type(vlan)) 81 | 82 | match = re.match(r'([A-Za-z]*)(.+)', vlan) 83 | if not match: 84 | raise AnsibleFilterError('unable to parse vlan %s' % vlan) 85 | 86 | index = match.group(2) 87 | indices = list() 88 | 89 | for item in index.split(','): 90 | tokens = item.split('-') 91 | 92 | if len(tokens) == 1: 93 | indices.append(int(tokens[0])) 94 | 95 | elif len(tokens) == 2: 96 | start, end = tokens 97 | for i in range(int(start), int(end) + 1): 98 | indices.append(i) 99 | i += 1 100 | 101 | return ['%d' % int(index) for index in indices] 102 | 103 | 104 | def to_lines(value): 105 | if isinstance(value, (list, set, tuple)): 106 | return value 107 | elif isinstance(value, string_types): 108 | return value.split('\n') 109 | raise AnsibleFilterError('cannot convert value to lines') 110 | 111 | 112 | class FilterModule(object): 113 | ''' Network interface filter ''' 114 | 115 | def filters(self): 116 | return { 117 | 'interface_split': interface_split, 118 | 'interface_range': interface_range, 119 | 'vlan_compress': vlan_compress, 120 | 'vlan_expand': vlan_expand, 121 | 'to_lines': to_lines 122 | } 123 | -------------------------------------------------------------------------------- /includes/init.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: read ansible_min_version from meta data 3 | include_vars: 4 | file: ../meta/main.yml 5 | name: ansible_galaxy_meta 6 | 7 | - name: validate minimum Ansible version 8 | fail: 9 | msg: "This role requires Ansible {{ ansible_galaxy_meta.galaxy_info.min_ansible_version }} or higher. You are currently running Ansible {{ ansible_version.full }}." 10 | when: 'ansible_version.full is version_compare(ansible_galaxy_meta.galaxy_info.min_ansible_version, "<")' 11 | 12 | - name: validate connection is network_cli 13 | fail: 14 | msg: "expected connection network_cli, got {{ ansible_connection }}" 15 | when: ansible_connection != 'network_cli' 16 | -------------------------------------------------------------------------------- /lib/network_engine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-network/network-engine/d9106df0c2a8531b90974a737d3a0ee67ef3e8e2/lib/network_engine/__init__.py -------------------------------------------------------------------------------- /lib/network_engine/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | from ansible.plugins.loader import PluginLoader 11 | 12 | template_loader = PluginLoader( 13 | 'TemplateEngine', 14 | 'network_engine.plugins.template', 15 | None, 16 | 'template_plugins', 17 | required_base_class='TemplateBase' 18 | ) 19 | 20 | parser_loader = PluginLoader( 21 | 'ParserEngine', 22 | 'network_engine.plugins.parser', 23 | None, 24 | 'parser_plugins', 25 | # required_base_class='ParserBase' 26 | ) 27 | -------------------------------------------------------------------------------- /lib/network_engine/plugins/parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-network/network-engine/d9106df0c2a8531b90974a737d3a0ee67ef3e8e2/lib/network_engine/plugins/parser/__init__.py -------------------------------------------------------------------------------- /lib/network_engine/plugins/parser/pattern_match.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | import re 11 | 12 | from ansible.module_utils.six import iteritems 13 | 14 | 15 | def get_value(m, i): 16 | return m.group(i) if m else None 17 | 18 | 19 | class ParserEngine(object): 20 | 21 | def __init__(self, text): 22 | self.text = text 23 | 24 | def match(self, regex, match_all=None, match_until=None, match_greedy=None): 25 | """ Perform the regular expression match against the content 26 | 27 | :args regex: The regular expression pattern to use 28 | :args content: The content to run the pattern against 29 | :args match_all: Specifies if all matches of pattern should be returned 30 | or just the first occurrence 31 | 32 | :returns: list object of matches or None if there where no matches found 33 | """ 34 | content = self.text 35 | 36 | if match_greedy: 37 | return self._match_greedy(content, regex, end=match_until, match_all=match_all) 38 | elif match_all: 39 | return self._match_all(content, regex) 40 | else: 41 | return self._match(content, regex) 42 | 43 | def _match_all(self, content, pattern): 44 | match = self.re_matchall(pattern, content) 45 | if match: 46 | return match 47 | 48 | def _match(self, content, pattern): 49 | match = self.re_search(pattern, content) 50 | return match 51 | 52 | def _match_greedy(self, content, start, end=None, match_all=None): 53 | """ Filter a section of the content text for matching 54 | 55 | :args content: The content to match against 56 | :args start: The start of the section data 57 | :args end: The end of the section data 58 | :args match_all: Whether or not to match all of the instances 59 | 60 | :returns: a list object of all matches 61 | """ 62 | section_data = list() 63 | 64 | if match_all: 65 | while True: 66 | section_range = self._get_section_range(content, start, end) 67 | if not section_range: 68 | break 69 | 70 | sidx, eidx = section_range 71 | 72 | if eidx is not None: 73 | section_data.append(content[sidx: eidx]) 74 | content = content[eidx:] 75 | else: 76 | section_data.append(content[sidx:]) 77 | break 78 | 79 | else: 80 | section_data.append(content) 81 | 82 | return section_data 83 | 84 | def _get_section_range(self, content, start, end=None): 85 | 86 | context_start_re = re.compile(start, re.M) 87 | if end: 88 | context_end_re = re.compile(end, re.M) 89 | include_end = True 90 | else: 91 | context_end_re = context_start_re 92 | include_end = False 93 | 94 | context_start = re.search(context_start_re, content) 95 | if not context_start: 96 | return 97 | 98 | string_start = context_start.start() 99 | end = context_start.end() + 1 100 | 101 | context_end = re.search(context_end_re, content[end:]) 102 | if not context_end: 103 | return (string_start, None) 104 | 105 | if include_end: 106 | string_end = end + context_end.end() 107 | else: 108 | string_end = end + context_end.start() 109 | 110 | return (string_start, string_end) 111 | 112 | def _get_context_data(self, entry, content): 113 | name = entry['name'] 114 | 115 | context = entry.get('context', {}) 116 | context_data = list() 117 | 118 | if context: 119 | while True: 120 | context_range = self._get_context_range(name, context, content) 121 | 122 | if not context_range: 123 | break 124 | 125 | start, end = context_range 126 | 127 | if end is not None: 128 | context_data.append(content[start: end]) 129 | content = content[end:] 130 | else: 131 | context_data.append(content[start:]) 132 | break 133 | 134 | else: 135 | context_data.append(content) 136 | 137 | return context_data 138 | 139 | def re_search(self, regex, value): 140 | obj = {'matches': []} 141 | regex = re.compile(regex, re.M) 142 | match = regex.search(value) 143 | if match: 144 | items = list(match.groups()) 145 | if regex.groupindex: 146 | for name, index in iteritems(regex.groupindex): 147 | obj[name] = items[index - 1] 148 | obj['matches'] = items 149 | return obj 150 | 151 | def re_matchall(self, regex, value): 152 | objects = list() 153 | regex = re.compile(regex) 154 | for match in re.findall(regex.pattern, value, re.M): 155 | obj = {} 156 | obj['matches'] = match 157 | if regex.groupindex: 158 | for name, index in iteritems(regex.groupindex): 159 | if len(regex.groupindex) == 1: 160 | obj[name] = match 161 | else: 162 | obj[name] = match[index - 1] 163 | objects.append(obj) 164 | return objects 165 | -------------------------------------------------------------------------------- /lib/network_engine/plugins/template/__init__.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | import collections 11 | 12 | from ansible.module_utils.six import iteritems, string_types 13 | from ansible.errors import AnsibleUndefinedVariable 14 | 15 | 16 | class TemplateBase(object): 17 | 18 | def __init__(self, templar): 19 | self._templar = templar 20 | 21 | def __call__(self, data, variables, convert_bare=False): 22 | return self.template(data, variables, convert_bare) 23 | 24 | def run(self, template, variables): 25 | pass 26 | 27 | def template(self, data, variables, convert_bare=False): 28 | 29 | if isinstance(data, collections.Mapping): 30 | templated_data = {} 31 | for key, value in iteritems(data): 32 | templated_key = self.template(key, variables, convert_bare=convert_bare) 33 | templated_value = self.template(value, variables, convert_bare=convert_bare) 34 | templated_data[templated_key] = templated_value 35 | return templated_data 36 | 37 | elif isinstance(data, collections.Iterable) and not isinstance(data, string_types): 38 | return [self.template(i, variables, convert_bare=convert_bare) for i in data] 39 | 40 | else: 41 | data = data or {} 42 | tmp_avail_vars = self._templar._available_variables 43 | self._templar.set_available_variables(variables) 44 | try: 45 | resp = self._templar.template(data, convert_bare=convert_bare) 46 | except AnsibleUndefinedVariable: 47 | resp = None 48 | pass 49 | finally: 50 | self._templar.set_available_variables(tmp_avail_vars) 51 | return resp 52 | 53 | def _update(self, d, u): 54 | for k, v in iteritems(u): 55 | if isinstance(v, collections.Mapping): 56 | d[k] = self._update(d.get(k, {}), v) 57 | else: 58 | d[k] = v 59 | return d 60 | 61 | def _check_conditional(self, when, variables): 62 | conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" 63 | return self.template(conditional % when, variables) 64 | -------------------------------------------------------------------------------- /lib/network_engine/plugins/template/json_template.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | import collections 11 | 12 | from ansible.module_utils.six import string_types 13 | 14 | from network_engine.plugins.template import TemplateBase 15 | 16 | 17 | class TemplateEngine(TemplateBase): 18 | 19 | def run(self, template, variables=None): 20 | 21 | templated_items = {} 22 | 23 | for item in template: 24 | key = self.template(item['key'], variables) 25 | 26 | # FIXME moving to the plugin system breaks this 27 | when = item.get('when') 28 | if when is not None: 29 | if not self._check_conditional(when, variables): 30 | continue 31 | 32 | if 'value' in item: 33 | value = item.get('value') 34 | items = None 35 | item_type = None 36 | 37 | elif 'object' in item: 38 | items = item.get('object') 39 | item_type = 'dict' 40 | 41 | elif 'elements' in item: 42 | items = item.get('elements') 43 | item_type = 'list' 44 | 45 | loop = item.get('repeat_for') 46 | loop_data = self.template(loop, variables) if loop else None 47 | loop_var = item.get('repeat_var', 'item') 48 | 49 | if items: 50 | if loop: 51 | if isinstance(loop_data, collections.Iterable) and not isinstance(loop_data, string_types): 52 | templated_value = list() 53 | 54 | for loop_item in loop_data: 55 | variables[loop_var] = loop_item 56 | if isinstance(items, string_types): 57 | templated_value.append(self.template(items, variables)) 58 | else: 59 | templated_value.append(self.run(items, variables)) 60 | 61 | if item_type == 'list': 62 | templated_items[key] = templated_value 63 | 64 | elif item_type == 'dict': 65 | if key not in templated_items: 66 | templated_items[key] = {} 67 | 68 | for t in templated_value: 69 | templated_items[key] = self._update(templated_items[key], t) 70 | else: 71 | templated_items[key] = [] 72 | 73 | else: 74 | val = self.run(items, variables) 75 | 76 | if item_type == 'list': 77 | templated_value = [val] 78 | else: 79 | templated_value = val 80 | 81 | templated_items[key] = templated_value 82 | 83 | else: 84 | templated_value = self.template(value, variables) 85 | templated_items[key] = templated_value 86 | 87 | return templated_items 88 | -------------------------------------------------------------------------------- /lib/network_engine/plugins/template/normal.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | from network_engine.plugins.template import TemplateBase 11 | 12 | 13 | class TemplateEngine(TemplateBase): 14 | pass 15 | -------------------------------------------------------------------------------- /lib/network_engine/utils.py: -------------------------------------------------------------------------------- 1 | # (c) 2018, Ansible by Red Hat, inc 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | # 7 | import os 8 | 9 | from itertools import chain 10 | 11 | from ansible.module_utils.six import iteritems 12 | from ansible.module_utils._text import to_bytes, to_text 13 | from ansible.module_utils.network.common.utils import sort_list 14 | from ansible.utils.display import Display 15 | from ansible.utils.path import unfrackpath 16 | 17 | 18 | display = Display() 19 | 20 | 21 | def dict_merge(base, other): 22 | """ Return a new dict object that combines base and other 23 | 24 | This will create a new dict object that is a combination of the key/value 25 | pairs from base and other. When both keys exist, the value will be 26 | selected from other. If the value is a list object, the two lists will 27 | be combined and duplicate entries removed. 28 | 29 | :param base: dict object to serve as base 30 | :param other: dict object to combine with base 31 | 32 | :returns: new combined dict object 33 | """ 34 | if not isinstance(base, dict): 35 | raise AssertionError("`base` must be of type ") 36 | if not isinstance(other, dict): 37 | raise AssertionError("`other` must be of type ") 38 | 39 | combined = dict() 40 | 41 | for key, value in iteritems(base): 42 | if isinstance(value, dict): 43 | if key in other: 44 | item = other.get(key) 45 | if item is not None: 46 | if isinstance(other[key], dict): 47 | combined[key] = dict_merge(value, other[key]) 48 | else: 49 | combined[key] = other[key] 50 | else: 51 | combined[key] = item 52 | else: 53 | combined[key] = value 54 | elif isinstance(value, list): 55 | if key in other: 56 | item = other.get(key) 57 | if item is not None: 58 | try: 59 | combined[key] = list(set(chain(value, item))) 60 | except TypeError: 61 | value.extend([i for i in item if i not in value]) 62 | combined[key] = value 63 | else: 64 | combined[key] = item 65 | else: 66 | combined[key] = value 67 | else: 68 | if key in other: 69 | other_value = other.get(key) 70 | if other_value is not None: 71 | if sort_list(base[key]) != sort_list(other_value): 72 | combined[key] = other_value 73 | else: 74 | combined[key] = value 75 | else: 76 | combined[key] = other_value 77 | else: 78 | combined[key] = value 79 | 80 | for key in set(other.keys()).difference(base.keys()): 81 | combined[key] = other.get(key) 82 | 83 | return combined 84 | 85 | 86 | def generate_source_path(paths, source): 87 | """ 88 | Find file in first path in stack. 89 | 90 | :param paths: A list of text strings which are the path to look for the filename in. 91 | :param src: A text string which is the filename to search for. 92 | :rtype: A text string. 93 | :returns: An absolute path to the filename ``src`` if found. 94 | """ 95 | 96 | b_source = to_bytes(source) 97 | 98 | result = None 99 | search = [] 100 | 101 | if source.startswith('~') or source.startswith(os.path.sep): 102 | # path is absolute, no relative needed, check existence and return source 103 | test_path = unfrackpath(b_source, follow=False) 104 | if os.path.exists(to_bytes(test_path, errors='surrogate_or_strict')): 105 | result = test_path 106 | 107 | else: 108 | for path in paths: 109 | upath = unfrackpath(path, follow=False) 110 | b_upath = to_bytes(upath, errors='surrogate_or_strict') 111 | search.append(os.path.join(b_upath, b_source)) 112 | 113 | for candidate in search: 114 | display.vvvvv(u'looking for "%s" at "%s"' % (source, to_text(candidate))) 115 | if os.path.exists(candidate) and os.path.isfile(candidate): 116 | result = to_text(candidate) 117 | break 118 | 119 | return result 120 | -------------------------------------------------------------------------------- /library/command_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2018 Red Hat 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | __metaclass__ = type 9 | 10 | 11 | ANSIBLE_METADATA = {'metadata_version': '1.1', 12 | 'status': ['preview'], 13 | 'supported_by': 'network'} 14 | 15 | 16 | DOCUMENTATION = ''' 17 | --- 18 | module: command_parser 19 | short_description: Parses text into JSON facts based on rules 20 | description: 21 | - Provides a rules base text parser that is closely modeled after the Ansible 22 | playbook language. This parser will iterate of the rules and parse the 23 | output of structured ASCII text into a JSON data structure that can be 24 | added to the inventory host facts. 25 | version_added: "2.5" 26 | options: 27 | dir: 28 | description: 29 | - The path to the directory that contains the parsers. The module will 30 | load all parsers found in this directory and pass the content through 31 | the them. This argument is mutually exclusive with C(file). 32 | default: null 33 | file: 34 | description: 35 | - The path to the parser to load from disk on the Ansible 36 | controller. This can be either the absolute path or relative path. 37 | This argument is mutually exclusive with C(dir). 38 | Default path is {{ playbook_dir }}/parser_templates/{{ ansible_network_os }} 39 | or {{ playbook_dir }}/parser_templates or {{ playbook_dir }} 40 | default: "{{ playbook_dir }}/parser_templates/{{ ansible_network_os }}" 41 | content: 42 | description: 43 | - The text content to pass to the parser engine. This argument provides 44 | the input to the text parser for generating the JSON data. 45 | required: true 46 | author: 47 | - Peter Sprygada (@privateip) 48 | ''' 49 | 50 | EXAMPLES = ''' 51 | - command_parser: 52 | file: files/parser_templates/show_interface.yaml 53 | content: "{{ lookup('file', 'output/show_interfaces.txt') }}" 54 | ''' 55 | -------------------------------------------------------------------------------- /library/net_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2018, Red Hat, Inc. 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | __metaclass__ = type 9 | 10 | 11 | ANSIBLE_METADATA = {'metadata_version': '1.1', 12 | 'status': ['preview'], 13 | 'supported_by': 'network'} 14 | 15 | 16 | DOCUMENTATION = """ 17 | --- 18 | module: net_facts 19 | version_added: "2.7" 20 | short_description: Collect device capabilities from Network devices 21 | description: 22 | - Collect basic fact capabilities from Network devices and return 23 | the capabilities as Ansible facts. 24 | 25 | author: 26 | - Trishna Guha (@trishnaguha) 27 | options: {} 28 | """ 29 | 30 | EXAMPLES = """ 31 | - facts: 32 | """ 33 | 34 | RETURN = """ 35 | """ 36 | from ansible.module_utils.basic import AnsibleModule 37 | from ansible.module_utils.connection import Connection 38 | 39 | 40 | def main(): 41 | """ main entry point for Ansible module 42 | """ 43 | argument_spec = {} 44 | 45 | module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) 46 | 47 | connection = Connection(module._socket_path) 48 | facts = connection.get_capabilities() 49 | facts = module.from_json(facts) 50 | result = { 51 | 'changed': False, 52 | 'ansible_facts': {'ansible_network_facts': {'capabilities': facts['device_info']}} 53 | } 54 | module.exit_json(**result) 55 | 56 | 57 | if __name__ == '__main__': 58 | main() 59 | -------------------------------------------------------------------------------- /library/textfsm_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2018 Red Hat 5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 | 7 | from __future__ import absolute_import, division, print_function 8 | __metaclass__ = type 9 | 10 | 11 | ANSIBLE_METADATA = {'metadata_version': '1.1', 12 | 'status': ['preview'], 13 | 'supported_by': 'network'} 14 | 15 | DOCUMENTATION = ''' 16 | --- 17 | module: textfsm_parser 18 | author: Peter Sprygada (@privateip) 19 | short_description: Parses text into JSON facts using TextFSM 20 | description: 21 | - Provides textfsm rule based templates to parse data from text. 22 | The template acting as parser will iterate of the rules and parse 23 | the output of structured ASCII text into a JSON data structure 24 | that can be added to the inventory host facts. 25 | requirements: 26 | - textfsm 27 | version_added: "2.5" 28 | options: 29 | file: 30 | description: 31 | - Path to the TextFSM parser to use to parse the output from a command. 32 | The C(file) argument accepts either a relative or absolute path 33 | to the TextFSM file. 34 | default: null 35 | src: 36 | description: 37 | - The C(src) argument can be used to load the content of a TextFSM 38 | parser file. This argument allow the TextFSM parser to be loaded 39 | from an external source. See EXAMPLES. 40 | default: null 41 | content: 42 | description: 43 | - The output of the command to parse using the rules in the TextFSM 44 | file. The content should be a text string. 45 | required: true 46 | name: 47 | description: 48 | - The C(name) argument is used to define the top-level fact name to 49 | hold the output of the parser. If this argument is not provided, 50 | the output from parsing will not be exported. 51 | default: null 52 | ''' 53 | 54 | EXAMPLES = ''' 55 | - name: parse the content of a command 56 | textfsm_parser: 57 | file: files/parser_templates/show_interface.yaml 58 | content: "{{ lookup('file', 'output/show_interfaces.txt') }}" 59 | 60 | - name: store returned facts into a key call output 61 | textfsm_parser: 62 | file: files/parser_templates/show_interface.yaml 63 | content: "{{ lookup('file', 'output/show_interfaces.txt') }}" 64 | name: output 65 | 66 | - name: read the parser from an url 67 | textfsm_parser: 68 | src: "{{ lookup('url', 'http://server/path/to/parser') }}" 69 | content: "{{ lookup('file', 'output/show_interfaces.txt') }}" 70 | ''' 71 | -------------------------------------------------------------------------------- /lookup_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-network/network-engine/d9106df0c2a8531b90974a737d3a0ee67ef3e8e2/lookup_plugins/__init__.py -------------------------------------------------------------------------------- /lookup_plugins/config_template.py: -------------------------------------------------------------------------------- 1 | # (c) 2012, Michael DeHaan 2 | # (c) 2012-17 Ansible Project 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # 5 | # You should have received a copy of the GNU General Public License 6 | # along with Ansible. If not, see . 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | DOCUMENTATION = """ 11 | lookup: config_template 12 | author: Peter Sprygada (privateip) 13 | version_added: "2.7" 14 | short_description: retrieve contents of file after templating with Jinja2 15 | description: 16 | - This lookup plugin implements the standard template plugin with a slight 17 | twist in that it supports using default(omit) to remove an entire line 18 | options: 19 | _terms: 20 | description: list of files to template 21 | """ 22 | 23 | EXAMPLES = """ 24 | - name: show templating results 25 | debug: msg="{{ lookup('config_template', './some_template.j2') }} 26 | """ 27 | 28 | RETURN = """ 29 | _raw: 30 | description: file(s) content after templating 31 | """ 32 | from ansible.plugins.lookup.template import LookupModule as LookupBase 33 | 34 | 35 | class LookupModule(LookupBase): 36 | 37 | def run(self, terms, variables, **kwargs): 38 | 39 | ret = super(LookupModule, self).run(terms, variables, **kwargs) 40 | 41 | omit = variables['omit'] 42 | filtered = list() 43 | 44 | for line in ret[0].split('\n'): 45 | if all((line, omit not in line, not line.startswith('!'))): 46 | filtered.append(line) 47 | 48 | return [filtered] 49 | -------------------------------------------------------------------------------- /lookup_plugins/json_template.py: -------------------------------------------------------------------------------- 1 | # (c) 2018 Red Hat, Inc. 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | from __future__ import (absolute_import, division, print_function) 7 | __metaclass__ = type 8 | 9 | DOCUMENTATION = """ 10 | lookup: json_template 11 | author: Ansible Network 12 | version_added: "2.5" 13 | short_description: retrieve and template device configuration 14 | description: 15 | - This plugin lookups the content(key-value pair) of a JSON file 16 | and returns network configuration in JSON format. 17 | options: 18 | _terms: 19 | description: File for lookup 20 | """ 21 | 22 | EXAMPLES = """ 23 | - name: show interface lookup result 24 | debug: msg="{{ lookup('json_template', './show_interface.json') }} 25 | """ 26 | 27 | RETURN = """ 28 | _raw: 29 | description: JSON file content 30 | """ 31 | 32 | import json 33 | import os 34 | import sys 35 | 36 | from ansible.errors import AnsibleError, AnsibleParserError 37 | from ansible.plugins.lookup import LookupBase, display 38 | from ansible.module_utils._text import to_bytes 39 | 40 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.path.pardir, 'lib')) 41 | from network_engine.plugins import template_loader 42 | 43 | 44 | class LookupModule(LookupBase): 45 | 46 | def run(self, terms, variables, **kwargs): 47 | 48 | ret = list() 49 | 50 | self.ds = variables.copy() 51 | self.template = template_loader.get('json_template', self._templar) 52 | 53 | display.debug("File lookup term: %s" % terms[0]) 54 | 55 | lookupfile = self.find_file_in_search_path(variables, 'files', terms[0]) 56 | display.vvvv("File lookup using %s as file" % lookupfile) 57 | try: 58 | if lookupfile: 59 | with open(to_bytes(lookupfile, errors='surrogate_or_strict'), 'rb') as f: 60 | json_data = list() 61 | json_data.append(json.load(f)) 62 | ret.append(self.template.run(json_data, self.ds)) 63 | else: 64 | raise AnsibleParserError() 65 | except AnsibleParserError: 66 | raise AnsibleError("could not locate file in lookup: %s" % terms[0]) 67 | 68 | return ret 69 | -------------------------------------------------------------------------------- /lookup_plugins/netcfg_diff.py: -------------------------------------------------------------------------------- 1 | # (c) 2018 Red Hat, Inc. 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # 4 | # You should have received a copy of the GNU General Public License 5 | # along with Ansible. If not, see . 6 | from __future__ import (absolute_import, division, print_function) 7 | __metaclass__ = type 8 | 9 | DOCUMENTATION = """ 10 | lookup: netcfg_diff 11 | author: Ansible Network 12 | version_added: "2.5" 13 | short_description: Generates a text configuration difference between two configuration 14 | mainly used with network devices. 15 | description: 16 | - This plugin lookups will generate a difference between two text configuration that 17 | is supported by Network OS. This difference can be used to identify the 18 | exact change that is required to be pushed to remote host. 19 | options: 20 | _terms: 21 | description: 22 | - Specifies the wanted text configuration. Theis is usually 23 | the text configuration that is expected to be present on remote host. 24 | required: True 25 | have: 26 | description: 27 | - Specifies the text configuration. The C(have) is usually 28 | the text configuration that is active on remote host. 29 | required: True 30 | match: 31 | description: 32 | - Instructs the module on the way to perform the matching of 33 | the set of text commands between C(want) and C(have). 34 | If match is set to I(line), commands are matched line by line. If 35 | match is set to I(strict), command lines are matched with respect 36 | to position. If match is set to I(exact), command lines 37 | must be an equal match. 38 | default: line 39 | choices: ['line', 'strict', 'exact'] 40 | replace: 41 | description: 42 | - Instructs the module on the way to perform the configuration 43 | diff. If the replace argument is set to I(line) then 44 | the modified lines are pushed in the generated diff. If the replace argument 45 | is set to I(block) then the entire command block is pushed in the generated 46 | diff if any line is not correct. 47 | default: line 48 | choices: ['line', 'block'] 49 | indent: 50 | description: 51 | - Specifies the indentation used for the block in text configuration. The value of C(indent) 52 | is specific on network platform to which the text configuration in C(want) and C(have) 53 | conforms to. 54 | default: 1 55 | type: int 56 | ignore_lines: 57 | description: 58 | - This specifies the lines to be ignored while generating diff. The value of C(ignore_lines) can 59 | also be a python regex. 60 | 61 | """ 62 | 63 | EXAMPLES = """ 64 | - name: generate diff between two text configuration 65 | debug: msg="{{ lookup('netcfg_diff', want, have=have) }} 66 | """ 67 | 68 | RETURN = """ 69 | _raw: 70 | description: The text difference between values of want and have with want as base reference 71 | """ 72 | 73 | from ansible.plugins.lookup import LookupBase 74 | from ansible.module_utils.network.common.config import NetworkConfig, dumps 75 | from ansible.errors import AnsibleError 76 | 77 | 78 | MATCH_CHOICES = ('line', 'strict', 'exact') 79 | REPLACE_CHOICES = ('line', 'block') 80 | 81 | 82 | class LookupModule(LookupBase): 83 | 84 | def run(self, terms, variables, **kwargs): 85 | 86 | ret = [] 87 | 88 | try: 89 | want = terms[0] 90 | except IndexError: 91 | raise AnsibleError("value of 'want' must be specified") 92 | 93 | try: 94 | have = kwargs['have'] 95 | except KeyError: 96 | raise AnsibleError("value of 'have' must be specified") 97 | 98 | match = kwargs.get('match', 'line') 99 | if match not in MATCH_CHOICES: 100 | choices_str = ", ".join(MATCH_CHOICES) 101 | raise AnsibleError("value of match must be one of: %s, got: %s" % (choices_str, match)) 102 | 103 | replace = kwargs.get('replace', 'line') 104 | if replace not in REPLACE_CHOICES: 105 | choices_str = ", ".join(REPLACE_CHOICES) 106 | raise AnsibleError("value of replace must be one of: %s, got: %s" % (choices_str, replace)) 107 | 108 | indent = int(kwargs.get('indent', 1)) 109 | ignore_lines = kwargs.get('ignore_lines') 110 | 111 | running_obj = NetworkConfig(indent=indent, contents=have, ignore_lines=ignore_lines) 112 | candidate_obj = NetworkConfig(indent=indent, contents=want, ignore_lines=ignore_lines) 113 | 114 | configobjs = candidate_obj.difference(running_obj, match=match, replace=replace) 115 | 116 | diff = dumps(configobjs, output='commands') 117 | ret.append(diff) 118 | 119 | return ret 120 | -------------------------------------------------------------------------------- /lookup_plugins/network_template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (c) 2018, Ansible by Red Hat, inc 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | # 6 | # You should have received a copy of the GNU General Public License 7 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 8 | # 9 | from __future__ import (absolute_import, division, print_function) 10 | __metaclass__ = type 11 | 12 | DOCUMENTATION = """ 13 | lookup: network_template 14 | author: Ansible Network 15 | version_added: "2.5" 16 | short_description: template device configuration 17 | description: 18 | - This plugin will lookup the file and template it into a network 19 | configuration. 20 | deprecated: 21 | removed_in: "2.7.7" 22 | options: 23 | _terms: 24 | description: list of files to template 25 | """ 26 | 27 | EXAMPLES = """ 28 | - name: show config template results 29 | debug: msg="{{ lookup('network_template', './config_template.j2') }} 30 | """ 31 | 32 | RETURN = """ 33 | _raw: 34 | description: file(s) content after templating 35 | """ 36 | 37 | 38 | import collections 39 | 40 | from ansible.plugins.lookup import LookupBase, display 41 | from ansible.module_utils.common._collections_compat import Mapping 42 | from ansible.module_utils.network.common.utils import to_list 43 | from ansible.module_utils.six import iteritems, string_types 44 | from ansible.module_utils._text import to_text, to_bytes 45 | from ansible.errors import AnsibleError, AnsibleUndefinedVariable 46 | 47 | 48 | class LookupModule(LookupBase): 49 | 50 | def run(self, terms, variables, **kwargs): 51 | self.ds = variables.copy() 52 | 53 | config_lines = list() 54 | 55 | for term in to_list(terms[0]): 56 | display.debug("File lookup term: %s" % term) 57 | 58 | lookupfile = self.find_file_in_search_path(variables, 'templates', term) 59 | display.vvvv("File lookup using %s as file" % lookupfile) 60 | 61 | if lookupfile: 62 | with open(to_bytes(lookupfile, errors='surrogate_or_strict'), 'rb'): 63 | tasks = self._loader.load_from_file(lookupfile) 64 | 65 | for task in tasks: 66 | task.pop('name', None) 67 | register = task.pop('register', None) 68 | 69 | when = task.pop('when', None) 70 | if when is not None: 71 | if not self._check_conditional(when, self.ds): 72 | display.vvv('skipping task due to conditional check failure') 73 | continue 74 | 75 | loop = task.pop('loop', None) 76 | 77 | if loop: 78 | loop = self.template(loop, self.ds) 79 | loop_result = list() 80 | 81 | if isinstance(loop, Mapping): 82 | for loop_key, loop_value in iteritems(loop): 83 | self.ds['item'] = {'key': loop_key, 'value': loop_value} 84 | res = self._process_directive(task) 85 | if res: 86 | loop_result.extend(to_list(res)) 87 | 88 | elif isinstance(loop, collections.Iterable) and not isinstance(loop, string_types): 89 | for loop_item in loop: 90 | self.ds['item'] = loop_item 91 | res = self._process_directive(task) 92 | if res: 93 | loop_result.extend(to_list(res)) 94 | 95 | config_lines.extend(loop_result) 96 | 97 | if register: 98 | self.ds[register] = loop_result 99 | 100 | else: 101 | res = self._process_directive(task) 102 | if res: 103 | config_lines.extend(to_list(res)) 104 | if register: 105 | self.ds[register] = res 106 | 107 | else: 108 | raise AnsibleError("the template file %s could not be found for the lookup" % term) 109 | 110 | return [to_text('\n'.join(config_lines)).strip()] 111 | 112 | def do_context(self, block): 113 | 114 | results = list() 115 | 116 | for entry in block: 117 | task = entry.copy() 118 | task.pop('name', None) 119 | task.pop('register', None) 120 | 121 | when = task.pop('when', None) 122 | if when is not None: 123 | if not self._check_conditional(when, self.ds): 124 | display.vvv('skipping context due to conditional check failure') 125 | continue 126 | 127 | loop = task.pop('loop', None) 128 | if loop: 129 | loop = self.template(loop, self.ds) 130 | 131 | if 'context' in task: 132 | res = self.do_context(task['context']) 133 | if res: 134 | results.extend(res) 135 | 136 | elif isinstance(loop, Mapping): 137 | loop_result = list() 138 | for loop_key, loop_value in iteritems(loop): 139 | self.ds['item'] = {'key': loop_key, 'value': loop_value} 140 | loop_result.extend(to_list(self._process_directive(task))) 141 | results.extend(loop_result) 142 | 143 | elif isinstance(loop, collections.Iterable) and not isinstance(loop, string_types): 144 | loop_result = list() 145 | for loop_item in loop: 146 | self.ds['item'] = loop_item 147 | loop_result.extend(to_list(self._process_directive(task))) 148 | results.extend(loop_result) 149 | 150 | else: 151 | res = self._process_directive(task) 152 | if res: 153 | results.extend(to_list(res)) 154 | 155 | return results 156 | 157 | def _process_directive(self, task): 158 | for directive, args in iteritems(task): 159 | if directive == 'context': 160 | meth = getattr(self, 'do_%s' % directive) 161 | if meth: 162 | return meth(args) 163 | else: 164 | meth = getattr(self, 'do_%s' % directive) 165 | if meth: 166 | return meth(**args) 167 | 168 | def do_lines_template(self, template, join=False, when=None, required=False): 169 | templated_lines = list() 170 | _processed = list() 171 | 172 | if when is not None: 173 | if not self._check_conditional(when, self.ds): 174 | display.vvv("skipping due to conditional failure") 175 | return templated_lines 176 | 177 | for line in to_list(template): 178 | res = self.template(line, self.ds) 179 | if res: 180 | _processed.append(res) 181 | elif not res and join: 182 | break 183 | 184 | if required and not _processed: 185 | raise AnsibleError('unabled to templated required line') 186 | elif _processed and join: 187 | templated_lines.append(' '.join(_processed)) 188 | elif _processed: 189 | templated_lines.extend(_processed) 190 | 191 | return templated_lines 192 | 193 | def _process_include(self, item, variables): 194 | name = item.get('name') 195 | include = item['include'] 196 | 197 | src = self.template(include, variables) 198 | source = self._find_needle('templates', src) 199 | 200 | when = item.get('when') 201 | 202 | if when: 203 | conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" 204 | if not self.template(conditional % when, variables, fail_on_undefined=False): 205 | display.vvvvv("include '%s' skipped due to conditional check failure" % name) 206 | return [] 207 | 208 | display.display('including file %s' % source) 209 | include_data = self._loader.load_from_file(source) 210 | 211 | template_data = item.copy() 212 | 213 | # replace include directive with block directive and contents of 214 | # included file. this will preserve other values such as loop, 215 | # loop_control, etc 216 | template_data.pop('include') 217 | template_data['block'] = include_data 218 | 219 | return self.build([template_data], variables) 220 | 221 | def template(self, data, variables, convert_bare=False): 222 | 223 | if isinstance(data, Mapping): 224 | templated_data = {} 225 | for key, value in iteritems(data): 226 | templated_key = self.template(key, variables, convert_bare=convert_bare) 227 | templated_data[templated_key] = self.template(value, variables, convert_bare=convert_bare) 228 | return templated_data 229 | 230 | elif isinstance(data, collections.Iterable) and not isinstance(data, string_types): 231 | return [self.template(i, variables, convert_bare=convert_bare) for i in data] 232 | 233 | else: 234 | data = data or {} 235 | tmp_avail_vars = self._templar._available_variables 236 | self._templar.set_available_variables(variables) 237 | try: 238 | resp = self._templar.template(data, convert_bare=convert_bare) 239 | resp = self._coerce_to_native(resp) 240 | except AnsibleUndefinedVariable: 241 | resp = None 242 | finally: 243 | self._templar.set_available_variables(tmp_avail_vars) 244 | return resp 245 | 246 | def _coerce_to_native(self, value): 247 | if not isinstance(value, bool): 248 | try: 249 | value = int(value) 250 | except Exception: 251 | if value is None or len(value) == 0: 252 | return None 253 | return value 254 | 255 | def _check_conditional(self, when, variables): 256 | conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" 257 | return self.template(conditional % when, variables) 258 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: ansible-network 4 | description: This role provides the foundation for building network roles by providing modules and plugins that are common to all Ansible Network roles. 5 | company: Red Hat 6 | 7 | issue_tracker_url: https://github.com/ansible-network/network-engine/issues 8 | 9 | license: GPLv3 10 | 11 | min_ansible_version: 2.7 12 | 13 | # If this a Container Enabled role, provide the minimum Ansible Container version. 14 | # min_ansible_container_version: 15 | 16 | # Optionally specify the branch Galaxy will use when accessing the GitHub 17 | # repo for this role. During role install, if no tags are available, 18 | # Galaxy will use this branch. During import Galaxy will access files on 19 | # this branch. If Travis integration is configured, only notifications for this 20 | # branch will be accepted. Otherwise, in all cases, the repo's default branch 21 | # (usually master) will be used. 22 | # github_branch: 23 | 24 | platforms: 25 | - name: network 26 | versions: 27 | - all 28 | 29 | galaxy_tags: 30 | - ansible 31 | - network 32 | - networking 33 | - engine 34 | - core 35 | - cli 36 | 37 | dependencies: [] 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansible 2 | textfsm 3 | -------------------------------------------------------------------------------- /tasks/cli.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: initialize function 3 | include_tasks: includes/init.yaml 4 | 5 | - name: run command on remote network node and use engine to parse output into JSON facts 6 | cli: 7 | command: "{{ network_engine_command }}" 8 | parser: "{{ network_engine_parser | default(omit) }}" 9 | engine: "{{ network_engine_engine | default(omit) }}" 10 | name: "{{ network_engine_name | default(omit) }}" 11 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for network-engine 3 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | ara 2 | flake8 3 | yamllint 4 | -------------------------------------------------------------------------------- /tests/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = False 3 | retry_files_enabled = False 4 | gathering = explicit 5 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/defaults/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser_path: "{{ role_path }}/parser_templates/{{ ansible_network_os }}" 3 | output_path: "{{ role_path }}/output/{{ ansible_network_os }}" 4 | export_type: "list" 5 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/output/ios/show_interfaces.txt: -------------------------------------------------------------------------------- 1 | GigabitEthernet0/0 is up, line protocol is up 2 | Hardware is iGbE, address is 5e00.0002.0000 (bia 5e00.0002.0000) 3 | Description: OOB Management 4 | Internet address is 10.8.38.65/24 5 | MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 6 | reliability 253/255, txload 1/255, rxload 1/255 7 | Encapsulation ARPA, loopback not set 8 | Keepalive set (10 sec) 9 | Full Duplex, Auto Speed, link type is auto, media type is RJ45 10 | output flow-control is unsupported, input flow-control is unsupported 11 | ARP type: ARPA, ARP Timeout 04:00:00 12 | Last input 00:00:00, output 00:00:00, output hang never 13 | Last clearing of "show interface" counters never 14 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 15 | Queueing strategy: fifo 16 | Output queue: 0/40 (size/max) 17 | 5 minute input rate 2000 bits/sec, 2 packets/sec 18 | 5 minute output rate 2000 bits/sec, 2 packets/sec 19 | 4973387 packets input, 816226566 bytes, 0 no buffer 20 | Received 228869 broadcasts (0 IP multicasts) 21 | 461509 runts, 0 giants, 0 throttles 22 | 461509 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 23 | 0 watchdog, 0 multicast, 0 pause input 24 | 3316083 packets output, 432440225 bytes, 0 underruns 25 | 0 output errors, 0 collisions, 3 interface resets 26 | 2303378 unknown protocol drops 27 | 0 babbles, 0 late collision, 0 deferred 28 | 1 lost carrier, 0 no carrier, 0 pause output 29 | 0 output buffer failures, 0 output buffers swapped out 30 | GigabitEthernet0/1 is up, line protocol is up 31 | Hardware is iGbE, address is fa16.3e4e.c5e5 (bia fa16.3e4e.c5e5) 32 | Description: test-interface 33 | MTU 2000 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 34 | reliability 255/255, txload 1/255, rxload 1/255 35 | Encapsulation ARPA, loopback not set 36 | Keepalive set (10 sec) 37 | Full Duplex, 1Gbps, link type is auto, media type is RJ45 38 | output flow-control is unsupported, input flow-control is unsupported 39 | ARP type: ARPA, ARP Timeout 04:00:00 40 | Last input 4d07h, output 4d07h, output hang never 41 | Last clearing of "show interface" counters never 42 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 43 | Queueing strategy: fifo 44 | Output queue: 0/40 (size/max) 45 | 5 minute input rate 0 bits/sec, 0 packets/sec 46 | 5 minute output rate 0 bits/sec, 0 packets/sec 47 | 89815 packets input, 27598643 bytes, 0 no buffer 48 | Received 1 broadcasts (0 IP multicasts) 49 | 0 runts, 0 giants, 0 throttles 50 | 0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 51 | 0 watchdog, 0 multicast, 0 pause input 52 | 404016 packets output, 27896846 bytes, 0 underruns 53 | 0 output errors, 0 collisions, 51 interface resets 54 | 89728 unknown protocol drops 55 | 0 babbles, 0 late collision, 0 deferred 56 | 28 lost carrier, 0 no carrier, 0 pause output 57 | 0 output buffer failures, 0 output buffers swapped out 58 | GigabitEthernet0/2 is up, line protocol is up 59 | Hardware is iGbE, address is fa16.3eca.c938 (bia fa16.3eca.c938) 60 | Description: test-interface-2 61 | MTU 2000 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 62 | reliability 255/255, txload 1/255, rxload 1/255 63 | Encapsulation ARPA, loopback not set 64 | Keepalive set (10 sec) 65 | Full Duplex, 1Gbps, link type is auto, media type is RJ45 66 | output flow-control is unsupported, input flow-control is unsupported 67 | ARP type: ARPA, ARP Timeout 04:00:00 68 | Last input 3w1d, output 00:00:08, output hang never 69 | Last clearing of "show interface" counters never 70 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 71 | Queueing strategy: fifo 72 | Output queue: 0/40 (size/max) 73 | 5 minute input rate 0 bits/sec, 0 packets/sec 74 | 5 minute output rate 0 bits/sec, 0 packets/sec 75 | 487240 packets input, 36339153 bytes, 0 no buffer 76 | Received 8 broadcasts (0 IP multicasts) 77 | 183253 runts, 0 giants, 0 throttles 78 | 183253 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 79 | 0 watchdog, 0 multicast, 0 pause input 80 | 1115575 packets output, 73936479 bytes, 0 underruns 81 | 0 output errors, 0 collisions, 12 interface resets 82 | 0 unknown protocol drops 83 | 0 babbles, 0 late collision, 0 deferred 84 | 5 lost carrier, 0 no carrier, 0 pause output 85 | 0 output buffer failures, 0 output buffers swapped out 86 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/output/ios/show_version.txt: -------------------------------------------------------------------------------- 1 | Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2) 2 | Technical Support: http://www.cisco.com/techsupport 3 | Copyright (c) 1986-2016 by Cisco Systems, Inc. 4 | Compiled Tue 22-Mar-16 16:19 by prod_rel_team 5 | 6 | 7 | ROM: Bootstrap program is IOSv 8 | 9 | an-ios-01 uptime is 10 weeks, 6 days, 22 hours, 30 minutes 10 | System returned to ROM by reload 11 | System image file is "flash0:/vios-adventerprisek9-m" 12 | Last reload reason: Unknown reason 13 | 14 | 15 | 16 | This product contains cryptographic features and is subject to United 17 | States and local country laws governing import, export, transfer and 18 | use. Delivery of Cisco cryptographic products does not imply 19 | third-party authority to import, export, distribute or use encryption. 20 | Importers, exporters, distributors and users are responsible for 21 | compliance with U.S. and local country laws. By using this product you 22 | agree to comply with applicable laws and regulations. If you are unable 23 | to comply with U.S. and local laws, return this product immediately. 24 | 25 | A summary of U.S. laws governing Cisco cryptographic products may be found at: 26 | http://www.cisco.com/wwl/export/crypto/tool/stqrg.html 27 | 28 | If you require further assistance please contact us by sending email to 29 | export@cisco.com. 30 | 31 | Cisco IOSv (revision 1.0) with with 460033K/62464K bytes of memory. 32 | Processor board ID 92O0KON393UV5P77JRKZ5 33 | 4 Gigabit Ethernet interfaces 34 | DRAM configuration is 72 bits wide with parity disabled. 35 | 256K bytes of non-volatile configuration memory. 36 | 2097152K bytes of ATA System CompactFlash 0 (Read/Write) 37 | 0K bytes of ATA CompactFlash 1 (Read/Write) 38 | 0K bytes of ATA CompactFlash 2 (Read/Write) 39 | 10080K bytes of ATA CompactFlash 3 (Read/Write) 40 | 41 | 42 | 43 | Configuration register is 0x0 44 | 45 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/parser_templates/ios/show_interfaces.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: parser meta data 3 | parser_metadata: 4 | version: 1.0 5 | command: show interface 6 | network_os: ios 7 | 8 | - name: match sections 9 | pattern_match: 10 | regex: "^(\\S+) is up," 11 | match_all: true 12 | match_greedy: true 13 | register: section 14 | 15 | - name: match interface values 16 | pattern_group: 17 | - name: match name 18 | pattern_match: 19 | regex: "^(\\S+)" 20 | content: "{{ item }}" 21 | register: name 22 | 23 | - name: match hardware 24 | pattern_match: 25 | regex: "Hardware is (\\S+)," 26 | content: "{{ item }}" 27 | register: type 28 | 29 | - name: match mtu 30 | pattern_match: 31 | regex: "MTU (\\d+)" 32 | content: "{{ item }}" 33 | register: mtu 34 | 35 | - name: match description 36 | pattern_match: 37 | regex: "Description: (.*)" 38 | content: "{{ item }}" 39 | register: description 40 | loop: "{{ section }}" 41 | register: interfaces 42 | 43 | - name: generate json data structure 44 | json_template: 45 | template: 46 | - key: "{{ item.name.matches.0 }}" 47 | object: 48 | - key: config 49 | object: 50 | - key: name 51 | value: "{{ item.name.matches.0 }}" 52 | - key: type 53 | value: "{{ item.type.matches.0 }}" 54 | - key: mtu 55 | value: "{{ item.mtu.matches.0 }}" 56 | - key: description 57 | value: "{{ item.description.matches.0 }}" 58 | loop: "{{ interfaces }}" 59 | export: true 60 | export_as: "{{ export_type }}" 61 | register: interface_facts 62 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/parser_templates/ios/show_interfaces_expand.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: parser meta data 3 | parser_metadata: 4 | version: 1.0 5 | command: show interface 6 | network_os: ios 7 | 8 | - name: match sections 9 | pattern_match: 10 | regex: "^(\\S+) is up," 11 | match_all: true 12 | match_greedy: true 13 | register: section 14 | 15 | - name: match interface values 16 | pattern_group: 17 | - name: match name 18 | pattern_match: 19 | regex: "^(\\S+)" 20 | content: "{{ item }}" 21 | register: name 22 | 23 | - name: match hardware 24 | pattern_match: 25 | regex: "Hardware is (\\S+)," 26 | content: "{{ item }}" 27 | register: type 28 | 29 | - name: match mtu 30 | pattern_match: 31 | regex: "MTU (\\d+)" 32 | content: "{{ item }}" 33 | register: mtu 34 | 35 | - name: match description 36 | pattern_match: 37 | regex: "Description: (.*)" 38 | content: "{{ item }}" 39 | register: description 40 | loop: "{{ section }}" 41 | register: interfaces 42 | 43 | - name: generate json data structure 44 | json_template: 45 | template: 46 | - key: "{{ item.name.matches.0 }}" 47 | object: 48 | - key: config 49 | object: 50 | - key: name 51 | value: "{{ item.name.matches.0 }}" 52 | - key: type 53 | value: "{{ item.type.matches.0 }}" 54 | - key: mtu 55 | value: "{{ item.mtu.matches.0 }}" 56 | - key: description 57 | value: "{{ item.description.matches.0 }}" 58 | loop: "{{ interfaces }}" 59 | export: true 60 | register: interface_facts 61 | extend: test.extension 62 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/parser_templates/ios/show_version.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: parser meta data 3 | parser_metadata: 4 | version: 1.0 5 | command: show version 6 | network_os: ios 7 | 8 | - name: match version 9 | pattern_match: 10 | regex: "Version (\\S+)," 11 | register: version 12 | 13 | - name: match model 14 | pattern_match: 15 | regex: "^Cisco (.+) \\(revision" 16 | register: model 17 | 18 | - name: match image 19 | pattern_match: 20 | regex: "^System image file is (\\S+)" 21 | register: image 22 | 23 | - name: match uptime 24 | pattern_match: 25 | regex: "uptime is (.+)" 26 | register: uptime 27 | 28 | - name: match total memory 29 | pattern_match: 30 | regex: "with (\\S+)/(\\w*) bytes of memory" 31 | register: total_mem 32 | 33 | - name: match free memory 34 | pattern_match: 35 | regex: "with \\w*/(\\S+) bytes of memory" 36 | register: free_mem 37 | 38 | - name: export system facts to playbook 39 | set_vars: 40 | model: "{{ model.matches.0 }}" 41 | image_file: "{{ image.matches.0 }}" 42 | uptime: "{{ uptime.matches.0 }}" 43 | version: "{{ version.matches.0 }}" 44 | memory: 45 | total: "{{ total_mem.matches.0 }}" 46 | free: "{{ free_mem.matches.0 }}" 47 | export: true 48 | register: system_facts 49 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/parser_templates/ios/show_version_expand.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: parser meta data 3 | parser_metadata: 4 | version: 1.0 5 | command: show version 6 | network_os: ios 7 | 8 | - name: match version 9 | pattern_match: 10 | regex: "Version (\\S+)," 11 | register: version 12 | 13 | - name: match model 14 | pattern_match: 15 | regex: "^Cisco (.+) \\(revision" 16 | register: model 17 | 18 | - name: match image 19 | pattern_match: 20 | regex: "^System image file is (\\S+)" 21 | register: image 22 | 23 | - name: match uptime 24 | pattern_match: 25 | regex: "uptime is (.+)" 26 | register: uptime 27 | 28 | - name: match total memory 29 | pattern_match: 30 | regex: "with (\\S+)/(\\w*) bytes of memory" 31 | register: total_mem 32 | 33 | - name: match free memory 34 | pattern_match: 35 | regex: "with \\w*/(\\S+) bytes of memory" 36 | register: free_mem 37 | 38 | - name: export system facts to playbook 39 | set_vars: 40 | model: "{{ model.matches.0 }}" 41 | image_file: "{{ image.matches.0 }}" 42 | uptime: "{{ uptime.matches.0 }}" 43 | version: "{{ version.matches.0 }}" 44 | memory: 45 | total: "{{ total_mem.matches.0 }}" 46 | free: "{{ free_mem.matches.0 }}" 47 | export: true 48 | register: system_facts 49 | extend: test.extension 50 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/tasks/ios.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "command_parser test for {{ ansible_network_os }} show_interface" 3 | command_parser: 4 | file: "{{ parser_path }}/show_interfaces.yaml" 5 | content: "{{ lookup('file', '{{ output_path }}/show_interfaces.txt') }}" 6 | register: result 7 | vars: 8 | - ansible_network_os: ios 9 | 10 | - assert: 11 | that: 12 | - "'interface_facts' in result.ansible_facts" 13 | - "'GigabitEthernet0/0' in result.ansible_facts.interface_facts[0]" 14 | - "'GigabitEthernet0/1' in result.ansible_facts.interface_facts[1]" 15 | - "result.ansible_facts.interface_facts[0]['GigabitEthernet0/0']['config']['name'] == 'GigabitEthernet0/0'" 16 | - "result.ansible_facts.interface_facts[0]['GigabitEthernet0/0']['config']['description'] == 'OOB Management'" 17 | - "result.ansible_facts.interface_facts[1]['GigabitEthernet0/1']['config']['name'] == 'GigabitEthernet0/1'" 18 | - "result.ansible_facts.interface_facts[1]['GigabitEthernet0/1']['config']['description'] == 'test-interface'" 19 | 20 | - name: "command_parser test for {{ ansible_network_os }} show_version" 21 | command_parser: 22 | file: "{{ parser_path }}/show_version.yaml" 23 | content: "{{ lookup('file', '{{ output_path }}/show_version.txt') }}" 24 | register: result 25 | vars: 26 | - ansible_network_os: ios 27 | 28 | - assert: 29 | that: 30 | - "'system_facts' in result.ansible_facts" 31 | - "'flash0:/vios-adventerprisek9-m' in result.ansible_facts.system_facts['image_file']" 32 | - "'IOSv' in result.ansible_facts.system_facts['model']" 33 | - "'15.6(2)T' in result.ansible_facts.system_facts['version']" 34 | - "'10 weeks, 6 days, 22 hours, 30 minutes' in result.ansible_facts.system_facts['uptime']" 35 | - "'62464K' in result.ansible_facts.system_facts['memory']['free']" 36 | - "'460033K' in result.ansible_facts.system_facts['memory']['total']" 37 | 38 | - name: "command_parser expansion test for {{ ansible_network_os }} show_version" 39 | command_parser: 40 | file: "{{ parser_path }}/show_version_expand.yaml" 41 | content: "{{ lookup('file', '{{ output_path }}/show_version.txt') }}" 42 | register: result 43 | vars: 44 | - ansible_network_os: ios 45 | 46 | - assert: 47 | that: 48 | - "'system_facts' in result.ansible_facts.test.extension" 49 | - "'flash0:/vios-adventerprisek9-m' in result.ansible_facts.test.extension.system_facts['image_file']" 50 | - "'IOSv' in result.ansible_facts.test.extension.system_facts['model']" 51 | - "'15.6(2)T' in result.ansible_facts.test.extension.system_facts['version']" 52 | - "'10 weeks, 6 days, 22 hours, 30 minutes' in result.ansible_facts.test.extension.system_facts['uptime']" 53 | - "'62464K' in result.ansible_facts.test.extension.system_facts['memory']['free']" 54 | - "'460033K' in result.ansible_facts.test.extension.system_facts['memory']['total']" 55 | 56 | - name: "command_parser expansion test for {{ ansible_network_os }} show_interface" 57 | command_parser: 58 | file: "{{ parser_path }}/show_interfaces_expand.yaml" 59 | content: "{{ lookup('file', '{{ output_path }}/show_interfaces.txt') }}" 60 | register: result 61 | vars: 62 | - ansible_network_os: ios 63 | 64 | - assert: 65 | that: 66 | - "'system_facts' in result.ansible_facts.test.extension" 67 | - "'flash0:/vios-adventerprisek9-m' in result.ansible_facts.test.extension.system_facts['image_file']" 68 | - "'IOSv' in result.ansible_facts.test.extension.system_facts['model']" 69 | - "'15.6(2)T' in result.ansible_facts.test.extension.system_facts['version']" 70 | - "'10 weeks, 6 days, 22 hours, 30 minutes' in result.ansible_facts.test.extension.system_facts['uptime']" 71 | - "'62464K' in result.ansible_facts.test.extension.system_facts['memory']['free']" 72 | - "'460033K' in result.ansible_facts.test.extension.system_facts['memory']['total']" 73 | - "'interface_facts' in result.ansible_facts.test.extension" 74 | - "'GigabitEthernet0/0' in result.ansible_facts.test.extension.interface_facts[0]" 75 | - "'GigabitEthernet0/1' in result.ansible_facts.test.extension.interface_facts[1]" 76 | - "result.ansible_facts.test.extension.interface_facts[0]['GigabitEthernet0/0']['config']['name'] == 'GigabitEthernet0/0'" 77 | - "result.ansible_facts.test.extension.interface_facts[0]['GigabitEthernet0/0']['config']['description'] == 'OOB Management'" 78 | - "result.ansible_facts.test.extension.interface_facts[1]['GigabitEthernet0/1']['config']['name'] == 'GigabitEthernet0/1'" 79 | - "result.ansible_facts.test.extension.interface_facts[1]['GigabitEthernet0/1']['config']['description'] == 'test-interface'" 80 | -------------------------------------------------------------------------------- /tests/command_parser/command_parser/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/command_parser/command_parser')[0] }}" 5 | 6 | - name: ios command_parser test 7 | import_tasks: ios.yaml 8 | -------------------------------------------------------------------------------- /tests/command_parser/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - command_parser 6 | -------------------------------------------------------------------------------- /tests/config_template/config_template/tasks/config_template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | test: string 4 | 5 | - set_fact: 6 | test_pass: "{{ lookup('config_template', 'pass.j2') }}" 7 | test_fail: "{{ lookup('config_template', 'fail.j2') }}" 8 | 9 | - assert: 10 | that: 11 | - "'test string' in test_pass" 12 | - "'test string' not in test_fail" 13 | -------------------------------------------------------------------------------- /tests/config_template/config_template/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/config_template/config_template')[0] }}" 5 | 6 | - name: config_template test 7 | import_tasks: config_template.yaml 8 | -------------------------------------------------------------------------------- /tests/config_template/config_template/templates/fail.j2: -------------------------------------------------------------------------------- 1 | test {{ bad | default(omit) }} 2 | -------------------------------------------------------------------------------- /tests/config_template/config_template/templates/pass.j2: -------------------------------------------------------------------------------- 1 | test {{ test }} 2 | -------------------------------------------------------------------------------- /tests/config_template/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - config_template 6 | -------------------------------------------------------------------------------- /tests/interface_range/interface_range/tasks/interface_range.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: interface_range Ethernet1-3 3 | debug: 4 | msg: "{{ 'Ethernet1-3' | interface_range }}" 5 | register: result 6 | 7 | - assert: 8 | that: 9 | - "'Ethernet1' in result.msg" 10 | - "'Ethernet2' in result.msg" 11 | - "'Ethernet3' in result.msg" 12 | 13 | - name: interface_range Ethernet1,3-4,5 14 | debug: 15 | msg: "{{ 'Ethernet1,3-4,5' | interface_range }}" 16 | register: result 17 | 18 | - assert: 19 | that: 20 | - "'Ethernet1' in result.msg" 21 | - "'Ethernet3' in result.msg" 22 | - "'Ethernet4' in result.msg" 23 | - "'Ethernet5' in result.msg" 24 | 25 | - name: interface_range Ethernet1/3-5,8 26 | debug: 27 | msg: "{{ 'Ethernet1/3-5,8' | interface_range }}" 28 | register: result 29 | 30 | - assert: 31 | that: 32 | - "'Ethernet1/3' in result.msg" 33 | - "'Ethernet1/4' in result.msg" 34 | - "'Ethernet1/5' in result.msg" 35 | - "'Ethernet1/8' in result.msg" 36 | -------------------------------------------------------------------------------- /tests/interface_range/interface_range/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/interface_range/interface_range')[0] }}" 5 | 6 | - name: interface_range test 7 | import_tasks: interface_range.yaml 8 | -------------------------------------------------------------------------------- /tests/interface_range/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - interface_range 6 | -------------------------------------------------------------------------------- /tests/interface_split/interface_split/tasks/interface_split.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: interface_split Ethernet1 3 | debug: 4 | msg: "{{ 'Ethernet1' | interface_split }}" 5 | register: result 6 | 7 | - assert: 8 | that: 9 | - "'1' in result.msg.index" 10 | - "'Ethernet' in result.msg.name" 11 | 12 | - name: interface_split Ethernet1 name 13 | debug: 14 | msg: "{{ 'Ethernet1' | interface_split('name') }}" 15 | register: result 16 | 17 | - assert: 18 | that: 19 | - "'Ethernet' in result.msg" 20 | 21 | - name: interface_split Ethernet1 index 22 | debug: 23 | msg: "{{ 'Ethernet1' | interface_split('index') }}" 24 | register: result 25 | 26 | - assert: 27 | that: 28 | - "'1' in result.msg" 29 | -------------------------------------------------------------------------------- /tests/interface_split/interface_split/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/interface_split/interface_split')[0] }}" 5 | 6 | - name: interface_split test 7 | import_tasks: interface_split.yaml 8 | -------------------------------------------------------------------------------- /tests/interface_split/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - interface_split 6 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /tests/json_template/json_template/defaults/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | template_path: "{{ role_path }}/templates" 3 | -------------------------------------------------------------------------------- /tests/json_template/json_template/tasks/json_lookup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: generate config 3 | debug: 4 | msg: "{{ lookup('json_template', '{{ template_path }}/config.json') }}" 5 | register: result 6 | 7 | - assert: 8 | that: 9 | - "'GigabitEthernet0/0' in result.msg" 10 | - "'config' in result['msg']['GigabitEthernet0/0']" 11 | - "'Configured by Ansible' in result['msg']['GigabitEthernet0/0']['config']['description']" 12 | - "result['msg']['GigabitEthernet0/0']['config']['mtu'] == '1500'" 13 | - "'iGbE' in result['msg']['GigabitEthernet0/0']['config']['type']" 14 | - "'GigabitEthernet0/0' in result['msg']['GigabitEthernet0/0']['config']['name']" 15 | -------------------------------------------------------------------------------- /tests/json_template/json_template/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/json_template/json_template')[0] }}" 5 | 6 | - name: json_template lookup plugin test 7 | import_tasks: json_lookup.yaml 8 | -------------------------------------------------------------------------------- /tests/json_template/json_template/templates/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "object": [ 3 | { 4 | "object": [ 5 | { 6 | "value": "GigabitEthernet0/0", 7 | "key": "name" 8 | }, 9 | { 10 | "value": "iGbE", 11 | "key": "type" 12 | }, 13 | { 14 | "value": "1500", 15 | "key": "mtu" 16 | }, 17 | { 18 | "value": "Configured by Ansible", 19 | "key": "description" 20 | } 21 | ], 22 | "key": "config" 23 | } 24 | ], 25 | "key": "GigabitEthernet0/0" 26 | } 27 | -------------------------------------------------------------------------------- /tests/json_template/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - json_template 6 | -------------------------------------------------------------------------------- /tests/netcfg_diff/netcfg_diff/defaults/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | want_file: "{{ role_path }}/files/{{ ansible_network_os }}/want.txt" 3 | have_file: "{{ role_path }}/files/{{ ansible_network_os }}/have.txt" 4 | 5 | want: "{{ lookup('file', want_file) }}" 6 | have: "{{ lookup('file', have_file) }}" 7 | -------------------------------------------------------------------------------- /tests/netcfg_diff/netcfg_diff/files/ios/have.txt: -------------------------------------------------------------------------------- 1 | Building configuration... 2 | 3 | 4 | Current configuration : 7250 bytes 5 | ! 6 | ! Last configuration change at 01:37:00 UTC Sun May 27 2018 by ansible 7 | ! 8 | version 14.6 9 | service timestamps debug datetime msec 10 | service timestamps log datetime msec 11 | no service password-encryption 12 | ! 13 | hostname ios01 14 | ! 15 | boot-start-marker 16 | boot-end-marker 17 | ! 18 | ! 19 | vrf definition Mgmt-intf 20 | ! 21 | address-family ipv6 22 | exit-address-family 23 | ! 24 | address-family ipv6 25 | exit-address-family 26 | ! 27 | no logging buffered 28 | no logging console 29 | ! 30 | crypto pki trustpoint example.com 31 | enrollment selfsigned 32 | subject-name cn=samsung-ubuntu.actionmystique.net-Certificate 33 | rsakeypair example.com 34 | revocation-check none 35 | ! 36 | crypto pki certificate chain example.com 37 | username cisco privilege 15 secret 5 $1$u6jn$cZ5bnTL3wGFL3362agGH11 38 | username ansible privilege 15 secret 5 $1$R503$jIYk.0/0/toCal0O08Yr70 39 | username test 40 | ! 41 | redundancy 42 | ! 43 | no cdp run 44 | ! 45 | interface Loopback0 46 | description Loopback 47 | ip address 192.168.255.2 255.255.255.255 48 | ! 49 | interface GigabitEthernet0/0 50 | description OOB Management 51 | vrf forwarding Mgmt-intf 52 | ip address 20.20.20.20 255.255.255.0 53 | duplex full 54 | speed auto 55 | media-type rj45 56 | ! 57 | interface GigabitEthernet0/1 58 | description test-interface 59 | mtu 2000 60 | no ip address 61 | ip ospf cost 12 62 | duplex full 63 | speed 1000 64 | media-type rj45 65 | ! 66 | router ospf 1 67 | passive-interface Loopback0 68 | network 172.31.0.8 0.0.0.3 area 0 69 | network 172.31.0.12 0.0.0.3 area 0 70 | network 172.31.0.24 0.0.0.3 area 0 71 | network 192.168.255.2 0.0.0.0 area 0 72 | ! 73 | router bgp 1 74 | template peer-session IBGP 75 | remote-as 65001 76 | update-source Loopback0 77 | exit-peer-session 78 | ! 79 | bgp router-id 192.168.255.2 80 | bgp log-neighbor-changes 81 | neighbor 192.168.255.1 remote-as 1 82 | neighbor 192.168.255.1 description iBGP peer nxos01 83 | neighbor 192.168.255.1 update-source Loopback0 84 | neighbor 192.168.255.4 remote-as 1 85 | neighbor 192.168.255.4 description iBGP peer csr01 86 | neighbor 192.168.255.4 update-source Loopback0 87 | neighbor 192.168.255.5 remote-as 1 88 | neighbor 192.168.255.5 description iBGP peer iosxr01 89 | neighbor 192.168.255.5 update-source Loopback0 90 | neighbor 192.168.255.6 remote-as 1 91 | neighbor 192.168.255.6 description iBGP peer nxos02 92 | neighbor 192.168.255.6 update-source Loopback0 93 | neighbor 192.168.255.7 remote-as 1 94 | neighbor 192.168.255.7 description iBGP peer csr02 95 | neighbor 192.168.255.7 update-source Loopback0 96 | neighbor 192.168.255.8 remote-as 1 97 | neighbor 192.168.255.8 description iBGP peer ios02 98 | neighbor 192.168.255.8 update-source Loopback0 99 | neighbor 192.168.255.9 remote-as 1 100 | neighbor 192.168.255.9 description iBGP peer iosxr02 101 | neighbor 192.168.255.9 update-source Loopback0 102 | neighbor 192.168.255.10 remote-as 1 103 | neighbor 192.168.255.10 description iBGP peer nxos9k01 104 | neighbor 192.168.255.10 update-source Loopback0 105 | neighbor 192.168.255.11 remote-as 1 106 | neighbor 192.168.255.11 description iBGP peer nxos9k02 107 | neighbor 192.168.255.11 update-source Loopback0 108 | ! 109 | address-family ipv4 110 | network 192.168.255.2 mask 255.255.255.255 111 | neighbor 192.168.255.1 activate 112 | neighbor 192.168.255.4 activate 113 | neighbor 192.168.255.5 activate 114 | neighbor 192.168.255.6 activate 115 | neighbor 192.168.255.7 activate 116 | neighbor 192.168.255.8 activate 117 | neighbor 192.168.255.9 activate 118 | neighbor 192.168.255.10 activate 119 | neighbor 192.168.255.11 activate 120 | exit-address-family 121 | ! 122 | ip forward-protocol nd 123 | ! 124 | ! 125 | no ip http server 126 | no ip http secure-server 127 | ip route 192.168.10.0 255.255.255.0 192.168.1.1 2 128 | ip route vrf Mgmt-intf 10.0.0.0 255.0.0.0 10.8.38.1 129 | ip ssh version 2 130 | ip ssh pubkey-chain 131 | username ansible 132 | key-hash ssh-rsa 23C70B25FA2899AB2B77CC5719DDE5CA 133 | ip ssh server algorithm authentication publickey password 134 | ip scp server enable 135 | ! 136 | logging trap warnings 137 | ! 138 | -------------------------------------------------------------------------------- /tests/netcfg_diff/netcfg_diff/files/ios/want.txt: -------------------------------------------------------------------------------- 1 | Building configuration... 2 | 3 | 4 | Current configuration : 7250 bytes 5 | ! 6 | ! Last configuration change at 01:37:00 UTC Sun May 27 2018 by ansible 7 | ! 8 | version 15.6 9 | service timestamps debug datetime msec 10 | service timestamps log datetime msec 11 | no service password-encryption 12 | ! 13 | hostname ios01 14 | ! 15 | boot-start-marker 16 | boot-end-marker 17 | ! 18 | ! 19 | vrf definition Mgmt-intf 20 | ! 21 | address-family ipv4 22 | exit-address-family 23 | ! 24 | address-family ipv6 25 | exit-address-family 26 | ! 27 | no logging buffered 28 | no logging console 29 | ! 30 | crypto pki trustpoint example.com 31 | enrollment selfsigned 32 | subject-name cn=samsung-ubuntu.actionmystique.net-Certificate 33 | revocation-check none 34 | rsakeypair example.com 35 | ! 36 | crypto pki certificate chain example.com 37 | username cisco privilege 15 secret 5 $1$u6jn$cZ5bnTL3wGFL3362agGH11 38 | username ansible privilege 15 secret 5 $1$R503$jIYk.0/0/toCal0O08Yr70 39 | username test 40 | ! 41 | redundancy 42 | ! 43 | no cdp run 44 | ! 45 | interface Loopback0 46 | description Loopback 47 | ip address 192.168.255.2 255.255.255.255 48 | ! 49 | interface GigabitEthernet0/0 50 | description OOB Management 51 | vrf forwarding Mgmt-intf 52 | ip address 20.20.20.20 255.255.255.0 53 | duplex full 54 | speed auto 55 | media-type rj45 56 | ! 57 | interface GigabitEthernet0/1 58 | description test-interface 59 | mtu 2000 60 | no ip address 61 | ip ospf cost 1 62 | shutdown 63 | duplex full 64 | speed 1000 65 | media-type rj45 66 | ! 67 | router ospf 1 68 | passive-interface Loopback0 69 | network 172.31.0.8 0.0.0.3 area 0 70 | network 172.31.0.12 0.0.0.3 area 0 71 | network 172.31.0.24 0.0.0.3 area 0 72 | network 192.168.255.2 0.0.0.0 area 0 73 | ! 74 | router bgp 1 75 | template peer-session IBGP 76 | remote-as 65001 77 | update-source Loopback0 78 | exit-peer-session 79 | ! 80 | bgp router-id 192.168.255.2 81 | bgp log-neighbor-changes 82 | neighbor 192.168.255.1 remote-as 1 83 | neighbor 192.168.255.1 description iBGP peer nxos01 84 | neighbor 192.168.255.1 update-source Loopback0 85 | neighbor 192.168.255.4 remote-as 1 86 | neighbor 192.168.255.4 description iBGP peer csr01 87 | neighbor 192.168.255.4 update-source Loopback0 88 | neighbor 192.168.255.5 remote-as 1 89 | neighbor 192.168.255.5 description iBGP peer iosxr01 90 | neighbor 192.168.255.5 update-source Loopback0 91 | neighbor 192.168.255.6 remote-as 1 92 | neighbor 192.168.255.6 description iBGP peer nxos02 93 | neighbor 192.168.255.6 update-source Loopback0 94 | neighbor 192.168.255.7 remote-as 1 95 | neighbor 192.168.255.7 description iBGP peer csr02 96 | neighbor 192.168.255.7 update-source Loopback0 97 | neighbor 192.168.255.8 remote-as 1 98 | neighbor 192.168.255.8 description iBGP peer ios02 99 | neighbor 192.168.255.8 update-source Loopback0 100 | neighbor 192.168.255.9 remote-as 1 101 | neighbor 192.168.255.9 description iBGP peer iosxr02 102 | neighbor 192.168.255.9 update-source Loopback0 103 | neighbor 192.168.255.10 remote-as 1 104 | neighbor 192.168.255.10 description iBGP peer nxos9k01 105 | neighbor 192.168.255.10 update-source Loopback0 106 | neighbor 192.168.255.11 remote-as 1 107 | neighbor 192.168.255.11 description iBGP peer nxos9k02 108 | neighbor 192.168.255.11 update-source Loopback0 109 | ! 110 | address-family ipv4 111 | network 192.168.255.2 mask 255.255.255.255 112 | neighbor 192.168.255.1 activate 113 | neighbor 192.168.255.4 activate 114 | neighbor 192.168.255.5 activate 115 | neighbor 192.168.255.6 activate 116 | neighbor 192.168.255.7 activate 117 | neighbor 192.168.255.8 activate 118 | neighbor 192.168.255.9 activate 119 | neighbor 192.168.255.10 activate 120 | neighbor 192.168.255.11 activate 121 | exit-address-family 122 | ! 123 | ip forward-protocol nd 124 | ! 125 | ! 126 | no ip http server 127 | no ip http secure-server 128 | ip route 192.168.10.0 255.255.255.0 192.168.1.1 2 129 | ip route vrf Mgmt-intf 10.0.0.0 255.0.0.0 10.8.38.1 130 | ip ssh version 2 131 | ip ssh pubkey-chain 132 | username ansible 133 | key-hash ssh-rsa 23C70B25FA2899AB2B77CC5719DDE5CA 134 | ip ssh server algorithm authentication publickey password 135 | ip scp server enable 136 | ! 137 | logging trap warnings 138 | ! 139 | -------------------------------------------------------------------------------- /tests/netcfg_diff/netcfg_diff/tasks/ios.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "config diff test for {{ ansible_network_os }} with default" 3 | set_fact: 4 | diff: "{{ lookup('netcfg_diff', want, have=have) }}" 5 | vars: 6 | - ansible_network_os: ios 7 | 8 | - assert: 9 | that: 10 | - "'version 15.6' in diff" 11 | - "'vrf definition Mgmt-intf\naddress-family ipv4' in diff" 12 | - "'crypto pki trustpoint example.com\nrevocation-check none\nrsakeypair example.com' not in diff" 13 | - "'no logging console' not in diff" 14 | 15 | - name: "config diff test for {{ ansible_network_os }} with strict match" 16 | set_fact: 17 | diff: "{{ lookup('netcfg_diff', want, have=have, match='strict') }}" 18 | vars: 19 | - ansible_network_os: ios 20 | 21 | - assert: 22 | that: 23 | - "'crypto pki trustpoint example.com\nrevocation-check none\nrsakeypair example.com' in diff" 24 | - "'no logging console' not in diff" 25 | 26 | - name: "config diff test for {{ ansible_network_os }} with exact match" 27 | set_fact: 28 | diff: "{{ lookup('netcfg_diff', want, have=have, match='exact') }}" 29 | vars: 30 | - ansible_network_os: ios 31 | 32 | - assert: 33 | that: 34 | - "'no logging console' in diff" 35 | 36 | - name: "config diff test for {{ ansible_network_os }} with block replace" 37 | set_fact: 38 | diff: "{{ lookup('netcfg_diff', want, have=have, replace='block') }}" 39 | vars: 40 | - ansible_network_os: ios 41 | 42 | - assert: 43 | that: 44 | - "'interface GigabitEthernet0/1' in diff" 45 | - "'description test-interface' in diff" 46 | - "'mtu 2000' in diff" 47 | - "'no ip address' in diff" 48 | - "'ip ospf cost 1' in diff" 49 | - "'shutdown' in diff" 50 | - "'duplex full' in diff" 51 | - "'speed 1000' in diff" 52 | - "'media-type rj45' in diff" 53 | 54 | - name: "config diff test for {{ ansible_network_os }} with block line" 55 | set_fact: 56 | diff: "{{ lookup('netcfg_diff', want, have=have, replace='line') }}" 57 | vars: 58 | - ansible_network_os: ios 59 | 60 | - assert: 61 | that: 62 | - "'interface GigabitEthernet0/1' in diff" 63 | - "'ip ospf cost 1' in diff" 64 | - "'shutdown' in diff" 65 | -------------------------------------------------------------------------------- /tests/netcfg_diff/netcfg_diff/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/netcfg_diff/netcfg_diff')[0] }}" 5 | 6 | - name: ios config diff 7 | import_tasks: ios.yaml 8 | -------------------------------------------------------------------------------- /tests/netcfg_diff/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - netcfg_diff 6 | -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # main entrypoint 3 | 4 | - import_playbook: command_parser/test.yml 5 | - import_playbook: textfsm_parser/test.yml 6 | - import_playbook: json_template/test.yml 7 | - import_playbook: vlan_compress/test.yml 8 | - import_playbook: vlan_expand/test.yml 9 | - import_playbook: netcfg_diff/test.yml 10 | - import_playbook: interface_range/test.yml 11 | - import_playbook: interface_split/test.yml 12 | - import_playbook: config_template/test.yml 13 | - import_playbook: validate_role_spec/test.yml 14 | -------------------------------------------------------------------------------- /tests/textfsm_parser/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - textfsm_parser 6 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/defaults/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser_path: "{{ role_path }}/parser_templates/{{ ansible_network_os }}" 3 | output_path: "{{ role_path }}/output/{{ ansible_network_os }}" 4 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/output/ios/show_interfaces.txt: -------------------------------------------------------------------------------- 1 | GigabitEthernet0/0 is up, line protocol is up 2 | Hardware is iGbE, address is 5e00.0002.0000 (bia 5e00.0002.0000) 3 | Description: OOB Management 4 | Internet address is 10.8.38.65/24 5 | MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 6 | reliability 253/255, txload 1/255, rxload 1/255 7 | Encapsulation ARPA, loopback not set 8 | Keepalive set (10 sec) 9 | Full Duplex, Auto Speed, link type is auto, media type is RJ45 10 | output flow-control is unsupported, input flow-control is unsupported 11 | ARP type: ARPA, ARP Timeout 04:00:00 12 | Last input 00:00:00, output 00:00:00, output hang never 13 | Last clearing of "show interface" counters never 14 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 15 | Queueing strategy: fifo 16 | Output queue: 0/40 (size/max) 17 | 5 minute input rate 2000 bits/sec, 2 packets/sec 18 | 5 minute output rate 2000 bits/sec, 2 packets/sec 19 | 4973387 packets input, 816226566 bytes, 0 no buffer 20 | Received 228869 broadcasts (0 IP multicasts) 21 | 461509 runts, 0 giants, 0 throttles 22 | 461509 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 23 | 0 watchdog, 0 multicast, 0 pause input 24 | 3316083 packets output, 432440225 bytes, 0 underruns 25 | 0 output errors, 0 collisions, 3 interface resets 26 | 2303378 unknown protocol drops 27 | 0 babbles, 0 late collision, 0 deferred 28 | 1 lost carrier, 0 no carrier, 0 pause output 29 | 0 output buffer failures, 0 output buffers swapped out 30 | GigabitEthernet0/1 is up, line protocol is up 31 | Hardware is iGbE, address is fa16.3e4e.c5e5 (bia fa16.3e4e.c5e5) 32 | Description: test-interface 33 | MTU 2000 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 34 | reliability 255/255, txload 1/255, rxload 1/255 35 | Encapsulation ARPA, loopback not set 36 | Keepalive set (10 sec) 37 | Full Duplex, 1Gbps, link type is auto, media type is RJ45 38 | output flow-control is unsupported, input flow-control is unsupported 39 | ARP type: ARPA, ARP Timeout 04:00:00 40 | Last input 4d07h, output 4d07h, output hang never 41 | Last clearing of "show interface" counters never 42 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 43 | Queueing strategy: fifo 44 | Output queue: 0/40 (size/max) 45 | 5 minute input rate 0 bits/sec, 0 packets/sec 46 | 5 minute output rate 0 bits/sec, 0 packets/sec 47 | 89815 packets input, 27598643 bytes, 0 no buffer 48 | Received 1 broadcasts (0 IP multicasts) 49 | 0 runts, 0 giants, 0 throttles 50 | 0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 51 | 0 watchdog, 0 multicast, 0 pause input 52 | 404016 packets output, 27896846 bytes, 0 underruns 53 | 0 output errors, 0 collisions, 51 interface resets 54 | 89728 unknown protocol drops 55 | 0 babbles, 0 late collision, 0 deferred 56 | 28 lost carrier, 0 no carrier, 0 pause output 57 | 0 output buffer failures, 0 output buffers swapped out 58 | GigabitEthernet0/2 is up, line protocol is up 59 | Hardware is iGbE, address is fa16.3eca.c938 (bia fa16.3eca.c938) 60 | Description: test-interface-2 61 | MTU 2000 bytes, BW 1000000 Kbit/sec, DLY 10 usec, 62 | reliability 255/255, txload 1/255, rxload 1/255 63 | Encapsulation ARPA, loopback not set 64 | Keepalive set (10 sec) 65 | Full Duplex, 1Gbps, link type is auto, media type is RJ45 66 | output flow-control is unsupported, input flow-control is unsupported 67 | ARP type: ARPA, ARP Timeout 04:00:00 68 | Last input 3w1d, output 00:00:08, output hang never 69 | Last clearing of "show interface" counters never 70 | Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 71 | Queueing strategy: fifo 72 | Output queue: 0/40 (size/max) 73 | 5 minute input rate 0 bits/sec, 0 packets/sec 74 | 5 minute output rate 0 bits/sec, 0 packets/sec 75 | 487240 packets input, 36339153 bytes, 0 no buffer 76 | Received 8 broadcasts (0 IP multicasts) 77 | 183253 runts, 0 giants, 0 throttles 78 | 183253 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 79 | 0 watchdog, 0 multicast, 0 pause input 80 | 1115575 packets output, 73936479 bytes, 0 underruns 81 | 0 output errors, 0 collisions, 12 interface resets 82 | 0 unknown protocol drops 83 | 0 babbles, 0 late collision, 0 deferred 84 | 5 lost carrier, 0 no carrier, 0 pause output 85 | 0 output buffer failures, 0 output buffers swapped out 86 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/output/ios/show_version.txt: -------------------------------------------------------------------------------- 1 | Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2) 2 | Technical Support: http://www.cisco.com/techsupport 3 | Copyright (c) 1986-2016 by Cisco Systems, Inc. 4 | Compiled Tue 22-Mar-16 16:19 by prod_rel_team 5 | 6 | 7 | ROM: Bootstrap program is IOSv 8 | 9 | an-ios-01 uptime is 10 weeks, 6 days, 22 hours, 30 minutes 10 | System returned to ROM by reload 11 | System image file is "flash0:/vios-adventerprisek9-m" 12 | Last reload reason: Unknown reason 13 | 14 | 15 | 16 | This product contains cryptographic features and is subject to United 17 | States and local country laws governing import, export, transfer and 18 | use. Delivery of Cisco cryptographic products does not imply 19 | third-party authority to import, export, distribute or use encryption. 20 | Importers, exporters, distributors and users are responsible for 21 | compliance with U.S. and local country laws. By using this product you 22 | agree to comply with applicable laws and regulations. If you are unable 23 | to comply with U.S. and local laws, return this product immediately. 24 | 25 | A summary of U.S. laws governing Cisco cryptographic products may be found at: 26 | http://www.cisco.com/wwl/export/crypto/tool/stqrg.html 27 | 28 | If you require further assistance please contact us by sending email to 29 | export@cisco.com. 30 | 31 | Cisco IOSv (revision 1.0) with with 460033K/62464K bytes of memory. 32 | Processor board ID 92O0KON393UV5P77JRKZ5 33 | 4 Gigabit Ethernet interfaces 34 | DRAM configuration is 72 bits wide with parity disabled. 35 | 256K bytes of non-volatile configuration memory. 36 | 2097152K bytes of ATA System CompactFlash 0 (Read/Write) 37 | 0K bytes of ATA CompactFlash 1 (Read/Write) 38 | 0K bytes of ATA CompactFlash 2 (Read/Write) 39 | 10080K bytes of ATA CompactFlash 3 (Read/Write) 40 | 41 | 42 | 43 | Configuration register is 0x0 44 | 45 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/parser_templates/ios/show_interfaces: -------------------------------------------------------------------------------- 1 | Value Required name (\S+) 2 | Value type ([\w ]+) 3 | Value description (.*) 4 | Value mtu (\d+) 5 | 6 | Start 7 | ^${name} is up 8 | ^\s+Hardware is ${type} -> Continue 9 | ^\s+Description: ${description} 10 | ^\s+MTU ${mtu} bytes, -> Record 11 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/parser_templates/ios/show_version: -------------------------------------------------------------------------------- 1 | Value version (\S+) 2 | Value model (.+) 3 | Value image (\S+) 4 | Value uptime (.+) 5 | 6 | 7 | Start 8 | ^.*Version ${version}, 9 | ^.*uptime is ${uptime} 10 | ^System image file is ${image} 11 | ^Cisco ${model} \(revision -> Record 12 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/tasks/ios.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: textfsm_parser test for {{ ansible_network_os }} show_interfaces 3 | textfsm_parser: 4 | file: "{{ parser_path }}/show_interfaces" 5 | content: "{{ lookup('file', '{{ output_path }}/show_interfaces.txt') }}" 6 | name: interface_facts 7 | register: result 8 | vars: 9 | - ansible_network_os: ios 10 | 11 | - assert: 12 | that: 13 | - "'interface_facts' in result.ansible_facts" 14 | - "result.ansible_facts.interface_facts[0]['name'] == 'GigabitEthernet0/0'" 15 | - "result.ansible_facts.interface_facts[0]['mtu'] == '1500'" 16 | - "result.ansible_facts.interface_facts[0]['description'] == 'OOB Management'" 17 | - "result.ansible_facts.interface_facts[0]['type'] == 'iGbE'" 18 | - "result.ansible_facts.interface_facts[1]['name'] == 'GigabitEthernet0/1'" 19 | - "result.ansible_facts.interface_facts[1]['mtu'] == '2000'" 20 | - "result.ansible_facts.interface_facts[1]['description'] == 'test-interface'" 21 | - "result.ansible_facts.interface_facts[1]['type'] == 'iGbE'" 22 | - "result.ansible_facts.interface_facts[2]['name'] == 'GigabitEthernet0/2'" 23 | - "result.ansible_facts.interface_facts[2]['mtu'] == '2000'" 24 | - "result.ansible_facts.interface_facts[2]['description'] == 'test-interface-2'" 25 | - "result.ansible_facts.interface_facts[2]['type'] == 'iGbE'" 26 | 27 | - name: textfsm_parser test for {{ ansible_network_os }} show_version 28 | textfsm_parser: 29 | file: "{{ parser_path }}/show_version" 30 | content: "{{ lookup('file', '{{ output_path }}/show_version.txt') }}" 31 | name: system_facts 32 | register: result 33 | vars: 34 | - ansible_network_os: ios 35 | 36 | - assert: 37 | that: 38 | - "'system_facts' in result.ansible_facts" 39 | - "'flash0:/vios-adventerprisek9-m' in result.ansible_facts.system_facts[0]['image']" 40 | - "result.ansible_facts.system_facts[0]['model'] == 'IOSv'" 41 | - "result.ansible_facts.system_facts[0]['uptime'] == '10 weeks, 6 days, 22 hours, 30 minutes'" 42 | - "result.ansible_facts.system_facts[0]['version'] == '15.6(2)T'" 43 | -------------------------------------------------------------------------------- /tests/textfsm_parser/textfsm_parser/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/textfsm_parser/textfsm_parser')[0] }}" 5 | 6 | - name: ios textfsm_parser test 7 | import_tasks: ios.yaml 8 | -------------------------------------------------------------------------------- /tests/to_lines/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - to_lines 6 | -------------------------------------------------------------------------------- /tests/to_lines/to_lines/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/to_lines/to_lines')[0] }}" 5 | 6 | - name: to_lines test 7 | import_tasks: to_lines.yaml 8 | -------------------------------------------------------------------------------- /tests/to_lines/to_lines/tasks/to_lines.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: convert string to lines 3 | set_fact: 4 | test: "{{ 'test\nstring' | to_lines }}" 5 | 6 | - assert: 7 | that: 8 | - test[0] == 'test' 9 | - test[1] == 'string' 10 | -------------------------------------------------------------------------------- /tests/validate_role_spec/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - validate_role_spec 6 | -------------------------------------------------------------------------------- /tests/validate_role_spec/validate_role_spec/meta/failedtest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mutually_exclusive: 3 | - ['int_arg', 'missing_arg'] 4 | -------------------------------------------------------------------------------- /tests/validate_role_spec/validate_role_spec/meta/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | argument_spec: 3 | required_arg: 4 | required: true 5 | int_arg: 6 | type: int 7 | bool_arg: 8 | type: bool 9 | default_arg: 10 | default: test 11 | optional_arg: 12 | 13 | mutually_exclusive: 14 | - ['int_arg', 'missing_arg'] 15 | 16 | required_together: 17 | - ['default_arg', 'optional_arg'] 18 | -------------------------------------------------------------------------------- /tests/validate_role_spec/validate_role_spec/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/validate_role_spec/validate_role_spec')[0] }}" 5 | 6 | - name: validate_role_spec test 7 | import_tasks: validate_role_spec.yaml 8 | 9 | - name: validate_role_spec failed test 10 | import_tasks: validate_role_spec_failed.yaml 11 | -------------------------------------------------------------------------------- /tests/validate_role_spec/validate_role_spec/tasks/validate_role_spec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | required_arg: value 4 | int_arg: 10 5 | bool_arg: true 6 | optional_arg: value 7 | 8 | - name: test validate_role_spec 9 | validate_role_spec: 10 | spec: test.yaml 11 | -------------------------------------------------------------------------------- /tests/validate_role_spec/validate_role_spec/tasks/validate_role_spec_failed.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test failed validate_role_spec 3 | validate_role_spec: 4 | spec: failedtest.yaml 5 | ignore_errors: true 6 | register: result 7 | 8 | - assert: 9 | that: 10 | - "result.failed == true" 11 | - "'missing required field in specification file: argument_spec' in result.msg" 12 | -------------------------------------------------------------------------------- /tests/vlan_compress/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - vlan_compress 6 | -------------------------------------------------------------------------------- /tests/vlan_compress/vlan_compress/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/vlan_compress/vlan_compress')[0] }}" 5 | 6 | - name: vlan_compress test 7 | import_tasks: vlan_compress.yaml 8 | -------------------------------------------------------------------------------- /tests/vlan_compress/vlan_compress/tasks/vlan_compress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: vlan_compress single vlan 3 | debug: 4 | msg: "{{ [1] | vlan_compress }}" 5 | register: result 6 | 7 | - assert: 8 | that: 9 | - "'1' in result.msg" 10 | 11 | - name: vlan_compress list of vlans1 12 | debug: 13 | msg: "{{ [1,2,3,4,5] | vlan_compress }}" 14 | register: result 15 | 16 | - assert: 17 | that: 18 | - "'1-5' in result.msg" 19 | 20 | - name: vlan_compress list of vlans2 21 | debug: 22 | msg: "{{ [1,2,3,5] | vlan_compress }}" 23 | register: result 24 | 25 | - assert: 26 | that: 27 | - "'1-3,5' in result.msg" 28 | 29 | - name: vlan_compress list of vlans3 30 | debug: 31 | msg: "{{ [1,2,4,5,6] | vlan_compress }}" 32 | register: result 33 | 34 | - assert: 35 | that: 36 | - "'1-2,4-6' in result.msg" 37 | -------------------------------------------------------------------------------- /tests/vlan_expand/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | roles: 5 | - vlan_expand 6 | -------------------------------------------------------------------------------- /tests/vlan_expand/vlan_expand/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import dependency role for test 3 | import_role: 4 | name: "{{ role_path.split('/tests/vlan_expand/vlan_expand')[0] }}" 5 | 6 | - name: vlan_expand test 7 | import_tasks: vlan_expand.yaml 8 | -------------------------------------------------------------------------------- /tests/vlan_expand/vlan_expand/tasks/vlan_expand.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: vlan_expand single vlan 3 | debug: 4 | msg: "{{ 'vlan1' | vlan_expand }}" 5 | register: result 6 | 7 | - assert: 8 | that: 9 | - "'1' in result.msg" 10 | 11 | - name: vlan_expand range of vlans1 12 | debug: 13 | msg: "{{ 'vlan1-5' | vlan_expand }}" 14 | register: result 15 | 16 | - assert: 17 | that: 18 | - "'1' in result.msg" 19 | - "'2' in result.msg" 20 | - "'3' in result.msg" 21 | - "'4' in result.msg" 22 | - "'5' in result.msg" 23 | 24 | - name: vlan_expand range of vlans2 25 | debug: 26 | msg: "{{ 'vlan1,3-5,7' | vlan_expand }}" 27 | register: result 28 | 29 | - assert: 30 | that: 31 | - "'1' in result.msg" 32 | - "'3' in result.msg" 33 | - "'4' in result.msg" 34 | - "'5' in result.msg" 35 | - "'7' in result.msg" 36 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | skipsdist = True 4 | envlist = linters,py27,py36,py37 5 | 6 | [testenv] 7 | install_command = pip install {opts} {packages} 8 | commands = 9 | ansible-playbook -i tests/inventory tests/test.yml 10 | deps = 11 | -r{toxinidir}/requirements.txt 12 | -r{toxinidir}/test-requirements.txt 13 | setenv = 14 | ANSIBLE_CALLBACK_PLUGINS = {envsitepackagesdir}/ara/plugins/callbacks 15 | 16 | [testenv:linters] 17 | basepython = python3 18 | commands = 19 | yamllint -s . 20 | flake8 {posargs} 21 | 22 | [testenv:venv] 23 | commands = {posargs} 24 | 25 | [flake8] 26 | # TODO(pabelanger): Follow sane flake8 rules for galaxy and sync across all of 27 | # ansible-network. 28 | ignore = E125,E402 29 | max-line-length = 160 30 | show-source = True 31 | exclude = .venv,.tox,dist,doc,build,*.egg 32 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for network-engine 3 | --------------------------------------------------------------------------------