├── .gitignore ├── LICENSE ├── README.md ├── requestment.txt └── source ├── Makefile ├── _static ├── css │ └── custom.css └── logo │ ├── nornir_logo_00.jpg │ ├── nornir_logo_01.jpg │ ├── nornir_logo_02.jpg │ └── nornir_logo_03.jpg ├── conf.py ├── configuration ├── configuration.ipynb ├── hosts.yaml ├── index.rst └── parameters.rst ├── howto ├── 01.advanced_filtering.ipynb ├── 02.filtering_deep_dive.ipynb ├── 03.handling_connections.ipynb ├── advanced_filtering │ ├── config.yaml │ └── inventory │ │ ├── groups.yaml │ │ └── hosts.yaml ├── filtering_deep_dive │ ├── config.yaml │ └── inventory │ │ ├── groups.yaml │ │ ├── groups_extract.yaml │ │ ├── groups_extract_alternate.yaml │ │ ├── hosts.yaml │ │ ├── hosts_extract.yaml │ │ └── hosts_extract_alternate.yaml ├── handling_connections │ ├── config.yaml │ └── inventory │ │ ├── hosts.yaml │ │ └── test-hosts.yaml └── index.rst ├── index.rst ├── make.bat ├── plugins ├── _static │ ├── execution_model.graffle │ ├── execution_model_1.png │ └── execution_model_2.png ├── execution_model.rst ├── index.rst └── plugins.ipynb └── tutorial ├── 00.index.ipynb ├── 01.overview.ipynb ├── 02.python.ipynb ├── 03.install.ipynb ├── 04.initializing_nornir.ipynb ├── 05.inventory.ipynb ├── 06.tasks.ipynb ├── 07.task_results.ipynb ├── 08.failed_tasks.ipynb ├── 09.processors.ipynb ├── files ├── config.yaml └── inventory │ ├── defaults.yaml │ ├── groups.yaml │ └── hosts.yaml ├── index.rst └── overview └── nornir.svg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | source/_build 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | .dmypy.json 113 | dmypy.json 114 | 115 | # Pyre type checker 116 | .pyre/ 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | 307 | including for purposes of Section 3(b); and 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public 411 | licenses. Notwithstanding, Creative Commons may elect to apply one of 412 | its public licenses to material it publishes and in those instances 413 | will be considered the “Licensor.” The text of the Creative Commons 414 | public licenses is dedicated to the public domain under the CC0 Public 415 | Domain Dedication. Except for the limited purpose of indicating that 416 | material is shared under a Creative Commons public license or as 417 | otherwise permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the 425 | public licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. 428 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nornir 中文手册 2 | 3 | [Nornir](https://github.com/nornir-automation/nornir) 是一个非常优秀的网络设备自动化框架。 4 | 5 | 6 | 7 | 8 | 本手册是基于 [官方文档](https://nornir.readthedocs.io/en/latest/) 的不完全中文翻译,内容相比官方文档有些增删改动,希望对想要使用 Nornir 的朋友有所帮助。 9 | 10 | 本人能力有限,文中难免会有疏漏或表意不当的地方,欢迎大家随时指正:vip@xdai.vip。 11 | 12 | 目前已经基本完成,可以通过 [nornir-docs-cn](https://nornir-docs-cn.readthedocs.io/) 进行在线阅读。 13 | 14 | 全文绝大部分内容使用 [Jupyter](https://jupyter.org/) 编写,可自行安装 Jupyter 后将本项目克隆到本地,在阅读过程中运行文中的代码块进行实践。 15 | 16 | 17 | ## 目录 18 | 19 | - 入门教程 20 | - HowTo 指南 21 | - 配置文件 22 | - 插件 23 | 24 | 25 | ## Nornir 一览图 26 | 27 | ![nornir](source/tutorial/overview/nornir.svg) 28 | 29 | 30 | ## 其他 31 | 32 | 后续还会继续对文档进行完善,加入一些应用场景。 33 | 34 | 如果在阅读中有任何问题,可以提 ISSUE 或者发邮件进行交流。 35 | -------------------------------------------------------------------------------- /requestment.txt: -------------------------------------------------------------------------------- 1 | nbsphinx==0.8.6 2 | -------------------------------------------------------------------------------- /source/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | div.pygments pre { 2 | font-size: 0.8em; 3 | padding: 0.5em 0.5em 0.5em 0.5em; 4 | } 5 | 6 | span.lineno { 7 | color: gray; 8 | } 9 | 10 | span.lineno::after { 11 | content: "|" 12 | } 13 | -------------------------------------------------------------------------------- /source/_static/logo/nornir_logo_00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/_static/logo/nornir_logo_00.jpg -------------------------------------------------------------------------------- /source/_static/logo/nornir_logo_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/_static/logo/nornir_logo_01.jpg -------------------------------------------------------------------------------- /source/_static/logo/nornir_logo_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/_static/logo/nornir_logo_02.jpg -------------------------------------------------------------------------------- /source/_static/logo/nornir_logo_03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/_static/logo/nornir_logo_03.jpg -------------------------------------------------------------------------------- /source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Nornir' 21 | copyright = '2021, xdai555' 22 | author = 'xdai555' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = 'v3.1.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "nbsphinx",] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The language for content autogenerated by Sphinx. Refer to documentation 39 | # for a list of supported languages. 40 | # 41 | # This is also used if you do content translation via gettext catalogs. 42 | # Usually you set "language" from the command line for these cases. 43 | language = 'zh_CN' 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | # This pattern also affects html_static_path and html_extra_path. 48 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/.ipynb_checkpoints"] 49 | 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | 63 | # issues_github_path = "nornir-automation/nornir" -------------------------------------------------------------------------------- /source/configuration/configuration.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "19c426ef-8df0-4269-913f-fc6aff21c9aa", 6 | "metadata": {}, 7 | "source": [ 8 | "## 配置文件\n", 9 | "\n", 10 | "初始化 Nornir 对象时需要加载配置,配置信息由一些配置块及其包含的参数组成,Nornir 默认情况下有 `core`、 `runner`、`inventory`、`ssh`、`logging` 五个部分的默认配置,如果有额外的配置需要指定,可以直接添加配置块,并在代码里面进行调用。\n", 11 | "\n", 12 | "Nornir 提供三种加载配置的方式:\n", 13 | "\n", 14 | " 1. 在代码中以字典类型配置\n", 15 | " 2. 使用系统环境变量\n", 16 | " 3. 使用 YAML 配置文件\n", 17 | "\n", 18 | "可以使用任意一种方式或者三种方式混合的方式提供配置信息,三种方式的优先级**从高到低**依次为:代码、系统环境变量、YAML 配置文件。\n", 19 | "\n", 20 | "### 使用代码\n", 21 | "\n", 22 | "```python\n", 23 | "nr = InitNornir(\n", 24 | " # 使用字典的格式来进行配置\n", 25 | " runner={\"plugin\": \"threaded\", \"options\": {\"num_workers\": 20}},\n", 26 | " logging={\"log_file\": \"mylogs\", \"level\": \"DEBUG\"}\n", 27 | ")\n", 28 | "```\n", 29 | "\n", 30 | "### 使用环境变量\n", 31 | "\n", 32 | "每个配置项都有对应的环境变量键值,可以在下一节查看具体的值,Nornir 初始化时如果相关配置信息没有从代码的字典中找到,则从系统环境变量中查找,下面示例使用 `os` 模块的相关配置来代替已经存在的环境变量,具体使用中应根据对应的系统进行配置。\n", 33 | "\n", 34 | "```python\n", 35 | "# 已经存在的系统环境变量\n", 36 | "import os\n", 37 | "os.environ.setdefault(\"NORNIR_RUNNER_OPTIONS\",\"{'num_workers': 100}\")\n", 38 | "os.environ.setdefault(\"NORNIR_INVENTORY_OPTIONS\",\"{'host_file':'./hosts.yaml',}\")\n", 39 | "\n", 40 | "# 初始化 Nornir,没有传递参数,环境变量中可以读取到相关配置\n", 41 | "from nornir import InitNornir\n", 42 | "nr=InitNornir()\n", 43 | "\n", 44 | "nr.config.runner.options # 查看线程数\n", 45 | "nr.inventory.hosts # 查看主机\n", 46 | "```\n", 47 | "\n", 48 | "\n", 49 | "### 使用配置文件\n", 50 | "\n", 51 | "默认情况下 Nornir 会从程序运行的当前目录读取 `hosts.yaml` 文件,如果不存在则会报错;如果 `hosts.yaml` 中有关于 `groups` 的配置,还会继续加载 `groups.yaml` 文件。\n", 52 | "\n", 53 | "```yaml\n", 54 | "---\n", 55 | "inventory:\n", 56 | " plugin: SimpleInventory\n", 57 | " options:\n", 58 | " host_file: \"advanced_filtering/inventory/hosts.yaml\"\n", 59 | " group_file: \"advanced_filtering/inventory/groups.yaml\"\n", 60 | "runner:\n", 61 | " plugin: threaded\n", 62 | " options:\n", 63 | " num_workers: 20\n", 64 | "```\n" 65 | ] 66 | } 67 | ], 68 | "metadata": { 69 | "kernelspec": { 70 | "display_name": "Python 3 (ipykernel)", 71 | "language": "python", 72 | "name": "python3" 73 | }, 74 | "language_info": { 75 | "codemirror_mode": { 76 | "name": "ipython", 77 | "version": 3 78 | }, 79 | "file_extension": ".py", 80 | "mimetype": "text/x-python", 81 | "name": "python", 82 | "nbconvert_exporter": "python", 83 | "pygments_lexer": "ipython3", 84 | "version": "3.8.10" 85 | } 86 | }, 87 | "nbformat": 4, 88 | "nbformat_minor": 5 89 | } 90 | -------------------------------------------------------------------------------- /source/configuration/hosts.yaml: -------------------------------------------------------------------------------- 1 | rt01: 2 | username: rt01 -------------------------------------------------------------------------------- /source/configuration/index.rst: -------------------------------------------------------------------------------- 1 | 配置文件 2 | ============ 3 | 4 | 本节主要介绍如何加载 Nornir 的配置文件及各个配置项的详细信息。 5 | 6 | 7 | .. toctree:: 8 | :glob: 9 | :maxdepth: 1 10 | 11 | 配置加载方式 12 | 配置参数详解 -------------------------------------------------------------------------------- /source/configuration/parameters.rst: -------------------------------------------------------------------------------- 1 | 配置参数详解 2 | =============== 3 | 4 | Nornir 的五个配置块及其对应参数的默认值和环境变量值。 5 | 6 | core 7 | ---- 8 | 9 | ``raise_on_error`` 10 | __________________ 11 | 12 | .. list-table:: 13 | :widths: 15 85 14 | 15 | * - **描述** 16 | - 如果配置为 ``True``,在至少有一台主机执行任务失败时, (:obj:`nornir.core.Nornir.run`) 方法会抛出异常 :obj:`nornir.core.exceptions.NornirExecutionError` 17 | * - **数据类型** 18 | - ``boolean`` 19 | * - **默认值** 20 | - ``False`` 21 | * - **是否需要该配置** 22 | - ``False`` 23 | * - **系统环境变量** 24 | - ``NORNIR_CORE_RAISE_ON_ERROR`` 25 | 26 | runner 27 | --------- 28 | 29 | ``plugin`` 30 | __________ 31 | 32 | .. list-table:: 33 | :widths: 15 85 34 | 35 | * - **描述** 36 | - 任务运行的线程插件,分为两种: ``Threaded`` (多线程)和 ``Serial`` (单线程);必须注册该插件 37 | * - **数据类型** 38 | - ``string`` 39 | * - **默认值** 40 | - ``Threaded``,默认线程数为 ``num_worker=20`` 41 | * - **是否需要该配置** 42 | - ``False`` 43 | * - **系统环境变量** 44 | - ``NORNIR_RUNNER_PLUGIN`` 45 | 46 | ``options`` 47 | ___________ 48 | 49 | .. list-table:: 50 | :widths: 15 85 51 | 52 | * - **描述** 53 | - 需要给插件传递的参数,默认为空字典 54 | * - **数据类型** 55 | - ``object`` 56 | * - **默认值** 57 | - ``{}`` 58 | * - **是否需要该配置** 59 | - ``False`` 60 | * - **系统环境变量** 61 | - ``NORNIR_RUNNER_OPTIONS`` 62 | 63 | inventory 64 | --------- 65 | 66 | ``plugin`` 67 | __________ 68 | 69 | .. list-table:: 70 | :widths: 15 85 71 | 72 | * - **描述** 73 | - 要使用的主机清单插件名;必须注册该插件 74 | * - **数据类型** 75 | - ``string`` 76 | * - **默认值** 77 | - ``SimpleInventory`` 78 | * - **是否需要该配置** 79 | - ``False`` 80 | * - **系统环境变量** 81 | - ``NORNIR_INVENTORY_PLUGIN`` 82 | 83 | ``options`` 84 | ___________ 85 | 86 | .. list-table:: 87 | :widths: 15 85 88 | 89 | * - **描述** 90 | - 需要给插件传递的参数,默认为空字典 91 | * - **数据类型** 92 | - ``object`` 93 | * - **默认值** 94 | - ``{}`` 95 | * - **是否需要该配置** 96 | - ``False`` 97 | * - **系统环境变量** 98 | - ``NORNIR_INVENTORY_OPTIONS`` 99 | 100 | ``transform_function`` 101 | ______________________ 102 | 103 | .. list-table:: 104 | :widths: 15 85 105 | 106 | * - **描述** 107 | - 要使用的转换函数插件名;必须注册该插件 108 | * - **数据类型** 109 | - ``string`` 110 | * - **默认值** 111 | - 112 | * - **是否需要该配置** 113 | - ``False`` 114 | * - **系统环境变量** 115 | - ``NORNIR_INVENTORY_TRANSFORM_FUNCTION`` 116 | 117 | ``transform_function_options`` 118 | ______________________________ 119 | 120 | .. list-table:: 121 | :widths: 15 85 122 | 123 | * - **描述** 124 | - 需要给插件传递的参数,默认为空字典 125 | * - **数据类型** 126 | - ``object`` 127 | * - **默认值** 128 | - ``{}`` 129 | * - **是否需要该配置** 130 | - ``False`` 131 | * - **系统环境变量** 132 | - ``NORNIR_INVENTORY_TRANSFORM_FUNCTION_OPTIONS`` 133 | 134 | 135 | 136 | 137 | 138 | ssh 139 | --- 140 | 141 | ``config_file`` 142 | _______________ 143 | 144 | .. list-table:: 145 | :widths: 15 85 146 | 147 | * - **描述** 148 | - 指定 ``ssh`` 配置文件的路径,可以用来配置相关参数 149 | * - **数据类型** 150 | - ``string`` 151 | * - **默认值** 152 | - ``~/.ssh/config`` 153 | * - **是否需要该配置** 154 | - ``False`` 155 | * - **系统环境变量** 156 | - ``NORNIR_SSH_CONFIG_FILE`` 157 | 158 | 159 | 160 | 161 | 162 | logging 163 | ------- 164 | 165 | 默认情况下,当调用 InitNornir 时,Nornir 会自动配置日志记录。 166 | 167 | 日志记录的配置可以根据以下选项进行修改。 168 | 169 | 如果想使用 Python 的 logging 模块配置日志,需要确保此配置中 ``enable`` 参数值为 False,以免发生冲突(Python 中日志配置为一次性的配置,只有第一次调用的配置会生效,随后的调用不会产生生效)。 170 | 171 | ``enabled`` 172 | ___________ 173 | 174 | .. list-table:: 175 | :widths: 15 85 176 | 177 | * - **描述** 178 | - 是否启用日志记录功能 179 | * - **数据类型** 180 | - ``boolean`` 181 | * - **默认值** 182 | - ``None`` 183 | * - **是否需要该配置** 184 | - ``False`` 185 | * - **系统环境变量** 186 | - ``NORNIR_LOGGING_ENABLED`` 187 | 188 | ``level`` 189 | _________ 190 | 191 | .. list-table:: 192 | :widths: 15 85 193 | 194 | * - **描述** 195 | - 日志记录的级别(CRITICAL > ERROR > WARNING > INFO > DEBUG) 196 | * - **数据类型** 197 | - ``string`` 198 | * - **默认值** 199 | - ``INFO`` 200 | * - **是否需要该配置** 201 | - ``False`` 202 | * - **系统环境变量** 203 | - ``NORNIR_LOGGING_LEVEL`` 204 | 205 | ``log_file`` 206 | ____________ 207 | 208 | .. list-table:: 209 | :widths: 15 85 210 | 211 | * - **描述** 212 | - 保存到日志文件的名称 213 | * - **数据类型** 214 | - ``string`` 215 | * - **默认值** 216 | - ``nornir.log`` 217 | * - **是否需要该配置** 218 | - ``False`` 219 | * - **系统环境变量** 220 | - ``NORNIR_LOGGING_LOG_FILE`` 221 | 222 | ``format`` 223 | __________ 224 | 225 | .. list-table:: 226 | :widths: 15 85 227 | 228 | * - **描述** 229 | - 日志信息的格式 230 | * - **数据类型** 231 | - ``string`` 232 | * - **默认值** 233 | - ``%(asctime)s - %(name)12s - %(levelname)8s - %(funcName)10s() - %(message)s`` 234 | * - **是否需要该配置** 235 | - ``False`` 236 | * - **系统环境变量** 237 | - ``NORNIR_LOGGING_FORMAT`` 238 | 239 | ``to_console`` 240 | ______________ 241 | 242 | .. list-table:: 243 | :widths: 15 85 244 | 245 | * - **描述** 246 | - 日志是否输出到控制台 247 | * - **数据类型** 248 | - ``boolean`` 249 | * - **默认值** 250 | - ``False`` 251 | * - **是否需要该配置** 252 | - ``False`` 253 | * - **系统环境变量** 254 | - ``NORNIR_LOGGING_TO_CONSOLE`` 255 | 256 | ``loggers`` 257 | ___________ 258 | 259 | .. list-table:: 260 | :widths: 15 85 261 | 262 | * - **描述** 263 | - 默认使用的 ``logger`` 对象 264 | * - **数据类型** 265 | - ``array`` 266 | * - **默认值** 267 | - ``['nornir']`` 268 | * - **是否需要该配置** 269 | - ``False`` 270 | * - **系统环境变量** 271 | - ``NORNIR_LOGGING_LOGGERS`` 272 | 273 | 274 | ``user_defined`` 275 | ---------------- 276 | 277 | 用户可以自行配置需要的 ```` 键值对, 使用时必须在 ``Config`` 对象下才能调用到该配置,例如: ``nr.config.user_defined.my_app_option`` 。 -------------------------------------------------------------------------------- /source/howto/01.advanced_filtering.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d59e9f28-6568-4b87-af46-11a88c3dbaf9", 6 | "metadata": {}, 7 | "source": [ 8 | "## 高级过滤方法\n", 9 | "\n", 10 | "这节内容主要介绍使用 `F` 对象来做高级过滤。如果你已经足够了解 `F` 对象的操作方法,可以直接到本节最后翻看两个新的列表过滤方法:`__any` 和 `__all`。\n", 11 | "\n", 12 | "先从初始化 `nornir` 对象开始,查看现在的主机清单和组:" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "id": "0522bc66-c20e-4fc1-b3b2-4c68da124b2e", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "from nornir import InitNornir\n", 23 | "from nornir.core.filter import F\n", 24 | "\n", 25 | "nr = InitNornir(config_file=\"advanced_filtering/config.yaml\")" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 15, 31 | "id": "0e084c74-371d-47c8-a973-2087dc3e4913", 32 | "metadata": { 33 | "jupyter": { 34 | "source_hidden": true 35 | }, 36 | "tags": [] 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "# %load advanced_filtering/inventory/hosts.yaml\n", 41 | "---\n", 42 | "cat:\n", 43 | " groups:\n", 44 | " - terrestrial\n", 45 | " - mammal\n", 46 | " data:\n", 47 | " domestic: true\n", 48 | " diet: omnivore\n", 49 | " additional_data:\n", 50 | " lifespan: 17\n", 51 | " famous_members:\n", 52 | " - garfield\n", 53 | " - felix\n", 54 | " - grumpy\n", 55 | "\n", 56 | "bat:\n", 57 | " groups:\n", 58 | " - terrestrial\n", 59 | " - mammal\n", 60 | " data:\n", 61 | " domestic: false\n", 62 | " fly: true\n", 63 | " diet: carnivore\n", 64 | " additional_data:\n", 65 | " lifespan: 15\n", 66 | " famous_members:\n", 67 | " - batman\n", 68 | " - count chocula\n", 69 | " - nosferatu\n", 70 | "\n", 71 | "eagle:\n", 72 | " groups:\n", 73 | " - terrestrial\n", 74 | " - bird\n", 75 | " data:\n", 76 | " domestic: false\n", 77 | " diet: carnivore\n", 78 | " additional_data:\n", 79 | " lifespan: 50\n", 80 | " famous_members:\n", 81 | " - thorondor\n", 82 | " - sam\n", 83 | "\n", 84 | "canary:\n", 85 | " groups:\n", 86 | " - terrestrial\n", 87 | " - bird\n", 88 | " data:\n", 89 | " domestic: true\n", 90 | " diet: herbivore\n", 91 | " additional_data:\n", 92 | " lifespan: 15\n", 93 | " famous_members:\n", 94 | " - tweetie\n", 95 | "\n", 96 | "caterpillaer:\n", 97 | " groups:\n", 98 | " - terrestrial\n", 99 | " - invertebrate\n", 100 | " data:\n", 101 | " domestic: false\n", 102 | " diet: herbivore\n", 103 | " additional_data:\n", 104 | " lifespan: 1\n", 105 | " famous_members:\n", 106 | " - Hookah-Smoking\n", 107 | "\n", 108 | "octopus:\n", 109 | " groups:\n", 110 | " - marine\n", 111 | " - invertebrate\n", 112 | " data:\n", 113 | " domestic: false\n", 114 | " diet: carnivore\n", 115 | " additional_data:\n", 116 | " lifespan: 1\n", 117 | " famous_members:\n", 118 | " - sharktopus\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 4, 124 | "id": "d56fddfd-587f-4ec8-8b12-efa938421bbd", 125 | "metadata": { 126 | "jupyter": { 127 | "source_hidden": true 128 | }, 129 | "tags": [] 130 | }, 131 | "outputs": [], 132 | "source": [ 133 | "# %load advanced_filtering/inventory/groups.yaml\n", 134 | "---\n", 135 | "mammal:\n", 136 | " data:\n", 137 | " reproduction: birth\n", 138 | " fly: false\n", 139 | "\n", 140 | "bird:\n", 141 | " data:\n", 142 | " reproduction: eggs\n", 143 | " fly: true\n", 144 | "\n", 145 | "invertebrate:\n", 146 | " data:\n", 147 | " reproduction: mitosis\n", 148 | " fly: false\n", 149 | "\n", 150 | "terrestrial: {}\n", 151 | "marine: {}\n" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "id": "7f7817c5-7c71-4c4c-b21d-453dbe1b2652", 157 | "metadata": {}, 158 | "source": [ 159 | "在上面的主机及主机组文件中,建立了具有不同属性的动物分类。`F` 对象可以只需在前面加上两个下划线和魔术方法的名称即可访问每种类型的魔术方法。例如,如果想检查一个列表是否包含一个特定的元素,你可以在前面加上 `__contains`。现在来查找属于鸟类(`bird`)的所有动物:" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 2, 165 | "id": "e1b7a0d5-7ded-46be-882f-8cb789aacc96", 166 | "metadata": {}, 167 | "outputs": [ 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | "dict_keys(['eagle', 'canary'])\n" 173 | ] 174 | } 175 | ], 176 | "source": [ 177 | "birds = nr.filter(F(groups__contains=\"bird\"))\n", 178 | "print(birds.inventory.hosts.keys())\n", 179 | "# dict_keys(['鹰', '金丝雀'])" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "id": "6796cb34-6915-4401-91d7-041d96f5cc60", 185 | "metadata": {}, 186 | "source": [ 187 | "还可以通过添加 `~` 来对 `F `对象进行取反:" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": 3, 193 | "id": "22d76cc3-1b28-4fdb-a7c3-b315f1543575", 194 | "metadata": {}, 195 | "outputs": [ 196 | { 197 | "name": "stdout", 198 | "output_type": "stream", 199 | "text": [ 200 | "dict_keys(['cat', 'bat', 'caterpillaer', 'octopus'])\n" 201 | ] 202 | } 203 | ], 204 | "source": [ 205 | "not_birds = nr.filter(~F(groups__contains=\"bird\"))\n", 206 | "print(not_birds.inventory.hosts.keys())\n", 207 | "# dict_keys(['猫', '蝙蝠', '毛毛虫', '章鱼'])" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "id": "74dfeab6-3ff8-4fe1-beec-04ac2ce5bd68", 213 | "metadata": {}, 214 | "source": [ 215 | "还可以组合 `F` 对象并使用符号 `&` 和 `|` 执行 AND 和 OR 运算:" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 4, 221 | "id": "7bf88fbe-3b06-4094-a8ec-45d87a068bac", 222 | "metadata": {}, 223 | "outputs": [ 224 | { 225 | "name": "stdout", 226 | "output_type": "stream", 227 | "text": [ 228 | "dict_keys(['cat', 'eagle', 'canary'])\n" 229 | ] 230 | } 231 | ], 232 | "source": [ 233 | "# 筛选鸟类(bird)或者家养动物(domestic)\n", 234 | "domestic_or_bird = nr.filter(F(groups__contains=\"bird\") | F(domestic=True))\n", 235 | "print(domestic_or_bird.inventory.hosts.keys())\n", 236 | "# dict_keys(['猫', '鹰', '金丝雀'])" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 5, 242 | "id": "539480e5-251d-47a0-98d0-1fec48821e50", 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "name": "stdout", 247 | "output_type": "stream", 248 | "text": [ 249 | "dict_keys(['cat'])\n" 250 | ] 251 | } 252 | ], 253 | "source": [ 254 | "# 筛选哺乳动物(mammal)并且是家养动物(domestic)\n", 255 | "domestic_mammals = nr.filter(F(groups__contains=\"mammal\") & F(domestic=True))\n", 256 | "print(domestic_mammals.inventory.hosts.keys())\n", 257 | "# dict_keys(['猫'])" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "id": "011f0a2e-88ed-491b-8360-b5e38434d814", 263 | "metadata": {}, 264 | "source": [ 265 | "也可以将所有符号进行组合:" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 6, 271 | "id": "412d93db-1bc8-44ff-b572-8c4a01ef6d9f", 272 | "metadata": {}, 273 | "outputs": [ 274 | { 275 | "name": "stdout", 276 | "output_type": "stream", 277 | "text": [ 278 | "dict_keys(['canary'])\n" 279 | ] 280 | } 281 | ], 282 | "source": [ 283 | "# 筛选会飞的动物(fly)并且不是食肉动物(cannivore)\n", 284 | "flying_not_carnivore = nr.filter(F(fly=True) & ~F(diet=\"carnivore\"))\n", 285 | "print(flying_not_carnivore.inventory.hosts.keys())\n", 286 | "# dict_keys(['金丝雀'])" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "7bf8edc0-f750-4d21-aafd-5fc4c055af19", 292 | "metadata": {}, 293 | "source": [ 294 | "可以像访问魔法方法一样访问嵌套数据,方法是在要访问的数据前面添加双下划线;在数据筛选之后,还能继续添加双下划线来访问最终数据的魔法方法。\n", 295 | "例如在示例数据中,筛选寿命(lifespan)最终的结果是一个整数,整数可以进行比较运算,因为它具有双下划线魔术方法,所以可以对最终的数据进行再次调用魔术方法。\n", 296 | "来筛选一下寿命(lifespan)大于 15 的动物:" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 7, 302 | "id": "6643ae5a-d3da-4174-ae31-0775091213d1", 303 | "metadata": {}, 304 | "outputs": [ 305 | { 306 | "name": "stdout", 307 | "output_type": "stream", 308 | "text": [ 309 | "dict_keys(['cat', 'bat', 'eagle', 'canary'])\n" 310 | ] 311 | } 312 | ], 313 | "source": [ 314 | "long_lived = nr.filter(F(additional_data__lifespan__ge=15)) # 调用了整数的 __ge__ \n", 315 | "print(long_lived.inventory.hosts.keys())\n", 316 | "# dict_keys(['猫', '蝙蝠', '鹰', '金丝雀'])" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 8, 322 | "id": "711432fe-3410-449a-b72b-7aab6837981a", 323 | "metadata": {}, 324 | "outputs": [ 325 | { 326 | "data": { 327 | "text/plain": [ 328 | "False" 329 | ] 330 | }, 331 | "execution_count": 8, 332 | "metadata": {}, 333 | "output_type": "execute_result" 334 | } 335 | ], 336 | "source": [ 337 | "# 结合这个例子,增加对上一个代码框的理解\n", 338 | "# 使用整数的魔术方法进行比较大小\n", 339 | "# 定义 a = 1,b = 2 \n", 340 | "a = 1\n", 341 | "b = 2\n", 342 | "# 调用 a 的 魔术方法,将 b 作为参数传入,等价于 a >= b\n", 343 | "a.__ge__(b)" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "id": "7f78be9a-5cca-49f5-b4da-cd34446f597c", 349 | "metadata": {}, 350 | "source": [ 351 | "除了 `__contains` 外,还有两个选项可以对列表进行处理:`any` 和 `all`。 `any` 代表列表中的元素是 OR 的关系,满足一个条件就可以;`all` 代表列表中的元素是 AND 的关系,必须满足所有的条件才行:" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 9, 357 | "id": "682c2bfe-25f6-4ed0-a25c-a759fa4f17c1", 358 | "metadata": {}, 359 | "outputs": [ 360 | { 361 | "name": "stdout", 362 | "output_type": "stream", 363 | "text": [ 364 | "dict_keys(['eagle', 'canary', 'caterpillaer', 'octopus'])\n" 365 | ] 366 | } 367 | ], 368 | "source": [ 369 | "# 筛选鸟类(bird)或者无脊椎动物(invertebrates)\n", 370 | "bird_or_invertebrates = nr.filter(F(groups__any=[\"bird\", \"invertebrate\"]))\n", 371 | "print(bird_or_invertebrates.inventory.hosts.keys())\n", 372 | "# dict_keys(['鹰', '金丝雀', '毛毛虫', '章鱼'])" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 10, 378 | "id": "a1cbde61-fd96-4682-b96b-bc23ee6195aa", 379 | "metadata": {}, 380 | "outputs": [ 381 | { 382 | "name": "stdout", 383 | "output_type": "stream", 384 | "text": [ 385 | "dict_keys(['octopus'])\n" 386 | ] 387 | } 388 | ], 389 | "source": [ 390 | "# 筛选海生动物(marine)并且是无脊椎动物(invertebrates)\n", 391 | "marine_and_invertebrates = nr.filter(F(groups__all=[\"marine\", \"invertebrate\"]))\n", 392 | "print(marine_and_invertebrates.inventory.hosts.keys())\n", 393 | "# dict_keys(['章鱼'])" 394 | ] 395 | }, 396 | { 397 | "cell_type": "markdown", 398 | "id": "3700f585-0014-4eb5-b844-589ba0386bca", 399 | "metadata": {}, 400 | "source": [ 401 | "从示例中可以看出,如果需要对多个组进行过滤操作的话,某些情况下使用 `any` 和 `all` 比使用 `__contains` 和多级运算 `&`、`~`、`|` 更为方便。\n", 402 | "\n", 403 | "下一节中,将以网络设备作为对象,深入了解过滤功能在网络自动化中的使用方法。" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "id": "85efb546-47aa-4da1-9ef9-f61e75ed0350", 409 | "metadata": {}, 410 | "source": [ 411 | "---\n", 412 | "[上一节](#) | [下一节](#) | [返回首页](#)" 413 | ] 414 | } 415 | ], 416 | "metadata": { 417 | "kernelspec": { 418 | "display_name": "Python 3 (ipykernel)", 419 | "language": "python", 420 | "name": "python3" 421 | }, 422 | "language_info": { 423 | "codemirror_mode": { 424 | "name": "ipython", 425 | "version": 3 426 | }, 427 | "file_extension": ".py", 428 | "mimetype": "text/x-python", 429 | "name": "python", 430 | "nbconvert_exporter": "python", 431 | "pygments_lexer": "ipython3", 432 | "version": "3.8.10" 433 | } 434 | }, 435 | "nbformat": 4, 436 | "nbformat_minor": 5 437 | } 438 | -------------------------------------------------------------------------------- /source/howto/02.filtering_deep_dive.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5a5300d0-394a-418a-ad83-da06a8254924", 6 | "metadata": {}, 7 | "source": [ 8 | "## 深入理解过滤器\n", 9 | "\n", 10 | "在本教程中,一起来探索 nornir 过滤的强大功能,并演示为什么 nornir 是一流的主机清单框架。\n", 11 | "\n", 12 | "本教程涉及的内容在入门教程中均有涉及,更多的是结合现网案例来进行说明过滤器的使用方法。\n", 13 | "\n", 14 | "本教程将演示一些常见的网络用例以及如何将 nornir 过滤精确地定位到目标主机或组。\n", 15 | "\n", 16 | "### 主机清单教程\n", 17 | "\n", 18 | "先初始化一个 Nornir 对象:" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "id": "370489fc-1fd8-4cf4-a190-7e6075eb1c46", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "from nornir import InitNornir\n", 29 | "from nornir.core.filter import F\n", 30 | "\n", 31 | "nr = InitNornir(config_file=\"filtering_deep_dive/config.yaml\")" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "id": "3854669e-0168-4957-92b7-1f138de32c92", 37 | "metadata": {}, 38 | "source": [ 39 | "接下来,加载本次教程使用的 hosts.yaml 文件。\n", 40 | "\n", 41 | "不要被库存的大小或其中的内容所淹没,因为我们将在本教程后面更详细地探讨这一点:" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "id": "63061627-4cd3-4436-a67c-033e6af673f7", 47 | "metadata": {}, 48 | "source": [ 49 | "接下来,让我们看看我们的教程 groups.yaml 文件。\n", 50 | "\n", 51 | "这里有多种组类型。\n", 52 | "\n", 53 | "一些似乎基于网络操作系统,一些基于操作环境,而另一些基于物理位置。\n", 54 | "\n", 55 | "nornir 对组的组成或结构没有任何限制,你可以随意进行配置:" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "c6492377-2372-4211-a996-5331083f8825", 61 | "metadata": {}, 62 | "source": [ 63 | "### 自定义主机清单数据\n", 64 | "\n", 65 | "在我们深入研究过滤之前,介绍一些关于自定义数据的核心概念很重要。\n", 66 | "\n", 67 | "nornir 允许您以您选择的任何键/值数据结构填充主机上的自定义数据或数据键下的对象组。\n", 68 | "\n", 69 | "您可以随意命名这些键以满足您的业务需求。\n", 70 | "\n", 71 | "首先,让我们探索一下引入 hosts.yaml 和 groups.yaml 文件的初始清单中的一个主机和一组的摘录。\n", 72 | "\n", 73 | "注意:这些文件已被缩减为每个文件仅包含一个条目,并且为了便于阅读还分散了一些注释" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "id": "a0fee2fb-ca83-42cf-8db5-3b94f28ec17e", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "id": "161832f5-e30a-47d2-b064-df0e828fd845", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "0c95c515-9e9a-4b71-b100-90f51860644e", 95 | "metadata": {}, 96 | "source": [ 97 | "正如您在上面的两个示例中看到的那样,您可以以任何您喜欢的方式使用自定义数据来记录您喜欢的任何信息。\n", 98 | "\n", 99 | "上面的示例是数据键下的“平面”数据结构,但您可以将数据结构嵌套在任何键/值结构中以满足您的需要。\n", 100 | "\n", 101 | "在下面的示例中,我们采用 10.0.0.16 的 mgmt_ip 值,并在备用数据结构下将其重定向到键 mgmt,以便将来扩展:" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "id": "89809d9c-a5d7-4745-a7e9-547296b04d42", 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "id": "e62d1cb9-59e9-4f2a-970a-822993271003", 115 | "metadata": {}, 116 | "source": [ 117 | "当然,随着时间的推移,您需要存储的自定义数据可能会发生变化,理想情况下,您可以使用尽可能多的业务信息来丰富您的自定义数据。\n", 118 | "\n", 119 | "在下面的示例中,一组与站点位置相关的新键/值对现在存储在位置键下。\n", 120 | "\n", 121 | "这意味着任何利用现有数据结构的现有代码都不需要重构:" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "id": "2323f2b6-2303-4b75-8e38-acc76756ee38", 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "id": "01daf6b5-8083-49dc-bcf9-1981fc05a52f", 135 | "metadata": {}, 136 | "source": [ 137 | "### 查看主机/组数据\n", 138 | "\n", 139 | "在我们恢复过滤之前,我们将向您展示如何访问所有可访问或归属于主机或组的数据。\n", 140 | "\n", 141 | "首先,我们将在单个主机 lab-csr-011.lab.norn.local 上初始化 nornir 并过滤清单:\n" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "id": "b4ad9f9f-8145-4aef-9e75-9b9a7c93b8f7", 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "id": "506dad45-bf64-4f56-85b4-fa47c7751c2c", 155 | "metadata": {}, 156 | "source": [ 157 | "我们现在可以通过转储字典结构打印出与主机 lab-csr-011.lab.norn.local 关联的所有数据:" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "id": "3f4b24b8-4f6a-4432-a3eb-28a719accf5c", 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "id": "f500e0c1-e396-49c1-b306-818969043e5f", 171 | "metadata": {}, 172 | "source": [ 173 | "正如您在上面看到的,我们所有与主机相关的数据都可以在任何后续代码中查看和使用。\n", 174 | "\n", 175 | "无论我们使用 JSON 还是 YAML,数据结构都保持不变。\n", 176 | "\n", 177 | "以下是显示某些键值的一些示例:" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "id": "eef0f2ee-c4cf-4b4e-879f-2cca7ab3dd69", 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [] 187 | } 188 | ], 189 | "metadata": { 190 | "kernelspec": { 191 | "display_name": "Python 3 (ipykernel)", 192 | "language": "python", 193 | "name": "python3" 194 | }, 195 | "language_info": { 196 | "codemirror_mode": { 197 | "name": "ipython", 198 | "version": 3 199 | }, 200 | "file_extension": ".py", 201 | "mimetype": "text/x-python", 202 | "name": "python", 203 | "nbconvert_exporter": "python", 204 | "pygments_lexer": "ipython3", 205 | "version": "3.8.10" 206 | } 207 | }, 208 | "nbformat": 4, 209 | "nbformat_minor": 5 210 | } 211 | -------------------------------------------------------------------------------- /source/howto/03.handling_connections.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ad648012-d4a0-4867-9e8a-3bafacb87b4b", 6 | "metadata": {}, 7 | "source": [ 8 | "## 处理设备连接\n", 9 | "\n", 10 | "### 自动处理\n", 11 | "\n", 12 | "默认情况下,Nornir 会自动处理设备的连接。这里指的意思是 Nornir 会自动连接到设备,执行完成任务后再退出设备。" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "id": "4799cee8-cd7b-41d7-b1f3-c37b97bc33cb", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "from nornir import InitNornir\n", 23 | "from nornir_utils.plugins.functions import print_result\n", 24 | "from nornir_napalm.plugins.tasks import napalm_get" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "id": "821f85ad-8beb-4d3d-a7ac-d6b74a80cf3c", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "nr = InitNornir(config_file=\"handling_connections/config.yaml\")\n", 35 | "r1 = nr.filter(name=\"rt01\")\n", 36 | "\n", 37 | "r = r1.run(\n", 38 | " task=napalm_get,\n", 39 | " getters=[\"facts\"]\n", 40 | ")" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "id": "4ccda22b-93b1-431e-93b6-f9398d8e38a6", 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "name": "stdout", 51 | "output_type": "stream", 52 | "text": [ 53 | "\u001b[1m\u001b[36mnapalm_get**********************************************************************\u001b[0m\n", 54 | "\u001b[0m\u001b[1m\u001b[34m* rt01 ** changed : False ******************************************************\u001b[0m\n", 55 | "\u001b[0m\u001b[1m\u001b[32mvvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 56 | "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 57 | " \u001b[0m'hostname'\u001b[0m: \u001b[0m'R1'\u001b[0m,\n", 58 | " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[]\u001b[0m,\n", 59 | " \u001b[0m'model'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 60 | " \u001b[0m'os_version'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 61 | " \u001b[0m'serial_number'\u001b[0m: \u001b[0m[]\u001b[0m,\n", 62 | " \u001b[0m'uptime'\u001b[0m: \u001b[0m-1\u001b[0m,\n", 63 | " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Huawei'\u001b[0m}\u001b[0m}\u001b[0m\n", 64 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 65 | "\u001b[0m" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "print_result(r)" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "id": "0d21c116-0c33-4dd4-b32e-fc785ad2015f", 76 | "metadata": {}, 77 | "source": [ 78 | "### 手动处理\n", 79 | "\n", 80 | "在某些情况下,可能需要手动管理设备的连接,让用户来决定什么时候连接到设备上,什么时候和设备断开连接。\n", 81 | "\n", 82 | "这时候可以使用 `open_connection`、`close_connection`、`close_connections` 和 `Nornir.close_connections` 这几个方法来实现:" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 4, 88 | "id": "0cd27d95-5957-4032-a16a-4cb01f5c9ecd", 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "开始连接:rt01\u001b[0m\n", 96 | "\u001b[0m连接成功: True\u001b[0m\n", 97 | "\u001b[0m" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "def task_manages_connection_manually(task):\n", 103 | " print(f\"开始连接:{task.host.name}\")\n", 104 | " task.host.open_connection(\"napalm\", configuration=task.nornir.config)\n", 105 | " r = task.run(\n", 106 | " task=napalm_get,\n", 107 | " getters=[\"facts\"]\n", 108 | " )\n", 109 | " print(f\"连接成功: {not r[0].failed}\")\n", 110 | " task.host.close_connection(\"napalm\")\n", 111 | "\n", 112 | "nr = InitNornir(config_file=\"handling_connections/config.yaml\")\n", 113 | "rtr = nr.filter(name=\"rt01\")\n", 114 | "r = rtr.run(\n", 115 | " task=task_manages_connection_manually,\n", 116 | ")" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 5, 122 | "id": "22e9d7c4-bc58-425b-b376-36a148e0570b", 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | "\u001b[1m\u001b[36mtask_manages_connection_manually************************************************\u001b[0m\n", 130 | "\u001b[0m\u001b[1m\u001b[34m* rt01 ** changed : False ******************************************************\u001b[0m\n", 131 | "\u001b[0m\u001b[1m\u001b[32mvvvv task_manages_connection_manually ** changed : False vvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 132 | "\u001b[0m\u001b[1m\u001b[32m---- napalm_get ** changed : False --------------------------------------------- INFO\u001b[0m\n", 133 | "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 134 | " \u001b[0m'hostname'\u001b[0m: \u001b[0m'R1'\u001b[0m,\n", 135 | " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[]\u001b[0m,\n", 136 | " \u001b[0m'model'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 137 | " \u001b[0m'os_version'\u001b[0m: \u001b[0m'Unknown'\u001b[0m,\n", 138 | " \u001b[0m'serial_number'\u001b[0m: \u001b[0m[]\u001b[0m,\n", 139 | " \u001b[0m'uptime'\u001b[0m: \u001b[0m-1\u001b[0m,\n", 140 | " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Huawei'\u001b[0m}\u001b[0m}\u001b[0m\n", 141 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END task_manages_connection_manually ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 142 | "\u001b[0m" 143 | ] 144 | } 145 | ], 146 | "source": [ 147 | "print_result(r)" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "id": "c2d2e634-f713-4141-a632-b31d5b23b7fe", 153 | "metadata": {}, 154 | "source": [ 155 | "### 指定连接参数\n", 156 | "\n", 157 | "使用 `open_connection` 时,可以指定所需要的任何参数,如果没有指定或者如果让 nornir 自动打开设备连接,nornir 将会从主机清单中读取这些连接参数。\n", 158 | "\n", 159 | "在主机清单中的 `connection_options` 中指定设备的连接方式,然后在 `extras` -> `optional_args` 中添加连接插件的额外参数,连接参数通常是下面这种格式:" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 6, 165 | "id": "fd693b2f-768c-4c08-a45f-def89f902140", 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "# %load handling_connections/inventory/test-hosts.yaml\n", 170 | "dev1.group_1:\n", 171 | " port: 22\n", 172 | " hostname: 192.168.56.20\n", 173 | " username: username\n", 174 | " password: password\n", 175 | " platform: ios\n", 176 | " connection_options:\n", 177 | " netmiko:\n", 178 | " port: 22\n", 179 | " hostname:\n", 180 | " username: user\n", 181 | " password: pass\n", 182 | " platform: ios\n", 183 | " extras:\n", 184 | " optional_args:\n", 185 | " secret: secret\n", 186 | " session_log: path/to/save_log\n", 187 | " \n", 188 | " napalm:\n", 189 | " platform: ios\n", 190 | " extras:\n", 191 | " optional_args:\n", 192 | " path: path/to/save_log\n", 193 | " \n", 194 | " dummy:\n", 195 | " hostname: dummy_from_host\n", 196 | " port:\n", 197 | " username:\n", 198 | " password:\n", 199 | " platform:\n", 200 | " extras:\n", 201 | " blah: from_host" 202 | ] 203 | } 204 | ], 205 | "metadata": { 206 | "kernelspec": { 207 | "display_name": "Python 3 (ipykernel)", 208 | "language": "python", 209 | "name": "python3" 210 | }, 211 | "language_info": { 212 | "codemirror_mode": { 213 | "name": "ipython", 214 | "version": 3 215 | }, 216 | "file_extension": ".py", 217 | "mimetype": "text/x-python", 218 | "name": "python", 219 | "nbconvert_exporter": "python", 220 | "pygments_lexer": "ipython3", 221 | "version": "3.8.10" 222 | } 223 | }, 224 | "nbformat": 4, 225 | "nbformat_minor": 5 226 | } 227 | -------------------------------------------------------------------------------- /source/howto/advanced_filtering/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | inventory: 3 | plugin: SimpleInventory 4 | options: 5 | host_file: "advanced_filtering/inventory/hosts.yaml" 6 | group_file: "advanced_filtering/inventory/groups.yaml" 7 | runner: 8 | plugin: threaded 9 | options: 10 | num_workers: 20 11 | -------------------------------------------------------------------------------- /source/howto/advanced_filtering/inventory/groups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mammal: 3 | data: 4 | reproduction: birth 5 | fly: false 6 | 7 | bird: 8 | data: 9 | reproduction: eggs 10 | fly: true 11 | 12 | invertebrate: 13 | data: 14 | reproduction: mitosis 15 | fly: false 16 | 17 | terrestrial: {} 18 | marine: {} 19 | -------------------------------------------------------------------------------- /source/howto/advanced_filtering/inventory/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | cat: 3 | groups: 4 | - terrestrial 5 | - mammal 6 | data: 7 | domestic: true 8 | diet: omnivore 9 | additional_data: 10 | lifespan: 17 11 | famous_members: 12 | - garfield 13 | - felix 14 | - grumpy 15 | 16 | bat: 17 | groups: 18 | - terrestrial 19 | - mammal 20 | data: 21 | domestic: false 22 | fly: true 23 | diet: carnivore 24 | additional_data: 25 | lifespan: 15 26 | famous_members: 27 | - batman 28 | - count chocula 29 | - nosferatu 30 | 31 | eagle: 32 | groups: 33 | - terrestrial 34 | - bird 35 | data: 36 | domestic: false 37 | diet: carnivore 38 | additional_data: 39 | lifespan: 50 40 | famous_members: 41 | - thorondor 42 | - sam 43 | 44 | canary: 45 | groups: 46 | - terrestrial 47 | - bird 48 | data: 49 | domestic: true 50 | diet: herbivore 51 | additional_data: 52 | lifespan: 15 53 | famous_members: 54 | - tweetie 55 | 56 | caterpillaer: 57 | groups: 58 | - terrestrial 59 | - invertebrate 60 | data: 61 | domestic: false 62 | diet: herbivore 63 | additional_data: 64 | lifespan: 1 65 | famous_members: 66 | - Hookah-Smoking 67 | 68 | octopus: 69 | groups: 70 | - marine 71 | - invertebrate 72 | data: 73 | domestic: false 74 | diet: carnivore 75 | additional_data: 76 | lifespan: 1 77 | famous_members: 78 | - sharktopus 79 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | inventory: 3 | plugin: SimpleInventory 4 | options: 5 | host_file: "filtering_deep_dive/inventory/hosts.yaml" 6 | group_file: "filtering_deep_dive/inventory/groups.yaml" 7 | runner: 8 | plugin: threaded 9 | options: 10 | num_workers: 20 11 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/groups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ios: 3 | platform: ios 4 | data: 5 | vendor: cisco 6 | junos: 7 | platform: junos 8 | data: 9 | vendor: juniper 10 | eos: 11 | platform: eos 12 | data: 13 | vendor: arista 14 | nxos: 15 | platform: nxos 16 | data: 17 | vendor: cisco 18 | nxos_ssh: 19 | platform: nxos_ssh 20 | data: 21 | vendor: cisco 22 | panos: 23 | platform: paloalto_panos 24 | data: 25 | vendor: palo alto 26 | lab: 27 | data: 28 | sla: 70 29 | production: false 30 | prod: 31 | data: 32 | sla: 90 33 | production: true 34 | test: 35 | data: 36 | sla: 80 37 | production: false 38 | mel: 39 | data: 40 | full_name: Melbourne 41 | country: Australia 42 | region: apac 43 | hemisphere: southern 44 | site_type: primary 45 | hbt: 46 | data: 47 | full_name: Hobart 48 | country: Australia 49 | region: apac 50 | hemisphere: southern 51 | site_type: tertiary 52 | chc: 53 | data: 54 | full_name: Christchurch 55 | country: New Zealand 56 | region: apac 57 | hemisphere: southern 58 | site_type: secondary 59 | ptl: 60 | data: 61 | full_name: Port Louis 62 | country: Mauritius 63 | region: amea 64 | hemisphere: southern 65 | site_type: primary 66 | mtl: 67 | data: 68 | full_name: Montreal 69 | country: Canada 70 | region: amer 71 | hemisphere: northern 72 | site_type: primary 73 | bcn: 74 | data: 75 | full_name: Barcelona 76 | country: Spain 77 | region: amea 78 | hemisphere: northern 79 | site_type: primary 80 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/groups_extract.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mel: 3 | data: # Anything under this key is custom data 4 | full_name: Melbourne # This is custom data 5 | country: Australia # So is this 6 | region: apac # Same as this 7 | hemisphere: southern # Also this too 8 | site_type: primary # Yes, and also this 9 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/groups_extract_alternate.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | mel: 3 | data: # Anything under this key is custom data 4 | full_name: Melbourne 5 | country: Australia 6 | region: apac 7 | hemisphere: southern 8 | site_type: primary 9 | location: # New location data is stored about the site 10 | address: 1 Wurundjeri Street 11 | suburb: Northcote 12 | zip_code: 3070 13 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | lab-csr-011.lab.norn.local: 3 | hostname: lab-csr-011.lab.norn.local 4 | groups: 5 | - ios 6 | - lab 7 | - mel 8 | data: 9 | mgmt_ip: 10.0.0.16 10 | vendor: cisco 11 | device_type: router 12 | os_version: 16.6.4 13 | site_code: mel 14 | 15 | dfjt-r001.lab.norn.local: 16 | hostname: dfjt-r001.lab.norn.local 17 | groups: 18 | - ios 19 | - lab 20 | - bcn 21 | data: 22 | mgmt_ip: 10.0.0.1 23 | vendor: cisco 24 | device_type: router 25 | os_version: 16.6.3 26 | site_code: bcn 27 | 28 | lab-arista-01.lab.norn.local: 29 | hostname: lab-arista-01.lab.norn.local 30 | groups: 31 | - eos 32 | - lab 33 | - mtl 34 | data: 35 | mgmt_ip: 10.0.0.11 36 | vendor: arista 37 | device_type: switch 38 | os_version: 4.22.0F 39 | site_code: mtl 40 | 41 | lab-arista-02.lab.norn.local: 42 | hostname: lab-arista-02.lab.norn.local 43 | groups: 44 | - eos 45 | - lab 46 | - mel 47 | data: 48 | mgmt_ip: 10.0.0.18 49 | vendor: arista 50 | device_type: switch 51 | os_version: 4.23.2F 52 | site_code: mel 53 | 54 | lab-junos-01.lab.norn.local: 55 | hostname: lab-junos-01.lab.norn.local 56 | groups: 57 | - junos 58 | - lab 59 | - mtl 60 | data: 61 | mgmt_ip: 10.0.0.15 62 | vendor: juniper 63 | device_type: router 64 | os_version: 18.4R2-S5 65 | site_code: mtl 66 | 67 | lab-nxos-01.lab.norn.local: 68 | hostname: lab-nxos-01.lab.norn.local 69 | groups: 70 | - nxos 71 | - lab 72 | - mtl 73 | data: 74 | mgmt_ip: 10.0.0.14 75 | vendor: cisco 76 | device_type: switch 77 | os_version: 9.3(6) 78 | site_code: mtl 79 | 80 | lab-paloalto-01.lab.djft.local: 81 | hostname: lab-paloalto-01.lab.djft.local 82 | groups: 83 | - panos 84 | - lab 85 | - mel 86 | data: 87 | mgmt_ip: 10.0.0.21 88 | vendor: palo alto 89 | device_type: firewall 90 | os_version: 10.0.3 91 | site_code: mel 92 | 93 | lab-paloalto-02.lab.norn.local: 94 | hostname: lab-paloalto-02.lab.norn.local 95 | groups: 96 | - panos 97 | - lab 98 | - bcn 99 | data: 100 | mgmt_ip: 10.0.0.22 101 | vendor: palo alto 102 | device_type: firewall 103 | os_version: 9.1.3-h1 104 | site_code: bcn 105 | 106 | lab-junos-06.lab.norn.local: 107 | hostname: lab-junos-06.lab.norn.local 108 | groups: 109 | - junos 110 | - lab 111 | - mel 112 | data: 113 | mgmt_ip: 10.0.0.23 114 | vendor: juniper 115 | device_type: switch 116 | os_version: 18.4R2-S5 117 | site_code: mel 118 | 119 | prd-csr-01.prd.norn.local: 120 | hostname: prd-csr-01.prd.norn.local 121 | groups: 122 | - ios 123 | - prod 124 | - mel 125 | data: 126 | mgmt_ip: 10.0.16.16 127 | vendor: cisco 128 | device_type: router 129 | os_version: 16.6.4 130 | site_code: mel 131 | 132 | dfjt-r001.prd.norn.local: 133 | hostname: dfjt-r001.prd.norn.local 134 | groups: 135 | - ios 136 | - prod 137 | - mel 138 | data: 139 | mgmt_ip: 10.0.16.1 140 | vendor: cisco 141 | device_type: router 142 | os_version: 16.6.3 143 | site_code: mel 144 | 145 | prd-arista-01.prd.norn.local: 146 | hostname: prd-arista-01.prd.norn.local 147 | groups: 148 | - eos 149 | - prod 150 | - mel 151 | data: 152 | mgmt_ip: 10.0.16.11 153 | vendor: arista 154 | device_type: switch 155 | os_version: 4.21.1F 156 | site_code: mel 157 | 158 | prd-arista-02.prd.nron.local: 159 | hostname: prd-arista-02.prd.norn.local 160 | groups: 161 | - eos 162 | - prod 163 | - mel 164 | data: 165 | mgmt_ip: 10.0.16.18 166 | vendor: arista 167 | device_type: switch 168 | os_version: 4.23.1F 169 | site_code: mel 170 | 171 | prd-junos-01.prd.norn.local: 172 | hostname: prd-junos-01.prd.norn.local 173 | groups: 174 | - junos 175 | - prod 176 | - mel 177 | data: 178 | mgmt_ip: 10.0.16.15 179 | vendor: juniper 180 | device_type: router 181 | os_version: 15.1R7-S6 182 | site_code: mel 183 | 184 | prd-nxos-01.prd.norn.local: 185 | hostname: prd-nxos-01.prd.norn.local 186 | groups: 187 | - nxos_ssh 188 | - prod 189 | - mel 190 | data: 191 | mgmt_ip: 10.0.16.14 192 | vendor: cisco 193 | device_type: switch 194 | os_version: 7.0(3) 195 | site_code: mel 196 | 197 | prd-paloalto-01.prd.norn.local: 198 | hostname: prd-paloalto-01.prd.norn.local 199 | groups: 200 | - panos 201 | - prod 202 | - mel 203 | data: 204 | mgmt_ip: 10.0.16.21 205 | vendor: palo alto 206 | device_type: firewall 207 | os_version: 10.0.3 208 | site_code: mel 209 | 210 | prd-paloalto-02.prd.norn.local: 211 | hostname: prd-paloalto-02.prd.norn.local 212 | groups: 213 | - panos 214 | - prod 215 | - mel 216 | data: 217 | mgmt_ip: 10.0.16.22 218 | vendor: palo alto 219 | device_type: firewall 220 | os_version: 9.1.6 221 | site_code: mel 222 | 223 | prd-junos-06.prd.norn.local: 224 | hostname: prd-junos-06.prd.norn.local 225 | groups: 226 | - junos 227 | - prod 228 | - mel 229 | data: 230 | mgmt_ip: 10.0.16.23 231 | vendor: juniper 232 | device_type: switch 233 | os_version: 12.1R3-S4 234 | site_code: mel 235 | 236 | tst-csr-01.tst.norn.local: 237 | hostname: tst-csr-01.tst.norn.local 238 | groups: 239 | - ios 240 | - test 241 | - mel 242 | data: 243 | mgmt_ip: 10.0.32.16 244 | vendor: cisco 245 | device_type: router 246 | os_version: 16.6.4 247 | site_code: mel 248 | 249 | dfjt-r001.tst.norn.local: 250 | hostname: dfjt-r001.tst.norn.local 251 | groups: 252 | - ios 253 | - test 254 | - mel 255 | data: 256 | mgmt_ip: 10.0.32.1 257 | vendor: cisco 258 | device_type: router 259 | os_version: 15.1.4 260 | site_code: mel 261 | 262 | tst-arista-01.tst.norn.local: 263 | hostname: tst-arista-01.tst.norn.local 264 | groups: 265 | - eos 266 | - test 267 | - ptl 268 | data: 269 | mgmt_ip: 10.0.32.11 270 | vendor: arista 271 | device_type: switch 272 | os_version: 4.21.1F 273 | site_code: ptl 274 | 275 | tstt-arista-02.tst.norn.local: 276 | hostname: tstt-arista-02.tst.norn.local 277 | groups: 278 | - eos 279 | - test 280 | - ptl 281 | data: 282 | mgmt_ip: 10.0.32.18 283 | vendor: arista 284 | device_type: switch 285 | os_version: 4.21.1F 286 | site_code: ptl 287 | 288 | tst-junos-01.tst.norn.local: 289 | hostname: tst-junos-01.tst.norn.local 290 | groups: 291 | - junos 292 | - test 293 | - ptl 294 | data: 295 | mgmt_ip: 10.0.32.15 296 | vendor: juniper 297 | device_type: router 298 | os_version: 15.1R7-S6 299 | site_code: ptl 300 | 301 | tst-nxos-01.tst.norn.local: 302 | hostname: tst-nxos-01.tst.norn.local 303 | groups: 304 | - nxos 305 | - test 306 | - chc 307 | data: 308 | mgmt_ip: 10.0.32.14 309 | vendor: cisco 310 | device_type: switch 311 | os_version: 7.0(4) 312 | site_code: chc 313 | 314 | tst-paloalto-01.tst.norn.local: 315 | hostname: tst-paloalto-01.tst.norn.local 316 | groups: 317 | - panos 318 | - test 319 | - chc 320 | data: 321 | mgmt_ip: 10.0.32.21 322 | vendor: palo alto 323 | device_type: firewall 324 | os_version: 10.0.3 325 | site_code: chc 326 | 327 | tst-paloalto-02.tst.norn.local: 328 | hostname: tst-paloalto-02.tst.norn.local 329 | groups: 330 | - panos 331 | - test 332 | - chc 333 | data: 334 | mgmt_ip: 10.0.32.22 335 | vendor: palo alto 336 | device_type: firewall 337 | os_version: 8.0.8 338 | site_code: chc 339 | 340 | tst-junos-06.tst.norn.local: 341 | hostname: tst-junos-06.tst.norn.local 342 | groups: 343 | - junos 344 | - test 345 | - chc 346 | data: 347 | mgmt_ip: 10.0.32.23 348 | vendor: juniper 349 | device_type: switch 350 | os_version: 12.1R3-S4 351 | site_code: chc 352 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/hosts_extract.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | lab-csr-011.lab.norn.local: 3 | hostname: lab-csr-011.lab.norn.local 4 | groups: 5 | - ios 6 | - lab 7 | - mel 8 | data: # Anything under this key is custom data 9 | mgmt_ip: 10.0.0.16 # This is custom data 10 | vendor: cisco # So is this 11 | device_type: router # Same as this 12 | os_version: 16.6.4 # Also this too 13 | site_code: mel # Yes, and also this 14 | -------------------------------------------------------------------------------- /source/howto/filtering_deep_dive/inventory/hosts_extract_alternate.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | lab-csr-011.lab.norn.local: 3 | hostname: lab-csr-011.lab.norn.local 4 | groups: 5 | - ios 6 | - lab 7 | - mel 8 | data: # Anything under this key is custom data 9 | ip_addresses: 10 | mgmt: 10.0.0.16 # Alternate way of managing mgmt_ip data 11 | vendor: cisco 12 | device_type: router 13 | os_version: 16.6.4 14 | site_code: mel 15 | -------------------------------------------------------------------------------- /source/howto/handling_connections/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | inventory: 3 | options: 4 | host_file: "handling_connections/inventory/hosts.yaml" 5 | group_file: "handling_connections/inventory/groups.yaml" 6 | 7 | -------------------------------------------------------------------------------- /source/howto/handling_connections/inventory/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rt01: 3 | username: netdevops 4 | password: netdevops 5 | hostname: 192.168.56.20 6 | port: 22 7 | platform: huawei_vrp 8 | -------------------------------------------------------------------------------- /source/howto/handling_connections/inventory/test-hosts.yaml: -------------------------------------------------------------------------------- 1 | dev1.group_1: 2 | port: 22 3 | hostname: 192.168.56.20 4 | username: username 5 | password: password 6 | platform: ios 7 | connection_options: 8 | netmiko: 9 | port: 22 10 | hostname: 11 | username: user 12 | password: pass 13 | platform: ios 14 | extras: 15 | optional_args: 16 | secret: secret 17 | session_log: path/to/save_log 18 | 19 | napalm: 20 | platform: ios 21 | extras: 22 | optional_args: 23 | path: path/to/save_log 24 | 25 | dummy: 26 | hostname: dummy_from_host 27 | port: 28 | username: 29 | password: 30 | platform: 31 | extras: 32 | blah: from_host -------------------------------------------------------------------------------- /source/howto/index.rst: -------------------------------------------------------------------------------- 1 | HowTo指南 2 | ============= 3 | 4 | HowTo 指南主要是根据特定场景来举例说明 Nornir 的基本/进阶用法,以及 Nornir 与其他模块相结合的扩展用法。 5 | 6 | 7 | .. toctree:: 8 | :glob: 9 | :maxdepth: 1 10 | 11 | 高级过滤方法 <01.advanced_filtering.ipynb> 12 | 处理设备连接 <03.handling_connections.ipynb> 13 | .. 安装指南 <03.install.ipynb> 14 | .. 初始化 Nornir <04.initializing_nornir.ipynb> 15 | .. 主机清单 <05.inventory.ipynb> 16 | .. 执行任务 <06.tasks.ipynb> 17 | .. 处理任务结果 <07.task_results.ipynb> 18 | .. 处理失败任务 <08.failed_tasks.ipynb> 19 | .. 处理器 <09.processors.ipynb> 20 | -------------------------------------------------------------------------------- /source/index.rst: -------------------------------------------------------------------------------- 1 | .. Nornir documentation master file, created by 2 | sphinx-quickstart on Sat Jul 31 12:58:42 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 欢迎阅读 Nornir 中文手册! 7 | ================================== 8 | 9 | .. image:: _static/logo/nornir_logo_02.jpg 10 | :height: 350px 11 | :width: 350px 12 | :target: https://github.com/nornir-automation/nornir 13 | 14 | 15 | 本手册是基于 `官方文档 `_ 的不完全翻译,内容相比官方文档有些增删改动,希望对想要使用 Nornir 的朋友有所帮助。 16 | 17 | 本人能力有限,文中难免会有疏漏或表意不当的地方,欢迎大家随时指正:vip@xdai.vip。 18 | 19 | Contents 20 | ======== 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | 25 | 主页 26 | 入门教程 27 | HowTo指南 28 | 配置文件 29 | 插件 30 | .. Upgrading 31 | .. Contribute 32 | .. Changelog 33 | .. api/index 34 | 35 | 36 | Nornir 一览图 37 | ------------------- 38 | 39 | 下图包含了 Nornir 的大部分概念及简单的交互逻辑,希望可以帮助刚开始使用 Nornir 的朋友稍微加深一些理解。 40 | 41 | 如有纰漏之处,还请指正。 42 | 43 | .. image:: tutorial/overview/nornir.svg 44 | 45 | 46 | Indices and tables 47 | ================== 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | -------------------------------------------------------------------------------- /source/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /source/plugins/_static/execution_model.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/plugins/_static/execution_model.graffle -------------------------------------------------------------------------------- /source/plugins/_static/execution_model_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/plugins/_static/execution_model_1.png -------------------------------------------------------------------------------- /source/plugins/_static/execution_model_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdai555/nornir_docs_cn/f7b7cd801a162a441df8a4e35fff45930f6d2659/source/plugins/_static/execution_model_2.png -------------------------------------------------------------------------------- /source/plugins/execution_model.rst: -------------------------------------------------------------------------------- 1 | 线程插件执行模型 2 | ================= 3 | 4 | Nornir 优点之一是它可以并行执行任务,工作方式如下: 5 | 6 | 1. 你可以通过带有参数 ``num_workers > 1`` (默认值是 ``20``) 的 ``nornir.core.Nornir.run`` 对象运行任务以达到并行的目的; 7 | 2. 如果 ``num_workers == 1`` ,nornir 通过简单的循环一个接一个地在所有主机上运行任务,通常用于故障排除/调试、写入磁盘/数据库或仅在屏幕上打印内容; 8 | 3. 并行执行任务时,nornir 会为每个主机使用不同的线程。 9 | 10 | 下面这个图说明了工作流程: 11 | 12 | .. image:: _static/execution_model_1.png 13 | 14 | 15 | Nornir 也支持创建包含其他任务的任务,即 **任务组(Grouping tasks)** 。当运行任务组时,同一个子任务在所有主机上并行执行,子任务之间按顺序执行,这样就可以按照特定的需求来控制执行流程。 16 | 17 | 例如,可以编写如下工作流: 18 | 19 | .. image:: _static/execution_model_2.png 20 | 21 | 为什么要编写这样的工作流?大多数情况下,我们会尽可能多的将任务拆分进而形成不同的任务组,这样就可以保证脚本运行的更快,尤其是有很多主机的时候。 但是,某些任务可能需要在确保其他一些任务完成后才能运行。例如如下场景: 22 | 23 | 1. 并行进行多台主机的配置 24 | 2. 并行进行配置验证及测试 25 | 3. 按照顺序启动服务 26 | -------------------------------------------------------------------------------- /source/plugins/index.rst: -------------------------------------------------------------------------------- 1 | 插件 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | 插件介绍 8 | 线程执行模型 9 | 10 | 11 | -------------------------------------------------------------------------------- /source/plugins/plugins.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "16ad5583-e886-44d8-a7af-2a5cd70a40ca", 6 | "metadata": {}, 7 | "source": [ 8 | "## 插件\n", 9 | "\n", 10 | "Nornir 是一个插件式系统,nornir3 中只包含了非常基础的插件,可以访问 [nornir.tech](https://nornir.tech/nornir/plugins) 来查看已经公开发布的第三方插件,本篇介绍 nornir 自带的基础插件。\n", 11 | "\n", 12 | "### 注册插件\n", 13 | "\n", 14 | "从 nornir3 开始,插件需要注册之后才能使用,包括:\n", 15 | "\n", 16 | " - 主机清单插件(inventory plugins)\n", 17 | " - 转换函数(transform functions)\n", 18 | " - 连接插件(connection plugins)\n", 19 | " - 运行插件(runners)\n", 20 | "\n", 21 | "插件注册有两种方式:\n", 22 | "\n", 23 | "1. 在打包发布插件时添加 [entry point](https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins) 来让 nornir 自动注册\n", 24 | "2. 在脚本中编写代码手动注册\n", 25 | "\n", 26 | "\n", 27 | "#### 自动注册\n", 28 | "\n", 29 | "如果使用 `setuptools` 打包发布,需要在 `setup.py` 中添加 `entry_points`:\n", 30 | "\n", 31 | "```python\n", 32 | "setup(\n", 33 | " # ...\n", 34 | " entry_points={\n", 35 | " \"PATH\": \"NAME = path.to:Plugin\",\n", 36 | " }\n", 37 | ")\n", 38 | "```\n", 39 | "\n", 40 | "如果使用 `peotry` 打包发布,需要在 pyproject.toml 中添加配置:\n", 41 | "\n", 42 | "```python\n", 43 | "[tool.poetry.plugins.\"PATH\"]\n", 44 | "\"NAME\" = \"path.to:Plugin\"\n", 45 | "```\n", 46 | "\n", 47 | "\n", 48 | "其中:\n", 49 | "\n", 50 | "`PATH` 是需要配置的插件类型,有以下几个值:\n", 51 | "\n", 52 | " - `nornir.plugins.inventory` - for inventory plugins\n", 53 | " - `nornir.plugins.transform_function` - for transform functions\n", 54 | " - `nornir.plugins.runners` - for runners\n", 55 | " - `nornir.plugins.connections` - for connection plugins\n", 56 | "\n", 57 | "`NAME` 是要引用插件的的名称, `path.to:Plugin` 是导入路径,例如定义一个名为 `inventory-name` 的主机清单插件:\n", 58 | "\n", 59 | "```python\n", 60 | "[tool.poetry.plugins.\"nornir.plugins.inventory\"]\n", 61 | "\"inventory-name\" = \"path.to:InventoryPlugin\"\n", 62 | "```\n", 63 | "\n", 64 | "使用带有 `entry point` 打包发布的插件,使用时 nornir 会自动进行注册。\n", 65 | "\n", 66 | "#### 手动注册\n", 67 | "\n", 68 | "如果需要使用本地编写的插件,或者发布时没有添加 `entry point` 的插件,需要以代码的方式来注册,例如注册一个名为 `inventory-name` 的主机清单插件:\n", 69 | "\n", 70 | "```python\n", 71 | "from nornir.core.plugins.inventory import InventoryPluginRegister\n", 72 | "from path.to import InventoryPlugin\n", 73 | "\n", 74 | "InventoryPluginRegister.register(\"inventory-name\", InventoryPlugin)\n", 75 | "```\n", 76 | "\n", 77 | "\n", 78 | "### 连接插件(Connections)\n", 79 | "\n", 80 | "是一种让 nornir 管理与设备的连接的插件,常见的有 netmiko、paramiko、napalm 等。\n", 81 | "\n", 82 | "\n", 83 | "### 主机清单插件(Inventory)\n", 84 | "\n", 85 | "是一种让 nornir 从外部源(如 YAML、CSV、DB 等)创建一个 `Inventory` 对象的插件,常见的有 SimpleInventory、ansible、netbox、table 等。\n", 86 | "\n", 87 | "#### SimpleInventory\n", 88 | "\n", 89 | "Nornir 默认包含 SimpleInventory 插件,它从 YAML 文件读取信息,源码定义如下:\n", 90 | "\n", 91 | "```python\n", 92 | "class nornir.plugins.inventory.__init__.SimpleInventory(\n", 93 | " host_file: str = 'hosts.yaml', \n", 94 | " group_file: str = 'groups.yaml', \n", 95 | " defaults_file: str = 'defaults.yaml', \n", 96 | " encoding: str = 'utf-8'\n", 97 | "):\n", 98 | "\n", 99 | " def load()-> nornir.core.inventory.Inventory:...\n", 100 | "```\n", 101 | "\n", 102 | "\n", 103 | "### 转换函数(Transform function)\n", 104 | "\n", 105 | "是一种独立于已使用的主机清单插件来操作主机的插件,可以帮助用户使用环境变量、加密存储或者类似的方式来对主机的数据进行扩展。\n", 106 | "\n", 107 | "\n", 108 | "### 运行插件(Runners)\n", 109 | "\n", 110 | "是一种定义如何在主机上执行任务的插件,分为单线程和多线程。\n", 111 | "\n", 112 | "#### SerialRunner\n", 113 | "\n", 114 | "SerialRunner 单线程插件,在每个主机上顺序执行任务,源码定义如下:\n", 115 | "\n", 116 | "```python\n", 117 | "class nornir.plugins.runners.__init__.SerialRunner():\n", 118 | " \n", 119 | " def run(\n", 120 | " task: nornir.core.task.Task, \n", 121 | " hosts: List[nornir.core.inventory.Host]\n", 122 | " ) -> nornir.core.task.AggregatedResult: ...\n", 123 | "\n", 124 | "```\n", 125 | "\n", 126 | "#### ThreadedRunner\n", 127 | "\n", 128 | "ThreadedRunner 使用多线程在每个主机上异步执行任务,源码定义如下:\n", 129 | "\n", 130 | "```python\n", 131 | "class nornir.plugins.runners.__init__.ThreadedRunner(num_workers: int = 20):\n", 132 | " \"\"\"Parameters:\n", 133 | " num_workers – number of threads to use\"\"\"\n", 134 | " \n", 135 | " def run(\n", 136 | " task: nornir.core.task.Task,\n", 137 | " hosts: List[nornir.core.inventory.Host]\n", 138 | " ) -> nornir.core.task.AggregatedResult: ...\n", 139 | "```\n", 140 | "\n", 141 | "有关 ThreadedRunner 的更多详细信息,请阅读 [任务执行模型](./execution_model.rst)。\n", 142 | "\n", 143 | "### 处理器(Processors)\n", 144 | "\n", 145 | "是一种让用户编写任意代码来控制任务执行事件的插件,具体使用方法见入门教程中的 [处理器](../tutorial/09.processors.ipynb)。\n" 146 | ] 147 | } 148 | ], 149 | "metadata": { 150 | "kernelspec": { 151 | "display_name": "Python 3 (ipykernel)", 152 | "language": "python", 153 | "name": "python3" 154 | }, 155 | "language_info": { 156 | "codemirror_mode": { 157 | "name": "ipython", 158 | "version": 3 159 | }, 160 | "file_extension": ".py", 161 | "mimetype": "text/x-python", 162 | "name": "python", 163 | "nbconvert_exporter": "python", 164 | "pygments_lexer": "ipython3", 165 | "version": "3.8.10" 166 | } 167 | }, 168 | "nbformat": 4, 169 | "nbformat_minor": 5 170 | } 171 | -------------------------------------------------------------------------------- /source/tutorial/00.index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "19e3df17-6500-4f57-be05-bbafd67feb89", 6 | "metadata": {}, 7 | "source": [ 8 | "# Learning Nornir\n", 9 | "\n", 10 | "欢迎阅读 Nornir 入门教程。\n", 11 | "\n", 12 | "\n", 13 | "## 目录\n", 14 | "\n", 15 | "- [初识 Nornir](01.overview.ipynb)\n", 16 | "- [需要了解的 Python 知识](02.python.ipynb)\n", 17 | "- [安装指南](03.install.ipynb)\n", 18 | "- [初始化 Nornir](04.initializing_nornir.ipynb)\n", 19 | "- [主机清单](05.inventory.ipynb)\n", 20 | "- [执行任务](06.tasks.ipynb)\n", 21 | "- [处理任务结果](07.task_results.ipynb)\n", 22 | "- [处理失败任务](08.failed_tasks.ipynb)\n", 23 | "- [处理器](09.processors.ipynb)" 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "kernelspec": { 29 | "display_name": "Python 3 (ipykernel)", 30 | "language": "python", 31 | "name": "python3" 32 | }, 33 | "language_info": { 34 | "codemirror_mode": { 35 | "name": "ipython", 36 | "version": 3 37 | }, 38 | "file_extension": ".py", 39 | "mimetype": "text/x-python", 40 | "name": "python", 41 | "nbconvert_exporter": "python", 42 | "pygments_lexer": "ipython3", 43 | "version": "3.8.10" 44 | } 45 | }, 46 | "nbformat": 4, 47 | "nbformat_minor": 5 48 | } 49 | -------------------------------------------------------------------------------- /source/tutorial/01.overview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "7c5b9dcf", 6 | "metadata": {}, 7 | "source": [ 8 | "## 初识 Nornir\n", 9 | " \n", 10 | "### Nornir 是什么?\n", 11 | "\n", 12 | "Nornir 是一个用 Python 编写的自动化框架。它与有一些其他自动化框架的不同之处在于,你只需要编写 Python 代码来使用 Nornir,而其他框架需要使用框架自定义的配置语言(伪语言([pseudo-language](https://encyclopedia2.thefreedictionary.com/pseudo+language)),注:需要用框架规定的语言格式来使用)。\n", 13 | "\n", 14 | "### 为什么要使用纯 Python?\n", 15 | "\n", 16 | "一般情况下,特定的配置语言可以快速上手使用。一段时间后,如果你需要使用更高级的特性,你可能必须需要另外一种编程语言来对其进行扩展(注:例如使用 SDK 进行二次封装),长此以往,二次封装后对原本的框架进行故障排除时造成很大的困难。\n", 17 | "\n", 18 | "由于 Nornir 使用的是纯 Python 代码,所以可以像处理任何其他 Python 代码一样对它进行故障排除和调试。\n", 19 | "\n", 20 | "### Nornir 和什么比较像?\n", 21 | "\n", 22 | "你可以将 Nornir 类比为 [Flask](https://flask.palletsprojects.com/en/2.0.x/),Flask 是一个可以创建 Web 应用的 Web 框架。它提供了非常简单的用户接口来让你无需使用特定的方式工作就能创建出强大的网站。\n", 23 | "\n", 24 | "Nornir 提供封装了许多复杂工作的用户接口来让你完成网络设备的自动化。" 25 | ] 26 | } 27 | ], 28 | "metadata": { 29 | "kernelspec": { 30 | "display_name": "Python 3 (ipykernel)", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.8.10" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 5 49 | } 50 | -------------------------------------------------------------------------------- /source/tutorial/02.python.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "eb2b090b-4824-4e99-99f0-7dbf60eb2998", 6 | "metadata": {}, 7 | "source": [ 8 | "## 需要具备的 Python 知识\n", 9 | "\n", 10 | "为了使用 Nornir,你必须具备一些 Python 知识。如果你已经是一个比较熟练的 Python 使用者,可以直接跳过本节,到下一节查看[安装指导](#安装指导)\n", 11 | "\n", 12 | "如果你从来没有使用 Python 写过代码,并且没有任何其他编程语言的经验,也不用害怕。你不必对 Python 非常精通,只需要学习下面列出的 Python 基础知识之后,再回来继续学习 Nornir!\n", 13 | "\n", 14 | "你可以参考 [Python 官方教程](https://docs.python.org/3/tutorial/) 进行学习。\n", 15 | "\n", 16 | "为了继续学习 Nornir,你需要:\n", 17 | "\n", 18 | "- 在你的电脑上配置 Python 环境\n", 19 | "- 安装 Virtualenv 和 Python 相关的包\n", 20 | "- 了解下面几个 Python 的基础概念\n", 21 | "\n", 22 | " - 变量(Variables)\n", 23 | " - 函数(Functions)\n", 24 | " - 引入(Imports)" 25 | ] 26 | } 27 | ], 28 | "metadata": { 29 | "kernelspec": { 30 | "display_name": "Python 3 (ipykernel)", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.8.10" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 5 49 | } 50 | -------------------------------------------------------------------------------- /source/tutorial/03.install.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b3a8158e-8f4e-4a2c-b3e4-7bb96fca3f17", 6 | "metadata": {}, 7 | "source": [ 8 | "## 安装指导\n", 9 | "\n", 10 | "### 安装 Nornir\n", 11 | "\n", 12 | "在安装 Nornir 之前,建议你创建自己的 Python 虚拟环境(Virtualenv)。这样可以保证你的操作不会影响到系统中 Python 的环境。\n", 13 | "\n", 14 | "> 本教程不提供 Python 虚拟环境的安装指导;也不提供 pip 相关的安装方法,假设你已经在系统中装好了这些。\n", 15 | "\n", 16 | "Nornir 发布在 Pypi 上,你可以像安装其他 Python 包一样使用 pip 工具来安装 Nornir。\n", 17 | "\n", 18 | "你可以直接运行下面的代码行来确定当前的 pip 环境。" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "id": "2aecfc18-2c06-4b56-93d9-ca5192afe5bf", 25 | "metadata": {}, 26 | "outputs": [ 27 | { 28 | "name": "stdout", 29 | "output_type": "stream", 30 | "text": [ 31 | "pip 21.2.1 from C:\\Users\\xdai\\AppData\\Roaming\\Python\\Python38\\site-packages\\pip (python 3.8)\n", 32 | "\n" 33 | ] 34 | } 35 | ], 36 | "source": [ 37 | "!pip3 --version" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "3c70b124-96b0-48cc-8862-58290ea86ec0", 43 | "metadata": {}, 44 | "source": [ 45 | "如果你已经有了 pip 工具,安装 Nornir 非常简单。运行以下代码来进行安装。\n", 46 | "\n", 47 | "```\n", 48 | "$pip install nornir\n", 49 | "Collecting nornir\n", 50 | " Downloading nornir-3.0.0-py3-none-any.whl (28 kB)\n", 51 | "Requirement already satisfied: typing_extensions<4.0,>=3.7 in /home/dbarroso/.virtualenvs/tmp-nornir/lib/python3.8/site-packages (from nornir) (3.7.4.2)\n", 52 | "Requirement already satisfied: mypy_extensions<0.5.0,>=0.4.1 in /home/dbarroso/.virtualenvs/tmp-nornir/lib/python3.8/site-packages (from nornir) (0.4.3)\n", 53 | "Collecting ruamel.yaml<0.17,>=0.16\n", 54 | " Using cached ruamel.yaml-0.16.10-py2.py3-none-any.whl (111 kB)\n", 55 | "Collecting ruamel.yaml.clib>=0.1.2; platform_python_implementation == \"CPython\" and python_version < \"3.9\"\n", 56 | " Using cached ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl (578 kB)\n", 57 | "Installing collected packages: colorama, ruamel.yaml.clib, ruamel.yaml, nornir\n", 58 | "Successfully installed nornir-3.0.0 ruamel.yaml-0.16.10 ruamel.yaml.clib-0.2.0\n", 59 | "```\n", 60 | "\n", 61 | "运行完命令后,如果最后一行是 `Successfully installed`,说明你已经成功安装了 Nornir。\n", 62 | "\n", 63 | "现在可以运行以下命令来验证 Nornir 使用成功安装。" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 2, 69 | "id": "8e09117b-3c9b-443e-8386-587b7bff8492", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "from nornir import InitNornir" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "id": "446c17d8-3acc-4b2a-a9a9-5f9fdc82b61a", 79 | "metadata": {}, 80 | "source": [ 81 | "如果你能成功运行这个引入命令,证明你已经成功安装了 Nornir。" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "7a1d3b4b-d419-455f-b566-0832dea8e295", 87 | "metadata": {}, 88 | "source": [ 89 | "### 插件\n", 90 | "\n", 91 | "Nornir 是一个插件式自动化框架,Nornir3 中只保留了最基本的插件,其他功能都通过第三方插件来扩展实现。关于插件的相关内容,请查看对应的章节;有关第三方插件的列表,请访问 [nornir.tech](https://nornir.tech/nornir/plugins/)。" 92 | ] 93 | } 94 | ], 95 | "metadata": { 96 | "kernelspec": { 97 | "display_name": "Python 3 (ipykernel)", 98 | "language": "python", 99 | "name": "python3" 100 | }, 101 | "language_info": { 102 | "codemirror_mode": { 103 | "name": "ipython", 104 | "version": 3 105 | }, 106 | "file_extension": ".py", 107 | "mimetype": "text/x-python", 108 | "name": "python", 109 | "nbconvert_exporter": "python", 110 | "pygments_lexer": "ipython3", 111 | "version": "3.8.10" 112 | } 113 | }, 114 | "nbformat": 4, 115 | "nbformat_minor": 5 116 | } 117 | -------------------------------------------------------------------------------- /source/tutorial/04.initializing_nornir.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "57fcb2cd-9a16-4c83-9a35-dd2ea95858aa", 6 | "metadata": {}, 7 | "source": [ 8 | "## 初始化 Nornir\n", 9 | "\n", 10 | "初始化 Nornir 对象的方法是使用 `InitNornir` 函数。\n", 11 | "\n", 12 | "`InitNornir` 可以使用配置文件、代码或者两者结合起来使用来初始化一个 Nornir 对象。\n", 13 | "\n", 14 | "先从配置文件开始看,下面是一个 Nornir 的[配置文件](files/config.yaml)。" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "id": "e463abf2-02c6-4073-ba2e-bb9502ca1b0d", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# %load files/config.yaml\n", 25 | "---\n", 26 | "inventory:\n", 27 | " plugin: SimpleInventory\n", 28 | " options:\n", 29 | " host_file: \"files/inventory/hosts.yaml\"\n", 30 | " group_file: \"files/inventory/groups.yaml\"\n", 31 | " defaults_file: \"files/inventory/defaults.yaml\"\n", 32 | "runner:\n", 33 | " plugin: threaded\n", 34 | " options:\n", 35 | " num_workers: 100" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "id": "bcd2eb96-c203-407d-883a-77ebc78a5aa0", 41 | "metadata": { 42 | "tags": [] 43 | }, 44 | "source": [ 45 | "现在你可以创建一个 Nornir 对象:" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "id": "cb667105-879f-4354-91ae-5957e51d1033", 52 | "metadata": { 53 | "tags": [] 54 | }, 55 | "outputs": [], 56 | "source": [ 57 | "from nornir import InitNornir\r\n", 58 | "nr = InitNornir(config_file=\"files/config.yaml\")" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "id": "8bec90e4-2adb-4c06-86da-271448d72fe1", 64 | "metadata": {}, 65 | "source": [ 66 | "也可以不用配置文件,通过传参的方式来初始化 Nornir 对象,如下:" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "id": "1090387f-f47a-4a21-adf0-3c0fc2818e28", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "from nornir import InitNornir\r\n", 77 | "\r\n", 78 | "nr = InitNornir(\r\n", 79 | " runner={\r\n", 80 | " \"plugin\": \"threaded\",\r\n", 81 | " \"options\": {\r\n", 82 | " \"num_workers\": 100,\r\n", 83 | " },\r\n", 84 | " },\r\n", 85 | " inventory={\r\n", 86 | " \"plugin\": \"SimpleInventory\",\r\n", 87 | " \"options\": {\r\n", 88 | " \"host_file\": \"files/inventory/hosts.yaml\",\r\n", 89 | " \"group_file\": \"files/inventory/groups.yaml\",\r\n", 90 | " },\r\n", 91 | " },\r\n", 92 | ")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "a3c114f0-da05-4084-89f3-2f554c618cf7", 98 | "metadata": {}, 99 | "source": [ 100 | "或者两种方式混合使用:" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 4, 106 | "id": "591d08f4-6ed3-42f6-b4d3-6e6a55b7681f", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "from nornir import InitNornir\n", 111 | "nr = InitNornir(\n", 112 | " config_file=\"files/config.yaml\",\n", 113 | " runner={\n", 114 | " \"plugin\": \"threaded\",\n", 115 | " \"options\": {\n", 116 | " \"num_workers\": 100,\n", 117 | " },\n", 118 | " },\n", 119 | ")" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "bb81fdff-f3e6-4853-82dc-895487f6a78c", 125 | "metadata": {}, 126 | "source": [ 127 | "Nornir 对象有一个 `dict` 方法,可以看到 data 和 inventory 相关的信息,执行下面代码可以查看:" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 5, 133 | "id": "17f7dfcd-0cb1-43e0-81f5-95717414f669", 134 | "metadata": { 135 | "collapsed": true, 136 | "jupyter": { 137 | "outputs_hidden": true 138 | }, 139 | "tags": [] 140 | }, 141 | "outputs": [ 142 | { 143 | "name": "stdout", 144 | "output_type": "stream", 145 | "text": [ 146 | "{'data': {'dry_run': False, 'failed_hosts': set()},\n", 147 | " 'inventory': {'defaults': {'connection_options': {},\n", 148 | " 'data': {'domain': 'netdevops.local'},\n", 149 | " 'hostname': None,\n", 150 | " 'password': None,\n", 151 | " 'platform': None,\n", 152 | " 'port': None,\n", 153 | " 'username': None},\n", 154 | " 'groups': {'bj': {'connection_options': {},\n", 155 | " 'data': {},\n", 156 | " 'groups': ['north', 'global'],\n", 157 | " 'hostname': None,\n", 158 | " 'name': 'bj',\n", 159 | " 'password': None,\n", 160 | " 'platform': None,\n", 161 | " 'port': None,\n", 162 | " 'username': None},\n", 163 | " 'global': {'connection_options': {},\n", 164 | " 'data': {'asn': 1,\n", 165 | " 'domain': 'global.local'},\n", 166 | " 'groups': [],\n", 167 | " 'hostname': None,\n", 168 | " 'name': 'global',\n", 169 | " 'password': None,\n", 170 | " 'platform': None,\n", 171 | " 'port': None,\n", 172 | " 'username': None},\n", 173 | " 'gz': {'connection_options': {},\n", 174 | " 'data': {'asn': 65000,\n", 175 | " 'vlans': {100: 'wired',\n", 176 | " 200: 'wireless'}},\n", 177 | " 'groups': [],\n", 178 | " 'hostname': None,\n", 179 | " 'name': 'gz',\n", 180 | " 'password': None,\n", 181 | " 'platform': None,\n", 182 | " 'port': None,\n", 183 | " 'username': None},\n", 184 | " 'north': {'connection_options': {},\n", 185 | " 'data': {'asn': 65100},\n", 186 | " 'groups': [],\n", 187 | " 'hostname': None,\n", 188 | " 'name': 'north',\n", 189 | " 'password': None,\n", 190 | " 'platform': None,\n", 191 | " 'port': None,\n", 192 | " 'username': None}},\n", 193 | " 'hosts': {'host00': {'connection_options': {},\n", 194 | " 'data': {},\n", 195 | " 'groups': ['gz', 'bj'],\n", 196 | " 'hostname': None,\n", 197 | " 'name': 'host00',\n", 198 | " 'password': None,\n", 199 | " 'platform': None,\n", 200 | " 'port': None,\n", 201 | " 'username': None},\n", 202 | " 'host01': {'connection_options': {},\n", 203 | " 'data': {},\n", 204 | " 'groups': ['bj', 'gz'],\n", 205 | " 'hostname': None,\n", 206 | " 'name': 'host01',\n", 207 | " 'password': None,\n", 208 | " 'platform': None,\n", 209 | " 'port': None,\n", 210 | " 'username': None},\n", 211 | " 'host01.bj': {'connection_options': {},\n", 212 | " 'data': {'nested_data': {'a_dict': {'a': 1,\n", 213 | " 'b': 2},\n", 214 | " 'a_list': [1,\n", 215 | " 2],\n", 216 | " 'a_string': 'this '\n", 217 | " 'is '\n", 218 | " 'a '\n", 219 | " 'web '\n", 220 | " 'server'},\n", 221 | " 'role': 'host',\n", 222 | " 'site': 'bj',\n", 223 | " 'type': 'host'},\n", 224 | " 'groups': ['bj'],\n", 225 | " 'hostname': '127.0.0.1',\n", 226 | " 'name': 'host01.bj',\n", 227 | " 'password': 'netdevops',\n", 228 | " 'platform': 'linux',\n", 229 | " 'port': 2201,\n", 230 | " 'username': 'netdevops'},\n", 231 | " 'host01.gz': {'connection_options': {},\n", 232 | " 'data': {'role': 'host',\n", 233 | " 'site': 'gz',\n", 234 | " 'type': 'host'},\n", 235 | " 'groups': ['gz'],\n", 236 | " 'hostname': None,\n", 237 | " 'name': 'host01.gz',\n", 238 | " 'password': None,\n", 239 | " 'platform': 'linux',\n", 240 | " 'port': None,\n", 241 | " 'username': None},\n", 242 | " 'leaf00.bj': {'connection_options': {},\n", 243 | " 'data': {'asn': 65100,\n", 244 | " 'role': 'leaf',\n", 245 | " 'site': 'bj',\n", 246 | " 'type': 'network_device'},\n", 247 | " 'groups': ['bj'],\n", 248 | " 'hostname': '127.0.0.1',\n", 249 | " 'name': 'leaf00.bj',\n", 250 | " 'password': 'netdevops',\n", 251 | " 'platform': 'hp_comware',\n", 252 | " 'port': 12443,\n", 253 | " 'username': 'netdevops'},\n", 254 | " 'leaf01.bj': {'connection_options': {},\n", 255 | " 'data': {'asn': 65101,\n", 256 | " 'role': 'leaf',\n", 257 | " 'site': 'bj',\n", 258 | " 'type': 'network_device'},\n", 259 | " 'groups': ['bj'],\n", 260 | " 'hostname': '127.0.0.1',\n", 261 | " 'name': 'leaf01.bj',\n", 262 | " 'password': '',\n", 263 | " 'platform': 'huawei',\n", 264 | " 'port': 12203,\n", 265 | " 'username': 'netdevops'},\n", 266 | " 'leaf01.gz': {'connection_options': {},\n", 267 | " 'data': {'role': 'leaf',\n", 268 | " 'site': 'gz',\n", 269 | " 'type': 'network_device'},\n", 270 | " 'groups': ['gz'],\n", 271 | " 'hostname': '127.0.0.1',\n", 272 | " 'name': 'leaf01.gz',\n", 273 | " 'password': 'netdevops',\n", 274 | " 'platform': 'eos',\n", 275 | " 'port': 12443,\n", 276 | " 'username': 'netdevops'},\n", 277 | " 'spine00.bj': {'connection_options': {},\n", 278 | " 'data': {'role': 'spine',\n", 279 | " 'site': 'bj',\n", 280 | " 'type': 'network_device'},\n", 281 | " 'groups': ['bj'],\n", 282 | " 'hostname': '127.0.0.1',\n", 283 | " 'name': 'spine00.bj',\n", 284 | " 'password': 'netdevops',\n", 285 | " 'platform': 'ios',\n", 286 | " 'port': 12444,\n", 287 | " 'username': 'netdevops'},\n", 288 | " 'spine01.bj': {'connection_options': {},\n", 289 | " 'data': {'role': 'spine',\n", 290 | " 'site': 'bj',\n", 291 | " 'type': 'network_device'},\n", 292 | " 'groups': ['bj'],\n", 293 | " 'hostname': '127.0.0.1',\n", 294 | " 'name': 'spine01.bj',\n", 295 | " 'password': '',\n", 296 | " 'platform': 'junos',\n", 297 | " 'port': 12204,\n", 298 | " 'username': 'netdevops'},\n", 299 | " 'spine01.gz': {'connection_options': {},\n", 300 | " 'data': {'role': 'spine',\n", 301 | " 'site': 'gz',\n", 302 | " 'type': 'network_device'},\n", 303 | " 'groups': ['gz'],\n", 304 | " 'hostname': '127.0.0.1',\n", 305 | " 'name': 'spine01.gz',\n", 306 | " 'password': 'netdevops',\n", 307 | " 'platform': 'eos',\n", 308 | " 'port': 12444,\n", 309 | " 'username': 'netdevops'}}}}\n" 310 | ] 311 | } 312 | ], 313 | "source": [ 314 | "from pprint import pprint as print\n", 315 | "print(nr.dict())" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "id": "3b080db1-cd72-4aeb-96c6-e728539ce2d6", 321 | "metadata": {}, 322 | "source": [ 323 | "这里看到的是运行时指定的 data 参数和所有的主机信息,配置相关的信息则存储在 config 的 `dict` 方法里,这里可以看到包括默认配置在内的所有配置:" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 6, 329 | "id": "5d3c7e41-0bff-4413-8419-62353c69d1dc", 330 | "metadata": { 331 | "tags": [] 332 | }, 333 | "outputs": [ 334 | { 335 | "name": "stdout", 336 | "output_type": "stream", 337 | "text": [ 338 | "{'core': {'raise_on_error': False},\n", 339 | " 'inventory': {'options': {'defaults_file': 'files/inventory/defaults.yaml',\n", 340 | " 'group_file': 'files/inventory/groups.yaml',\n", 341 | " 'host_file': 'files/inventory/hosts.yaml'},\n", 342 | " 'plugin': 'SimpleInventory',\n", 343 | " 'transform_function': '',\n", 344 | " 'transform_function_options': {}},\n", 345 | " 'logging': {'enabled': True,\n", 346 | " 'format': '%(asctime)s - %(name)12s - %(levelname)8s - '\n", 347 | " '%(funcName)10s() - %(message)s',\n", 348 | " 'level': 'INFO',\n", 349 | " 'log_file': 'nornir.log',\n", 350 | " 'loggers': ['nornir'],\n", 351 | " 'to_console': False},\n", 352 | " 'runner': {'options': {'num_workers': 100}, 'plugin': 'threaded'},\n", 353 | " 'ssh': {'config_file': 'C:\\\\Users\\\\xdai\\\\.ssh\\\\config'},\n", 354 | " 'user_defined': {}}\n" 355 | ] 356 | } 357 | ], 358 | "source": [ 359 | "print(nr.config.dict())" 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "id": "b063e535-4139-40c4-8623-28921c2b4637", 365 | "metadata": {}, 366 | "source": [ 367 | "从这两个例子可以看出,Nornir 数据相关的字段都是封装成字典的格式来返回给用户。如果想要取某个部分的值,就可以直接使用字典的方式来取,比如查看配置中的并发数量(注:Nornir 默认的线程池并发是 20):" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 7, 373 | "id": "9b15811a-b0f6-46f7-bc42-817de223901c", 374 | "metadata": {}, 375 | "outputs": [ 376 | { 377 | "name": "stdout", 378 | "output_type": "stream", 379 | "text": [ 380 | "100\n" 381 | ] 382 | } 383 | ], 384 | "source": [ 385 | "print(nr.config.runner.options[\"num_workers\"])" 386 | ] 387 | } 388 | ], 389 | "metadata": { 390 | "kernelspec": { 391 | "display_name": "Python 3 (ipykernel)", 392 | "language": "python", 393 | "name": "python3" 394 | }, 395 | "language_info": { 396 | "codemirror_mode": { 397 | "name": "ipython", 398 | "version": 3 399 | }, 400 | "file_extension": ".py", 401 | "mimetype": "text/x-python", 402 | "name": "python", 403 | "nbconvert_exporter": "python", 404 | "pygments_lexer": "ipython3", 405 | "version": "3.8.10" 406 | } 407 | }, 408 | "nbformat": 4, 409 | "nbformat_minor": 5 410 | } 411 | -------------------------------------------------------------------------------- /source/tutorial/05.inventory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "266f05b3-a916-4ad2-afd6-af7342f26f39", 6 | "metadata": { 7 | "tags": [] 8 | }, 9 | "source": [ 10 | "## 主机清单\n", 11 | "\n", 12 | "**主机清单(Inventory)** 是 nornir 最重要的部分,它由 hosts、groups、defaults 三部分组成。它还支持多种插件,默认情况下使用 `SimpleInventory` 插件。在之前的版本中,nornir 还支持 Ansible、Netbox 等主机格式的插件,3.0 版本之后,除了最核心的功能外,其他的功能都需要手动导入插件来使用。\n", 13 | "\n", 14 | "在本教程中使用 `SimpleInventory` 插件来了解主机清单相关的内容。\n", 15 | "\n", 16 | "可以在 [nornir.tech](https://nornir.tech/nornir/plugins/) 中获取当前已经公开发布的插件。\n", 17 | "\n", 18 | "在 `SimpleInventory` 插件中,需要 hosts、groups、defaults 三个文件来存储信息,其中 groups、defaults 文件不是必需的。\n", 19 | "\n", 20 | "主机相关的文件都使用 YAML 格式来保存数据,YAML 是一种可读性较好的标记语言,有关 YAML 的内容,可以查看 [YAML 入门教程](https://www.runoob.com/w3cnote/yaml-intro.html)或者 [YAML 官方手册](http://yaml.org/spec/1.2/spec.html)。\n", 21 | "\n", 22 | "现在来看一个 hosts 的示例文件:" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "id": "06d724f5-d558-418b-9791-2691adcf8740", 29 | "metadata": { 30 | "jupyter": { 31 | "source_hidden": true 32 | }, 33 | "tags": [] 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "# %load files/inventory/hosts.yaml\n", 38 | "---\n", 39 | "host01.bj:\n", 40 | " hostname: 127.0.0.1\n", 41 | " port: 2201\n", 42 | " username: netdevops\n", 43 | " password: netdevops\n", 44 | " platform: linux\n", 45 | " groups:\n", 46 | " - bj\n", 47 | " data:\n", 48 | " site: bj\n", 49 | " role: host\n", 50 | " type: host\n", 51 | " nested_data:\n", 52 | " a_dict:\n", 53 | " a: 1\n", 54 | " b: 2\n", 55 | " a_list: [1, 2]\n", 56 | " a_string: \"this is a web server\"\n", 57 | "\n", 58 | "spine00.bj:\n", 59 | " hostname: 127.0.0.1\n", 60 | " username: netdevops\n", 61 | " password: netdevops\n", 62 | " port: 12444\n", 63 | " platform: ios\n", 64 | " groups:\n", 65 | " - bj\n", 66 | " data:\n", 67 | " site: bj\n", 68 | " role: spine\n", 69 | " type: network_device\n", 70 | "\n", 71 | "spine01.bj:\n", 72 | " hostname: 127.0.0.1\n", 73 | " username: netdevops\n", 74 | " password: \"\"\n", 75 | " platform: junos\n", 76 | " port: 12204\n", 77 | " groups:\n", 78 | " - bj\n", 79 | " data:\n", 80 | " site: bj\n", 81 | " role: spine\n", 82 | " type: network_device\n", 83 | "\n", 84 | "leaf00.bj:\n", 85 | " hostname: 127.0.0.1\n", 86 | " username: netdevops\n", 87 | " password: netdevops\n", 88 | " port: 12443\n", 89 | " platform: hp_comware\n", 90 | " groups:\n", 91 | " - bj\n", 92 | " data:\n", 93 | " site: bj\n", 94 | " role: leaf\n", 95 | " type: network_device\n", 96 | " asn: 65100\n", 97 | "\n", 98 | "leaf01.bj:\n", 99 | " hostname: 127.0.0.1\n", 100 | " username: netdevops\n", 101 | " password: \"\"\n", 102 | " port: 12203\n", 103 | " platform: huawei\n", 104 | " groups:\n", 105 | " - bj\n", 106 | " data:\n", 107 | " site: bj\n", 108 | " role: leaf\n", 109 | " type: network_device\n", 110 | " asn: 65101\n", 111 | "\n", 112 | "host01.gz:\n", 113 | " groups:\n", 114 | " - gz\n", 115 | " platform: linux\n", 116 | " data:\n", 117 | " site: gz\n", 118 | " role: host\n", 119 | " type: host\n", 120 | "\n", 121 | "spine01.gz:\n", 122 | " hostname: 127.0.0.1\n", 123 | " username: netdevops\n", 124 | " password: netdevops\n", 125 | " port: 12444\n", 126 | " platform: eos\n", 127 | " groups:\n", 128 | " - gz\n", 129 | " data:\n", 130 | " site: gz\n", 131 | " role: spine\n", 132 | " type: network_device\n", 133 | "\n", 134 | "leaf01.gz:\n", 135 | " hostname: 127.0.0.1\n", 136 | " username: netdevops\n", 137 | " password: netdevops\n", 138 | " port: 12443\n", 139 | " platform: eos\n", 140 | " groups:\n", 141 | " - gz\n", 142 | " data:\n", 143 | " site: gz\n", 144 | " role: leaf\n", 145 | " type: network_device\n", 146 | "\n", 147 | "host00:\n", 148 | " groups:\n", 149 | " - gz\n", 150 | " - bj\n", 151 | "\n", 152 | "host01:\n", 153 | " groups:\n", 154 | " - bj\n", 155 | " - gz" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "f2e04a09-07ea-4b34-a971-e594c43dce25", 161 | "metadata": { 162 | "tags": [] 163 | }, 164 | "source": [ 165 | "主机文件是由键值对组成的映射表,其中最外层的是主机名,第二层是主机的一些基本信息,第三层、第四层是主机的其他相关信息。可以通过以下代码来查看一个主机对象的数据模型:" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 2, 171 | "id": "ab4f3ce8-039a-4c2f-abac-897733cf7b0b", 172 | "metadata": {}, 173 | "outputs": [ 174 | { 175 | "name": "stdout", 176 | "output_type": "stream", 177 | "text": [ 178 | "{\n", 179 | " \"name\": \"str\",\n", 180 | " \"connection_options\": {\n", 181 | " \"$connection_type\": {\n", 182 | " \"extras\": {\n", 183 | " \"$key\": \"$value\"\n", 184 | " },\n", 185 | " \"hostname\": \"str\",\n", 186 | " \"port\": \"int\",\n", 187 | " \"username\": \"str\",\n", 188 | " \"password\": \"str\",\n", 189 | " \"platform\": \"str\"\n", 190 | " }\n", 191 | " },\n", 192 | " \"groups\": [\n", 193 | " \"$group_name\"\n", 194 | " ],\n", 195 | " \"data\": {\n", 196 | " \"$key\": \"$value\"\n", 197 | " },\n", 198 | " \"hostname\": \"str\",\n", 199 | " \"port\": \"int\",\n", 200 | " \"username\": \"str\",\n", 201 | " \"password\": \"str\",\n", 202 | " \"platform\": \"str\"\n", 203 | "}\n" 204 | ] 205 | } 206 | ], 207 | "source": [ 208 | "from nornir.core.inventory import Host\n", 209 | "import json\n", 210 | "print(json.dumps(Host.schema(), indent=4))" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "id": "2b24b00b-0b13-431c-a99e-d4d0c2a1a6a8", 216 | "metadata": {}, 217 | "source": [ 218 | "通过这段代码可以看到一个主机对象可以包含的所有信息。\n", 219 | "\n", 220 | "如果需要登录设备,那么 `connection_options` 里面的 5 个参数 hostname、port、username、password、platform 是必须包含的(注:默认情况下,`connection_options` 会从第二层进行取值,如果设备的登录地址和资产管理地址不一样,可以在该选项里面单独指定),如果有额外的连接参数需要传递(如 enable password 、指定连接方式等),则需要在 `extras` 里面进行添加;其他字段都是可以选的,其中用户可以将所需的任意信息定义到 `data` 字段中。\n", 221 | "\n", 222 | "当然,如果主机信息只做资产管理的作用,没有登录设备的需求,除了最外层的主机名以外,其他字段都是可选的。" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "id": "7cfb43c3-fff7-4c94-9a2c-b377fb547736", 228 | "metadata": {}, 229 | "source": [ 230 | "groups 文件和 hosts 文件一样,也是由键值对映射组成,来看一个示例:" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 3, 236 | "id": "d22340a4-7cef-4251-a61e-29679d5e54c5", 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "# %load files/inventory/groups.yaml\n", 241 | "---\n", 242 | "global:\n", 243 | " data:\n", 244 | " domain: global.local\n", 245 | " asn: 1\n", 246 | "\n", 247 | "north:\n", 248 | " data:\n", 249 | " asn: 65100\n", 250 | "\n", 251 | "bj:\n", 252 | " groups:\n", 253 | " - north\n", 254 | " - global\n", 255 | "\n", 256 | "gz:\n", 257 | " data:\n", 258 | " asn: 65000\n", 259 | " vlans:\n", 260 | " 100: wired\n", 261 | " 200: wireless" 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "id": "5b6e4539-0f31-407b-b6f3-deba0692abbe", 267 | "metadata": {}, 268 | "source": [ 269 | "最后,defaults 文件与之前描述的 Host 对象架构一样,但是它只有 `data` 字段,没有其他外层键值对。" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 4, 275 | "id": "1d480dec-6968-4ae6-b72f-a7344d854133", 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "# %load files/inventory/defaults.yaml\n", 280 | "---\n", 281 | "data:\n", 282 | " domain: netdevops.local" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "id": "d7f3d340-d0d6-4623-9bf2-bd3ac1f5b29b", 288 | "metadata": {}, 289 | "source": [ 290 | "### 访问主机清单\n", 291 | "\n", 292 | "可以通过 nornir 对象的 `inventory` 属性来访问主机清单。" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 5, 298 | "id": "b2f3e48b-485d-4d33-aa41-26d7cd1ba91f", 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "from nornir import InitNornir\n", 303 | "nr = InitNornir(config_file=\"files/config.yaml\")" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "id": "631b422c-55bb-44af-9faa-d83f5ad6ee75", 309 | "metadata": {}, 310 | "source": [ 311 | "主机清单有两个类字典(dict-like)的属性:`hosts` 和 `groups`,通过访问该属性,可以获取到当前有哪些主机和组。" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "id": "05700e17-3ec9-49b4-938d-1f3641ef3165", 317 | "metadata": {}, 318 | "source": [ 319 | "查看加载的配置文件中包含哪些主机:" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": 6, 325 | "id": "b008a7df-508f-433e-a9b3-9c99d1fe1a8d", 326 | "metadata": {}, 327 | "outputs": [ 328 | { 329 | "data": { 330 | "text/plain": [ 331 | "{'host01.bj': Host: host01.bj,\n", 332 | " 'spine00.bj': Host: spine00.bj,\n", 333 | " 'spine01.bj': Host: spine01.bj,\n", 334 | " 'leaf00.bj': Host: leaf00.bj,\n", 335 | " 'leaf01.bj': Host: leaf01.bj,\n", 336 | " 'host01.gz': Host: host01.gz,\n", 337 | " 'spine01.gz': Host: spine01.gz,\n", 338 | " 'leaf01.gz': Host: leaf01.gz,\n", 339 | " 'host00': Host: host00,\n", 340 | " 'host01': Host: host01}" 341 | ] 342 | }, 343 | "execution_count": 6, 344 | "metadata": {}, 345 | "output_type": "execute_result" 346 | } 347 | ], 348 | "source": [ 349 | "nr.inventory.hosts" 350 | ] 351 | }, 352 | { 353 | "cell_type": "markdown", 354 | "id": "d95561a5-05be-410f-9f13-d4c705926cb1", 355 | "metadata": {}, 356 | "source": [ 357 | "查看加载的配置文件中包含哪些组:" 358 | ] 359 | }, 360 | { 361 | "cell_type": "code", 362 | "execution_count": 7, 363 | "id": "4486dc59-6684-4733-9e51-cb62089c4ed7", 364 | "metadata": {}, 365 | "outputs": [ 366 | { 367 | "data": { 368 | "text/plain": [ 369 | "{'global': Group: global,\n", 370 | " 'north': Group: north,\n", 371 | " 'bj': Group: bj,\n", 372 | " 'gz': Group: gz}" 373 | ] 374 | }, 375 | "execution_count": 7, 376 | "metadata": {}, 377 | "output_type": "execute_result" 378 | } 379 | ], 380 | "source": [ 381 | "nr.inventory.groups" 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "id": "1ec711af-866e-4130-a447-0ed9f528a873", 387 | "metadata": {}, 388 | "source": [ 389 | "主机和组都是类字典(dict-like)形式的对象,可以通过 `[$values]` 来访问它们的属性,以主机 `host01.bj` 为例,来查看一下这个主包含哪些属性:" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": 8, 395 | "id": "f79be93e-2cf1-4772-9889-2ecd8f0c552f", 396 | "metadata": {}, 397 | "outputs": [ 398 | { 399 | "data": { 400 | "text/plain": [ 401 | "dict_keys(['site', 'role', 'type', 'nested_data', 'asn', 'domain'])" 402 | ] 403 | }, 404 | "execution_count": 8, 405 | "metadata": {}, 406 | "output_type": "execute_result" 407 | } 408 | ], 409 | "source": [ 410 | "host = nr.inventory.hosts[\"host01.bj\"]\n", 411 | "host.keys()" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "id": "ba8b7d4d-9447-4d71-b547-3b1515222535", 417 | "metadata": {}, 418 | "source": [ 419 | "查看这个主机位于哪个站点:" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": 9, 425 | "id": "a3a6b801-9ff9-44bc-8a88-714c120cff1c", 426 | "metadata": {}, 427 | "outputs": [ 428 | { 429 | "data": { 430 | "text/plain": [ 431 | "'bj'" 432 | ] 433 | }, 434 | "execution_count": 9, 435 | "metadata": {}, 436 | "output_type": "execute_result" 437 | } 438 | ], 439 | "source": [ 440 | "host[\"site\"]" 441 | ] 442 | }, 443 | { 444 | "cell_type": "markdown", 445 | "id": "c465eabe-f67c-4e0d-b079-bdad7ae258cf", 446 | "metadata": {}, 447 | "source": [ 448 | "### 继承模型" 449 | ] 450 | }, 451 | { 452 | "cell_type": "markdown", 453 | "id": "359dd4cc-a23d-4ffd-8bcf-b123610d6552", 454 | "metadata": {}, 455 | "source": [ 456 | "Nornir 中,hosts、groups、defaults 数据之间有继承关系,下面来看一下继承是如何工作的。" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": 10, 462 | "id": "928259fc-6a2f-4bb6-a471-9c23114a00ea", 463 | "metadata": {}, 464 | "outputs": [], 465 | "source": [ 466 | "# %load files/inventory/groups.yaml\n", 467 | "---\n", 468 | "global:\n", 469 | " data:\n", 470 | " domain: global.local\n", 471 | " asn: 1\n", 472 | "\n", 473 | "north:\n", 474 | " data:\n", 475 | " asn: 65100\n", 476 | "\n", 477 | "bj:\n", 478 | " groups:\n", 479 | " - north\n", 480 | " - global\n", 481 | "\n", 482 | "gz:\n", 483 | " data:\n", 484 | " asn: 65000\n", 485 | " vlans:\n", 486 | " 100: wired\n", 487 | " 200: wireless" 488 | ] 489 | }, 490 | { 491 | "cell_type": "markdown", 492 | "id": "c4f802f1-6ff9-4254-88b6-07d0737b342a", 493 | "metadata": {}, 494 | "source": [ 495 | "在 `hosts.yaml` 中,可以看到 `host01.bj` 属于 `bj` 组,`bj` 组又属于 `north` 和 `global` 组;主机 `host01.gz` 属于 `gz` 组。\n", 496 | "\n", 497 | "在这里,nornir 的数据解析方式是:递归遍历所属的父组,并查看任意父组中是否包含相应的数据。" 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": 11, 503 | "id": "c5151c5a-daaf-45d8-8212-0652a5c8e506", 504 | "metadata": {}, 505 | "outputs": [ 506 | { 507 | "data": { 508 | "text/plain": [ 509 | "'global.local'" 510 | ] 511 | }, 512 | "execution_count": 11, 513 | "metadata": {}, 514 | "output_type": "execute_result" 515 | } 516 | ], 517 | "source": [ 518 | "host01_bj = nr.inventory.hosts[\"host01.bj\"]\n", 519 | "host01_bj[\"domain\"] # 继承自 `global` 组" 520 | ] 521 | }, 522 | { 523 | "cell_type": "code", 524 | "execution_count": 12, 525 | "id": "b04cd998-72e9-4e38-99ea-4602973b464d", 526 | "metadata": {}, 527 | "outputs": [ 528 | { 529 | "data": { 530 | "text/plain": [ 531 | "65100" 532 | ] 533 | }, 534 | "execution_count": 12, 535 | "metadata": {}, 536 | "output_type": "execute_result" 537 | } 538 | ], 539 | "source": [ 540 | "host01_bj[\"asn\"] # 继承自 `north` 组" 541 | ] 542 | }, 543 | { 544 | "cell_type": "markdown", 545 | "id": "147c91d6-4457-4506-8978-9d0e41487836", 546 | "metadata": {}, 547 | "source": [ 548 | "如果主机有数据,那么优先使用主机具有的数据,而不是从父组继承:" 549 | ] 550 | }, 551 | { 552 | "cell_type": "code", 553 | "execution_count": 13, 554 | "id": "a4609e6f-d898-4807-9a3a-04c17e28d1fc", 555 | "metadata": {}, 556 | "outputs": [ 557 | { 558 | "data": { 559 | "text/plain": [ 560 | "65101" 561 | ] 562 | }, 563 | "execution_count": 13, 564 | "metadata": {}, 565 | "output_type": "execute_result" 566 | } 567 | ], 568 | "source": [ 569 | "leaf01_bj = nr.inventory.hosts[\"leaf01.bj\"]\n", 570 | "leaf01_bj[\"asn\"] # 主机的 asn 为 65101,父组 `bj` 的 asn 为 65100" 571 | ] 572 | }, 573 | { 574 | "cell_type": "markdown", 575 | "id": "bef53c5c-e16a-41b7-be43-033c70af06b7", 576 | "metadata": {}, 577 | "source": [ 578 | "如果主机、父组都没有数据,那么会从 `defaults` 中继承:" 579 | ] 580 | }, 581 | { 582 | "cell_type": "code", 583 | "execution_count": 14, 584 | "id": "e943aa5e-f3b4-4180-9565-962b5139b363", 585 | "metadata": {}, 586 | "outputs": [ 587 | { 588 | "data": { 589 | "text/plain": [ 590 | "'netdevops.local'" 591 | ] 592 | }, 593 | "execution_count": 14, 594 | "metadata": {}, 595 | "output_type": "execute_result" 596 | } 597 | ], 598 | "source": [ 599 | "host01_gz = nr.inventory.hosts[\"host01.gz\"]\n", 600 | "host01_gz[\"domain\"] # 从 `defaults` 中继承数据" 601 | ] 602 | }, 603 | { 604 | "cell_type": "markdown", 605 | "id": "a26b9d9f-ce68-434f-b84d-d1d99fd0d402", 606 | "metadata": {}, 607 | "source": [ 608 | "如果 nornir 遍历了所有的父组,而且 `defaults` 中也没有数据,则会返回 `KeyError`:" 609 | ] 610 | }, 611 | { 612 | "cell_type": "code", 613 | "execution_count": 15, 614 | "id": "7c109e8e-e08f-4c7e-8ad8-19f5c3bed521", 615 | "metadata": {}, 616 | "outputs": [ 617 | { 618 | "name": "stdout", 619 | "output_type": "stream", 620 | "text": [ 621 | "无法找到数据:'non_existent'\n" 622 | ] 623 | } 624 | ], 625 | "source": [ 626 | "try:\n", 627 | " host01_gz[\"non_existent\"]\n", 628 | "except KeyError as e:\n", 629 | " print(f\"无法找到数据:{e}\")" 630 | ] 631 | }, 632 | { 633 | "cell_type": "markdown", 634 | "id": "e11cbce0-2a14-4209-850f-5660bb44e494", 635 | "metadata": {}, 636 | "source": [ 637 | "如果不想遍历父组的话,可以直接使用主机的 `data` 属性来访问。例如从上面的示例中 `host01_bj` 的 asn 是继承自父组 `north`,直接通过 `data` 来访问这个属性的话,不会遍历父组,而是返回 `KeyError` 的错误。" 638 | ] 639 | }, 640 | { 641 | "cell_type": "markdown", 642 | "id": "1563d565-a11f-45bc-b7a1-c6d67b62fb2e", 643 | "metadata": {}, 644 | "source": [ 645 | "**父组之间数据的优先级关系**\n", 646 | "\n", 647 | "Nornir 通过遍历所有父组来查找数据,那么如果多个父组里面有相同的数据,会如何取值?通过一个不恰当的例子来看一下,`host00` 和 `host01` 都属于 `bj` 和 `gz` 组,但是配置文件中的顺序有所差异:" 648 | ] 649 | }, 650 | { 651 | "cell_type": "code", 652 | "execution_count": 16, 653 | "id": "7d903163-8f63-4d80-8957-51e0f810233d", 654 | "metadata": {}, 655 | "outputs": [ 656 | { 657 | "name": "stdout", 658 | "output_type": "stream", 659 | "text": [ 660 | "[Group: gz, Group: bj]\n" 661 | ] 662 | }, 663 | { 664 | "data": { 665 | "text/plain": [ 666 | "65000" 667 | ] 668 | }, 669 | "execution_count": 16, 670 | "metadata": {}, 671 | "output_type": "execute_result" 672 | } 673 | ], 674 | "source": [ 675 | "host00 = nr.inventory.hosts[\"host00\"]\n", 676 | "print(host00.groups) # `gz` 的 asn 为 65000\n", 677 | "host00[\"asn\"]" 678 | ] 679 | }, 680 | { 681 | "cell_type": "code", 682 | "execution_count": 17, 683 | "id": "74eeea0f-c14f-437d-a1b6-4eb61b1dca79", 684 | "metadata": {}, 685 | "outputs": [ 686 | { 687 | "name": "stdout", 688 | "output_type": "stream", 689 | "text": [ 690 | "[Group: bj, Group: gz]\n" 691 | ] 692 | }, 693 | { 694 | "data": { 695 | "text/plain": [ 696 | "65100" 697 | ] 698 | }, 699 | "execution_count": 17, 700 | "metadata": {}, 701 | "output_type": "execute_result" 702 | } 703 | ], 704 | "source": [ 705 | "host01 = nr.inventory.hosts[\"host01\"]\n", 706 | "print(host01.groups) # `bj` 的 asn 为 65100,继承自 `north`\n", 707 | "host01[\"asn\"]" 708 | ] 709 | }, 710 | { 711 | "cell_type": "markdown", 712 | "id": "41500850-1c72-45b7-ab45-9f7ab6864cd4", 713 | "metadata": {}, 714 | "source": [ 715 | "可以看到如果主机属于多个组,数据解析是按照列表的先后顺序进行迭代,源码实现中是对数据的 `key` 做了判断,如果遍历已经找到了对应的 `key`,之后不会再更新数据。" 716 | ] 717 | }, 718 | { 719 | "cell_type": "markdown", 720 | "id": "73046bd0-d0e0-4f2d-95f3-5e2cbcd7b92b", 721 | "metadata": {}, 722 | "source": [ 723 | "### 主机清单的过滤方法" 724 | ] 725 | }, 726 | { 727 | "cell_type": "markdown", 728 | "id": "4f0d38b4-7c4a-48e4-a33d-5bec72129c08", 729 | "metadata": {}, 730 | "source": [ 731 | "到目前已经看到 `nr.inventory.hosts` 和 `nr.inventory.groups` 是类字典(dict-like)的对象,可以使用它们来遍历所有主机和组或直接访问任何特定的主机和组。现在来看看如何进行一些更高级的过滤:根据主机的属性对来对一组主机进行操作。\n", 732 | "\n", 733 | "过滤主机最简单的方法是通过 `filter` 传入键值对()参数,例如筛选站点是 `bj` 的机器:" 734 | ] 735 | }, 736 | { 737 | "cell_type": "code", 738 | "execution_count": 18, 739 | "id": "5a8de479-0e67-443e-9014-88a1123c50a0", 740 | "metadata": {}, 741 | "outputs": [ 742 | { 743 | "data": { 744 | "text/plain": [ 745 | "{'host01.bj': Host: host01.bj,\n", 746 | " 'spine00.bj': Host: spine00.bj,\n", 747 | " 'spine01.bj': Host: spine01.bj,\n", 748 | " 'leaf00.bj': Host: leaf00.bj,\n", 749 | " 'leaf01.bj': Host: leaf01.bj}" 750 | ] 751 | }, 752 | "execution_count": 18, 753 | "metadata": {}, 754 | "output_type": "execute_result" 755 | } 756 | ], 757 | "source": [ 758 | "nr.filter(site='bj').inventory.hosts" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "id": "e6e69cd4-f328-4578-a12f-0c72ccf1b864", 764 | "metadata": {}, 765 | "source": [ 766 | "也可以使用多个键值对来进行过滤,例如筛选站点是 `bj` 而且角色为 `spine` 的设备:" 767 | ] 768 | }, 769 | { 770 | "cell_type": "code", 771 | "execution_count": 19, 772 | "id": "3b530a2a-6cbe-40f4-9976-9fb433d6fab9", 773 | "metadata": { 774 | "tags": [] 775 | }, 776 | "outputs": [ 777 | { 778 | "data": { 779 | "text/plain": [ 780 | "{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}" 781 | ] 782 | }, 783 | "execution_count": 19, 784 | "metadata": {}, 785 | "output_type": "execute_result" 786 | } 787 | ], 788 | "source": [ 789 | "nr.filter(site='bj', role='spine').inventory.hosts" 790 | ] 791 | }, 792 | { 793 | "cell_type": "markdown", 794 | "id": "b64c3ead-f37d-4cc4-aba0-562c73870d82", 795 | "metadata": {}, 796 | "source": [ 797 | "`filter` 方法也可以进行叠加使用:" 798 | ] 799 | }, 800 | { 801 | "cell_type": "code", 802 | "execution_count": 20, 803 | "id": "d31bf05f-528b-41d7-a152-95d856cf94cc", 804 | "metadata": {}, 805 | "outputs": [ 806 | { 807 | "data": { 808 | "text/plain": [ 809 | "{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}" 810 | ] 811 | }, 812 | "execution_count": 20, 813 | "metadata": {}, 814 | "output_type": "execute_result" 815 | } 816 | ], 817 | "source": [ 818 | "nr.filter(site='bj').filter(role='spine').inventory.hosts" 819 | ] 820 | }, 821 | { 822 | "cell_type": "markdown", 823 | "id": "84b719d9-a470-467a-bbec-2f5277adae0b", 824 | "metadata": {}, 825 | "source": [ 826 | "或者赋值给对象,进行再次过滤:" 827 | ] 828 | }, 829 | { 830 | "cell_type": "code", 831 | "execution_count": 21, 832 | "id": "d867e9b2-ba2e-4166-a8a4-3a89f169efc2", 833 | "metadata": {}, 834 | "outputs": [], 835 | "source": [ 836 | "bj = nr.filter(site='bj')" 837 | ] 838 | }, 839 | { 840 | "cell_type": "code", 841 | "execution_count": 22, 842 | "id": "b166c6de-371c-469c-87fb-994799b01a87", 843 | "metadata": {}, 844 | "outputs": [ 845 | { 846 | "data": { 847 | "text/plain": [ 848 | "{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}" 849 | ] 850 | }, 851 | "execution_count": 22, 852 | "metadata": {}, 853 | "output_type": "execute_result" 854 | } 855 | ], 856 | "source": [ 857 | "bj.filter(role='spine').inventory.hosts" 858 | ] 859 | }, 860 | { 861 | "cell_type": "code", 862 | "execution_count": 23, 863 | "id": "aa77d62d-611e-47a2-9109-ff9f5e24a8a0", 864 | "metadata": {}, 865 | "outputs": [ 866 | { 867 | "data": { 868 | "text/plain": [ 869 | "{'leaf00.bj': Host: leaf00.bj, 'leaf01.bj': Host: leaf01.bj}" 870 | ] 871 | }, 872 | "execution_count": 23, 873 | "metadata": {}, 874 | "output_type": "execute_result" 875 | } 876 | ], 877 | "source": [ 878 | "bj.filter(role='leaf').inventory.hosts" 879 | ] 880 | }, 881 | { 882 | "cell_type": "markdown", 883 | "id": "ff1e8b55-f487-4c90-8f17-6a0d4b070fc1", 884 | "metadata": {}, 885 | "source": [ 886 | "还可以根据组进行过滤,例如查找所有属于 `bj` 组的主机:" 887 | ] 888 | }, 889 | { 890 | "cell_type": "code", 891 | "execution_count": 24, 892 | "id": "aba5c6e3-ce1a-479a-859b-2ed36422a054", 893 | "metadata": {}, 894 | "outputs": [ 895 | { 896 | "data": { 897 | "text/plain": [ 898 | "{Host: host00,\n", 899 | " Host: host01,\n", 900 | " Host: host01.bj,\n", 901 | " Host: leaf00.bj,\n", 902 | " Host: leaf01.bj,\n", 903 | " Host: spine00.bj,\n", 904 | " Host: spine01.bj}" 905 | ] 906 | }, 907 | "execution_count": 24, 908 | "metadata": {}, 909 | "output_type": "execute_result" 910 | } 911 | ], 912 | "source": [ 913 | "nr.inventory.children_of_group('bj')" 914 | ] 915 | }, 916 | { 917 | "cell_type": "markdown", 918 | "id": "71dc18c1-5d6e-4b8e-84b0-588b1e7e24f8", 919 | "metadata": {}, 920 | "source": [ 921 | "### 高级过滤方法" 922 | ] 923 | }, 924 | { 925 | "cell_type": "markdown", 926 | "id": "a4dd56dc-e615-4c16-97c7-ddac126a41c1", 927 | "metadata": {}, 928 | "source": [ 929 | "有时候使用键值对无法满足过滤需求,还可以使用更高级的过滤方式:\n", 930 | "\n", 931 | "1. 过滤函数(filter function)\n", 932 | "2. 过滤对象(filter object)\n", 933 | "\n", 934 | "#### 过滤函数(filter functions)\n", 935 | "\n", 936 | "Filter 方法里面的 `filter_func` 参数可以通过传入自定义代码来进行主机过滤。过滤函数的格式应该是 `my_func(host)`,其中参数是一个主机对象(Host)并且返回值必须是 `True` 或 `False` 来确定过滤结果是否是需要的主机。" 937 | ] 938 | }, 939 | { 940 | "cell_type": "code", 941 | "execution_count": 25, 942 | "id": "8c8c3b84-90e3-411e-95cd-c412646a4a47", 943 | "metadata": {}, 944 | "outputs": [ 945 | { 946 | "data": { 947 | "text/plain": [ 948 | "{'spine00.bj': Host: spine00.bj,\n", 949 | " 'spine01.bj': Host: spine01.bj,\n", 950 | " 'spine01.gz': Host: spine01.gz}" 951 | ] 952 | }, 953 | "execution_count": 25, 954 | "metadata": {}, 955 | "output_type": "execute_result" 956 | } 957 | ], 958 | "source": [ 959 | "# 过滤名字主机名长度为 10 的主机\n", 960 | "def has_long_name(host):\n", 961 | " return len(host.name) == 10\n", 962 | "\n", 963 | "nr.filter(filter_func=has_long_name).inventory.hosts" 964 | ] 965 | }, 966 | { 967 | "cell_type": "code", 968 | "execution_count": 26, 969 | "id": "c86b71b7-61a7-432b-a593-212eea1bb5a2", 970 | "metadata": {}, 971 | "outputs": [ 972 | { 973 | "data": { 974 | "text/plain": [ 975 | "{'host00': Host: host00, 'host01': Host: host01}" 976 | ] 977 | }, 978 | "execution_count": 26, 979 | "metadata": {}, 980 | "output_type": "execute_result" 981 | } 982 | ], 983 | "source": [ 984 | "# 或者使用 lambda 函数\n", 985 | "nr.filter(filter_func=lambda h: len(h.name)==6).inventory.hosts" 986 | ] 987 | }, 988 | { 989 | "cell_type": "markdown", 990 | "id": "c31924f0-739a-42fd-95a2-d0d18c810234", 991 | "metadata": {}, 992 | "source": [ 993 | "#### 过滤对象(filter object)\n", 994 | "\n", 995 | "使用过滤对象 `F` 来叠加创建复杂查询对象。\n", 996 | "\n", 997 | "`F` 对象作为 `filter` 方法的参数,也接受键值对传参,可以使用叠加的双下划线来访问到任意数据(类似于字典的 `[]` 取值),也可以使用 `__contains` 来检查一个元素中是否包含指定字符。同时还支持将多个 `F` 对象进行位运算(`&`、`|`、`~`)来返回查询对象。\n", 998 | "\n", 999 | "> 注:`__contains__` 一般情况下是 Python 容器对象的方法,在 nornir 中,groups 是一个列表,所以对组进行过滤时,应该使用 `__contains`。\n", 1000 | "\n", 1001 | "来看几个例子:" 1002 | ] 1003 | }, 1004 | { 1005 | "cell_type": "code", 1006 | "execution_count": 27, 1007 | "id": "767b54fa-16b6-4012-aa1c-411f286c907b", 1008 | "metadata": {}, 1009 | "outputs": [], 1010 | "source": [ 1011 | "# 首先引入 F 对象\n", 1012 | "from nornir.core.filter import F" 1013 | ] 1014 | }, 1015 | { 1016 | "cell_type": "code", 1017 | "execution_count": 28, 1018 | "id": "f097a6b2-5105-4eeb-a21d-21084ac586a2", 1019 | "metadata": {}, 1020 | "outputs": [ 1021 | { 1022 | "data": { 1023 | "text/plain": [ 1024 | "{'host01.bj': Host: host01.bj,\n", 1025 | " 'spine00.bj': Host: spine00.bj,\n", 1026 | " 'spine01.bj': Host: spine01.bj,\n", 1027 | " 'leaf00.bj': Host: leaf00.bj,\n", 1028 | " 'leaf01.bj': Host: leaf01.bj,\n", 1029 | " 'host00': Host: host00,\n", 1030 | " 'host01': Host: host01}" 1031 | ] 1032 | }, 1033 | "execution_count": 28, 1034 | "metadata": {}, 1035 | "output_type": "execute_result" 1036 | } 1037 | ], 1038 | "source": [ 1039 | "# 查看属于 `bj` 组的设备\n", 1040 | "bj = nr.filter(F(groups__contains='bj'))\n", 1041 | "bj.inventory.hosts" 1042 | ] 1043 | }, 1044 | { 1045 | "cell_type": "code", 1046 | "execution_count": 29, 1047 | "id": "ff0ba431-b969-40cd-a084-fbae1ae7134e", 1048 | "metadata": {}, 1049 | "outputs": [ 1050 | { 1051 | "data": { 1052 | "text/plain": [ 1053 | "{'host01.bj': Host: host01.bj}" 1054 | ] 1055 | }, 1056 | "execution_count": 29, 1057 | "metadata": {}, 1058 | "output_type": "execute_result" 1059 | } 1060 | ], 1061 | "source": [ 1062 | "# 查看 `bj` 组中,系统是 `linux` 的设备\n", 1063 | "bj_linux = nr.filter(F(groups__contains='bj') & F(platform='linux'))\n", 1064 | "bj_linux.inventory.hosts" 1065 | ] 1066 | }, 1067 | { 1068 | "cell_type": "code", 1069 | "execution_count": 30, 1070 | "id": "972e3a1e-d093-4fd4-9cda-9829a2715786", 1071 | "metadata": {}, 1072 | "outputs": [ 1073 | { 1074 | "data": { 1075 | "text/plain": [ 1076 | "{'spine00.bj': Host: spine00.bj,\n", 1077 | " 'spine01.gz': Host: spine01.gz,\n", 1078 | " 'leaf01.gz': Host: leaf01.gz}" 1079 | ] 1080 | }, 1081 | "execution_count": 30, 1082 | "metadata": {}, 1083 | "output_type": "execute_result" 1084 | } 1085 | ], 1086 | "source": [ 1087 | "# 查看系统是 `ios` 或者 `eos` 的设备\n", 1088 | "ios_or_eos = nr.filter(F(platform='ios') | F(platform='eos'))\n", 1089 | "ios_or_eos.inventory.hosts" 1090 | ] 1091 | }, 1092 | { 1093 | "cell_type": "code", 1094 | "execution_count": 31, 1095 | "id": "64644acd-7b71-4141-aba4-de15fceba501", 1096 | "metadata": {}, 1097 | "outputs": [], 1098 | "source": [ 1099 | "# 查看 `gz` 组中,角色不是 `spine` 的设备\n", 1100 | "gz_not_spine = nr.filter(F(groups__contains='gz') & ~F(role='spine'))" 1101 | ] 1102 | }, 1103 | { 1104 | "cell_type": "code", 1105 | "execution_count": 32, 1106 | "id": "15e7066b-39f9-42ca-84b3-4d05c4a668cc", 1107 | "metadata": {}, 1108 | "outputs": [ 1109 | { 1110 | "data": { 1111 | "text/plain": [ 1112 | "{'host01.gz': Host: host01.gz,\n", 1113 | " 'leaf01.gz': Host: leaf01.gz,\n", 1114 | " 'host00': Host: host00,\n", 1115 | " 'host01': Host: host01}" 1116 | ] 1117 | }, 1118 | "execution_count": 32, 1119 | "metadata": {}, 1120 | "output_type": "execute_result" 1121 | } 1122 | ], 1123 | "source": [ 1124 | "gz_not_spine.inventory.hosts" 1125 | ] 1126 | }, 1127 | { 1128 | "cell_type": "code", 1129 | "execution_count": 33, 1130 | "id": "4b8a8148-169f-4c9e-bfdf-5e02d42501e2", 1131 | "metadata": {}, 1132 | "outputs": [ 1133 | { 1134 | "data": { 1135 | "text/plain": [ 1136 | "{'host01.bj': Host: host01.bj}" 1137 | ] 1138 | }, 1139 | "execution_count": 33, 1140 | "metadata": {}, 1141 | "output_type": "execute_result" 1142 | } 1143 | ], 1144 | "source": [ 1145 | "# 使用 `__` 来查看用户自定义的数据,并检查 dicts/lists/strings 是否包含元素\n", 1146 | "nested_dict = nr.filter(F(nested_data__a_dict__a=1))\n", 1147 | "nested_dict.inventory.hosts" 1148 | ] 1149 | }, 1150 | { 1151 | "cell_type": "code", 1152 | "execution_count": 34, 1153 | "id": "bd03b28b-2643-4c2a-9419-c04e9ffa976d", 1154 | "metadata": {}, 1155 | "outputs": [ 1156 | { 1157 | "data": { 1158 | "text/plain": [ 1159 | "{'host01.bj': Host: host01.bj}" 1160 | ] 1161 | }, 1162 | "execution_count": 34, 1163 | "metadata": {}, 1164 | "output_type": "execute_result" 1165 | } 1166 | ], 1167 | "source": [ 1168 | "nested_list = nr.filter(F(nested_data__a_list__contains=1))\n", 1169 | "nested_list.inventory.hosts" 1170 | ] 1171 | }, 1172 | { 1173 | "cell_type": "code", 1174 | "execution_count": 35, 1175 | "id": "616601c5-04be-4cde-81ac-676f6bbba50c", 1176 | "metadata": {}, 1177 | "outputs": [ 1178 | { 1179 | "data": { 1180 | "text/plain": [ 1181 | "{'host01.bj': Host: host01.bj}" 1182 | ] 1183 | }, 1184 | "execution_count": 35, 1185 | "metadata": {}, 1186 | "output_type": "execute_result" 1187 | } 1188 | ], 1189 | "source": [ 1190 | "nested_string = nr.filter(F(nested_data__a_string__contains='web'))\n", 1191 | "nested_string.inventory.hosts" 1192 | ] 1193 | }, 1194 | { 1195 | "cell_type": "code", 1196 | "execution_count": 36, 1197 | "id": "5d58f181-cab5-481e-ac9f-1a1e0acae8c1", 1198 | "metadata": {}, 1199 | "outputs": [ 1200 | { 1201 | "data": { 1202 | "text/plain": [ 1203 | "{'spine00.bj': Host: spine00.bj,\n", 1204 | " 'spine01.bj': Host: spine01.bj,\n", 1205 | " 'spine01.gz': Host: spine01.gz,\n", 1206 | " 'leaf01.gz': Host: leaf01.gz}" 1207 | ] 1208 | }, 1209 | "execution_count": 36, 1210 | "metadata": {}, 1211 | "output_type": "execute_result" 1212 | } 1213 | ], 1214 | "source": [ 1215 | "# 也可以对键值对的数据进行 `__contains` 查找\n", 1216 | "host_os = nr.filter(F(platform__contains='os'))\n", 1217 | "host_os.inventory.hosts" 1218 | ] 1219 | } 1220 | ], 1221 | "metadata": { 1222 | "kernelspec": { 1223 | "display_name": "Python 3 (ipykernel)", 1224 | "language": "python", 1225 | "name": "python3" 1226 | }, 1227 | "language_info": { 1228 | "codemirror_mode": { 1229 | "name": "ipython", 1230 | "version": 3 1231 | }, 1232 | "file_extension": ".py", 1233 | "mimetype": "text/x-python", 1234 | "name": "python", 1235 | "nbconvert_exporter": "python", 1236 | "pygments_lexer": "ipython3", 1237 | "version": "3.8.10" 1238 | } 1239 | }, 1240 | "nbformat": 4, 1241 | "nbformat_minor": 5 1242 | } 1243 | -------------------------------------------------------------------------------- /source/tutorial/06.tasks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "072c8433-fd46-4ee1-8a49-29847507dd9f", 6 | "metadata": {}, 7 | "source": [ 8 | "## 任务\n", 9 | "\n", 10 | "之前的内容中已经了解了如何初始化 nornir 对象并查看其主机清单和主机组信息,这节内容说明了如何在主机或主机组中执行任务。\n", 11 | "\n", 12 | "任务是针对单台主机实现某种功能的一段可以重复使用的代码,例如收集信息等。\n", 13 | "\n", 14 | "在 nornir 中, **任务(Tasks)** 是一个将 `Task` 对象作为第一个参数并且返回值是 `Result` 对象的函数。\n", 15 | "\n", 16 | "在旧版本中,nornir 提供了一些内置的任务可以直接使用。从 3.0 版本开始,为了保持框架的纯粹性,剔除了除核心功能外的插件代码,现在需要自己来编写 Task 或者使用其他人贡献出来的插件。\n", 17 | "\n", 18 | "可以在 [nornir.tech](https://nornir.tech/nornir/plugins/) 中获取当前已经公开发布的插件。。\n", 19 | "\n", 20 | "现在来看一个 `Task` 的示例:" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "544b4be3-4e71-4a0f-94ba-bd1619b83935", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "# 初始化一个 nornir 对象\n", 31 | "# 导入 print_result 模块来处理 Result 对象\n", 32 | "from nornir import InitNornir\n", 33 | "from nornir_utils.plugins.functions import print_result\n", 34 | "\n", 35 | "nr = InitNornir(config_file=\"files/config.yaml\")\n", 36 | "# 为了保持内容简洁,只针对一些主机进行操作\n", 37 | "nr = nr.filter(site='bj',role='spine')" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "id": "28afe024-ce1b-40a8-8e9f-832416010ffd", 44 | "metadata": { 45 | "tags": [] 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "# 首先导入 Task 、Result 模块\n", 50 | "from nornir.core.task import Task, Result\n", 51 | "\n", 52 | "# 定义一个 task,作用是让主机输出 hello world。\n", 53 | "def hello_world(task: Task) -> Result:\n", 54 | " return Result(\n", 55 | " host=task.host,\n", 56 | " result=f\"{task.host.name} says hello world!\"\n", 57 | " )" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "id": "6b859d12-e49e-47d0-bb95-616fab036cf4", 63 | "metadata": {}, 64 | "source": [ 65 | "要运行这个 task,需要使用 nornir 对象的 `run` 方法,将 task 函数作为参数传递给 `run`,要获取到任务执行的结果,需要使用 `print_result` 方法打印出来:" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 3, 71 | "id": "084800b8-148b-4fa7-9bb9-4f76ecd90435", 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "name": "stdout", 76 | "output_type": "stream", 77 | "text": [ 78 | "\u001b[1m\u001b[36mhello_world*********************************************************************\u001b[0m\n", 79 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 80 | "\u001b[0m\u001b[1m\u001b[32mvvvv hello_world ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 81 | "\u001b[0mspine00.bj says hello world!\u001b[0m\n", 82 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hello_world ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 83 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 84 | "\u001b[0m\u001b[1m\u001b[32mvvvv hello_world ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 85 | "\u001b[0mspine01.bj says hello world!\u001b[0m\n", 86 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hello_world ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 87 | "\u001b[0m" 88 | ] 89 | } 90 | ], 91 | "source": [ 92 | "result = nr.run(task=hello_world)\n", 93 | "print_result(result)" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "151b5698-bc85-43b8-b91b-019ae2639447", 99 | "metadata": {}, 100 | "source": [ 101 | "定义 Task 函数时,支持 `**kwargs` 来传参,这样可以扩展 task 的功能性,例如:" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 4, 107 | "id": "2055d39a-bd48-49ef-9031-b3174d8b422b", 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "def say(task: Task, text: str) -> Result:\n", 112 | " return Result(\n", 113 | " host=task.host,\n", 114 | " result=f\"{task.host.name} says {text}\"\n", 115 | " )" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "39eb1f95-fd42-481d-b720-2219e86bf74d", 121 | "metadata": {}, 122 | "source": [ 123 | "然后可以像之前一样使用 nornir 对象的 `run` 方法来运行 task,这次需要指定额外的参数 `text`:" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "id": "9a571a96-cec4-4302-aead-b3377a38cd50", 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "name": "stdout", 134 | "output_type": "stream", 135 | "text": [ 136 | "\u001b[1m\u001b[36m再见~*****************************************************************************\u001b[0m\n", 137 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 138 | "\u001b[0m\u001b[1m\u001b[32mvvvv 再见~ ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 139 | "\u001b[0mspine00.bj says byebye!\u001b[0m\n", 140 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END 再见~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 141 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 142 | "\u001b[0m\u001b[1m\u001b[32mvvvv 再见~ ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 143 | "\u001b[0mspine01.bj says byebye!\u001b[0m\n", 144 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END 再见~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 145 | "\u001b[0m" 146 | ] 147 | } 148 | ], 149 | "source": [ 150 | "result = nr.run(\n", 151 | " name=\"再见~\",\n", 152 | " task=say,\n", 153 | " text=\"byebye!\"\n", 154 | ")\n", 155 | "print_result(result)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "360e0fa6-26a0-41de-b490-d63f5f6944fc", 161 | "metadata": {}, 162 | "source": [ 163 | "需要注意的是,在这个例子中传入了 `name` 参数来作为这个 task 的描述性名字,这个参数会在结果中显示出来。如果没有指定这个参数的话,则会使用 task 函数的名字。" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "5c5fed3f-dfaf-442b-b533-93ea6d8e101c", 169 | "metadata": {}, 170 | "source": [ 171 | "### 任务组\n", 172 | "\n", 173 | "一个任务(Tasks)可以调用其他的任务,这样就可以使用多个功能来组成更复杂的功能,这称为任务组(Grouping tasks)。\n", 174 | "\n", 175 | "来定义一个新的 task:" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 6, 181 | "id": "968d9cb6-cdd5-43b0-bab9-e29fe695b3c0", 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "def count(task: Task, number: int) -> Result:\n", 186 | " return Result(\n", 187 | " host=task.host,\n", 188 | " result=f\"{[n for n in range(0, number)]}\" \n", 189 | " )" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "id": "c4614348-ee17-4712-8967-1c1440d20cf6", 195 | "metadata": {}, 196 | "source": [ 197 | "然后将这个新的 task `count` 和之前的 `say` 结合起来,形成任务组,实现更复杂的工作流:" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 7, 203 | "id": "4bf4c205-7dd5-4d2e-b9d7-37e7f94e1ae2", 204 | "metadata": {}, 205 | "outputs": [], 206 | "source": [ 207 | "def greet_and_count(task: Task, number: int) -> Result:\n", 208 | " task.run(\n", 209 | " name=\"你好~\",\n", 210 | " task=say,\n", 211 | " text=\"Hi~\",\n", 212 | " )\n", 213 | " \n", 214 | " task.run(\n", 215 | " name=\"计数\",\n", 216 | " task=count,\n", 217 | " number=number,\n", 218 | " )\n", 219 | " \n", 220 | " task.run(\n", 221 | " name=\"再见\",\n", 222 | " task=say,\n", 223 | " text=\"byebye.\"\n", 224 | " )\n", 225 | " \n", 226 | " # 计算打招呼打了奇数次还是偶数次\n", 227 | " even_or_odds = \"even\" if number % 2 == 1 else \"odd\"\n", 228 | " return Result(\n", 229 | " host=task.host,\n", 230 | " result = f\"{task.host} counted {even_or_odds} times!\",\n", 231 | " )" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "id": "0913e01c-4286-4f93-961c-c1e006f6c692", 237 | "metadata": {}, 238 | "source": [ 239 | "来简单分析一个这个 task:\n", 240 | "1. 首先调用了 `say` 任务并传入了文本 “Hi~”;\n", 241 | "2. 之后调用了 `count` 任务,它接收中在父任务 `greet_and_count` 也定义的参数 `number`,这样可以在执行父任务时动态调整这部分参数;\n", 242 | "3. 然后再次调用了 `say` 任务,这次传入了文本 “byebye”;\n", 243 | "4. 之后 `if` 来判断计数情况;\n", 244 | "5. 最后返回了 `Result` 对象,将需要的信息返回。\n", 245 | "\n", 246 | "现在可以像调用一个普通的 task 一样来调用新定义的任务组:" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 8, 252 | "id": "0a4e97e2-2bb1-4572-89d0-d2556f9f4473", 253 | "metadata": {}, 254 | "outputs": [ 255 | { 256 | "name": "stdout", 257 | "output_type": "stream", 258 | "text": [ 259 | "\u001b[1m\u001b[36m对打招呼次数进行计数**********************************************************************\u001b[0m\n", 260 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 261 | "\u001b[0m\u001b[1m\u001b[32mvvvv 对打招呼次数进行计数 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 262 | "\u001b[0mspine00.bj counted even times!\u001b[0m\n", 263 | "\u001b[0m\u001b[1m\u001b[32m---- 你好~ ** changed : False ---------------------------------------------------- INFO\u001b[0m\n", 264 | "\u001b[0mspine00.bj says Hi~\u001b[0m\n", 265 | "\u001b[0m\u001b[1m\u001b[32m---- 计数 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 266 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 267 | "\u001b[0m\u001b[1m\u001b[32m---- 再见 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 268 | "\u001b[0mspine00.bj says byebye.\u001b[0m\n", 269 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END 对打招呼次数进行计数 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 270 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 271 | "\u001b[0m\u001b[1m\u001b[32mvvvv 对打招呼次数进行计数 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 272 | "\u001b[0mspine01.bj counted even times!\u001b[0m\n", 273 | "\u001b[0m\u001b[1m\u001b[32m---- 你好~ ** changed : False ---------------------------------------------------- INFO\u001b[0m\n", 274 | "\u001b[0mspine01.bj says Hi~\u001b[0m\n", 275 | "\u001b[0m\u001b[1m\u001b[32m---- 计数 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 276 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 277 | "\u001b[0m\u001b[1m\u001b[32m---- 再见 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 278 | "\u001b[0mspine01.bj says byebye.\u001b[0m\n", 279 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END 对打招呼次数进行计数 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 280 | "\u001b[0m" 281 | ] 282 | } 283 | ], 284 | "source": [ 285 | "result = nr.run(\n", 286 | " name=\"对打招呼次数进行计数\",\n", 287 | " task=greet_and_count,\n", 288 | " number=5,\n", 289 | ")\n", 290 | "print_result(result)" 291 | ] 292 | } 293 | ], 294 | "metadata": { 295 | "kernelspec": { 296 | "display_name": "Python 3 (ipykernel)", 297 | "language": "python", 298 | "name": "python3" 299 | }, 300 | "language_info": { 301 | "codemirror_mode": { 302 | "name": "ipython", 303 | "version": 3 304 | }, 305 | "file_extension": ".py", 306 | "mimetype": "text/x-python", 307 | "name": "python", 308 | "nbconvert_exporter": "python", 309 | "pygments_lexer": "ipython3", 310 | "version": "3.8.10" 311 | } 312 | }, 313 | "nbformat": 4, 314 | "nbformat_minor": 5 315 | } 316 | -------------------------------------------------------------------------------- /source/tutorial/07.task_results.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "13de96df-cfc3-4870-acb2-805777733804", 6 | "metadata": {}, 7 | "source": [ 8 | "## 处理任务结果\n", 9 | "\n", 10 | "在这节中一起来看一下如何处理任务(Tasks)的运行结果。\n", 11 | "\n", 12 | "先看下面的示例:" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "id": "4a1c2b92-5446-4857-97b1-0f3c7b6be747", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import logging\n", 23 | "\n", 24 | "from nornir import InitNornir\n", 25 | "from nornir.core.task import Task, Result\n", 26 | "\n", 27 | "nr = InitNornir(config_file=\"files/config.yaml\")\n", 28 | "spine_bj = nr.filter(site=\"bj\", role=\"spine\")\n", 29 | "\n", 30 | "def count(task: Task, number: int) -> Result:\n", 31 | " return Result(\n", 32 | " host=task.host,\n", 33 | " result=f\"{[n for n in range(0, number)]}\"\n", 34 | " )\n", 35 | "\n", 36 | "def say(task: Task, text: str) -> Result:\n", 37 | " if task.host.name == \"spine01.bj\":\n", 38 | " raise Exception(f\"{task.host.name} 不能输出信息\")\n", 39 | " return Result(\n", 40 | " host=task.host,\n", 41 | " result=f\"{task.host.name} says {text}\"\n", 42 | " )" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "id": "0cd44e4d-1bc5-4353-b559-7b39d1ad67ce", 48 | "metadata": {}, 49 | "source": [ 50 | "这个示例与之前示例的区别是:通过 if 判断让主机 `spine01.bj` 强制抛出了一个错误信息。\n", 51 | "\n", 52 | "再继续编写任务组:" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 2, 58 | "id": "0ffe143a-03e7-4ace-a200-2044a5024e10", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "def greet_and_count(task: Task, number: int) -> Result:\n", 63 | " task.run(\n", 64 | " name=\"你好~\",\n", 65 | " severity_level=logging.DEBUG,\n", 66 | " task=say,\n", 67 | " text=\"Hi~\",\n", 68 | " )\n", 69 | " \n", 70 | " task.run(\n", 71 | " name=\"计数\",\n", 72 | " task=count,\n", 73 | " number=number,\n", 74 | " )\n", 75 | " \n", 76 | " task.run(\n", 77 | " name=\"再见\",\n", 78 | " severity_level=logging.DEBUG,\n", 79 | " task=say,\n", 80 | " text=\"byebye.\"\n", 81 | " )\n", 82 | " \n", 83 | " # 计算打招呼打了奇数次还是偶数次\n", 84 | " even_or_odds = \"even\" if number % 2 == 1 else \"odd\"\n", 85 | " return Result(\n", 86 | " host=task.host,\n", 87 | " result = f\"{task.host} counted {even_or_odds} times!\",\n", 88 | " )" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "e85c411c-24e2-4fcb-817e-896a12b1af28", 94 | "metadata": {}, 95 | "source": [ 96 | "这个任务组与之前编写的任务组一样,不同的地方是添加了 `severity_level=logging.DEBUG` 来输出任务执行的日志。现在来运行一下任务组,并把运行结果赋值给 `result`:" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 3, 102 | "id": "7256e098-a237-4329-8eb3-6d5e291342bd", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "result = spine_bj.run(\n", 107 | " task=greet_and_count,\n", 108 | " number=5\n", 109 | ")" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "id": "90f86119-4df0-4c76-ad11-14342f82e034", 115 | "metadata": {}, 116 | "source": [ 117 | "### 简单的任务处理方法\n", 118 | "\n", 119 | "大多数情况下,如果只想知道任务的执行结果,可以使用 `nornir_utils` 里面的 `print_result` 函数,之前的示例中已经在使用它来查看结果了。" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 4, 125 | "id": "cece02d8-4a1d-469a-9cee-ee3b036ae1be", 126 | "metadata": {}, 127 | "outputs": [ 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "\u001b[1m\u001b[36mgreet_and_count*****************************************************************\u001b[0m\n", 133 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 134 | "\u001b[0m\u001b[1m\u001b[32mvvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 135 | "\u001b[0mspine00.bj counted even times!\u001b[0m\n", 136 | "\u001b[0m\u001b[1m\u001b[32m---- 计数 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 137 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 138 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 139 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 140 | "\u001b[0m\u001b[1m\u001b[31mvvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR\u001b[0m\n", 141 | "\u001b[0mSubtask: 你好~ (failed)\n", 142 | "\u001b[0m\n", 143 | "\u001b[0m\u001b[1m\u001b[31m---- 你好~ ** changed : False ---------------------------------------------------- ERROR\u001b[0m\n", 144 | "\u001b[0mTraceback (most recent call last):\n", 145 | " File \"c:\\program files\\python38\\lib\\site-packages\\nornir\\core\\task.py\", line 99, in start\n", 146 | " r = self.task(self, **self.params)\n", 147 | " File \"C:\\Users\\xdai\\AppData\\Local\\Temp/ipykernel_16088/2400762698.py\", line 17, in say\n", 148 | " raise Exception(f\"{task.host.name} 不能输出信息\")\n", 149 | "Exception: spine01.bj 不能输出信息\n", 150 | "\u001b[0m\n", 151 | "\u001b[0m\u001b[1m\u001b[31m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 152 | "\u001b[0m" 153 | ] 154 | } 155 | ], 156 | "source": [ 157 | "from nornir_utils.plugins.functions import print_result\n", 158 | "\n", 159 | "print_result(result)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "id": "6f21f423-ed87-4420-93d4-32ac9d135af5", 165 | "metadata": {}, 166 | "source": [ 167 | "从结果中可以看到两台 `spine` 设备的执行结果,显示出来了两台主机上 `count` 任务的执行结果及第二台主机 `say` 任务的结果,仍然有一些其他的结果没有显示出来,下文将说明原因。\n", 168 | "\n", 169 | "现在来通过字典取值方式单独查看一下某台设备的任务执行结果:" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 5, 175 | "id": "ee776c70-ac18-4467-ac80-cc4126d06229", 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "name": "stdout", 180 | "output_type": "stream", 181 | "text": [ 182 | "\u001b[1m\u001b[32mvvvv spine00.bj: greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 183 | "\u001b[0mspine00.bj counted even times!\u001b[0m\n", 184 | "\u001b[0m\u001b[1m\u001b[32m---- 计数 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 185 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 186 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 187 | "\u001b[0m" 188 | ] 189 | } 190 | ], 191 | "source": [ 192 | "print_result(result[\"spine00.bj\"])" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "id": "3ac3b374-2b0c-4a94-aa93-a165c564ac37", 198 | "metadata": {}, 199 | "source": [ 200 | "或者查看某一个任务的执行结果:" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 6, 206 | "id": "decae5e3-72f0-4f73-b8b7-d213a7466986", 207 | "metadata": {}, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "\u001b[1m\u001b[32m---- spine00.bj: 计数 ** changed : False ----------------------------------------- INFO\u001b[0m\n", 214 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 215 | "\u001b[0m" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "print_result(result[\"spine00.bj\"][2])" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "id": "8fcaed67-5b8c-493e-9ad3-553b0ff1f6bb", 226 | "metadata": {}, 227 | "source": [ 228 | "从上面的几个处理结果的示例中可以看到,并不是所有的处理结果都显示出来了,这是因为指定了 `severity_level` 参数,可以用指定的日志级别来记录任务的执行结果。\n", 229 | "\n", 230 | "`print_result` 可以按照日志规则打印结果,默认情况下,它只打印严重级别大于 `INFO` 的任务(如果任务中没有指定日志级别,默认值也是`INFO`)。\n", 231 | " \n", 232 | "如果任务执行失败的话,它的严重级别是 `ERROR`,比 `INFO` 大,所以可以显示出来。上面的 `spine02.bj` 的第一个任务就是显示出来的错误信息。\n", 233 | "\n", 234 | "> 日志级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG\n", 235 | "\n", 236 | "可以通过设置 `print_result` 的参数来调整输出:" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 7, 242 | "id": "cd8b9e9a-f73f-4cdf-87d6-da469877482c", 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "name": "stdout", 247 | "output_type": "stream", 248 | "text": [ 249 | "\u001b[1m\u001b[36mgreet_and_count*****************************************************************\u001b[0m\n", 250 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 251 | "\u001b[0m\u001b[1m\u001b[32mvvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 252 | "\u001b[0mspine00.bj counted even times!\u001b[0m\n", 253 | "\u001b[0m\u001b[1m\u001b[32m---- 你好~ ** changed : False ---------------------------------------------------- DEBUG\u001b[0m\n", 254 | "\u001b[0mspine00.bj says Hi~\u001b[0m\n", 255 | "\u001b[0m\u001b[1m\u001b[32m---- 计数 ** changed : False ----------------------------------------------------- INFO\u001b[0m\n", 256 | "\u001b[0m[0, 1, 2, 3, 4]\u001b[0m\n", 257 | "\u001b[0m\u001b[1m\u001b[32m---- 再见 ** changed : False ----------------------------------------------------- DEBUG\u001b[0m\n", 258 | "\u001b[0mspine00.bj says byebye.\u001b[0m\n", 259 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 260 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 261 | "\u001b[0m\u001b[1m\u001b[31mvvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR\u001b[0m\n", 262 | "\u001b[0mSubtask: 你好~ (failed)\n", 263 | "\u001b[0m\n", 264 | "\u001b[0m\u001b[1m\u001b[31m---- 你好~ ** changed : False ---------------------------------------------------- ERROR\u001b[0m\n", 265 | "\u001b[0mTraceback (most recent call last):\n", 266 | " File \"c:\\program files\\python38\\lib\\site-packages\\nornir\\core\\task.py\", line 99, in start\n", 267 | " r = self.task(self, **self.params)\n", 268 | " File \"C:\\Users\\xdai\\AppData\\Local\\Temp/ipykernel_16088/2400762698.py\", line 17, in say\n", 269 | " raise Exception(f\"{task.host.name} 不能输出信息\")\n", 270 | "Exception: spine01.bj 不能输出信息\n", 271 | "\u001b[0m\n", 272 | "\u001b[0m\u001b[1m\u001b[31m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 273 | "\u001b[0m" 274 | ] 275 | } 276 | ], 277 | "source": [ 278 | "print_result(result, severity_level=logging.DEBUG)" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "id": "aef8365e-30fa-413d-a2cf-1a4e08c9a4dd", 284 | "metadata": {}, 285 | "source": [ 286 | "现在通过给 `print_result` 传递参数,已经可以看到所有任务的执行结果了,从显示任务名那一行的内容最后可以看到日志级别。" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "a3a47252-6f3e-42f7-85db-58fba12e58f4", 292 | "metadata": {}, 293 | "source": [ 294 | "### 更详细的任务处理方法\n", 295 | "\n", 296 | "从上一小节的示例中,已经说明了如果处理任务的结果,现在详细说明一下。任务组(Task Groups)的返回结果是 `AggregatedResult` 对象,它是个类字典(dict-like)对象,所以可以像操作字典一样进行迭代或者访问。" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 8, 302 | "id": "31a8b40b-66b9-464f-9995-c8ac0217f373", 303 | "metadata": {}, 304 | "outputs": [ 305 | { 306 | "data": { 307 | "text/plain": [ 308 | "AggregatedResult (greet_and_count): {'spine00.bj': MultiResult: [Result: \"greet_and_count\", Result: \"你好~\", Result: \"计数\", Result: \"再见\"], 'spine01.bj': MultiResult: [Result: \"greet_and_count\", Result: \"你好~\"]}" 309 | ] 310 | }, 311 | "execution_count": 8, 312 | "metadata": {}, 313 | "output_type": "execute_result" 314 | } 315 | ], 316 | "source": [ 317 | "result" 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": 9, 323 | "id": "3fef7546-7a11-4d5a-9f46-2a4be37daa49", 324 | "metadata": {}, 325 | "outputs": [ 326 | { 327 | "data": { 328 | "text/plain": [ 329 | "dict_keys(['spine00.bj', 'spine01.bj'])" 330 | ] 331 | }, 332 | "execution_count": 9, 333 | "metadata": {}, 334 | "output_type": "execute_result" 335 | } 336 | ], 337 | "source": [ 338 | "result.keys()" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": 10, 344 | "id": "0f149fae-a51c-4e94-b900-f0c6e3680191", 345 | "metadata": {}, 346 | "outputs": [ 347 | { 348 | "data": { 349 | "text/plain": [ 350 | "MultiResult: [Result: \"greet_and_count\", Result: \"你好~\", Result: \"计数\", Result: \"再见\"]" 351 | ] 352 | }, 353 | "execution_count": 10, 354 | "metadata": {}, 355 | "output_type": "execute_result" 356 | } 357 | ], 358 | "source": [ 359 | "result[\"spine00.bj\"]" 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "id": "865012c2-03f8-4778-b18f-53352ec2493e", 365 | "metadata": {}, 366 | "source": [ 367 | "从上面的示例输出中可以看到,`AggregatedResult` 中的每个键都有一个`MultiResult` 对象。这个对象是一个类列表(list-like)的对象,里面存放着 `Result` 对象,可以使用列表的操作方式来迭代或访问 `Result` 对象:" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 11, 373 | "id": "b165ddbd-77b4-45b8-95c6-8f1f92cf78fb", 374 | "metadata": {}, 375 | "outputs": [ 376 | { 377 | "data": { 378 | "text/plain": [ 379 | "Result: \"greet_and_count\"" 380 | ] 381 | }, 382 | "execution_count": 11, 383 | "metadata": {}, 384 | "output_type": "execute_result" 385 | } 386 | ], 387 | "source": [ 388 | "result[\"spine00.bj\"][0]" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "id": "8391186a-6d76-44e8-97be-6b2e5f959dab", 394 | "metadata": {}, 395 | "source": [ 396 | "从 `MultiResult` 和 `Result` 中可以看到执行对象中是否有错误或变化:" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 12, 402 | "id": "6959a68b-3958-4f61-8af9-4b810aecabf9", 403 | "metadata": {}, 404 | "outputs": [ 405 | { 406 | "name": "stdout", 407 | "output_type": "stream", 408 | "text": [ 409 | "changed: False\u001b[0m\n", 410 | "\u001b[0mfailed: False\u001b[0m\n", 411 | "\u001b[0m" 412 | ] 413 | } 414 | ], 415 | "source": [ 416 | "print(f'changed: {result[\"spine00.bj\"].changed}')\n", 417 | "print(f'failed: {result[\"spine00.bj\"].failed}')" 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": 13, 423 | "id": "3d5b6078-ce4b-4570-893e-84c8df7a379a", 424 | "metadata": {}, 425 | "outputs": [ 426 | { 427 | "name": "stdout", 428 | "output_type": "stream", 429 | "text": [ 430 | "changed: False\u001b[0m\n", 431 | "\u001b[0mfailed: True\u001b[0m\n", 432 | "\u001b[0m" 433 | ] 434 | } 435 | ], 436 | "source": [ 437 | "print(f'changed: {result[\"spine01.bj\"].changed}')\n", 438 | "print(f'failed: {result[\"spine01.bj\"].failed}')" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "id": "d1fad053-522e-4000-a526-1a591c5e0d8d", 444 | "metadata": {}, 445 | "source": [ 446 | "如果运行前后对目标系统造成了改变,可以通过 `diff` 显示出来,当前示例中执行的任务组没有产生变化,所以输出为空:" 447 | ] 448 | }, 449 | { 450 | "cell_type": "code", 451 | "execution_count": 14, 452 | "id": "b126edc1-201e-4517-8cb5-3a130f99756a", 453 | "metadata": {}, 454 | "outputs": [ 455 | { 456 | "name": "stdout", 457 | "output_type": "stream", 458 | "text": [ 459 | "diff: \u001b[0m\n", 460 | "\u001b[0m" 461 | ] 462 | } 463 | ], 464 | "source": [ 465 | "print(f'diff: {result[\"spine01.bj\"].diff}')" 466 | ] 467 | } 468 | ], 469 | "metadata": { 470 | "kernelspec": { 471 | "display_name": "Python 3 (ipykernel)", 472 | "language": "python", 473 | "name": "python3" 474 | }, 475 | "language_info": { 476 | "codemirror_mode": { 477 | "name": "ipython", 478 | "version": 3 479 | }, 480 | "file_extension": ".py", 481 | "mimetype": "text/x-python", 482 | "name": "python", 483 | "nbconvert_exporter": "python", 484 | "pygments_lexer": "ipython3", 485 | "version": "3.8.10" 486 | } 487 | }, 488 | "nbformat": 4, 489 | "nbformat_minor": 5 490 | } 491 | -------------------------------------------------------------------------------- /source/tutorial/08.failed_tasks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "709000f9-22f6-4ad4-8889-b1c46a61fe39", 6 | "metadata": {}, 7 | "source": [ 8 | "## 处理失败任务\n", 9 | "\n", 10 | "任务执行失败是不可避免的,现在接着上一节的示例来看下如何处理失败的任务。\n", 11 | "\n", 12 | "上一节中的示例代码:" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "id": "a043500c-36e1-4a15-8c2a-9ed7618d4021", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import logging\n", 23 | "\n", 24 | "from nornir import InitNornir\n", 25 | "from nornir.core.task import Task, Result\n", 26 | "from nornir_utils.plugins.functions import print_result\n", 27 | "\n", 28 | "nr = InitNornir(config_file=\"files/config.yaml\")\n", 29 | "spine_bj = nr.filter(site=\"bj\", role=\"spine\")\n", 30 | "\n", 31 | "def count(task: Task, number: int) -> Result:\n", 32 | " return Result(\n", 33 | " host=task.host,\n", 34 | " result=f\"{[n for n in range(0, number)]}\"\n", 35 | " )\n", 36 | "\n", 37 | "def say(task: Task, text: str) -> Result:\n", 38 | " if task.host.name == \"spine01.bj\":\n", 39 | " raise Exception(f\"{task.host.name} 不能输出信息\")\n", 40 | " return Result(\n", 41 | " host=task.host,\n", 42 | " result=f\"{task.host.name} says {text}\"\n", 43 | " )\n", 44 | "\n", 45 | "def greet_and_count(task: Task, number: int) -> Result:\n", 46 | " task.run(\n", 47 | " name=\"你好~\",\n", 48 | " severity_level=logging.DEBUG,\n", 49 | " task=say,\n", 50 | " text=\"Hi~\",\n", 51 | " )\n", 52 | " \n", 53 | " task.run(\n", 54 | " name=\"计数\",\n", 55 | " task=count,\n", 56 | " number=number,\n", 57 | " )\n", 58 | " \n", 59 | " task.run(\n", 60 | " name=\"再见\",\n", 61 | " severity_level=logging.DEBUG,\n", 62 | " task=say,\n", 63 | " text=\"byebye.\"\n", 64 | " )\n", 65 | " \n", 66 | " # 计算打招呼打了奇数次还是偶数次\n", 67 | " even_or_odds = \"even\" if number % 2 == 1 else \"odd\"\n", 68 | " return Result(\n", 69 | " host=task.host,\n", 70 | " result = f\"{task.host} counted {even_or_odds} times!\",\n", 71 | " )\n", 72 | "\n", 73 | "result = spine_bj.run(\n", 74 | " task=greet_and_count,\n", 75 | " number=5\n", 76 | ")" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "d14ecf13-52e2-4a11-9d52-3abd359f939c", 82 | "metadata": {}, 83 | "source": [ 84 | "在这段示例代码中,任务 `say` 针对 `spine01.bj` 主机抛出了一个异常,这导致整个任务的执行结果是失败的:" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 2, 90 | "id": "76f3073f-8c09-424b-aef7-b11b3ad90cd4", 91 | "metadata": {}, 92 | "outputs": [ 93 | { 94 | "data": { 95 | "text/plain": [ 96 | "True" 97 | ] 98 | }, 99 | "execution_count": 2, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "result.failed" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 3, 111 | "id": "b9a95775-ba6f-450f-a53a-550829c02238", 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "data": { 116 | "text/plain": [ 117 | "{'spine01.bj': MultiResult: [Result: \"greet_and_count\", Result: \"你好~\"]}" 118 | ] 119 | }, 120 | "execution_count": 3, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "# 查看是哪些主机导致了失败\n", 127 | "result.failed_hosts" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "id": "9a11abf6-1064-4c2e-b3f9-12bb389f52e9", 133 | "metadata": {}, 134 | "source": [ 135 | "如果任务发生了失败,可以通过 `exception` 显示异常信息:" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 4, 141 | "id": "1704e314-e1fd-433c-998e-0c5b1f185f29", 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "data": { 146 | "text/plain": [ 147 | "nornir.core.exceptions.NornirSubTaskError()" 148 | ] 149 | }, 150 | "execution_count": 4, 151 | "metadata": {}, 152 | "output_type": "execute_result" 153 | } 154 | ], 155 | "source": [ 156 | "result[\"spine01.bj\"].exception" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "id": "966eaa9e-cb8c-4f5c-9b24-d430276af46d", 162 | "metadata": {}, 163 | "source": [ 164 | "上一条命令显示结果是子任务错误,可以通过列表取值来查看错误信息:" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 5, 170 | "id": "52da447d-6d24-43b0-ab13-c8dfa215e882", 171 | "metadata": {}, 172 | "outputs": [ 173 | { 174 | "data": { 175 | "text/plain": [ 176 | "Exception('spine01.bj 不能输出信息')" 177 | ] 178 | }, 179 | "execution_count": 5, 180 | "metadata": {}, 181 | "output_type": "execute_result" 182 | } 183 | ], 184 | "source": [ 185 | "result[\"spine01.bj\"][1].exception" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "id": "65773d77-14c9-44ea-a369-46b90e93fc0d", 191 | "metadata": {}, 192 | "source": [ 193 | "想要查看更具体的信息,可以使用 `print_result` 查看具体的异常信息:" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 6, 199 | "id": "fc9155d6-3b3d-4f61-8f77-54d4685b0c36", 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "name": "stdout", 204 | "output_type": "stream", 205 | "text": [ 206 | "\u001b[1m\u001b[31mvvvv spine01.bj: greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR\u001b[0m\n", 207 | "\u001b[0mSubtask: 你好~ (failed)\n", 208 | "\u001b[0m\n", 209 | "\u001b[0m\u001b[1m\u001b[31m---- 你好~ ** changed : False ---------------------------------------------------- ERROR\u001b[0m\n", 210 | "\u001b[0mTraceback (most recent call last):\n", 211 | " File \"c:\\program files\\python38\\lib\\site-packages\\nornir\\core\\task.py\", line 99, in start\n", 212 | " r = self.task(self, **self.params)\n", 213 | " File \"C:\\Users\\xdai\\AppData\\Local\\Temp/ipykernel_35768/1441132238.py\", line 18, in say\n", 214 | " raise Exception(f\"{task.host.name} 不能输出信息\")\n", 215 | "Exception: spine01.bj 不能输出信息\n", 216 | "\u001b[0m\n", 217 | "\u001b[0m\u001b[1m\u001b[31m^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 218 | "\u001b[0m" 219 | ] 220 | } 221 | ], 222 | "source": [ 223 | "print_result(result[\"spine01.bj\"])" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "eb208e6b-6921-478e-bbff-4e7ab51940de", 229 | "metadata": {}, 230 | "source": [ 231 | "在处理任务执行结果的过程中,如果有执行出错的话,还会抛出 `NornirExecutionError` 异常,可以使用 `raise_on_error` 方法来引出这个异常,然后使用 `try` 子句进行处理:" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 7, 237 | "id": "3c7c9a9a-a187-4368-9753-a41d7977c77c", 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "name": "stdout", 242 | "output_type": "stream", 243 | "text": [ 244 | "ERROR!!!\u001b[0m\n", 245 | "\u001b[0m" 246 | ] 247 | } 248 | ], 249 | "source": [ 250 | "from nornir.core.exceptions import NornirExecutionError\n", 251 | "try:\n", 252 | " result.raise_on_error()\n", 253 | "except NornirExecutionError:\n", 254 | " print(\"ERROR!!!\")" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "id": "a7be75d8-b58a-491b-b288-91e56911f80a", 260 | "metadata": {}, 261 | "source": [ 262 | "### 跳过失败的主机\n", 263 | "\n", 264 | "Nornir 会跟踪记录任务执行失败的主机,然后不在该主机上运行其他新的任务。\n", 265 | "\n", 266 | "现在定义一个新的任务,并使用之前示例筛选出来的主机组 `spine_bj` 来执行该任务。\n", 267 | "\n", 268 | "这里需要注意一下: `spine_bj` 中有两个主机,但是之前示例中,`spine01.bj` 在执行任务组 `greet_and_count` 中失败了。" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 8, 274 | "id": "146ee53c-65c3-44e6-bcad-13427c804f45", 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "data": { 279 | "text/plain": [ 280 | "{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}" 281 | ] 282 | }, 283 | "execution_count": 8, 284 | "metadata": {}, 285 | "output_type": "execute_result" 286 | } 287 | ], 288 | "source": [ 289 | "spine_bj.inventory.hosts" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": 9, 295 | "id": "785ccd14-f778-4d92-8568-5c2e2bf0c18e", 296 | "metadata": {}, 297 | "outputs": [], 298 | "source": [ 299 | "from nornir.core.task import Result\n", 300 | "\n", 301 | "def hi(task: Task) -> Result:\n", 302 | " return Result(\n", 303 | " host=task.host,\n", 304 | " result=f\"{task.host.name}: Hi, I am still here!\"\n", 305 | " )\n", 306 | "\n", 307 | "result = spine_bj.run(hi)" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 10, 313 | "id": "b191b165-1c9e-4150-a55f-8d2187336b80", 314 | "metadata": {}, 315 | "outputs": [ 316 | { 317 | "name": "stdout", 318 | "output_type": "stream", 319 | "text": [ 320 | "\u001b[1m\u001b[36mhi******************************************************************************\u001b[0m\n", 321 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 322 | "\u001b[0m\u001b[1m\u001b[32mvvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 323 | "\u001b[0mspine00.bj: Hi, I am still here!\u001b[0m\n", 324 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 325 | "\u001b[0m" 326 | ] 327 | } 328 | ], 329 | "source": [ 330 | "print_result(result)" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "id": "e547a601-6162-4f75-a399-1c3340360198", 336 | "metadata": {}, 337 | "source": [ 338 | "查看执行出来的结果,只有第一台主机 `spine00.bj` 成功执行了新的任务。\n", 339 | "\n", 340 | "如果需要新任务在失败的主机上执行,需要在执行调用时添加 `on_failed=True`:" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": 11, 346 | "id": "994fc0b6-4c32-4489-aac8-81d4d2141aff", 347 | "metadata": {}, 348 | "outputs": [ 349 | { 350 | "name": "stdout", 351 | "output_type": "stream", 352 | "text": [ 353 | "\u001b[1m\u001b[36mhi******************************************************************************\u001b[0m\n", 354 | "\u001b[0m\u001b[1m\u001b[34m* spine00.bj ** changed : False ************************************************\u001b[0m\n", 355 | "\u001b[0m\u001b[1m\u001b[32mvvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 356 | "\u001b[0mspine00.bj: Hi, I am still here!\u001b[0m\n", 357 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 358 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 359 | "\u001b[0m\u001b[1m\u001b[32mvvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 360 | "\u001b[0mspine01.bj: Hi, I am still here!\u001b[0m\n", 361 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 362 | "\u001b[0m" 363 | ] 364 | } 365 | ], 366 | "source": [ 367 | "result = spine_bj.run(task=hi, on_failed=True)\n", 368 | "print_result(result)" 369 | ] 370 | }, 371 | { 372 | "cell_type": "markdown", 373 | "id": "f3b903e1-3253-46cd-b0f6-1a9bd0de1b90", 374 | "metadata": {}, 375 | "source": [ 376 | "如果只想在失败的主机上执行新任务,可以使用 `on_good` 参数:\n", 377 | ">上一个代码框中使用了 `on_failed=True`,导致两个主机都执行成功了。如果想要验证 `on_good` ,需要再执行一下之前导致错误的任务组来看到这次的结果\n" 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 12, 383 | "id": "6666a0da-b987-4e56-956c-700aee5a67f4", 384 | "metadata": {}, 385 | "outputs": [], 386 | "source": [ 387 | "# 这是上一节示例中执行失败的任务组,再次执行一下,来验证 `on_good`\n", 388 | "result = spine_bj.run(\n", 389 | " task=greet_and_count,\n", 390 | " number=5\n", 391 | ")" 392 | ] 393 | }, 394 | { 395 | "cell_type": "code", 396 | "execution_count": 13, 397 | "id": "8e00e934-bb42-4d03-a786-d3e49551cf3f", 398 | "metadata": {}, 399 | "outputs": [ 400 | { 401 | "name": "stdout", 402 | "output_type": "stream", 403 | "text": [ 404 | "\u001b[1m\u001b[36mhi******************************************************************************\u001b[0m\n", 405 | "\u001b[0m\u001b[1m\u001b[34m* spine01.bj ** changed : False ************************************************\u001b[0m\n", 406 | "\u001b[0m\u001b[1m\u001b[32mvvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", 407 | "\u001b[0mspine01.bj: Hi, I am still here!\u001b[0m\n", 408 | "\u001b[0m\u001b[1m\u001b[32m^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", 409 | "\u001b[0m" 410 | ] 411 | } 412 | ], 413 | "source": [ 414 | "result = spine_bj.run(task=hi, on_failed=True, on_good=False)\n", 415 | "print_result(result)" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "id": "43bc4c19-2e46-4d3f-b419-85be0730d59c", 421 | "metadata": {}, 422 | "source": [ 423 | "可以看到只在失败的主机上执行了新任务。\n", 424 | "\n", 425 | "如何实现的呢?\n", 426 | "\n", 427 | "为了实现这种效果,nornir 是通过在 `data` 对象中添加了 `failed_hosts` 字段来让任务之间共享失败的主机(有关 `data` 对象,可以回顾一下[初始化 Nornir](04.initializing_nornir.ipynb)):" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": 14, 433 | "id": "46892ad9-d756-4918-b949-092e933fd0f5", 434 | "metadata": {}, 435 | "outputs": [ 436 | { 437 | "data": { 438 | "text/plain": [ 439 | "{'spine01.bj'}" 440 | ] 441 | }, 442 | "execution_count": 14, 443 | "metadata": {}, 444 | "output_type": "execute_result" 445 | } 446 | ], 447 | "source": [ 448 | "nr.data.failed_hosts" 449 | ] 450 | }, 451 | { 452 | "cell_type": "markdown", 453 | "id": "9b3b1547-57cb-4968-8d78-c4b20d8cb5d1", 454 | "metadata": {}, 455 | "source": [ 456 | "如果要将某些主机标记为成功并让它们重新符合执行新任务的资格,可以使用函数 `recovery_host` 为某个主机单独执行此操作,或者使用 `reset_failed_hosts` 完全重置失败列表:" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": 15, 462 | "id": "5e241eab-075c-4723-8df9-d70cc1ff180b", 463 | "metadata": {}, 464 | "outputs": [ 465 | { 466 | "data": { 467 | "text/plain": [ 468 | "set()" 469 | ] 470 | }, 471 | "execution_count": 15, 472 | "metadata": {}, 473 | "output_type": "execute_result" 474 | } 475 | ], 476 | "source": [ 477 | "nr.data.recover_host('spine01.bj')\n", 478 | "nr.data.failed_hosts" 479 | ] 480 | }, 481 | { 482 | "cell_type": "code", 483 | "execution_count": 16, 484 | "id": "12ae5049-c295-4901-9842-5059e364145c", 485 | "metadata": {}, 486 | "outputs": [ 487 | { 488 | "data": { 489 | "text/plain": [ 490 | "set()" 491 | ] 492 | }, 493 | "execution_count": 16, 494 | "metadata": {}, 495 | "output_type": "execute_result" 496 | } 497 | ], 498 | "source": [ 499 | "nr.data.reset_failed_hosts()\n", 500 | "nr.data.failed_hosts" 501 | ] 502 | }, 503 | { 504 | "cell_type": "markdown", 505 | "id": "dcbb2a8b-7ae0-4f2d-86ad-362e22cce8ec", 506 | "metadata": {}, 507 | "source": [ 508 | "### 自动抛出异常\n", 509 | "\n", 510 | "一般情况下,如果任务执行出错,只能在最终打印任务结果时看到错误信息,如果需要及时反馈或者处理失败的失误,可以在初始化 nornir 对象时添加 `raise_on_error` 来让任务出错时自动引发异常:" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": 17, 516 | "id": "1372c084-21e2-4b9f-87ac-2bed5040804e", 517 | "metadata": {}, 518 | "outputs": [ 519 | { 520 | "name": "stdout", 521 | "output_type": "stream", 522 | "text": [ 523 | "ERROR!!!\u001b[0m\n", 524 | "\u001b[0m" 525 | ] 526 | } 527 | ], 528 | "source": [ 529 | "nr = InitNornir(\n", 530 | " config_file=\"files/config.yaml\",\n", 531 | " core = {\"raise_on_error\": True}\n", 532 | " )\n", 533 | "spine_bj = nr.filter(site='bj', role='spine')\n", 534 | "\n", 535 | "try:\n", 536 | " result = spine_bj.run(\n", 537 | " task=greet_and_count,\n", 538 | " number=5,\n", 539 | " )\n", 540 | "except NornirExecutionError:\n", 541 | " print(\"ERROR!!!\")" 542 | ] 543 | }, 544 | { 545 | "cell_type": "markdown", 546 | "id": "7421fda6-bd04-43c7-b751-8d89248cb424", 547 | "metadata": {}, 548 | "source": [ 549 | "### 工作流\n", 550 | "\n", 551 | "由任务组组成的工作流(Workflow)适用于大多数使用场景,因为它可以跳过出错的主机,并且 `print_result` 也提供了足够的信息来了解任务执行的结果。\n", 552 | "\n", 553 | "对于更复杂的工作流,也可以通过 nornir 来实现,因为这个框架足够灵活,接下来就来看看强大的处理器。" 554 | ] 555 | } 556 | ], 557 | "metadata": { 558 | "kernelspec": { 559 | "display_name": "Python 3 (ipykernel)", 560 | "language": "python", 561 | "name": "python3" 562 | }, 563 | "language_info": { 564 | "codemirror_mode": { 565 | "name": "ipython", 566 | "version": 3 567 | }, 568 | "file_extension": ".py", 569 | "mimetype": "text/x-python", 570 | "name": "python", 571 | "nbconvert_exporter": "python", 572 | "pygments_lexer": "ipython3", 573 | "version": "3.8.10" 574 | } 575 | }, 576 | "nbformat": 4, 577 | "nbformat_minor": 5 578 | } 579 | -------------------------------------------------------------------------------- /source/tutorial/09.processors.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2d7544be-20ba-4972-97ad-80379dd27c49", 6 | "metadata": {}, 7 | "source": [ 8 | "## 处理器\n", 9 | "\n", 10 | "在 Nornir 中 **处理器(Processors)** 是一种可以通过自定义代码处理某些事件的插件,它就是一个可以处理任务的装饰器,它在不改变任务结果的前提下,让用户可以自己编写代码对任务结果进行加工,为处理任务提供了更多的扩展性。它有一些优点:\n", 11 | "\n", 12 | "1. 由于处理器是基于事件(event-based)的,所以可以异步处理事件,例如在某台主机完成任务后马上处理该主机的结果,不用需等待其它主机完成任务。\n", 13 | "2. 基于事件编写的代码更简洁,更容易理解。\n", 14 | "\n", 15 | "来通过几个例子来看看处理器是如何工作的,先初始化一个 nornir 对象:" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 1, 21 | "id": "7d01e5bf-06bd-4319-a427-c50a0bab5555", 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "from typing import Dict\n", 26 | "from nornir import InitNornir\n", 27 | "\n", 28 | "nr = InitNornir(config_file=\"files/config.yaml\")" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "a09d38b2-70a9-4197-8e20-12d20163131d", 34 | "metadata": {}, 35 | "source": [ 36 | "编写一个处理器,它的作用是打印一些有关任务执行的信息:" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 2, 42 | "id": "8b6b783b-1a91-4686-8065-20bc4acf1aec", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from nornir.core import Nornir\n", 47 | "from nornir.core.inventory import Host\n", 48 | "from nornir.core.task import Task, AggregatedResult, MultiResult, Result, Task\n", 49 | "\n", 50 | "class PrintResult:\n", 51 | " # 任务开始运行时执行的动作\n", 52 | " def task_started(self, task: Task) -> None:\n", 53 | " print(f\" 任务[{task.name}] 开始执行 \".center(79, \"=\"))\n", 54 | " \n", 55 | " # 任务运行结束后执行的动作\n", 56 | " def task_completed(self, task: Task, result: AggregatedResult) -> None:\n", 57 | " print(f\" 任务[{task.name}]执行结束 \".center(79, \"=\"))\n", 58 | " \n", 59 | " # 任务分配给单台主机运行时执行的动作\n", 60 | " def task_instance_started(self, task: Task, host: Host) -> None:\n", 61 | " print(f\"任务[{task.name}]分配给主机[{host.name}]开始执行.\\n\")\n", 62 | " \n", 63 | " # 任务分配给单台主机运行完成后执行的动作\n", 64 | " def task_instance_completed(\n", 65 | " self, task: Task, host: Host, result: MultiResult\n", 66 | " ) -> None:\n", 67 | " print(f\"任务[{task.name}]分配给主机[{host.name}]执行完成,执行结果:{result.result} \\n\")\n", 68 | " \n", 69 | " # 子任务开始运行时执行的动作\n", 70 | " def subtask_instance_started(self, task: Task, host: Host) -> None:\n", 71 | " pass\n", 72 | " \n", 73 | " # 子任务结束运行时执行的动作\n", 74 | " def subtask_instance_completed(\n", 75 | " self, task: Task, host: Host, result: MultiResult\n", 76 | " ) -> None:\n", 77 | " pass" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "c5c306b0-0e83-4d95-9a64-f736c72f487a", 83 | "metadata": {}, 84 | "source": [ 85 | "编写一个简单的任务,让自定义的处理器 `PrintResult` 来处理结果:" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 3, 91 | "id": "3ebaaafc-d799-4def-9335-72b8ea571543", 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "def greeter(task: Task, greet: str) -> Result:\n", 96 | " return Result(\n", 97 | " host=task.host,\n", 98 | " result=f\"{greet}! My name is {task.host.name}!\"\n", 99 | " )" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "id": "a3fdfab3-4f09-4c92-9148-408f3e435064", 105 | "metadata": {}, 106 | "source": [ 107 | "要使用自定义的处理器,需要用到 nornir 对象的 `with_processors` 方法,这个方法需要传递一个 `Processer` 的列表对象 `Processers`,然后返回一个带有 `Processers` 的 nornir 对象:" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 4, 113 | "id": "cd6009a8-6604-4a43-a12d-9790c38257b5", 114 | "metadata": {}, 115 | "outputs": [ 116 | { 117 | "name": "stdout", 118 | "output_type": "stream", 119 | "text": [ 120 | "================================= 任务[Hi] 开始执行 =================================\n", 121 | "任务[Hi]分配给主机[spine00.bj]开始执行.\n", 122 | "\n", 123 | "任务[Hi]分配给主机[spine00.bj]执行完成,执行结果:Hi! My name is spine00.bj! \n", 124 | "\n", 125 | "任务[Hi]分配给主机[spine01.bj]开始执行.\n", 126 | "\n", 127 | "任务[Hi]分配给主机[spine01.gz]开始执行.\n", 128 | "任务[Hi]分配给主机[spine01.bj]执行完成,执行结果:Hi! My name is spine01.bj! \n", 129 | "\n", 130 | "\n", 131 | "任务[Hi]分配给主机[spine01.gz]执行完成,执行结果:Hi! My name is spine01.gz! \n", 132 | "\n", 133 | "================================== 任务[Hi]执行结束 =================================\n", 134 | "================================= 任务[Bye] 开始执行 ================================\n", 135 | "任务[Bye]分配给主机[spine00.bj]开始执行.\n", 136 | "任务[Bye]分配给主机[spine01.bj]开始执行.\n", 137 | "\n", 138 | "任务[Bye]分配给主机[spine01.bj]执行完成,执行结果:Bye! My name is spine01.bj! \n", 139 | "\n", 140 | "\n", 141 | "任务[Bye]分配给主机[spine01.gz]开始执行.\n", 142 | "\n", 143 | "任务[Bye]分配给主机[spine01.gz]执行完成,执行结果:Bye! My name is spine01.gz! \n", 144 | "\n", 145 | "任务[Bye]分配给主机[spine00.bj]执行完成,执行结果:Bye! My name is spine00.bj! \n", 146 | "\n", 147 | "================================= 任务[Bye]执行结束 =================================\n" 148 | ] 149 | }, 150 | { 151 | "data": { 152 | "text/plain": [ 153 | "AggregatedResult (Bye): {'spine00.bj': MultiResult: [Result: \"Bye\"], 'spine01.bj': MultiResult: [Result: \"Bye\"], 'spine01.gz': MultiResult: [Result: \"Bye\"]}" 154 | ] 155 | }, 156 | "execution_count": 4, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "# 为了保持简洁,这里使用过滤器过滤所有角色是 `spine` 的主机来执行任务\n", 163 | "nr = nr.filter(role=\"spine\")\n", 164 | "nr_with_processors = nr.with_processors([PrintResult()])\n", 165 | "\n", 166 | "nr_with_processors.run(\n", 167 | " task=greeter,\n", 168 | " greet=\"Hi\",\n", 169 | " name=\"Hi\",\n", 170 | ")\n", 171 | "\n", 172 | "nr_with_processors.run(\n", 173 | " task=greeter,\n", 174 | " greet=\"Bye\",\n", 175 | " name=\"Bye\",\n", 176 | ")" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "id": "c76df34c-c18b-49e4-b1b9-14f0855ef35a", 182 | "metadata": {}, 183 | "source": [ 184 | "可以看到,任务执行完成后,它的过程都被打印出来了,这是由自定义的处理器 `PrintResult` 来完成的。\n", 185 | "\n", 186 | "打印结果是无序的,因为默认情况下 nornir 的任务是多线程异步执行的。\n", 187 | "\n", 188 | "前面说到 `with_processors` 方法需要传递一个 `Processers` 对象,这个对象是由 `Processer` 组成的列表。\n", 189 | "\n", 190 | "现在来再定义一个处理器,它的任务是将任务的信息保存在字典中。" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 5, 196 | "id": "3786c6dc-a2d7-43ed-948a-2cdb2bef719d", 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "class SaveResultToDict:\n", 201 | " def __init__(self, data: Dict[str, None]) -> None:\n", 202 | " self.data = data\n", 203 | " \n", 204 | " def task_started(self, task: Task) -> None:\n", 205 | " self.data[task.name] = {}\n", 206 | " self.data[task.name][\"started\"] = True\n", 207 | " print(f\"任务开始信息已经保存到 {self.data.keys()}!\")\n", 208 | " \n", 209 | " def task_completed(self, task: Task, result: AggregatedResult) -> None:\n", 210 | " self.data[task.name][\"completed\"] = True\n", 211 | " print(f\"任务完成信息已经保存到 {self.data.keys()}!\")\n", 212 | "\n", 213 | " def task_instance_started(self, task: Task, host: Host) -> None:\n", 214 | " self.data[task.name][host.name] = {\"started\": True}\n", 215 | " print(f\"主机[{host.name}]任务开始信息已经保存到 {self.data.keys()}!\")\n", 216 | " \n", 217 | " def task_instance_completed(\n", 218 | " self, task: Task, host: Host, result: MultiResult\n", 219 | " ) -> None:\n", 220 | " self.data[task.name][host.name] = {\n", 221 | " \"completed\": True,\n", 222 | " \"result\": result.result,\n", 223 | " }\n", 224 | " print(f\"主机[{host.name}]任务完成信息已经保存到 {self.data.keys()}!\")\n", 225 | "\n", 226 | " def subtask_instance_started(self, task: Task, host: Host) -> None:\n", 227 | " pass\n", 228 | "\n", 229 | " def subtask_instance_completed(\n", 230 | " self, task: Task, host: Host, result: MultiResult\n", 231 | " ) -> None:\n", 232 | " pass " 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "id": "1c6d64ea-8b13-4b0c-b783-8b24924e0b58", 238 | "metadata": {}, 239 | "source": [ 240 | "现在来再次执行任务 `greeter`,这次使用两个处理器 `SaveResultToDict` 和 `PrintResult` 来对任务进行处理:" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 6, 246 | "id": "4d631210-dd69-4ddc-8ae1-a563f6198b09", 247 | "metadata": {}, 248 | "outputs": [ 249 | { 250 | "name": "stdout", 251 | "output_type": "stream", 252 | "text": [ 253 | "================================= 任务[Hi] 开始执行 =================================\n", 254 | "任务开始信息已经保存到 dict_keys(['Hi'])!\n", 255 | "任务[Hi]分配给主机[spine00.bj]开始执行.\n", 256 | "\n", 257 | "主机[spine00.bj]任务开始信息已经保存到 dict_keys(['Hi'])!任务[Hi]分配给主机[spine01.bj]开始执行.\n", 258 | "\n", 259 | "\n", 260 | "任务[Hi]分配给主机[spine00.bj]执行完成,执行结果:Hi! My name is spine00.bj! \n", 261 | "\n", 262 | "主机[spine00.bj]任务完成信息已经保存到 dict_keys(['Hi'])!\n", 263 | "任务[Hi]分配给主机[spine01.gz]开始执行.\n", 264 | "\n", 265 | "主机[spine01.gz]任务开始信息已经保存到 dict_keys(['Hi'])!\n", 266 | "任务[Hi]分配给主机[spine01.gz]执行完成,执行结果:Hi! My name is spine01.gz! \n", 267 | "\n", 268 | "主机[spine01.gz]任务完成信息已经保存到 dict_keys(['Hi'])!\n", 269 | "主机[spine01.bj]任务开始信息已经保存到 dict_keys(['Hi'])!\n", 270 | "任务[Hi]分配给主机[spine01.bj]执行完成,执行结果:Hi! My name is spine01.bj! \n", 271 | "\n", 272 | "主机[spine01.bj]任务完成信息已经保存到 dict_keys(['Hi'])!\n", 273 | "================================== 任务[Hi]执行结束 =================================\n", 274 | "任务完成信息已经保存到 dict_keys(['Hi'])!\n", 275 | "================================= 任务[Bye] 开始执行 ================================\n", 276 | "任务开始信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 277 | "任务[Bye]分配给主机[spine00.bj]开始执行.\n", 278 | "\n", 279 | "主机[spine00.bj]任务开始信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 280 | "任务[Bye]分配给主机[spine00.bj]执行完成,执行结果:Bye! My name is spine00.bj! \n", 281 | "\n", 282 | "主机[spine00.bj]任务完成信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 283 | "任务[Bye]分配给主机[spine01.bj]开始执行.\n", 284 | "任务[Bye]分配给主机[spine01.gz]开始执行.\n", 285 | "\n", 286 | "主机[spine01.gz]任务开始信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 287 | "任务[Bye]分配给主机[spine01.gz]执行完成,执行结果:Bye! My name is spine01.gz! \n", 288 | "\n", 289 | "主机[spine01.gz]任务完成信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 290 | "\n", 291 | "主机[spine01.bj]任务开始信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 292 | "任务[Bye]分配给主机[spine01.bj]执行完成,执行结果:Bye! My name is spine01.bj! \n", 293 | "\n", 294 | "主机[spine01.bj]任务完成信息已经保存到 dict_keys(['Hi', 'Bye'])!\n", 295 | "================================= 任务[Bye]执行结束 =================================\n", 296 | "任务完成信息已经保存到 dict_keys(['Hi', 'Bye'])!\n" 297 | ] 298 | }, 299 | { 300 | "data": { 301 | "text/plain": [ 302 | "AggregatedResult (Bye): {'spine00.bj': MultiResult: [Result: \"Bye\"], 'spine01.bj': MultiResult: [Result: \"Bye\"], 'spine01.gz': MultiResult: [Result: \"Bye\"]}" 303 | ] 304 | }, 305 | "execution_count": 6, 306 | "metadata": {}, 307 | "output_type": "execute_result" 308 | } 309 | ], 310 | "source": [ 311 | "data = {}\n", 312 | "\n", 313 | "nr_with_processors = nr.with_processors([PrintResult(),SaveResultToDict(data)])\n", 314 | "\n", 315 | "nr_with_processors.run(\n", 316 | " task=greeter,\n", 317 | " greet=\"Hi\",\n", 318 | " name=\"Hi\",\n", 319 | ")\n", 320 | "\n", 321 | "nr_with_processors.run(\n", 322 | " task=greeter,\n", 323 | " greet=\"Bye\",\n", 324 | " name=\"Bye\",\n", 325 | ")" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "id": "415869cf-db6d-4eb6-a713-37ecc49a8bc0", 331 | "metadata": {}, 332 | "source": [ 333 | "任务已经成功执行,并且两个处理器都按照预期进行工作,任务执行的最后也打印出了最后的结果: `AggregatedResult` 对象,事实上如果处理器里面已经对结果进行除了,这个对象也不需要再给它赋值然后再使用 `print_result` 打印出来了。\n", 334 | "\n", 335 | "> 这里注意一点,因为 `Processers` 是一个列表,所以它里面 `Processer` 的执行顺序是按照列表的顺序来运行的。\n", 336 | "\n", 337 | "接下来看一下处理器 `SaveResultToDict` 对 `data` 做的操作:" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": 7, 343 | "id": "7755b62d-750b-40b2-9bf8-c5c4fc90073c", 344 | "metadata": {}, 345 | "outputs": [ 346 | { 347 | "name": "stdout", 348 | "output_type": "stream", 349 | "text": [ 350 | "{\n", 351 | " \"Hi\": {\n", 352 | " \"started\": true,\n", 353 | " \"spine00.bj\": {\n", 354 | " \"completed\": true,\n", 355 | " \"result\": \"Hi! My name is spine00.bj!\"\n", 356 | " },\n", 357 | " \"spine01.gz\": {\n", 358 | " \"completed\": true,\n", 359 | " \"result\": \"Hi! My name is spine01.gz!\"\n", 360 | " },\n", 361 | " \"spine01.bj\": {\n", 362 | " \"completed\": true,\n", 363 | " \"result\": \"Hi! My name is spine01.bj!\"\n", 364 | " },\n", 365 | " \"completed\": true\n", 366 | " },\n", 367 | " \"Bye\": {\n", 368 | " \"started\": true,\n", 369 | " \"spine00.bj\": {\n", 370 | " \"completed\": true,\n", 371 | " \"result\": \"Bye! My name is spine00.bj!\"\n", 372 | " },\n", 373 | " \"spine01.gz\": {\n", 374 | " \"completed\": true,\n", 375 | " \"result\": \"Bye! My name is spine01.gz!\"\n", 376 | " },\n", 377 | " \"spine01.bj\": {\n", 378 | " \"completed\": true,\n", 379 | " \"result\": \"Bye! My name is spine01.bj!\"\n", 380 | " },\n", 381 | " \"completed\": true\n", 382 | " }\n", 383 | "}\n" 384 | ] 385 | } 386 | ], 387 | "source": [ 388 | "import json\n", 389 | "print(json.dumps(data, indent=4))" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "id": "bd793e90-1de4-443d-b932-a40aa2669130", 395 | "metadata": {}, 396 | "source": [ 397 | "通过以上两个示例,可以看到 **处理器(Processers)** 处理器的强大功能,通过它来操作处理任务结果更加简单,也无需通过 `print_result` 来查看任务结果。" 398 | ] 399 | }, 400 | { 401 | "cell_type": "markdown", 402 | "id": "e05d1cc5-d53b-4c05-ae69-4683ed3df251", 403 | "metadata": {}, 404 | "source": [ 405 | "### 一些想法\n", 406 | "\n", 407 | "借助处理器还可以做哪些其他事情?\n", 408 | "\n", 409 | "1. 将任务执行事件发送到 slack/IRC/logging_system\n", 410 | "2. 让使用者可以关注到正在的执行的任务情况而无需等待所有主机的任务执行完成(尤其是当设备数量很多时)\n", 411 | "3. 如果某些任务失败,及时通知/发出警报\n", 412 | "4. 根据业务场景尽情发挥吧!\n", 413 | "\n", 414 | "\n", 415 | "\n", 416 | "Nornir 基础教程到这里就结束了,如果想要更加深入的了解,请继续阅读进阶部分。" 417 | ] 418 | } 419 | ], 420 | "metadata": { 421 | "kernelspec": { 422 | "display_name": "Python 3 (ipykernel)", 423 | "language": "python", 424 | "name": "python3" 425 | }, 426 | "language_info": { 427 | "codemirror_mode": { 428 | "name": "ipython", 429 | "version": 3 430 | }, 431 | "file_extension": ".py", 432 | "mimetype": "text/x-python", 433 | "name": "python", 434 | "nbconvert_exporter": "python", 435 | "pygments_lexer": "ipython3", 436 | "version": "3.8.10" 437 | } 438 | }, 439 | "nbformat": 4, 440 | "nbformat_minor": 5 441 | } 442 | -------------------------------------------------------------------------------- /source/tutorial/files/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | inventory: 3 | plugin: SimpleInventory 4 | options: 5 | host_file: "files/inventory/hosts.yaml" 6 | group_file: "files/inventory/groups.yaml" 7 | defaults_file: "files/inventory/defaults.yaml" 8 | runner: 9 | plugin: threaded 10 | options: 11 | num_workers: 100 -------------------------------------------------------------------------------- /source/tutorial/files/inventory/defaults.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | data: 3 | domain: netdevops.local -------------------------------------------------------------------------------- /source/tutorial/files/inventory/groups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | global: 3 | data: 4 | domain: global.local 5 | asn: 1 6 | 7 | north: 8 | data: 9 | asn: 65100 10 | 11 | bj: 12 | groups: 13 | - north 14 | - global 15 | 16 | gz: 17 | data: 18 | asn: 65000 19 | vlans: 20 | 100: wired 21 | 200: wireless -------------------------------------------------------------------------------- /source/tutorial/files/inventory/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | host01.bj: 3 | hostname: 127.0.0.1 4 | port: 2201 5 | username: netdevops 6 | password: netdevops 7 | platform: linux 8 | groups: 9 | - bj 10 | data: 11 | site: bj 12 | role: host 13 | type: host 14 | nested_data: 15 | a_dict: 16 | a: 1 17 | b: 2 18 | a_list: [1, 2] 19 | a_string: "this is a web server" 20 | 21 | spine00.bj: 22 | hostname: 127.0.0.1 23 | username: netdevops 24 | password: netdevops 25 | port: 12444 26 | platform: ios 27 | groups: 28 | - bj 29 | data: 30 | site: bj 31 | role: spine 32 | type: network_device 33 | 34 | spine01.bj: 35 | hostname: 127.0.0.1 36 | username: netdevops 37 | password: "" 38 | platform: junos 39 | port: 12204 40 | groups: 41 | - bj 42 | data: 43 | site: bj 44 | role: spine 45 | type: network_device 46 | 47 | leaf00.bj: 48 | hostname: 127.0.0.1 49 | username: netdevops 50 | password: netdevops 51 | port: 12443 52 | platform: hp_comware 53 | groups: 54 | - bj 55 | data: 56 | site: bj 57 | role: leaf 58 | type: network_device 59 | asn: 65100 60 | 61 | leaf01.bj: 62 | hostname: 127.0.0.1 63 | username: netdevops 64 | password: "" 65 | port: 12203 66 | platform: huawei 67 | groups: 68 | - bj 69 | data: 70 | site: bj 71 | role: leaf 72 | type: network_device 73 | asn: 65101 74 | 75 | host01.gz: 76 | groups: 77 | - gz 78 | platform: linux 79 | data: 80 | site: gz 81 | role: host 82 | type: host 83 | 84 | spine01.gz: 85 | hostname: 127.0.0.1 86 | username: netdevops 87 | password: netdevops 88 | port: 12444 89 | platform: eos 90 | groups: 91 | - gz 92 | data: 93 | site: gz 94 | role: spine 95 | type: network_device 96 | 97 | leaf01.gz: 98 | hostname: 127.0.0.1 99 | username: netdevops 100 | password: netdevops 101 | port: 12443 102 | platform: eos 103 | groups: 104 | - gz 105 | data: 106 | site: gz 107 | role: leaf 108 | type: network_device 109 | 110 | host00: 111 | groups: 112 | - gz 113 | - bj 114 | 115 | host01: 116 | groups: 117 | - bj 118 | - gz -------------------------------------------------------------------------------- /source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | 入门教程 2 | ======== 3 | 4 | 欢迎阅读 Nornir 入门教程。 5 | 6 | 7 | .. toctree:: 8 | :glob: 9 | :maxdepth: 1 10 | 11 | 初识 Nornir <01.overview.ipynb> 12 | 需要具备的 Python 知识 <02.python.ipynb> 13 | 安装指南 <03.install.ipynb> 14 | 初始化 Nornir <04.initializing_nornir.ipynb> 15 | 主机清单 <05.inventory.ipynb> 16 | 执行任务 <06.tasks.ipynb> 17 | 处理任务结果 <07.task_results.ipynb> 18 | 处理失败任务 <08.failed_tasks.ipynb> 19 | 处理器 <09.processors.ipynb> 20 | --------------------------------------------------------------------------------