From fc8f9f5db2d01117d1a492081f5cef77777090b7 Mon Sep 17 00:00:00 2001 From: Brad Haas Date: Thu, 14 Feb 2019 12:42:04 -0500 Subject: [PATCH 01/32] fix issue #319 - ascii color codes appear instead of color in output --- nornir/plugins/functions/text/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nornir/plugins/functions/text/__init__.py b/nornir/plugins/functions/text/__init__.py index 52af4578..ba9c1bc9 100644 --- a/nornir/plugins/functions/text/__init__.py +++ b/nornir/plugins/functions/text/__init__.py @@ -11,7 +11,7 @@ LOCK = threading.Lock() -init(autoreset=True, convert=False, strip=False) +init(autoreset=True, convert=True, strip=False) def print_title(title: str) -> None: From 8e519c9cf2cc6596b942eaedd40ca73f1e230072 Mon Sep 17 00:00:00 2001 From: Brad Haas Date: Sun, 17 Feb 2019 11:47:06 -0500 Subject: [PATCH 02/32] remove convert parameter from colorama init --- nornir/plugins/functions/text/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nornir/plugins/functions/text/__init__.py b/nornir/plugins/functions/text/__init__.py index ba9c1bc9..68238c31 100644 --- a/nornir/plugins/functions/text/__init__.py +++ b/nornir/plugins/functions/text/__init__.py @@ -11,7 +11,7 @@ LOCK = threading.Lock() -init(autoreset=True, convert=True, strip=False) +init(autoreset=True, strip=False) def print_title(title: str) -> None: From 354fd93619fdae40fcfc64f9413e0fbbdeadc0be Mon Sep 17 00:00:00 2001 From: fallenarc <41013523+fallenarc@users.noreply.github.com> Date: Mon, 25 Feb 2019 20:02:19 -0600 Subject: [PATCH 03/32] Fix comment under class Netmiko(ConnectionPlugin): Incorrectly stated that this function uses the Napalm driver. Should say Netmiko. --- nornir/plugins/connections/netmiko.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nornir/plugins/connections/netmiko.py b/nornir/plugins/connections/netmiko.py index 347bbb8e..b7172f6a 100644 --- a/nornir/plugins/connections/netmiko.py +++ b/nornir/plugins/connections/netmiko.py @@ -17,7 +17,7 @@ class Netmiko(ConnectionPlugin): """ - This plugin connects to the device using the NAPALM driver and sets the + This plugin connects to the device using the Netmiko driver and sets the relevant connection. Inventory: From 981cc918a2e448f79b38241a39a0249ccbf6ac16 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 28 Feb 2019 09:14:14 +0000 Subject: [PATCH 04/32] Additional upgrade guidance for 1-to-2.x (#335) --- docs/upgrading/1_to_2.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/upgrading/1_to_2.rst b/docs/upgrading/1_to_2.rst index d11fcfe8..ae615f0c 100644 --- a/docs/upgrading/1_to_2.rst +++ b/docs/upgrading/1_to_2.rst @@ -29,3 +29,30 @@ Changes to the configuration ---------------------------- The format of the configuration has slightly changed. Some of the options that used to be under the root object, for instance ``num_workers``, ``jinja_filters`` and ``raise_on_error`` are now under ``core`` and ``jinja2`` sections. For details, go to the `configuration section <../configuration/index.rst>`_ + +Beware, that where top-level options have now been moved into new sections, if you were previously passing these options to ``InitNornir`` in code and other options via configuration file, you may notice a change in behaviour. + +Changes to templates +-------------------- + +In Nornir 1.x, all host data was made directly available as template variables. To avoid the potential for conflicts, nornir 2.x, host data is namespaced under the ``host`` variable. + +Change from:: + + My hostname is: {{ hostname }} + +to:: + + My hostname is: {{ host.hostname }} + +Changes to transform functions +------------------------------ + +In nornir 2.x, a transform function passed in the ``inventory`` configuration to ``InitNonir`` must be serialisable (so it may not be a lambda). If you need to pass parameters to the transform function, use the new ``transform_function_options`` parameter. + +Changes to ``Inventory`` plugins +-------------------------------- + +The inventory plugin system has changed quite significantly and the base class is now ``nornir.core.deserializer.inventory.Inventory``. Inventory plugins are also now based on Pydantic (https://github.com/samuelcolvin/pydantic) and so instances of the plugin class do not allow arbitrary class members. You may need to alter the way your inventory plugin works, particularly if you need to maintain state during initialisation. + +To see some simple examples, look at the ``SimpleInventory`` and ``NetboxInventory`` plugins which ship with Nornir. The ``AnsibleInventory`` plugin is a complete example of a more complex system. From c39297cb82a242f880ec8ed521966b44be735cdc Mon Sep 17 00:00:00 2001 From: David Barroso Date: Sun, 3 Mar 2019 13:06:01 +0100 Subject: [PATCH 05/32] fix order of preference when deserializing config (#309) * fix order of preference when deserializing config * bump pydantic --- nornir/core/deserializer/configuration.py | 71 +++++++++++++------ poetry.lock | 4 +- pyproject.toml | 2 +- tests/core/deserializer/test_configuration.py | 22 ++++++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/nornir/core/deserializer/configuration.py b/nornir/core/deserializer/configuration.py index ae878fe5..749f53df 100644 --- a/nornir/core/deserializer/configuration.py +++ b/nornir/core/deserializer/configuration.py @@ -1,7 +1,7 @@ import importlib import logging from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Type, cast +from typing import Any, Callable, Dict, List, Optional, Type, Union, cast from nornir.core import configuration from nornir.core.deserializer.inventory import Inventory @@ -14,7 +14,13 @@ logger = logging.getLogger(__name__) -class SSHConfig(BaseSettings): +class BaseNornirSettings(BaseSettings): + def _build_values(self, init_kwargs: Dict[str, Any]) -> Dict[str, Any]: + config_settings = init_kwargs.pop("__config_settings__", {}) + return {**config_settings, **self._build_environ(), **init_kwargs} + + +class SSHConfig(BaseNornirSettings): config_file: str = Schema( default="~/.ssh/config", description="Path to ssh configuration file" ) @@ -24,13 +30,13 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.SSHConfig: + def deserialize(cls, **kwargs: Any) -> configuration.SSHConfig: s = SSHConfig(**kwargs) s.config_file = str(Path(s.config_file).expanduser()) return configuration.SSHConfig(**s.dict()) -class InventoryConfig(BaseSettings): +class InventoryConfig(BaseNornirSettings): plugin: str = Schema( default="nornir.plugins.inventory.simple.SimpleInventory", description="Import path to inventory plugin", @@ -54,7 +60,7 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.InventoryConfig: + def deserialize(cls, **kwargs: Any) -> configuration.InventoryConfig: inv = InventoryConfig(**kwargs) return configuration.InventoryConfig( plugin=cast(Type[Inventory], _resolve_import_from_string(inv.plugin)), @@ -64,7 +70,7 @@ def deserialize(cls, **kwargs) -> configuration.InventoryConfig: ) -class LoggingConfig(BaseSettings): +class LoggingConfig(BaseNornirSettings): level: str = Schema(default="debug", description="Logging level") file: str = Schema(default="nornir.log", descritpion="Logging file") format: str = Schema( @@ -81,7 +87,7 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.LoggingConfig: + def deserialize(cls, **kwargs: Any) -> configuration.LoggingConfig: logging_config = LoggingConfig(**kwargs) return configuration.LoggingConfig( level=getattr(logging, logging_config.level.upper()), @@ -92,7 +98,7 @@ def deserialize(cls, **kwargs) -> configuration.LoggingConfig: ) -class Jinja2Config(BaseSettings): +class Jinja2Config(BaseNornirSettings): filters: str = Schema( default="", description="Path to callable returning jinja filters to be used" ) @@ -102,14 +108,14 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.Jinja2Config: + def deserialize(cls, **kwargs: Any) -> configuration.Jinja2Config: c = Jinja2Config(**kwargs) jinja_filter_func = _resolve_import_from_string(c.filters) jinja_filters = jinja_filter_func() if jinja_filter_func else {} return configuration.Jinja2Config(filters=jinja_filters) -class CoreConfig(BaseSettings): +class CoreConfig(BaseNornirSettings): num_workers: int = Schema( default=20, description="Number of Nornir worker threads that are run at the same time by default", @@ -128,12 +134,12 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.CoreConfig: + def deserialize(cls, **kwargs: Any) -> configuration.CoreConfig: c = CoreConfig(**kwargs) return configuration.CoreConfig(**c.dict()) -class Config(BaseSettings): +class Config(BaseNornirSettings): core: CoreConfig = CoreConfig() inventory: InventoryConfig = InventoryConfig() ssh: SSHConfig = SSHConfig() @@ -148,13 +154,32 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs) -> configuration.Config: + def deserialize( + cls, __config_settings__: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> configuration.Config: + __config_settings__ = __config_settings__ or {} c = Config( - core=CoreConfig(**kwargs.pop("core", {})), - ssh=SSHConfig(**kwargs.pop("ssh", {})), - inventory=InventoryConfig(**kwargs.pop("inventory", {})), - logging=LoggingConfig(**kwargs.pop("logging", {})), - jinja2=Jinja2Config(**kwargs.pop("jinja2", {})), + core=CoreConfig( + __config_settings__=__config_settings__.pop("core", {}), + **kwargs.pop("core", {}), + ), + ssh=SSHConfig( + __config_settings__=__config_settings__.pop("ssh", {}), + **kwargs.pop("ssh", {}), + ), + inventory=InventoryConfig( + __config_settings__=__config_settings__.pop("inventory", {}), + **kwargs.pop("inventory", {}), + ), + logging=LoggingConfig( + __config_settings__=__config_settings__.pop("logging", {}), + **kwargs.pop("logging", {}), + ), + jinja2=Jinja2Config( + __config_settings__=__config_settings__.pop("jinja2", {}), + **kwargs.pop("jinja2", {}), + ), + __config_settings__=__config_settings__, **kwargs, ) return configuration.Config( @@ -167,16 +192,18 @@ def deserialize(cls, **kwargs) -> configuration.Config: ) @classmethod - def load_from_file(cls, config_file: str, **kwargs) -> configuration.Config: + def load_from_file(cls, config_file: str, **kwargs: Any) -> configuration.Config: config_dict: Dict[str, Any] = {} if config_file: yml = ruamel.yaml.YAML(typ="safe") with open(config_file, "r") as f: config_dict = yml.load(f) or {} - return Config.deserialize(**{**config_dict, **kwargs}) + return Config.deserialize(__config_settings__=config_dict, **kwargs) -def _resolve_import_from_string(import_path: Any) -> Optional[Callable[..., Any]]: +def _resolve_import_from_string( + import_path: Union[Callable[..., Any], str] +) -> Optional[Callable[..., Any]]: try: if not import_path: return None @@ -184,7 +211,7 @@ def _resolve_import_from_string(import_path: Any) -> Optional[Callable[..., Any] return import_path module_name, obj_name = import_path.rsplit(".", 1) module = importlib.import_module(module_name) - return getattr(module, obj_name) + return cast(Callable[..., Any], getattr(module, obj_name)) except Exception as e: logger.error(f"failed to load import_path '{import_path}'\n{e}") raise diff --git a/poetry.lock b/poetry.lock index 0613d983..cb297f90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -573,7 +573,7 @@ description = "Data validation and settings management using python 3.6 type hin name = "pydantic" optional = false python-versions = ">=3.6" -version = "0.17" +version = "0.18.2" [package.dependencies] [package.dependencies.dataclasses] @@ -913,7 +913,7 @@ py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e7682 pyasn1 = ["061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b", "0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd", "5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed", "65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee", "79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff", "8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3", "9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a", "98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699", "b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5", "b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0", "d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8", "da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] -pydantic = ["2ae265d717a9f117c5f89b1e5c5cb1565ac7e9b329207fd191cb4ac7a9ed79a9", "ae6c3013d430bcbcbbc9f971cdd1870ce3b599873f14b2b525044e0d335fd55e"] +pydantic = ["9f023811b6cefd203c5fd8fd15a4152f04e79e531b8f676ab1244dfe06ce8024", "edbb08b561feda505374c0f25e4b54466a0a0c702ed6b2efaabdc3890d1a82e7"] pydocstyle = ["2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", "5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", "ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"] pyeapi = ["5be222c290e9e14827ee15541f0e61e290e0223bdead6151233aa7baa5b1b3c8"] pyflakes = ["5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", "f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"] diff --git a/pyproject.toml b/pyproject.toml index a4d1096e..aede582d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ paramiko = ">=2.1.1, <3" requests = "^2" "ruamel.yaml" = "^0.15.85" mypy_extensions = "^0.4.1" -pydantic = "^0.17.0" +pydantic = "^0.18.2" [tool.poetry.dev-dependencies] decorator = "*" nbval = "*" diff --git a/tests/core/deserializer/test_configuration.py b/tests/core/deserializer/test_configuration.py index 0275d703..b6385021 100644 --- a/tests/core/deserializer/test_configuration.py +++ b/tests/core/deserializer/test_configuration.py @@ -176,3 +176,25 @@ def test_get_user_defined_from_file(self): os.path.join(dir_path, "config.yaml") ) assert config.user_defined["asd"] == "qwe" + + def test_order_of_resolution_config_is_lowest(self): + config = ConfigDeserializer.load_from_file( + os.path.join(dir_path, "config.yaml") + ) + assert config.core.num_workers == 10 + + def test_order_of_resolution_env_is_higher_than_config(self): + os.environ["NORNIR_CORE_NUM_WORKERS"] = "20" + config = ConfigDeserializer.load_from_file( + os.path.join(dir_path, "config.yaml") + ) + os.environ.pop("NORNIR_CORE_NUM_WORKERS") + assert config.core.num_workers == 20 + + def test_order_of_resolution_code_is_higher_than_env(self): + os.environ["NORNIR_CORE_NUM_WORKERS"] = "20" + config = ConfigDeserializer.load_from_file( + os.path.join(dir_path, "config.yaml"), core={"num_workers": 30} + ) + os.environ.pop("NORNIR_CORE_NUM_WORKERS") + assert config.core.num_workers == 30 From c778a71d07ce558b1bf2f897fa5ae43df27bad1f Mon Sep 17 00:00:00 2001 From: David Barroso Date: Tue, 5 Mar 2019 10:31:51 +0100 Subject: [PATCH 06/32] test overriding partially a section (#337) --- tests/core/test_InitNornir.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/core/test_InitNornir.py b/tests/core/test_InitNornir.py index 97275557..bde6a863 100644 --- a/tests/core/test_InitNornir.py +++ b/tests/core/test_InitNornir.py @@ -56,6 +56,14 @@ def test_InitNornir_programmatically(self): assert len(nr.inventory.hosts) assert len(nr.inventory.groups) + def test_InitNornir_override_partial_section(self): + nr = InitNornir( + config_file=os.path.join(dir_path, "a_config.yaml"), + core={"raise_on_error": True}, + ) + assert nr.config.core.num_workers == 100 + assert nr.config.core.raise_on_error + def test_InitNornir_combined(self): nr = InitNornir( config_file=os.path.join(dir_path, "a_config.yaml"), From 06a79f4f8a4f4204251507d9e8535c65de60935b Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Wed, 6 Mar 2019 09:51:26 +0000 Subject: [PATCH 07/32] Update the 1-to-2 upgrade guide for custom inventory data (#340) Highlights the requirement to move custom inventory data under a `data` subkey when upgrading to nornir 2.x --- docs/upgrading/1_to_2.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/upgrading/1_to_2.rst b/docs/upgrading/1_to_2.rst index ae615f0c..1059474a 100644 --- a/docs/upgrading/1_to_2.rst +++ b/docs/upgrading/1_to_2.rst @@ -4,6 +4,9 @@ Upgrading to nornir 2.x from 1.x Changes in the inventory ------------------------ +Connection parameters +~~~~~~~~~~~~~~~~~~~~~ + When specifying connection parameters, in nornir 1.x those parameters where specified with attributes like ``nornir_username``, ``nornir_password``, etc. All of those have been removed and now the only supported parameters are: * ``hostname`` @@ -14,6 +17,36 @@ When specifying connection parameters, in nornir 1.x those parameters where spec You can check the following how to for more details on `how to <../howto/handling_connections.rst>`_ use these parameters. +Custom inventory data +~~~~~~~~~~~~~~~~~~~~~ + +Any custom host or group data keys, other than core supported keys, must also be moved under a data subkey:: + + --- + host host1.cmh: + hostname: 127.0.0.1 + username: vagrant + password: vagrant + platform: linux + groups: + - cmh + site: cmh # example custom data + +to:: + + --- + host1.cmh: + hostname: 127.0.0.1 + username: vagrant + password: vagrant + platform: linux + groups: + - cmh + data: + site: cmh + +See `the inventory tutorial <../tutorials/intro/inventory.ipynb>`_ for more information on how to structure inventory data. + Changed to path importing ``InitNornir`` ---------------------------------------- From 344fa4ead1edaec8f554323e267993f745ca701a Mon Sep 17 00:00:00 2001 From: Dmitry Figol Date: Wed, 6 Mar 2019 10:57:30 +0100 Subject: [PATCH 08/32] Improve logging (#316) * Improve logging Fix #302, #230 * Nornir logging is configured only when no changes have been done to Python logging * No more duplicate logs * Replace format and f-strings in logs to % strings according to best practices * Improved messages for some logs * Fix build errors * Add .ipynb_checkpoints to sphinx ignored list * Run black and pylama on specific folders * Pin pydantic until 0.19.0 is released https://github.com/samuelcolvin/pydantic/issues/254 * Add backwards compatibility and address comments * Fix handling_connections notebook validation * Add nbval sanitization to Makefile --- Makefile | 4 +- .../configuration-parameters.j2 | 2 +- docs/conf.py | 2 +- docs/configuration/index.rst | 6 + docs/howto/handling_connections.ipynb | 70 +++++----- docs/nbval_sanitize.cfg | 3 + docs/plugins/tasks/data/echo_data.ipynb | 2 +- nornir/core/__init__.py | 24 ++-- nornir/core/configuration.py | 113 +++++++++------- nornir/core/deserializer/configuration.py | 30 +++-- nornir/core/exceptions.py | 4 + nornir/core/task.py | 20 ++- nornir/init_nornir.py | 26 +++- nornir/plugins/inventory/ansible.py | 10 +- nornir/plugins/inventory/simple.py | 6 +- poetry.lock | 86 +++++++----- pyproject.toml | 2 + setup.cfg | 4 +- tests/conftest.py | 9 -- tests/core/deserializer/test_configuration.py | 19 +-- tests/core/test_InitNornir.py | 126 +++++++++++++++++- .../output_data/failed_with_severity.stdout | 6 +- .../functions/text/test_print_result.py | 4 + 23 files changed, 390 insertions(+), 188 deletions(-) create mode 100644 docs/nbval_sanitize.cfg diff --git a/Makefile b/Makefile index 58819e26..6b48cb34 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ mypy: .PHONY: _nbval_docker _nbval_docker: /root/.poetry/bin/poetry install - pytest --nbval \ + pytest --nbval --sanitize-with docs/nbval_sanitize.cfg \ docs/plugins \ docs/howto \ docs/tutorials/intro/initializing_nornir.ipynb \ @@ -67,7 +67,7 @@ nbval: run nornir \ make _nbval_docker -PHONY: tests +.PHONY: tests tests: build_test_container black sphinx pylama mypy nbval make pytest PYTEST=3.6 make pytest PYTEST=3.7 diff --git a/docs/_data_templates/configuration-parameters.j2 b/docs/_data_templates/configuration-parameters.j2 index 08c49d84..89aea954 100644 --- a/docs/_data_templates/configuration-parameters.j2 +++ b/docs/_data_templates/configuration-parameters.j2 @@ -15,7 +15,7 @@ * - **Type** - ``{{ v["type"] }}`` * - **Default** - - {{ "``{}``".format(v["default"]) if v["default"] else "" }} + - {{ "``{}``".format(v.get("default")) if v.get("default") != "" else "" }} * - **Required** - ``{{ v["required"] or false }}`` * - **Environment Variable** diff --git a/docs/conf.py b/docs/conf.py index 05199c56..20993014 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,7 +83,7 @@ def extract_version(): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/.ipynb_checkpoints"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index df1cffff..c0bc740c 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -15,6 +15,12 @@ A similar example using a ``yaml`` file: .. include:: ../howto/advanced_filtering/config.yaml :code: yaml +Logging +------------ + +| By default, Nornir automatically configures logging when ``InitNornir`` is called. Logging configuration can be modified and available options are described in the section below. If you want to use Python logging module to configure logging, make sure to set ``logging.enabled`` parameter to ``False`` in order to avoid potential issues. +| In some situations Nornir will detect previous logging configuration and will emit :obj:`nornir.core.exceptions.ConflictingConfigurationWarning` + Next, you can find each section and their corresponding options. .. include:: generated/parameters.rst diff --git a/docs/howto/handling_connections.ipynb b/docs/howto/handling_connections.ipynb index b3effe7f..f450c1f4 100644 --- a/docs/howto/handling_connections.ipynb +++ b/docs/howto/handling_connections.ipynb @@ -136,40 +136,40 @@ "name": "stdout", "output_type": "stream", "text": [ - "dev1.group_1:\r\n", - " port: 22\r\n", - " hostname: dev1.group_1\r\n", - " username:\r\n", - " password: a_password\r\n", - " platform: eos\r\n", - " data:\r\n", - " my_var: comes_from_dev1.group_1\r\n", - " www_server: nginx\r\n", - " role: www\r\n", - " nested_data:\r\n", - " a_dict:\r\n", - " a: 1\r\n", - " b: 2\r\n", - " a_list: [1, 2]\r\n", - " a_string: asdasd\r\n", - " groups:\r\n", - " - group_1\r\n", - " connection_options:\r\n", - " paramiko:\r\n", - " port: 22\r\n", - " hostname:\r\n", - " username: root\r\n", - " password: docker\r\n", - " platform: linux\r\n", - " extras: {}\r\n", - " dummy:\r\n", - " hostname: dummy_from_host\r\n", - " port:\r\n", - " username:\r\n", - " password:\r\n", - " platform:\r\n", - " extras:\r\n", - " blah: from_host\r\n", + "dev1.group_1:\n", + " port: 22\n", + " hostname: dev1.group_1\n", + " username:\n", + " password: a_password\n", + " platform: eos\n", + " data:\n", + " my_var: comes_from_dev1.group_1\n", + " www_server: nginx\n", + " role: www\n", + " nested_data:\n", + " a_dict:\n", + " a: 1\n", + " b: 2\n", + " a_list: [1, 2]\n", + " a_string: asdasd\n", + " groups:\n", + " - group_1\n", + " connection_options:\n", + " paramiko:\n", + " port: 22\n", + " hostname:\n", + " username: root\n", + " password: docker\n", + " platform: linux\n", + " extras: {}\n", + " dummy:\n", + " hostname: dummy_from_host\n", + " port:\n", + " username:\n", + " password:\n", + " platform:\n", + " extras:\n", + " blah: from_host\n", "\u001b[0m\u001b[0m" ] } @@ -195,7 +195,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.8" } }, "nbformat": 4, diff --git a/docs/nbval_sanitize.cfg b/docs/nbval_sanitize.cfg new file mode 100644 index 00000000..b18e55f4 --- /dev/null +++ b/docs/nbval_sanitize.cfg @@ -0,0 +1,3 @@ +[regex1] +regex: \r\n +replace: \n \ No newline at end of file diff --git a/docs/plugins/tasks/data/echo_data.ipynb b/docs/plugins/tasks/data/echo_data.ipynb index 3987976c..17285671 100644 --- a/docs/plugins/tasks/data/echo_data.ipynb +++ b/docs/plugins/tasks/data/echo_data.ipynb @@ -148,7 +148,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.6.8" } }, "nbformat": 4, diff --git a/nornir/core/__init__.py b/nornir/core/__init__.py index 2c662197..e0325cfc 100644 --- a/nornir/core/__init__.py +++ b/nornir/core/__init__.py @@ -7,6 +7,8 @@ from nornir.core.state import GlobalState from nornir.core.task import AggregatedResult, Task +logger = logging.getLogger(__name__) + class Nornir(object): """ @@ -27,14 +29,9 @@ class Nornir(object): """ def __init__( - self, - inventory: Inventory, - config: Config = None, - logger: logging.Logger = None, - data: GlobalState = None, + self, inventory: Inventory, config: Config = None, data: GlobalState = None ) -> None: self.data = data if data is not None else GlobalState() - self.logger = logger or logging.getLogger(__name__) self.inventory = inventory @@ -118,12 +115,17 @@ def run( if name in self.data.failed_hosts: run_on.append(host) - self.logger.info( - "Running task '{}' with num_workers: {}".format( - kwargs.get("name") or task.__name__, num_workers + num_hosts = len(self.inventory.hosts) + task_name = kwargs.get("name") or task.__name__ + if num_hosts: + logger.info( + f"Running task %r with args %s on %d hosts", + task_name, + kwargs, + num_hosts, ) - ) - self.logger.debug(kwargs) + else: + logger.warning("Task %r has not been run – 0 hosts selected", task_name) if num_workers == 1: result = self._run_serial(task, run_on, **kwargs) diff --git a/nornir/core/configuration.py b/nornir/core/configuration.py index 0733b71a..c2d40e91 100644 --- a/nornir/core/configuration.py +++ b/nornir/core/configuration.py @@ -1,7 +1,11 @@ import logging -import logging.config -from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Type +import logging.handlers +import sys +import warnings +from pathlib import Path +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING, Type, List +from nornir.core.exceptions import ConflictingConfigurationWarning if TYPE_CHECKING: from nornir.core.deserializer.inventory import Inventory # noqa @@ -31,11 +35,18 @@ def __init__( class LoggingConfig(object): - __slots__ = "level", "file", "format", "to_console", "loggers" + __slots__ = "enabled", "level", "file", "format", "to_console", "loggers" def __init__( - self, level: str, file_: str, format_: str, to_console: bool, loggers: List[str] + self, + enabled: Optional[bool], + level: str, + file_: str, + format_: str, + to_console: bool, + loggers: List[str], ) -> None: + self.enabled = enabled self.level = level self.file = file_ self.format = format_ @@ -43,50 +54,56 @@ def __init__( self.loggers = loggers def configure(self) -> None: - rootHandlers: List[str] = [] - root = { - "level": "CRITICAL" if self.loggers else self.level.upper(), - "handlers": rootHandlers, - "formatter": "simple", - } - handlers: Dict[str, Any] = {} - loggers: Dict[str, Any] = {} - dictConfig = { - "version": 1, - "disable_existing_loggers": False, - "formatters": {"simple": {"format": self.format}}, - "handlers": handlers, - "loggers": loggers, - "root": root, - } - handlers_list = [] - if self.file: - rootHandlers.append("info_file_handler") - handlers_list.append("info_file_handler") - handlers["info_file_handler"] = { - "class": "logging.handlers.RotatingFileHandler", - "level": "NOTSET", - "formatter": "simple", - "filename": self.file, - "maxBytes": 10485760, - "backupCount": 20, - "encoding": "utf8", - } - if self.to_console: - rootHandlers.append("info_console") - handlers_list.append("info_console") - handlers["info_console"] = { - "class": "logging.StreamHandler", - "level": "NOTSET", - "formatter": "simple", - "stream": "ext://sys.stdout", - } - - for logger in self.loggers: - loggers[logger] = {"level": self.level, "handlers": handlers_list} - - if rootHandlers: - logging.config.dictConfig(dictConfig) + if not self.enabled: + return + + root_logger = logging.getLogger() + if root_logger.hasHandlers() or root_logger.level != logging.WARNING: + msg = ( + "Native Python logging configuration has been detected, but Nornir " + "logging is enabled too. " + "This can lead to unexpected logging results. " + "Please set logging.enabled config to False " + "to disable automatic Nornir logging configuration. Refer to " + "https://nornir.readthedocs.io/en/stable/configuration/index.html#logging" # noqa + ) + warnings.warn(msg, ConflictingConfigurationWarning) + + formatter = logging.Formatter(self.format) + # log INFO and DEBUG to stdout + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(formatter) + stdout_handler.setLevel(logging.DEBUG) + stdout_handler.addFilter(lambda record: record.levelno <= logging.INFO) + # log WARNING, ERROR and CRITICAL to stderr + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setFormatter(formatter) + stderr_handler.setLevel(logging.WARNING) + + for logger_name in self.loggers: + logger_ = logging.getLogger(logger_name) + logger_.propagate = False + logger_.setLevel(self.level) + if logger_.hasHandlers(): + # Don't add handlers if some handlers are already attached to the logger + # This is crucial to avoid duplicate handlers + # Alternative would be to clear all handlers and reconfigure them + # with Nornir + # There are several situations this branch can be executed: + # multiple calls to InitNornir, + # logging.config.dictConfig configuring 'nornir' logger, etc. + # The warning is not emitted in this scenario + continue + if self.file: + handler = logging.handlers.RotatingFileHandler( + str(Path(self.file)), maxBytes=1024 * 1024 * 10, backupCount=20 + ) + handler.setFormatter(formatter) + logger_.addHandler(handler) + + if self.to_console: + logger_.addHandler(stdout_handler) + logger_.addHandler(stderr_handler) class Jinja2Config(object): diff --git a/nornir/core/deserializer/configuration.py b/nornir/core/deserializer/configuration.py index 749f53df..318edc5c 100644 --- a/nornir/core/deserializer/configuration.py +++ b/nornir/core/deserializer/configuration.py @@ -1,7 +1,7 @@ import importlib import logging from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Type, Union, cast +from typing import Any, Callable, Dict, Optional, Type, Union, List, cast from nornir.core import configuration from nornir.core.deserializer.inventory import Inventory @@ -71,8 +71,11 @@ def deserialize(cls, **kwargs: Any) -> configuration.InventoryConfig: class LoggingConfig(BaseNornirSettings): - level: str = Schema(default="debug", description="Logging level") - file: str = Schema(default="nornir.log", descritpion="Logging file") + enabled: Optional[bool] = Schema( + default=None, description="Whether to configure logging or not" + ) + level: str = Schema(default="INFO", description="Logging level") + file: str = Schema(default="nornir.log", description="Logging file") format: str = Schema( default="%(asctime)s - %(name)12s - %(levelname)8s - %(funcName)10s() - %(message)s", description="Logging format", @@ -87,14 +90,15 @@ class Config: ignore_extra = False @classmethod - def deserialize(cls, **kwargs: Any) -> configuration.LoggingConfig: - logging_config = LoggingConfig(**kwargs) + def deserialize(cls, **kwargs) -> configuration.LoggingConfig: + conf = cls(**kwargs) return configuration.LoggingConfig( - level=getattr(logging, logging_config.level.upper()), - file_=logging_config.file, - format_=logging_config.format, - to_console=logging_config.to_console, - loggers=logging_config.loggers, + enabled=conf.enabled, + level=conf.level.upper(), + file_=conf.file, + format_=conf.format, + to_console=conf.to_console, + loggers=conf.loggers, ) @@ -211,7 +215,7 @@ def _resolve_import_from_string( return import_path module_name, obj_name = import_path.rsplit(".", 1) module = importlib.import_module(module_name) - return cast(Callable[..., Any], getattr(module, obj_name)) - except Exception as e: - logger.error(f"failed to load import_path '{import_path}'\n{e}") + return getattr(module, obj_name) + except Exception: + logger.error("Failed to import %r", import_path, exc_info=True) raise diff --git a/nornir/core/exceptions.py b/nornir/core/exceptions.py index 1767d474..5cfab96c 100644 --- a/nornir/core/exceptions.py +++ b/nornir/core/exceptions.py @@ -104,3 +104,7 @@ def __init__(self, task: "Task", result: "Result"): def __str__(self) -> str: return "Subtask: {} (failed)\n".format(self.task) + + +class ConflictingConfigurationWarning(UserWarning): + pass diff --git a/nornir/core/task.py b/nornir/core/task.py index 6dcff5c3..2014d4f1 100644 --- a/nornir/core/task.py +++ b/nornir/core/task.py @@ -9,6 +9,9 @@ from nornir.core.inventory import Host +logger = logging.getLogger(__name__) + + class Task(object): """ A task is basically a wrapper around a function that has to be run against multiple devices. @@ -59,21 +62,30 @@ def start(self, host, nornir): self.host = host self.nornir = nornir - logger = logging.getLogger(__name__) try: - logger.info("{}: {}: running task".format(self.host.name, self.name)) + logger.debug("Host %r: running task %r", self.host.name, self.name) r = self.task(self, **self.params) if not isinstance(r, Result): r = Result(host=host, result=r) except NornirSubTaskError as e: tb = traceback.format_exc() - logger.error("{}: {}".format(self.host, tb)) + logger.error( + "Host %r: task %r failed with traceback:\n%s", + self.host.name, + self.name, + tb, + ) r = Result(host, exception=e, result=str(e), failed=True) except Exception as e: tb = traceback.format_exc() - logger.error("{}: {}".format(self.host, tb)) + logger.error( + "Host %r: task %r failed with traceback:\n%s", + self.host.name, + self.name, + tb, + ) r = Result(host, exception=e, result=tb, failed=True) r.name = self.name diff --git a/nornir/init_nornir.py b/nornir/init_nornir.py index 394c4ebc..01862c05 100644 --- a/nornir/init_nornir.py +++ b/nornir/init_nornir.py @@ -1,4 +1,6 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional + +import warnings from nornir.core import Nornir from nornir.core.connections import Connections @@ -22,13 +24,16 @@ def cls_to_string(cls: Callable[..., Any]) -> str: def InitNornir( config_file: str = "", dry_run: bool = False, - configure_logging: bool = True, + configure_logging: Optional[bool] = None, **kwargs: Dict[str, Any], ) -> Nornir: """ Arguments: config_file(str): Path to the configuration file (optional) dry_run(bool): Whether to simulate changes or not + configure_logging: Whether to configure logging or not. This argument is being + deprecated. Please use logging.enabled parameter in the configuration + instead. **kwargs: Extra information to pass to the :obj:`nornir.core.configuration.Config` object @@ -49,8 +54,21 @@ def InitNornir( data = GlobalState(dry_run=dry_run) - if configure_logging: - conf.logging.configure() + if configure_logging is not None: + msg = ( + "'configure_logging' argument is deprecated, please use " + "'logging.enabled' parameter in the configuration instead: " + "https://nornir.readthedocs.io/en/stable/configuration/index.html" + ) + warnings.warn(msg, DeprecationWarning) + + if conf.logging.enabled is None: + if configure_logging is not None: + conf.logging.enabled = configure_logging + else: + conf.logging.enabled = True + + conf.logging.configure() inv = conf.inventory.plugin.deserialize( transform_function=conf.inventory.transform_function, diff --git a/nornir/plugins/inventory/ansible.py b/nornir/plugins/inventory/ansible.py index 9b8c9d74..60cf343c 100644 --- a/nornir/plugins/inventory/ansible.py +++ b/nornir/plugins/inventory/ansible.py @@ -134,12 +134,10 @@ def read_vars_file(element: str, path: str, is_host: bool = True) -> VarsDict: ) if vars_file.is_file(): with open(vars_file) as f: - logger.debug( - "AnsibleInventory: reading var file: %s", vars_file - ) + logger.debug("AnsibleInventory: reading var file %r", vars_file) return cast(Dict[str, Any], YAML.load(f)) logger.debug( - "AnsibleInventory: no vars file was found with the path %s " + "AnsibleInventory: no vars file was found with the path %r " "and one of the supported extensions: %s", vars_file_base, VARS_FILENAME_EXTENSIONS, @@ -251,9 +249,7 @@ def parse(hostsfile: str) -> Tuple[HostsDict, GroupsDict, DefaultsDict]: try: parser = YAMLParser(hostsfile) except (ScannerError, ComposerError): - logger.error( - "couldn't parse '{}' as neither a ini nor yaml file".format(hostsfile) - ) + logger.error("AnsibleInventory: file %r is not INI or YAML file", hostsfile) raise parser.parse() diff --git a/nornir/plugins/inventory/simple.py b/nornir/plugins/inventory/simple.py index 4b9ab59d..bd59fc96 100644 --- a/nornir/plugins/inventory/simple.py +++ b/nornir/plugins/inventory/simple.py @@ -6,6 +6,8 @@ import ruamel.yaml +logger = logging.getLogger(__name__) + class SimpleInventory(Inventory): def __init__( @@ -26,7 +28,7 @@ def __init__( with open(group_file, "r") as f: groups = yml.load(f) or {} else: - logging.debug("{}: doesn't exist".format(group_file)) + logger.debug("File %r was not found", group_file) groups = {} defaults: VarsDict = {} @@ -35,6 +37,6 @@ def __init__( with open(defaults_file, "r") as f: defaults = yml.load(f) or {} else: - logging.debug("{}: doesn't exist".format(defaults_file)) + logger.debug("File %r was not found", defaults_file) defaults = {} super().__init__(hosts=hosts, groups=groups, defaults=defaults, *args, **kwargs) diff --git a/poetry.lock b/poetry.lock index cb297f90..d7a461a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,8 +36,8 @@ category = "dev" description = "Classes Without Boilerplate" name = "attrs" optional = false -python-versions = "*" -version = "18.2.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" [[package]] category = "dev" @@ -87,7 +87,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = false python-versions = "*" -version = "1.12.1" +version = "1.12.2" [package.dependencies] pycparser = "*" @@ -130,7 +130,7 @@ description = "cryptography is a package which provides cryptographic recipes an name = "cryptography" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.5" +version = "2.6.1" [package.dependencies] asn1crypto = ">=0.21.0" @@ -160,7 +160,7 @@ description = "Flake8 and pylama plugin that checks the ordering of import state name = "flake8-import-order" optional = false python-versions = "*" -version = "0.18" +version = "0.18.1" [package.dependencies] pycodestyle = "*" @@ -202,7 +202,7 @@ description = "IPython: Productive Interactive Computing" name = "ipython" optional = false python-versions = ">=3.5" -version = "7.2.0" +version = "7.3.0" [package.dependencies] appnope = "*" @@ -231,7 +231,7 @@ description = "An autocompletion tool for Python that can be used for text edito name = "jedi" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.2" +version = "0.13.3" [package.dependencies] parso = ">=0.3.0" @@ -253,7 +253,13 @@ description = "An implementation of JSON Schema validation for Python" name = "jsonschema" optional = false python-versions = "*" -version = "2.6.0" +version = "3.0.1" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0" +setuptools = "*" +six = ">=1.11.0" [[package]] category = "main" @@ -306,7 +312,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li name = "lxml" optional = false python-versions = "*" -version = "4.3.1" +version = "4.3.2" [[package]] category = "main" @@ -314,7 +320,7 @@ description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.0" +version = "1.1.1" [[package]] category = "dev" @@ -512,7 +518,7 @@ description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.8.1" +version = "0.9.0" [[package]] category = "dev" @@ -520,7 +526,7 @@ description = "Library for building powerful interactive command lines in Python name = "prompt-toolkit" optional = false python-versions = "*" -version = "2.0.8" +version = "2.0.9" [package.dependencies] six = ">=1.9.0" @@ -541,7 +547,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7.0" +version = "1.8.0" [[package]] category = "main" @@ -609,7 +615,7 @@ description = "passive checker of Python programs" name = "pyflakes" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.1.0" +version = "2.1.1" [[package]] category = "dev" @@ -657,6 +663,17 @@ version = "1.3.0" cffi = ">=1.4.1" six = "*" +[[package]] +category = "dev" +description = "Persistent/Functional/Immutable data structures" +name = "pyrsistent" +optional = false +python-versions = "*" +version = "0.14.11" + +[package.dependencies] +six = "*" + [[package]] category = "main" description = "Python Serial Port Extension" @@ -671,7 +688,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "4.2.1" +version = "4.3.0" [package.dependencies] atomicwrites = ">=1.0" @@ -723,7 +740,7 @@ description = "Python bindings for 0MQ" name = "pyzmq" optional = false python-versions = ">=2.7,!=3.0*,!=3.1*,!=3.2*" -version = "17.1.2" +version = "18.0.0" [[package]] category = "main" @@ -757,7 +774,7 @@ description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip pres name = "ruamel.yaml" optional = false python-versions = "*" -version = "0.15.88" +version = "0.15.89" [[package]] category = "main" @@ -856,7 +873,7 @@ python-versions = "*" version = "0.1.7" [metadata] -content-hash = "ad8c5de57301b5d529395ec487563c7bac46db9fabfb549331c319a518b40989" +content-hash = "90596857c51e4925f054deeae4b8eed69032212dd1c2e0d6459187b0c166d0f7" python-versions = ">= 3.6, < 3.8" [metadata.hashes] @@ -864,33 +881,33 @@ appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", " appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"] atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] -attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] +attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] bcrypt = ["0ba875eb67b011add6d8c5b76afbd92166e98b1f1efab9433d5dc0fafc76e203", "21ed446054c93e209434148ef0b362432bb82bbdaf7beef70a32c221f3e33d1c", "28a0459381a8021f57230954b9e9a65bb5e3d569d2c253c5cac6cb181d71cf23", "2aed3091eb6f51c26b7c2fad08d6620d1c35839e7a362f706015b41bd991125e", "2fa5d1e438958ea90eaedbf8082c2ceb1a684b4f6c75a3800c6ec1e18ebef96f", "3a73f45484e9874252002793518da060fb11eaa76c30713faa12115db17d1430", "3e489787638a36bb466cd66780e15715494b6d6905ffdbaede94440d6d8e7dba", "44636759d222baa62806bbceb20e96f75a015a6381690d1bc2eda91c01ec02ea", "678c21b2fecaa72a1eded0cf12351b153615520637efcadc09ecf81b871f1596", "75460c2c3786977ea9768d6c9d8957ba31b5fbeb0aae67a5c0e96aab4155f18c", "8ac06fb3e6aacb0a95b56eba735c0b64df49651c6ceb1ad1cf01ba75070d567f", "8fdced50a8b646fff8fa0e4b1c5fd940ecc844b43d1da5a980cb07f2d1b1132f", "9b2c5b640a2da533b0ab5f148d87fb9989bf9bcb2e61eea6a729102a6d36aef9", "a9083e7fa9adb1a4de5ac15f9097eb15b04e2c8f97618f1b881af40abce382e1", "b7e3948b8b1a81c5a99d41da5fb2dc03ddb93b5f96fcd3fd27e643f91efa33e1", "b998b8ca979d906085f6a5d84f7b5459e5e94a13fc27c28a3514437013b6c2f6", "dd08c50bc6f7be69cd7ba0769acca28c846ec46b7a8ddc2acf4b9ac6f8a7457e", "de5badee458544ab8125e63e39afeedfcf3aef6a6e2282ac159c95ae7472d773", "ede2a87333d24f55a4a7338a6ccdccf3eaa9bed081d1737e0db4dbd1a4f7e6b6"] black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] certifi = ["47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", "993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"] -cffi = ["0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6", "27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709", "2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a", "3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282", "358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556", "37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134", "39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d", "42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da", "5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1", "5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509", "5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1", "7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3", "7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96", "7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2", "8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28", "9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c", "9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033", "a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d", "aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea", "bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0", "bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469", "bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071", "c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719", "d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a", "df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6", "f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a", "f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf", "f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729"] +cffi = ["00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f", "0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11", "0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d", "21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891", "2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf", "2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c", "2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed", "3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b", "4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a", "51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585", "59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea", "59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f", "610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33", "70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145", "71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a", "8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3", "9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f", "9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd", "b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804", "b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d", "c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92", "c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f", "c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84", "c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb", "cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7", "e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7", "e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35", "fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] coverage = ["06123b58a1410873e22134ca2d88bd36680479fe354955b3579fb8ff150e4d27", "09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", "0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", "0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", "0d34245f824cc3140150ab7848d08b7e2ba67ada959d77619c986f2062e1f0e8", "10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", "1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", "1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", "258b21c5cafb0c3768861a6df3ab0cfb4d8b495eee5ec660e16f928bf7385390", "2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", "3ad59c84c502cd134b0088ca9038d100e8fb5081bbd5ccca4863f3804d81f61d", "447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", "46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", "4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", "510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", "5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", "5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", "5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", "6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", "6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", "71afc1f5cd72ab97330126b566bbf4e8661aab7449f08895d21a5d08c6b051ff", "7349c27128334f787ae63ab49d90bf6d47c7288c63a0a5dfaa319d4b4541dd2c", "77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", "828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", "859714036274a75e6e57c7bab0c47a4602d2a8cfaaa33bbdb68c8359b2ed4f5c", "85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", "869ef4a19f6e4c6987e18b315721b8b971f7048e6eaea29c066854242b4e98d9", "8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", "977e2d9a646773cc7428cdd9a34b069d6ee254fadfb4d09b3f430e95472f3cf3", "99bd767c49c775b79fdcd2eabff405f1063d9d959039c0bdd720527a7738748a", "a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", "aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", "ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", "b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", "bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", "c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", "d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", "d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", "da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", "ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", "ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"] -cryptography = ["05b3ded5e88747d28ee3ef493f2b92cbb947c1e45cf98cfef22e6d38bb67d4af", "06826e7f72d1770e186e9c90e76b4f84d90cdb917b47ff88d8dc59a7b10e2b1e", "08b753df3672b7066e74376f42ce8fc4683e4fd1358d34c80f502e939ee944d2", "2cd29bd1911782baaee890544c653bb03ec7d95ebeb144d714b0f5c33deb55c7", "31e5637e9036d966824edaa91bf0aa39dc6f525a1c599f39fd5c50340264e079", "42fad67d7072216a49e34f923d8cbda9edacbf6633b19a79655e88a1b4857063", "4946b67235b9d2ea7d31307be9d5ad5959d6c4a8f98f900157b47abddf698401", "522fdb2809603ee97a4d0ef2f8d617bc791eb483313ba307cb9c0a773e5e5695", "6f841c7272645dd7c65b07b7108adfa8af0aaea57f27b7f59e01d41f75444c85", "7d335e35306af5b9bc0560ca39f740dfc8def72749645e193dd35be11fb323b3", "8504661ffe324837f5c4607347eeee4cf0fcad689163c6e9c8d3b18cf1f4a4ad", "9260b201ce584d7825d900c88700aa0bd6b40d4ebac7b213857bd2babee9dbca", "9a30384cc402eac099210ab9b8801b2ae21e591831253883decdb4513b77a3cd", "9e29af877c29338f0cab5f049ccc8bd3ead289a557f144376c4fbc7d1b98914f", "ab50da871bc109b2d9389259aac269dd1b7c7413ee02d06fe4e486ed26882159", "b13c80b877e73bcb6f012813c6f4a9334fcf4b0e96681c5a15dac578f2eedfa0", "bfe66b577a7118e05b04141f0f1ed0959552d45672aa7ecb3d91e319d846001e", "e091bd424567efa4b9d94287a952597c05d22155a13716bf5f9f746b9dc906d3", "fa2b38c8519c5a3aa6e2b4e1cf1a549b54acda6adb25397ff542068e73d1ed00"] +cryptography = ["066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1", "210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705", "26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6", "2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1", "2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8", "409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151", "45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d", "48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659", "6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537", "6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e", "8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb", "9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c", "9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9", "9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5", "acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad", "c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a", "d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460", "d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd", "e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6"] dataclasses = ["454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", "6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"] decorator = ["33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e", "cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b"] -flake8-import-order = ["9be5ca10d791d458eaa833dd6890ab2db37be80384707b0f76286ddd13c16cbf", "feca2fd0a17611b33b7fa84449939196c2c82764e262486d5c3e143ed77d387b"] +flake8-import-order = ["90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", "a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"] future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] ipykernel = ["0aeb7ec277ac42cc2b59ae3d08b10909b2ec161dc6908096210527162b53675d", "0fc0bf97920d454102168ec2008620066878848fcfca06c22b669696212e292f"] -ipython = ["6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12", "f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742"] +ipython = ["06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39", "5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"] ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] -jedi = ["571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", "c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"] +jedi = ["2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b", "2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"] jinja2 = ["74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"] -jsonschema = ["000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", "6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"] +jsonschema = ["0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", "a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"] junos-eznc = ["b96bccd4ce9c9127d91ca6145b868ae53a378186455eb087d57420d3dae276bd", "d97d8babf650abca25a096825aa6d88573d340481a0b0793afcdac4a7bee09d3"] jupyter-client = ["b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551", "c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987"] jupyter-core = ["927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970", "ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"] -lxml = ["0537eee4902e8bf4f41bfee8133f7edf96533dd175930a12086d6a40d62376b2", "0562ec748abd230ab87d73384e08fa784f9b9cee89e28696087d2d22c052cc27", "09e91831e749fbf0f24608694e4573be0ef51430229450c39c83176cc2e2d353", "1ae4c0722fc70c0d4fba43ae33c2885f705e96dce1db41f75ae14a2d2749b428", "1c630c083d782cbaf1f7f37f6cac87bda9cff643cf2803a5f180f30d97955cef", "2fe74e3836bd8c0fa7467ffae05545233c7f37de1eb765cacfda15ad20c6574a", "37af783c2667ead34a811037bda56a0b142ac8438f7ed29ae93f82ddb812fbd6", "3f2d9eafbb0b24a33f56acd16f39fc935756524dcb3172892721c54713964c70", "47d8365a8ef14097aa4c65730689be51851b4ade677285a3b2daa03b37893e26", "510e904079bc56ea784677348e151e1156040dbfb736f1d8ea4b9e6d0ab2d9f4", "58d0851da422bba31c7f652a7e9335313cf94a641aa6d73b8f3c67602f75b593", "7940d5c2185ffb989203dacbb28e6ae88b4f1bb25d04e17f94b0edd82232bcbd", "7cf39bb3a905579836f7a8f3a45320d9eb22f16ab0c1e112efb940ced4d057a5", "9563a23c1456c0ab550c087833bc13fcc61013a66c6420921d5b70550ea312bf", "95b392952935947e0786a90b75cc33388549dcb19af716b525dae65b186138fc", "983129f3fd3cef5c3cf067adcca56e30a169656c00fcc6c648629dbb850b27fa", "a0b75b1f1854771844c647c464533def3e0a899dd094a85d1d4ed72ecaaee93d", "b5db89cc0ef624f3a81214b7961a99f443b8c91e88188376b6b322fd10d5b118", "c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a", "c0c5a7d4aafcc30c9b6d8613a362567e32e5f5b708dc41bc3a81dac56f8af8bb", "d4d63d85eacc6cb37b459b16061e1f100d154bee89dc8d8f9a6128a5a538e92e", "da5e7e941d6e71c9c9a717c93725cda0708c2474f532e3680ac5e39ec57d224d", "dccad2b3c583f036f43f80ac99ee212c2fa9a45151358d55f13004d095e683b2", "df46307d39f2aeaafa1d25309b8a8d11738b73e9861f72d4d0a092528f498baa", "e70b5e1cb48828ddd2818f99b1662cb9226dc6f57d07fc75485405c77da17436", "ea825562b8cd057cbc9810d496b8b5dec37a1e2fc7b27bc7c1e72ce94462a09a"] -markupsafe = ["048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", "130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", "19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", "1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", "1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", "1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", "1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", "31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", "3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", "4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", "525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", "52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", "52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", "5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", "5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", "5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", "7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", "83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", "857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", "98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", "bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", "d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", "e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", "edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", "efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", "f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", "f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", "fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"] +lxml = ["0358b9e9642bc7d39aac5cffe9884a99a5ca68e5e2c1b89e570ed60da9139908", "091a359c4dafebbecd3959d9013f1b896b5371859165e4e50b01607a98d9e3e2", "1998e4e60603c64bcc35af61b4331ab3af087457900d3980e18d190e17c3a697", "2000b4088dee9a41f459fddaf6609bba48a435ce6374bb254c5ccdaa8928c5ba", "2afb0064780d8aaf165875be5898c1866766e56175714fa5f9d055433e92d41d", "2d8f1d9334a4e3ff176d096c14ded3100547d73440683567d85b8842a53180bb", "2e38db22f6a3199fd63675e1b4bd795d676d906869047398f29f38ca55cb453a", "3181f84649c1a1ca62b19ddf28436b1b2cb05ae6c7d2628f33872e713994c364", "37462170dfd88af8431d04de6b236e6e9c06cda71e2ca26d88ef2332fd2a5237", "3a9d8521c89bf6f2a929c3d12ad3ad7392c774c327ea809fd08a13be6b3bc05f", "3d0bbd2e1a28b4429f24fd63a122a450ce9edb7a8063d070790092d7343a1aa4", "483d60585ce3ee71929cea70949059f83850fa5e12deb9c094ed1c8c2ec73cbd", "4888be27d5cba55ce94209baef5bcd7bbd7314a3d17021a5fc10000b3a5f737d", "64b0d62e4209170a2a0c404c446ab83b941a0003e96604d2e4f4cb735f8a2254", "68010900898fdf139ac08549c4dba8206c584070a960ffc530aebf0c6f2794ef", "872ecb066de602a0099db98bd9e57f4cfc1d62f6093d94460c787737aa08f39e", "88a32b03f2e4cd0e63f154cac76724709f40b3fc2f30139eb5d6f900521b44ed", "b1dc7683da4e67ab2bebf266afa68098d681ae02ce570f0d1117312273d2b2ac", "b29e27ce9371810250cb1528a771d047a9c7b0f79630dc7dc5815ff828f4273b", "ce197559596370d985f1ce6b7051b52126849d8159040293bf8b98cb2b3e1f78", "d45cf6daaf22584eff2175f48f82c4aa24d8e72a44913c5aff801819bb73d11f", "e2ff9496322b2ce947ba4a7a5eb048158de9d6f3fe9efce29f1e8dd6878561e6", "f7b979518ec1f294a41a707c007d54d0f3b3e1fd15d5b26b7e99b62b10d9a72e", "f9c7268e9d16e34e50f8246c4f24cf7353764affd2bc971f0379514c246e3f6b", "f9c839806089d79de588ee1dde2dae05dc1156d3355dfeb2b51fde84d9c960ad", "ff962953e2389226adc4d355e34a98b0b800984399153c6678f2367b11b4d4b8"] +markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] more-itertools = ["0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", "590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"] mypy = ["308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7", "e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"] @@ -906,30 +923,31 @@ paramiko = ["3c16b2bfb4c0d810b24c40155dbfd113c0521e7e6ee593d704e84b4c658a1f3b", parso = ["4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", "68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"] pexpect = ["2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", "3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"] pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"] -pluggy = ["8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"] -prompt-toolkit = ["88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba", "c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e", "df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010"] +pluggy = ["19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", "84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"] +prompt-toolkit = ["11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", "2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", "977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"] ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] -py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"] +py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] pyasn1 = ["061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b", "0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd", "5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed", "65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee", "79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff", "8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3", "9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a", "98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699", "b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5", "b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0", "d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8", "da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] pydantic = ["9f023811b6cefd203c5fd8fd15a4152f04e79e531b8f676ab1244dfe06ce8024", "edbb08b561feda505374c0f25e4b54466a0a0c702ed6b2efaabdc3890d1a82e7"] pydocstyle = ["2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", "5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", "ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"] pyeapi = ["5be222c290e9e14827ee15541f0e61e290e0223bdead6151233aa7baa5b1b3c8"] -pyflakes = ["5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", "f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"] +pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pygments = ["5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", "e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"] pyiosxr = ["0e25d6871b48db81511c03261924ef23169c66e80321801b9bb221a3b74e370e"] pylama = ["7e0327ee9b2a350ed73fe54c240894e534e2bccfb23a59ed5ce89f5a5689ee94", "f81bf3bbd15db802b620903df491e5cd6469dcd542424ce6718425037dcc4d10"] pynacl = ["05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", "0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", "0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", "1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", "2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", "2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", "30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", "37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", "4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", "57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", "5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", "6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", "7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", "a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", "a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", "aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", "bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", "e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", "f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"] +pyrsistent = ["3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"] pyserial = ["6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", "e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"] -pytest = ["80cfd9c8b9e93f419abcc0400e9f595974a98e44b6863a77d3e1039961bfc9c4", "c2396a15726218a2dfef480861c4ba37bd3952ebaaa5b0fede3fc23fddcd7f8c"] +pytest = ["067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", "9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"] pytest-cov = ["0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", "230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"] python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] -pyzmq = ["25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8", "2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349", "30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba", "32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946", "41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf", "4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46", "55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59", "65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786", "6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40", "75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7", "76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d", "7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3", "8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d", "9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce", "9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca", "a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1", "a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed", "a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf", "ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27", "c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8", "c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810", "d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b", "e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19", "e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192", "ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"] +pyzmq = ["07a03450418694fb07e76a0191b6bc9f411afc8e364ca2062edcf28bb0e51c63", "15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b", "1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6", "1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4", "228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc", "264ac9dcee6a7af2bce4b61f2d19e5926118a5caa629b50f107ef6318670a364", "2b5a43da65f5dec857184d5c2ce13b80071019e96358f146bdecff7238765bc9", "3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba", "3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a", "411def5b4cbe6111856040a55c8048df113882e90c57ce88de4a48f0189441ac", "4b77e96a7ffc1c5e08eaf274db554f227b31717d086adca1bb42b12ef35a7194", "4c87fa3e449e1f4ab9170cdfe8213dc0ba34a11b160e6adecafa892e451a29b6", "4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda", "54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba", "60acabd86808a16a895a247fd2bf7a127284a33562d79687bb5df163cff068b2", "618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac", "69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae", "6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1", "6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9", "75c87f1dc1e65cea4b709f2ebc78fa18d4b475e41463502aec9cd26208b88e0f", "97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c", "abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69", "ae3d2627d74195ddc95675f2f814aca998381b73dc4341b9e10e3e191e1bdb0b", "b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b", "cbabdced5b137cd56aa22633f13ac5690029a0ad43ab6c05f53206e489178362"] requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] requests-mock = ["7a5fa99db5e3a2a961b6f20ed40ee6baeff73503cf0a553cc4d679409e6170fb", "8ca0628dc66d3f212878932fd741b02aa197ad53fd2228164800a169a4a826af"] -"ruamel.yaml" = ["0289a685479d059b94683cd6cb47ffb790c05c20a6c4da395361025d52493d0a", "0adf1d9b8e88dc6b151a3199b1dd7be0c8ee10d6c2ebd2a9e2a13224f4481cdf", "13657c26780bba5824764cddb0f2933217fd59cfcca0e2ee1b2f759e7e58ef8e", "3815f688de7316fcd3ba5ceda642e902044c5c1a8fb5e4dc245d99db3eb3121b", "4e61c0b96805d1e2ec53cb1698ca6086a47aa1e1d09857144eb60216e7894ce3", "4f0d57ead5414456cb899c3746a8d30f566c22bb90c97da76f76e79147cb2d61", "51916929902ff054e189d29bc418788a5dc3a4e89a89065beed694f537383ca3", "5b7ea0ee24680157666f730f3a8c173f386c66e8c103458af20d97276e7e54d3", "6b1b1ee0a028b9cdc1bc3ec1f75480fc0d3fbcc9e0212a716b129b6f26e34587", "6db27f789c7efdbc59b8650c37a09dde0db019560bc19a07c905b65158a18bba", "7a8c8f825fd52f3586d583f621cdf3a03b9dc8833933ae401554b246b48026d5", "7ab0c27094ef27a21e0094dc671c456bd4a62811c14e27407ae8bc3aa8cc8111", "8e06bcf212b45dffe6c2415693c32b4c7d4ff55c03268a3033217f7a673d07ba", "9826e3c85549b3fc87786466a7a96dcadec59802a9ed077b905349ef1cac7b14", "ac56193c47a31c9efa151064a9e921865cdad0f7a991d229e7197e12fe8e0cd7", "aef88ec2927b0454709026a761918c02b69e5df9c061b49634d7993c0848580d", "d8591fdfd076d8121a456aaff0bbea6d5753023896f4559b710d4e56d1ac6418", "e033423fd6b4ddfd47f0f5ebe81e896129d85fd5219c5e66effb4de06a1fea7a", "e05017af8c1164fee33aa2677df7eaeb6d2fa76e22baf7960f9e8f1b04657151", "e4cd2ccd4d455206826a7c59fda13a9008ae994de66a7b0df2c0bb81121fab01", "e4f525efdecc075e6b0d96df0ae4bd2ad17c7280ebe66035f468c5c3da53fe0d", "fd5f09c399cdc92586b54ee28f68f23f1d5649177d7ceb22ec975b5e69e1b722"] +"ruamel.yaml" = ["0ae64b150ec39667ce2815227271207d27a6218bc501b73e70a7403f2cf987ee", "0e6aa488ec65fcaf7cca609d83a2b0e13d429a6080dd6ef3fd89dcbf67f13c1d", "1085219e373381c8a4ac911be2f167d2c67485af82199db0abee11cfe06283d5", "181aeecf6d037e34c325c191b0a14188921964dd02c34a4d01bf7881060c22d0", "1bafe5773c559ef8e06d53f35bfcaad0510ef069db8b3fb50bf9816b3c531633", "20bc4549b86f7e5a558c06e51c97821d8b7658d4b01d3e21713a53db6f45779a", "325b5b1df1b29ecaaa3dab6745ee0a35d51e58aa19b90cab8ba11c498d438cb7", "3e89d657a11fee61120d304efcab92409ea4a21c9629626cbb7aabbac7b713d9", "551142c578813ee25e4952dda820701f0201d0292e8807e8fb5490d19bab8181", "6d8bf658b64ebeccb67bccccd01833cdbe5369c1436365d41052d23e5cbf716f", "76aadb21b3186599057c8874104c8467270307e12faed124dc5ee1cfcd4e240f", "86d034aa9e2ab3eacc5f75f5cd6a469a2af533b6d9e60ea92edbba540d21b9b7", "aa4bfa597c58f2efcae98add337ada85ded45288936a15ddc2d29ff3f5ce3ddf", "b76a4cf08e9392765cf6a25eae63a12fb27c6cc4a8ee5416a49cd54627151f46", "bd9976f13d14f6cb6da1748c7678198b6f9346cbbe0ad18eb74c024a7a2c0d08", "c3c1af293b9e6a84b5da3954f4ae81b30d07a626717cfb6f099ffe0901ff1eac", "c64ae6da2da174fce281a088d7634c6e545db5fb989a3cecec2aa9cef2634a92", "ca742877ea6948cb80c70aaf5d76011694eb072d98264a704b04ffc2d4c0d440", "cfefc391d5a3a7e8b1c6195f364d2f11f898f07eef277ef8e93a4e3ccafbd7c2", "f004249eb8b873c4f48361dc814450e47dab53b5312334cff0a9d381a4589f03", "fc6a2b2f4453944ab8bbe4d6d2eba0c864c098baa8487b08e4340a93a1f811c7", "fffc4dd4097451c0ae1c6cb89bbff1a8248fda6366adb08d1c7bbc4ea2e6a637"] scp = ["1b7fd4d7e9e966542ed64a9716de7846d8a49efc5e0923e233c391119e6207b1", "cfcc275c249ae59480f88fa55c4bd7795ce8b48d3a9b8fd635d82958084b4124"] selectors2 = ["81b77c4c6f607248b1d6bbdb5935403fef294b224b842a830bbfabb400c81884", "ed3b473edddb85d4ca89e2beca9f01fffd411b3105e8f3c8d57d9edc11106bda"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] diff --git a/pyproject.toml b/pyproject.toml index aede582d..a1296fe9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,8 @@ requests = "^2" mypy_extensions = "^0.4.1" pydantic = "^0.18.2" [tool.poetry.dev-dependencies] +# https://github.com/jupyter/notebook/issues/4399 +tornado = "^5.1" decorator = "*" nbval = "*" pytest = "*" diff --git a/setup.cfg b/setup.cfg index 4ecc5640..200d73f8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ universal=1 [pylama] linters = mccabe,pep8,pyflakes,import_order ignore = D203,C901 -skip = .tox/* +skip = .tox/*,.venv/* [pylama:pep8] max_line_length = 100 @@ -26,6 +26,8 @@ max-line-length = 100 [tool:pytest] #addopts = --cov=nornir --cov-report=term-missing -vs python_paths = ./ +filterwarnings = + ignore::nornir.core.exceptions.ConflictingConfigurationWarning [mypy] # The mypy configurations: http://bit.ly/2zEl9WI diff --git a/tests/conftest.py b/tests/conftest.py index af7890fd..7c820e24 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import logging import os from nornir import InitNornir @@ -10,14 +9,6 @@ global_data = GlobalState(dry_run=True) -logging.basicConfig( - filename="tests.log", - filemode="w", - level=logging.DEBUG, - format="%(asctime)s - %(name)s - %(levelname)s - %(funcName)20s() - %(message)s", -) - - @pytest.fixture(scope="session", autouse=True) def nornir(request): """Initializes nornir""" diff --git a/tests/core/deserializer/test_configuration.py b/tests/core/deserializer/test_configuration.py index b6385021..abf1c8c1 100644 --- a/tests/core/deserializer/test_configuration.py +++ b/tests/core/deserializer/test_configuration.py @@ -1,4 +1,3 @@ -import logging import os from pathlib import Path @@ -25,6 +24,7 @@ class Test(object): def test_config_defaults(self): c = ConfigDeserializer() assert c.dict() == { + "core": {"num_workers": 20, "raise_on_error": False}, "inventory": { "plugin": "nornir.plugins.inventory.simple.SimpleInventory", "options": {}, @@ -33,14 +33,14 @@ def test_config_defaults(self): }, "ssh": {"config_file": "~/.ssh/config"}, "logging": { - "level": "debug", + "enabled": None, + "level": "INFO", "file": "nornir.log", "format": DEFAULT_LOG_FORMAT, "to_console": False, "loggers": ["nornir"], }, "jinja2": {"filters": ""}, - "core": {"num_workers": 20, "raise_on_error": False}, "user_defined": {}, } @@ -59,7 +59,8 @@ def test_config_basic(self): }, "ssh": {"config_file": "~/.ssh/config"}, "logging": { - "level": "debug", + "enabled": None, + "level": "INFO", "file": "", "format": DEFAULT_LOG_FORMAT, "to_console": False, @@ -78,11 +79,11 @@ def test_deserialize_defaults(self): assert not c.core.raise_on_error assert c.user_defined == {} - assert c.logging.level == logging.DEBUG + assert c.logging.enabled is None + assert c.logging.level == "INFO" assert c.logging.file == "nornir.log" assert c.logging.format == DEFAULT_LOG_FORMAT assert not c.logging.to_console - assert c.logging.loggers == ["nornir"] assert c.ssh.config_file == str(Path("~/.ssh/config").expanduser()) @@ -95,7 +96,7 @@ def test_deserialize_basic(self): c = ConfigDeserializer.deserialize( core={"num_workers": 30}, user_defined={"my_opt": True}, - logging={"file": "", "level": "info"}, + logging={"file": "", "level": "DEBUG"}, ssh={"config_file": "~/.ssh/alt_config"}, inventory={"plugin": "nornir.plugins.inventory.ansible.AnsibleInventory"}, ) @@ -105,11 +106,11 @@ def test_deserialize_basic(self): assert not c.core.raise_on_error assert c.user_defined == {"my_opt": True} - assert c.logging.level == logging.INFO + assert c.logging.enabled is None + assert c.logging.level == "DEBUG" assert c.logging.file == "" assert c.logging.format == DEFAULT_LOG_FORMAT assert not c.logging.to_console - assert c.logging.loggers == ["nornir"] assert c.ssh.config_file == str(Path("~/.ssh/alt_config").expanduser()) diff --git a/tests/core/test_InitNornir.py b/tests/core/test_InitNornir.py index bde6a863..3ffee200 100644 --- a/tests/core/test_InitNornir.py +++ b/tests/core/test_InitNornir.py @@ -1,13 +1,38 @@ +import logging +import logging.config import os import pytest - from nornir import InitNornir from nornir.core.deserializer.inventory import Inventory +from nornir.core.exceptions import ConflictingConfigurationWarning dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_InitNornir") +LOGGING_DICT = { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "standard": { + "format": "[%(asctime)s] %(levelname)-8s {%(name)s:%(lineno)d} %(message)s" + } + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "standard", + } + }, + "loggers": { + "app": {"handlers": ["console"], "level": "INFO", "propagate": False}, + "nornir": {"handlers": ["console"], "level": "WARNING", "propagate": False}, + }, + "root": {"handlers": ["console"], "level": "DEBUG"}, +} + def transform_func(host): host["processed_by_transform_function"] = True @@ -149,3 +174,102 @@ def test_InitNornir_different_transform_function_by_string_with_bad_options(self }, ) assert nr + + +class TestLogging: + @classmethod + def cleanup(cls): + # this does not work as setup_method, because pytest injects + # _pytest.logging.LogCaptureHandler handler to the root logger + # and StreamHandler to _pytest.capture.EncodedFile to other loggers + root_logger = logging.getLogger() + for handler in root_logger.handlers: + root_logger.removeHandler(handler) + root_logger.setLevel(logging.WARNING) + + for logger_name in ["nornir", "app"]: + logger_ = logging.getLogger(logger_name) + for handler in logger_.handlers: + logger_.removeHandler(handler) + logger_.setLevel(logging.NOTSET) + + @classmethod + def teardown_class(cls): + cls.cleanup() + + def test_InitNornir_logging_defaults(self): + self.cleanup() + InitNornir( + config_file=os.path.join(dir_path, "a_config.yaml"), + core={"num_workers": 200}, + ) + nornir_logger = logging.getLogger("nornir") + + assert nornir_logger.level == logging.INFO + assert len(nornir_logger.handlers) == 1 + assert isinstance(nornir_logger.handlers[0], logging.FileHandler) + + def test_InitNornir_logging_to_console(self): + self.cleanup() + InitNornir( + config_file=os.path.join(dir_path, "a_config.yaml"), + logging={"to_console": True}, + ) + nornir_logger = logging.getLogger("nornir") + + assert nornir_logger.level == logging.INFO + assert len(nornir_logger.handlers) == 3 + assert any( + isinstance(handler, logging.FileHandler) + for handler in nornir_logger.handlers + ) + assert any( + isinstance(handler, logging.StreamHandler) + for handler in nornir_logger.handlers + ) + + def test_InitNornir_logging_disabled(self): + self.cleanup() + InitNornir( + config_file=os.path.join(dir_path, "a_config.yaml"), + logging={"enabled": False}, + ) + nornir_logger = logging.getLogger("nornir") + + assert nornir_logger.level == logging.NOTSET + + def test_InitNornir_logging_disabled_alt(self): + self.cleanup() + with pytest.warns(DeprecationWarning): + InitNornir( + config_file=os.path.join(dir_path, "a_config.yaml"), + configure_logging=False, + ) + nornir_logger = logging.getLogger("nornir") + assert nornir_logger.level == logging.NOTSET + + def test_InitNornir_logging_basicConfig(self): + self.cleanup() + logging.basicConfig() + with pytest.warns(ConflictingConfigurationWarning): + InitNornir(config_file=os.path.join(dir_path, "a_config.yaml")) + nornir_logger = logging.getLogger("nornir") + + assert logging.getLogger().hasHandlers() + assert nornir_logger.level == logging.INFO + assert nornir_logger.hasHandlers() + + def test_InitNornir_logging_dictConfig(self): + self.cleanup() + logging.config.dictConfig(LOGGING_DICT) + with pytest.warns(ConflictingConfigurationWarning): + InitNornir(config_file=os.path.join(dir_path, "a_config.yaml")) + + nornir_logger = logging.getLogger("nornir") + root_logger = logging.getLogger() + app_logger = logging.getLogger("app") + + assert root_logger.hasHandlers() + assert root_logger.level == logging.DEBUG + assert nornir_logger.hasHandlers() + assert app_logger.level == logging.INFO diff --git a/tests/plugins/functions/text/output_data/failed_with_severity.stdout b/tests/plugins/functions/text/output_data/failed_with_severity.stdout index 391824ef..dbc09297 100644 --- a/tests/plugins/functions/text/output_data/failed_with_severity.stdout +++ b/tests/plugins/functions/text/output_data/failed_with_severity.stdout @@ -20,10 +20,6 @@ Hello from CRITICAL Exception('Unknown Error -> Contact your system administrator',) ^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * dev5.no_group ** changed : False ********************************************* -vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR -NornirSubTaskError() ---- echo_task ** changed : False ---------------------------------------------- CRITICAL Hello from CRITICAL ----- parse_data ** changed : False --------------------------------------------- ERROR -KeyError('values',) -^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/test_print_result.py b/tests/plugins/functions/text/test_print_result.py index b7e70ed3..57ee14eb 100644 --- a/tests/plugins/functions/text/test_print_result.py +++ b/tests/plugins/functions/text/test_print_result.py @@ -49,6 +49,9 @@ def parse_data(task): data["changed"] = False data["failed"] = True + elif "dev5.no_group" == task.host.name: + data["values"] = [13, 14, 15] + if data["failed"]: raise Exception("Unknown Error -> Contact your system administrator") @@ -97,5 +100,6 @@ def test_print_changed_host(self, nornir): @wrap_cli_test(output="{}/failed_with_severity".format(output_dir)) def test_print_failed_with_severity(self, nornir): + nornir.config.logging.configure() result = nornir.run(read_data) print_result(result, vars=["exception", "output"], severity_level=logging.ERROR) From a5878b43474af413101e49a20e9dcc4dba1ec026 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 8 Mar 2019 10:03:48 -0800 Subject: [PATCH 09/32] Use new version of Netmiko; update poetry.lock --- poetry.lock | 15 ++++++--------- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index d7a461a6..e5025e98 100644 --- a/poetry.lock +++ b/poetry.lock @@ -442,19 +442,16 @@ description = "Multi-vendor library to simplify Paramiko SSH connections to netw name = "netmiko" optional = false python-versions = "*" -version = "2.3.1" +version = "2.3.3" [package.dependencies] paramiko = ">=2.4.2" pyserial = "*" pyyaml = "*" scp = ">=0.10.0" +setuptools = ">=38.4.0" textfsm = "*" -[package.source] -reference = "9c0996ddc3d2de18242bf1a722188bd5aa4c8983" -type = "git" -url = "https://github.com/ktbyers/netmiko" [[package]] category = "main" description = "A library for managing Cisco devices through NX-API using XML or jsonrpc." @@ -740,7 +737,7 @@ description = "Python bindings for 0MQ" name = "pyzmq" optional = false python-versions = ">=2.7,!=3.0*,!=3.1*,!=3.2*" -version = "18.0.0" +version = "18.0.1" [[package]] category = "main" @@ -873,7 +870,7 @@ python-versions = "*" version = "0.1.7" [metadata] -content-hash = "90596857c51e4925f054deeae4b8eed69032212dd1c2e0d6459187b0c166d0f7" +content-hash = "bfbe279b16238ff511f004f92c5bf0509bdb8474eaa32fb93fea67b893fe6dbc" python-versions = ">= 3.6, < 3.8" [metadata.hashes] @@ -917,7 +914,7 @@ nbformat = ["b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b", nbval = ["3f18b87af4e94ccd073263dd58cd3eebabe9f5e4d6ab535b39d3af64811c7eda", "74ff5e2c90a50b1ddf7edd02978c4e43221b1ee252dc14fcaa4230aae4492eda"] ncclient = ["3ab58ee0d71069cb5b0e2f29a4e605d1d8417bd10af45b73ee3e817fe389fadc"] netaddr = ["38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", "56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"] -netmiko = [] +netmiko = ["e7e9af5aeebea54488d9cad92f0a85117ca5ffa816decc63a669d88c5b2018fd"] nxapi-plumbing = ["6f54f9983f023bd75b60acd907ff47f559541cc2b98f4fa638b8585ca0de0fb5"] paramiko = ["3c16b2bfb4c0d810b24c40155dbfd113c0521e7e6ee593d704e84b4c658a1f3b", "a8975a7df3560c9f1e2b43dc54ebd40fd00a7017392ca5445ce7df409f900fcb"] parso = ["4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", "68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"] @@ -944,7 +941,7 @@ pytest = ["067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", "9 pytest-cov = ["0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", "230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"] python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] -pyzmq = ["07a03450418694fb07e76a0191b6bc9f411afc8e364ca2062edcf28bb0e51c63", "15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b", "1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6", "1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4", "228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc", "264ac9dcee6a7af2bce4b61f2d19e5926118a5caa629b50f107ef6318670a364", "2b5a43da65f5dec857184d5c2ce13b80071019e96358f146bdecff7238765bc9", "3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba", "3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a", "411def5b4cbe6111856040a55c8048df113882e90c57ce88de4a48f0189441ac", "4b77e96a7ffc1c5e08eaf274db554f227b31717d086adca1bb42b12ef35a7194", "4c87fa3e449e1f4ab9170cdfe8213dc0ba34a11b160e6adecafa892e451a29b6", "4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda", "54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba", "60acabd86808a16a895a247fd2bf7a127284a33562d79687bb5df163cff068b2", "618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac", "69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae", "6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1", "6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9", "75c87f1dc1e65cea4b709f2ebc78fa18d4b475e41463502aec9cd26208b88e0f", "97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c", "abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69", "ae3d2627d74195ddc95675f2f814aca998381b73dc4341b9e10e3e191e1bdb0b", "b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b", "cbabdced5b137cd56aa22633f13ac5690029a0ad43ab6c05f53206e489178362"] +pyzmq = ["1651e52ed91f0736afd6d94ef9f3259b5534ce8beddb054f3d5ca989c4ef7c4f", "5ccb9b3d4cd20c000a9b75689d5add8cd3bce67fcbd0f8ae1b59345247d803af", "5e120c4cd3872e332fb35d255ad5998ebcee32ace4387b1b337416b6b90436c7", "5e2a3707c69a7281a9957f83718815fd74698cba31f6d69f9ed359921f662221", "63d51add9af8d0442dc90f916baf98fdc04e3b0a32afec4bfc83f8d85e72959f", "65c5a0bdc49e20f7d6b03a661f71e2fda7a99c51270cafe71598146d09810d0d", "66828fabe911aa545d919028441a585edb7c9c77969a5fea6722ef6e6ece38ab", "7d79427e82d9dad6e9b47c0b3e7ae5f9d489b1601e3a36ea629bb49501a4daf3", "824ee5d3078c4eae737ffc500fbf32f2b14e6ec89b26b435b7834febd70120cf", "89dc0a83cccec19ff3c62c091e43e66e0183d1e6b4658c16ee4e659518131494", "8b319805f6f7c907b101c864c3ca6cefc9db8ce0791356f180b1b644c7347e4c", "90facfb379ab47f94b19519c1ecc8ec8d10813b69d9c163117944948bdec5d15", "a0a178c7420021fc0730180a914a4b4b3092ce9696ceb8e72d0f60f8ce1655dd", "a7a89591ae315baccb8072f216614b3e59aed7385aef4393a6c741783d6ee9cf", "ba2578f0ae582452c02ed9fac2dc477b08e80ce05d2c0885becf5fff6651ccb0", "c69b0055c55702f5b0b6b354133e8325b9a56dbc80e1be2d240bead253fb9825", "ca434e1858fe222380221ddeb81e86f45522773344c9da63c311d17161df5e06", "d4b8ecfc3d92f114f04d5c40f60a65e5196198b827503341521dda12d8b14939", "d706025c47b09a54f005953ebe206f6d07a22516776faa4f509aaff681cc5468", "d8f27e958f8a2c0c8ffd4d8855c3ce8ac3fa1e105f0491ce31729aa2b3229740", "dbd264298f76b9060ce537008eb989317ca787c857e23cbd1b3ddf89f190a9b1", "e926d66f0df8fdbf03ba20583af0f215e475c667fb033d45fd031c66c63e34c9", "efc3bd48237f973a749f7312f68062f1b4ca5c2032a0673ca3ea8e46aa77187b", "f59bc782228777cbfe04555707a9c56d269c787ed25d6d28ed9d0fbb41cb1ad2", "f8da5322f4ff5f667a0d5a27e871b560c6637153c81e318b35cb012b2a98835c"] requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] requests-mock = ["7a5fa99db5e3a2a961b6f20ed40ee6baeff73503cf0a553cc4d679409e6170fb", "8ca0628dc66d3f212878932fd741b02aa197ad53fd2228164800a169a4a826af"] "ruamel.yaml" = ["0ae64b150ec39667ce2815227271207d27a6218bc501b73e70a7403f2cf987ee", "0e6aa488ec65fcaf7cca609d83a2b0e13d429a6080dd6ef3fd89dcbf67f13c1d", "1085219e373381c8a4ac911be2f167d2c67485af82199db0abee11cfe06283d5", "181aeecf6d037e34c325c191b0a14188921964dd02c34a4d01bf7881060c22d0", "1bafe5773c559ef8e06d53f35bfcaad0510ef069db8b3fb50bf9816b3c531633", "20bc4549b86f7e5a558c06e51c97821d8b7658d4b01d3e21713a53db6f45779a", "325b5b1df1b29ecaaa3dab6745ee0a35d51e58aa19b90cab8ba11c498d438cb7", "3e89d657a11fee61120d304efcab92409ea4a21c9629626cbb7aabbac7b713d9", "551142c578813ee25e4952dda820701f0201d0292e8807e8fb5490d19bab8181", "6d8bf658b64ebeccb67bccccd01833cdbe5369c1436365d41052d23e5cbf716f", "76aadb21b3186599057c8874104c8467270307e12faed124dc5ee1cfcd4e240f", "86d034aa9e2ab3eacc5f75f5cd6a469a2af533b6d9e60ea92edbba540d21b9b7", "aa4bfa597c58f2efcae98add337ada85ded45288936a15ddc2d29ff3f5ce3ddf", "b76a4cf08e9392765cf6a25eae63a12fb27c6cc4a8ee5416a49cd54627151f46", "bd9976f13d14f6cb6da1748c7678198b6f9346cbbe0ad18eb74c024a7a2c0d08", "c3c1af293b9e6a84b5da3954f4ae81b30d07a626717cfb6f099ffe0901ff1eac", "c64ae6da2da174fce281a088d7634c6e545db5fb989a3cecec2aa9cef2634a92", "ca742877ea6948cb80c70aaf5d76011694eb072d98264a704b04ffc2d4c0d440", "cfefc391d5a3a7e8b1c6195f364d2f11f898f07eef277ef8e93a4e3ccafbd7c2", "f004249eb8b873c4f48361dc814450e47dab53b5312334cff0a9d381a4589f03", "fc6a2b2f4453944ab8bbe4d6d2eba0c864c098baa8487b08e4340a93a1f811c7", "fffc4dd4097451c0ae1c6cb89bbff1a8248fda6366adb08d1c7bbc4ea2e6a637"] diff --git a/pyproject.toml b/pyproject.toml index a1296fe9..9ed9e09d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ python = ">= 3.6, < 3.8" colorama = "^0.4.1" jinja2 = "^2" napalm = "^2" -netmiko = { git = "https://github.com/ktbyers/netmiko", branch = "environment_markers" } +netmiko = ">=2.3.3, <3" paramiko = ">=2.1.1, <3" requests = "^2" "ruamel.yaml" = "^0.15.85" From ee693baf4b97f71eb817bc029f3b32aa6fc1f93c Mon Sep 17 00:00:00 2001 From: Justin Haefner <12788741+justinhaef@users.noreply.github.com> Date: Tue, 12 Mar 2019 10:35:48 -0500 Subject: [PATCH 10/32] Document typos (#351) * fixing random typos * Fixed typos but keeping python version the same. --- docs/howto/advanced_filtering.ipynb | 2 +- docs/howto/transforming_inventory_data.ipynb | 2 +- docs/howto/writing_a_custom_inventory_plugin.ipynb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/howto/advanced_filtering.ipynb b/docs/howto/advanced_filtering.ipynb index 9ee6e1a1..3382cde2 100644 --- a/docs/howto/advanced_filtering.ipynb +++ b/docs/howto/advanced_filtering.ipynb @@ -153,7 +153,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you can see we have built ourselves a collection of animals with different properties. The ``F`` object let's you access the magic methods of each typesby just prepeding two underscores and the the name of the magic method. For instance, if you want to check if a list contains a particular element you can just prepend ``__contains``. Let's use this feature to retrieve all the animals that belong to the group ``bird``:" + "As you can see we have built ourselves a collection of animals with different properties. The ``F`` object let's you access the magic methods of each types by just prepeding two underscores and the the name of the magic method. For instance, if you want to check if a list contains a particular element you can just prepend ``__contains``. Let's use this feature to retrieve all the animals that belong to the group ``bird``:" ] }, { diff --git a/docs/howto/transforming_inventory_data.ipynb b/docs/howto/transforming_inventory_data.ipynb index f011b17f..81b67383 100644 --- a/docs/howto/transforming_inventory_data.ipynb +++ b/docs/howto/transforming_inventory_data.ipynb @@ -55,7 +55,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You can modify inventory data (regardless of the plugin you are using) on the fly easily by password a ``transform_function`` like this:" + "You can modify inventory data (regardless of the plugin you are using) on the fly easily by passing a ``transform_function`` like this:" ] }, { diff --git a/docs/howto/writing_a_custom_inventory_plugin.ipynb b/docs/howto/writing_a_custom_inventory_plugin.ipynb index abd89c48..73f4f7ea 100644 --- a/docs/howto/writing_a_custom_inventory_plugin.ipynb +++ b/docs/howto/writing_a_custom_inventory_plugin.ipynb @@ -60,7 +60,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A dynamic inventory basically would have to retrieve the data, rearrange in a similar way as in the example above and cal ``super``. Now, let's see how to use it:" + "A dynamic inventory basically would have to retrieve the data, rearrange in a similar way as in the example above and call ``super``. Now, let's see how to use it:" ] }, { From b17e537214504e913e9e00a2077d145f2eded30e Mon Sep 17 00:00:00 2001 From: Sam Byers Date: Thu, 14 Mar 2019 10:43:30 -0400 Subject: [PATCH 11/32] fixed typos (#353) --- nornir/plugins/tasks/text/template_file.py | 2 +- nornir/plugins/tasks/text/template_string.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nornir/plugins/tasks/text/template_file.py b/nornir/plugins/tasks/text/template_file.py index 8d870483..fa83cdfb 100644 --- a/nornir/plugins/tasks/text/template_file.py +++ b/nornir/plugins/tasks/text/template_file.py @@ -14,7 +14,7 @@ def template_file( **kwargs: Any ) -> Result: """ - Renders contants of a file with jinja2. All the host data is available in the tempalte + Renders contants of a file with jinja2. All the host data is available in the template Arguments: template: filename diff --git a/nornir/plugins/tasks/text/template_string.py b/nornir/plugins/tasks/text/template_string.py index 25316cdd..a5e607d4 100644 --- a/nornir/plugins/tasks/text/template_string.py +++ b/nornir/plugins/tasks/text/template_string.py @@ -10,7 +10,7 @@ def template_string( task: Task, template: str, jinja_filters: FiltersDict = None, **kwargs: Any ) -> Result: """ - Renders a string with jinja2. All the host data is available in the tempalte + Renders a string with jinja2. All the host data is available in the template Arguments: template (string): template string From 52e879550fdd052727c6eecdfe6d43028de39805 Mon Sep 17 00:00:00 2001 From: Walter De Smedt Date: Fri, 15 Mar 2019 20:46:42 +0100 Subject: [PATCH 12/32] Print result (#345) * Add support for OrderedDict to print_result() * Add support for OrderedDict in print_result() --- nornir/plugins/functions/text/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nornir/plugins/functions/text/__init__.py b/nornir/plugins/functions/text/__init__.py index 68238c31..c002775e 100644 --- a/nornir/plugins/functions/text/__init__.py +++ b/nornir/plugins/functions/text/__init__.py @@ -2,6 +2,8 @@ import pprint import threading from typing import List, Optional, cast +from collections import OrderedDict +import json from colorama import Fore, Style, init @@ -61,7 +63,10 @@ def _print_individual_result( # for consistency between py3.6 and py3.7 print(f"{x.__class__.__name__}{x.args}") elif x and not isinstance(x, str): - pprint.pprint(x, indent=2) + if isinstance(x, OrderedDict): + print(json.dumps(x, indent=2)) + else: + pprint.pprint(x, indent=2) elif x: print(x) From 2a986d258047fb3591b01223115a2b84cd712d83 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 18 Mar 2019 10:04:37 +0100 Subject: [PATCH 13/32] prepare 2.1 release (#342) --- CHANGELOG.rst | 33 ++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f54da7a5..4089e354 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,14 +1,41 @@ +2.1.0 - March 6 2019 +==================== + +* [CORE_ENHANCEMENTS] inventory's transform function supports options #292 +* [CORE_ENHANCEMENTS] minor improvements to tests #293 #296 #306 #307 #312 #337 +* [CORE_ENHANCEMENTS] mypy improvements #308 +* [CORE_ENHANCEMENTS] expand user home when deserializing configuration #304 +* [CORE_ENHANCEMENTS] fix order of preference when deserializing config #309 +* [CORE_ENHANCEMENTS] fix and deprecate dict() function #314 +* [CORE_ENHANCEMENTS] migrate to poetry #315 +* [CORE_ENHANCEMENTS] Improve logging #316 +* [CORE_BUGFIX] (windows only) fix issue #319 - ascii color codes appear instead of color in output #320 #323 +* [PLUGIN_IMPROVEMENT] napalm and netmiko plugins support now reading ssh configuration from file #298 +* [PLUGIN_BUGFIX] fix paramiko chan.recv_exit_status() call order #313 +* [PLUGIN_BUGFIX] temporary fix for enum34 and netmiko-poetry issue #322 +* [PLUGIN_IMPROVEMENT] Print OrderDicts nicely in print_result #345 +* [DOCS] Various improvements #303 #305 #310 #318 #331 #335 #340 + +Thanks to the following people for their contributions: + +* `bradh11 `_ +* `fallenarc `_ +* `floatingstatic `_ +* `jimmelville `_ +* `optiz0r `_ +* `wdesmedt `_ + 2.0.0 - December 17 2018 ======================== -For details about upgrading to 2.0.0 see the [https://nornir.readthedocs.io/en/2.0.0-beta/upgrading/1_to_2.html](notes). +For details about upgrading to 2.0.0 see the `notes `_. + [CORE_ENHANCEMENTS] Lots of core enhancements, too many to document + [CORE_ENHANCEMENTS] Changes on how the inventory -+ [CORE_ENHANCEMENTS] New ``F`` object for advanced filtering of hosts [docs](file:///Users/dbarroso/workspace/nornir/docs/_build/html/howto/advanced_filtering.html) ++ [CORE_ENHANCEMENTS] New ``F`` object for `advanced filtering of hosts `_ + [CORE_ENHANCEMENTS] Improvements on how to serialize/deserialize user facing data like the configuration and the inventory + [CORE_ENHANCEMENTS] Connections are now their own type of plugin -+ [CORE_ENHANCEMENTS] Ability to handle connections manually [docs](file:///Users/dbarroso/workspace/nornir/docs/_build/html/howto/handling_connections.html) ++ [CORE_ENHANCEMENTS] Ability to `handle connections manually `_ + [CORE_BUGFIX] Lots + [PLUGIN_BUGFIX] Lots + [PLUGIN_NEW] netmiko_save_config diff --git a/pyproject.toml b/pyproject.toml index 9ed9e09d..9641b855 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nornir" -version = "2.0.0" +version = "2.1.0" description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices" authors = ["David Barroso "] readme = "README.md" From 741389106280bb20c177926bacb16815c358bfba Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 18 Mar 2019 10:20:47 +0100 Subject: [PATCH 14/32] fix date --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4089e354..25eed5ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,5 @@ -2.1.0 - March 6 2019 -==================== +2.1.0 - March 18 2019 +===================== * [CORE_ENHANCEMENTS] inventory's transform function supports options #292 * [CORE_ENHANCEMENTS] minor improvements to tests #293 #296 #306 #307 #312 #337 From fde462621f2816a14ecfa9424456b01aa98047d1 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Mon, 18 Mar 2019 10:26:18 +0100 Subject: [PATCH 15/32] added more contributors --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 25eed5ea..fb607002 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,9 @@ Thanks to the following people for their contributions: * `jimmelville `_ * `optiz0r `_ * `wdesmedt `_ +* `dmfigol `_ +* `ktbyers `_ +* `dbarrosop `_ 2.0.0 - December 17 2018 ======================== From f997bfeb9149554ad741ef74c8dc6aafed1e36c9 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Mon, 18 Mar 2019 14:55:45 -0400 Subject: [PATCH 16/32] Adds missing commas in documentation; rewords one sentence in docs/tutorials/intro/python.rst --- docs/index.rst | 2 +- docs/tutorials/intro/install.rst | 6 +++--- docs/tutorials/intro/overview.rst | 6 +++--- docs/tutorials/intro/python.rst | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5e5d682f..aa5e9f0f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ Welcome to nornir's documentation! Nornir is an automation framework written in python to be used with python. Most automation frameworks hide the language they are written in by using some cumbersome pseudo-language -which usually is almost Turing complete but lacks tooling to debug and troubleshoot. Integrating +which usually is almost Turing complete, but lacks tooling to debug and troubleshoot. Integrating with other systems is also usually quite hard as they usually have complex APIs if any at all. Some of the other common problems of those pseudo-languages is that are usually quite bad at dealing with data and re-usability is limited. diff --git a/docs/tutorials/intro/install.rst b/docs/tutorials/intro/install.rst index 0276bbb7..bd6847e4 100644 --- a/docs/tutorials/intro/install.rst +++ b/docs/tutorials/intro/install.rst @@ -1,7 +1,7 @@ Installing Nornir ================== -Before you go ahead and install Nornir it's recommended to create your own Python virtualenv. That way you have complete control of your environment and you don't risk overwriting your systems Python environment. +Before you go ahead and install Nornir, it's recommended to create your own Python virtualenv. That way you have complete control of your environment and you don't risk overwriting your systems Python environment. .. note:: @@ -10,7 +10,7 @@ Before you go ahead and install Nornir it's recommended to create your own Pytho Nornir is published to `PyPI `_ and can be installed like most other Python packages using the pip tool. You can verify that you have pip installed by typing: .. code-block:: bash - + pip --version pip 9.0.3 from /Users/patrick/nornir/lib/python3.6/site-packages (python 3.6) @@ -28,7 +28,7 @@ As you would assume, the installation is then very easy. [...] Successfully installed MarkupSafe-1.0 asn1crypto-0.24.0 bcrypt-3.1.4 nornir-2.0.0 -Please note that the above output has been abbreviated for readability. Your output will be quite a bit longer. You should see that `nornir` is successfully installed. +Please note that the above output has been abbreviated for readability. Your output will be quite a bit longer. You should see that `nornir` is successfully installed. Now we can verify that Nornir is installed and that you are able to import the package from Python. diff --git a/docs/tutorials/intro/overview.rst b/docs/tutorials/intro/overview.rst index ef9e4fa9..c1ceff2c 100644 --- a/docs/tutorials/intro/overview.rst +++ b/docs/tutorials/intro/overview.rst @@ -5,9 +5,9 @@ Nornir is an automation framework written in Python. These days there exists sev Why does this matter? --------------------- -Typically, a specific configuration language is easy to use in a basic way. Though after a while you need to use more advanced features and might have to extend that configuration language with another programming language. While this works it can be very hard to troubleshoot once it's started to grow. +Typically, a specific configuration language is easy to use in a basic way. Though after a while, you need to use more advanced features and might have to extend that configuration language with another programming language. While this works, it can be very hard to troubleshoot once it's started to grow. -As Nornir allows you to use pure Python code you can troubleshoot and debug it in the same way as you would do with any other Python code. +As Nornir allows you to use pure Python code, you can troubleshoot and debug it in the same way as you would do with any other Python code. What does it compare to? ------------------------ @@ -17,4 +17,4 @@ Nornir lets you automate your environment by providing you an interface which do How much Python do you need do know? ------------------------------------ -As you write Python code to control Nornir it's assumed that you are somewhat familiar with Python. But how good do you have to be with Python in order to make use of Nornir? That's actually the topic for the next section *spoiler alert* Not a lot! +As you write Python code to control Nornir, it's assumed that you are somewhat familiar with Python. But how good do you have to be with Python in order to make use of Nornir? That's actually the topic for the next section *spoiler alert* Not a lot! diff --git a/docs/tutorials/intro/python.rst b/docs/tutorials/intro/python.rst index 91f45f0f..662f6208 100644 --- a/docs/tutorials/intro/python.rst +++ b/docs/tutorials/intro/python.rst @@ -7,7 +7,7 @@ If you haven't written any code before you might be heading somewhere else now. Do you know Excel? -Chances are that you know how to use Excel. It's simple right. You just open a sheet and enter some data. It's used by a lot of finance people and unfortunately it's one of the most used IPAM solutions. Though aside from a simple tool to enter data in a sheet Excel has an insane amount of features. Most people will only use 5% of all the features. How good are you at Excel? Does it matter? +Chances are that you know how to use Excel. It's simple right. You just open a sheet and enter some data. It's used by a lot of finance people and unfortunately it's one of the most used IPAM solutions. Though aside from being a simple data entry tool, Excel has an insane amount of features. Most people will only use 5% of all the features. How good are you at Excel? Does it matter? It's the same way with Python, it can take very long time to fully master it. The good part is that you don't have to become a master. As long as you know the very basics you will be able to use Nornir. From c3d8df9f01c668d68431404232ec1fac30601b07 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Mon, 18 Mar 2019 15:24:38 -0400 Subject: [PATCH 17/32] Removed unnecessary comma in intro/overview.rst --- docs/tutorials/intro/overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/intro/overview.rst b/docs/tutorials/intro/overview.rst index c1ceff2c..1f9fbc5b 100644 --- a/docs/tutorials/intro/overview.rst +++ b/docs/tutorials/intro/overview.rst @@ -5,7 +5,7 @@ Nornir is an automation framework written in Python. These days there exists sev Why does this matter? --------------------- -Typically, a specific configuration language is easy to use in a basic way. Though after a while, you need to use more advanced features and might have to extend that configuration language with another programming language. While this works, it can be very hard to troubleshoot once it's started to grow. +Typically, a specific configuration language is easy to use in a basic way. Though after a while you need to use more advanced features and might have to extend that configuration language with another programming language. While this works, it can be very hard to troubleshoot once it's started to grow. As Nornir allows you to use pure Python code, you can troubleshoot and debug it in the same way as you would do with any other Python code. From 6246088c81a35a674942bebd99650f3996d1f603 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Mon, 18 Mar 2019 17:06:27 -0400 Subject: [PATCH 18/32] Fix typo in inventory.ipynb --- docs/tutorials/intro/inventory.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/intro/inventory.ipynb b/docs/tutorials/intro/inventory.ipynb index 8cd900fd..0c2dae2e 100644 --- a/docs/tutorials/intro/inventory.ipynb +++ b/docs/tutorials/intro/inventory.ipynb @@ -1276,7 +1276,7 @@ "source": [ "##### Filter Object\n", "\n", - "You can also use a filter object to create incrementally a complext query object. Let's see how it works by example:" + "You can also use a filter objects to incrementally create a complex query objects. Let's see how it works by example:" ] }, { From 4f330c0064184d1d962fefd81355e90db743420e Mon Sep 17 00:00:00 2001 From: David Barroso Date: Tue, 19 Mar 2019 12:02:00 +0100 Subject: [PATCH 19/32] fix minor deployment issues --- .travis.yml | 2 +- CHANGELOG.rst | 10 ++++++++++ pyproject.toml | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84aa73d3..e13b3496 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ deploy: script: poetry publish on: tags: true - condition: "$TRAVIS_PYTHON_VERSION == 3.7" + condition: "$PYTHON == 3.7" after_success: - coveralls env: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fb607002..d069a6e0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,13 @@ +2.1.1 - March 19 2019 +===================== + +* [MISC] Workaround to sdispater/poetry#743 +* [MISC] Fix automated deployment to pypi + +Thanks to the following people for their contributions: + +* `dbarrosop `_ + 2.1.0 - March 18 2019 ===================== diff --git a/pyproject.toml b/pyproject.toml index 9641b855..4944a019 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nornir" -version = "2.1.0" +version = "2.1.1" description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices" authors = ["David Barroso "] readme = "README.md" @@ -11,7 +11,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">= 3.6, < 3.8" +python = "^3.6" colorama = "^0.4.1" jinja2 = "^2" napalm = "^2" From 75e9dfdafb4b8effcdb5280b9e2f6d2f5dfeaf57 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Tue, 19 Mar 2019 12:02:43 +0100 Subject: [PATCH 20/32] update changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d069a6e0..52a09a04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ 2.1.1 - March 19 2019 ===================== -* [MISC] Workaround to sdispater/poetry#743 -* [MISC] Fix automated deployment to pypi +* [MISC] Workaround to sdispater/poetry#743 #358 +* [MISC] Fix automated deployment to pypi #358 Thanks to the following people for their contributions: From 2ef209079feab886aa4478e03600d7ee07fc2ccd Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Wed, 20 Mar 2019 11:40:51 -0400 Subject: [PATCH 21/32] Changes 'childs' to 'children' in grouping_tasks.ipynb --- docs/tutorials/intro/grouping_tasks.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/intro/grouping_tasks.ipynb b/docs/tutorials/intro/grouping_tasks.ipynb index d2e1bd9c..5b116105 100644 --- a/docs/tutorials/intro/grouping_tasks.ipynb +++ b/docs/tutorials/intro/grouping_tasks.ipynb @@ -210,7 +210,7 @@ "However, this was a `dry_run`. Let's set the `dry_run` variable to `False` so changes are commited and then run the code again:\n", "\n", "
\n", - "**Note:** The `dry_run` value is shared between the main nornir objects and its childs so in the snippet below `nr.data.dry_run = False` and `cmh.data.dry_run = False` are equivalent.\n", + "**Note:** The `dry_run` value is shared between the main nornir objects and its children so in the snippet below `nr.data.dry_run = False` and `cmh.data.dry_run = False` are equivalent.\n", "
" ] }, From 43e7843f8e3e61d711c13c0d5372a3c42af122bf Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Wed, 20 Mar 2019 16:49:38 -0400 Subject: [PATCH 22/32] Proofreading docs: Improves wording of sentence slightly --- docs/howto/transforming_inventory_data.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/transforming_inventory_data.ipynb b/docs/howto/transforming_inventory_data.ipynb index 81b67383..3531d2b2 100644 --- a/docs/howto/transforming_inventory_data.ipynb +++ b/docs/howto/transforming_inventory_data.ipynb @@ -154,7 +154,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now everything we have to do is put the import path as the ``transform_function`` configuration option:" + "Now the only thing left to do is put the import path as the ``transform_function`` configuration option:" ] }, { From 830c4cd070c0ef74c19219925b2eb812abd1ddd8 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Wed, 20 Mar 2019 16:51:10 -0400 Subject: [PATCH 23/32] Proofreading docs: Adds missing 'of' in a sentence --- docs/tutorials/intro/failed_tasks.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/intro/failed_tasks.ipynb b/docs/tutorials/intro/failed_tasks.ipynb index 2cf57cc0..da6a2a98 100644 --- a/docs/tutorials/intro/failed_tasks.ipynb +++ b/docs/tutorials/intro/failed_tasks.ipynb @@ -409,7 +409,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To achieve this `nornir` keeps a list failed hosts in it's shared [data](../../ref/api/nornir.rst#nornir.core.state.GlobalState) object:" + "To achieve this `nornir` keeps a list of failed hosts in it's shared [data](../../ref/api/nornir.rst#nornir.core.state.GlobalState) object:" ] }, { From 4dbdd4dd8703ce4068509b9e7370867ab53c6e04 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Wed, 20 Mar 2019 16:52:39 -0400 Subject: [PATCH 24/32] Proofreading docs: Adds missing comma --- docs/tutorials/intro/grouping_tasks.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/intro/grouping_tasks.ipynb b/docs/tutorials/intro/grouping_tasks.ipynb index 5b116105..aeb151cf 100644 --- a/docs/tutorials/intro/grouping_tasks.ipynb +++ b/docs/tutorials/intro/grouping_tasks.ipynb @@ -300,7 +300,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As the configuration should've been commited already if we run it again the task \"Loading Configuration on the device\" should tell us that `changed : False` and should return an empty diff. Let's see if that's true:" + "As the configuration should've been commited already, if we run it again the task \"Loading Configuration on the device\" should tell us that `changed : False` and should return an empty diff. Let's see if that's true:" ] }, { From 913dcf50ad2763af85296c12116e6f6da4b4b478 Mon Sep 17 00:00:00 2001 From: Eitan Akman Date: Wed, 20 Mar 2019 16:55:06 -0400 Subject: [PATCH 25/32] Proofreading docs: Changes 'where' --> 'were'; Re-words sentence --- docs/tutorials/intro/task_results.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/intro/task_results.ipynb b/docs/tutorials/intro/task_results.ipynb index e9dc529f..ad07d5cb 100644 --- a/docs/tutorials/intro/task_results.ipynb +++ b/docs/tutorials/intro/task_results.ipynb @@ -209,7 +209,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you probably noticed, not all the tasks where printed. If you check the tests they all got a new argument `severity_level`. This let's us flag tasks with any of the logging levels. Then `print_result` is able to following logging rules to print the results. By default only tasks marked as `INFO` will be printed (this is also the default for the tasks if none is specified). Note that a failed task will have its severity level changed to `ERROR` regardless of the one specified by the user.\n", + "As you probably noticed, not all the tasks were printed. If you check the tests, they all got a new argument `severity_level`. This let's us flag tasks with any of the logging levels. Then `print_result` is able to following logging rules to print the results. By default only tasks marked as `INFO` will be printed (this is also the default for the tasks if none is specified). Note that a failed task will have its severity level changed to `ERROR` regardless of the one specified by the user.\n", "\n", "Now let's tell `print_result` to print tasks marked as `DEBUG`." ] @@ -298,7 +298,7 @@ "source": [ "## The programmatic way\n", "\n", - "We have hinted already how to deal with result objects already but let's elaborate on that. To begin with, task groups will return an [AggregatedResult](../../ref/api/task.rst#nornir.core.task.AggregatedResult). This object is a dict-like object you can use to iterate over or access directly hosts:" + "We have hinted at how to deal with result objects already, but let's elaborate on that. To begin with, task groups will return an [AggregatedResult](../../ref/api/task.rst#nornir.core.task.AggregatedResult). This object is a dict-like object you can use to iterate over or access hosts directly:" ] }, { From 164ba21d700ab45a796b66e77491dd49f100fa85 Mon Sep 17 00:00:00 2001 From: Brady Lamprecht Date: Fri, 22 Mar 2019 02:41:36 +0000 Subject: [PATCH 26/32] Updating howto documentation for including 'ConnectionOptions' --- docs/howto/transforming_inventory_data.ipynb | 67 +++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/howto/transforming_inventory_data.ipynb b/docs/howto/transforming_inventory_data.ipynb index 81b67383..7f7ff472 100644 --- a/docs/howto/transforming_inventory_data.ipynb +++ b/docs/howto/transforming_inventory_data.ipynb @@ -265,9 +265,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "rtr00.password: None\n", + "rtr01.password: None\n" + ] + } + ], "source": [ "from nornir import InitNornir\n", "\n", @@ -295,6 +304,58 @@ "for name, host in nr.inventory.hosts.items():\n", " print(f\"{name}.password: {host.password}\")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using ConnectionOptions\n", + "\n", + "Additionally, you might come across the need to include certain `ConnectionOptions` to be able to connect to your devices. \n", + "Documentation specific to `nornir` can be found [here](../plugins/connections/index.rst) and for `napalm` can be found [here](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments).\n", + "\n", + "The following example shows how to correctly do so in the `helpers.py`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from nornir.core.inventory import ConnectionOptions\n", + "\n", + "def adapt_host_data(host):\n", + " \n", + " # This function receives a Host object for manipulation\n", + " host.username = host[\"user\"]\n", + " host.password = host[\"password\"]\n", + " host.connection_options[\"napalm\"] = ConnectionOptions(\n", + " extras={\n", + " \"optional_args\": {\n", + " \"secret\":host.password\n", + " }\n", + " }\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also specify it as a `default` for your entire inventory in this manner:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from nornir.core.inventory import ConnectionOptions\n", + "\n", + "nr.inventory.defaults.connection_options = ConnectionOptions(extras={\"optional_args\":{\"secret\":\"my_secret\"}})" + ] } ], "metadata": { @@ -313,7 +374,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.6" } }, "nbformat": 4, From dd6732f4127d17fdd84e5b51128ccbd478ec6360 Mon Sep 17 00:00:00 2001 From: Brady Lamprecht Date: Fri, 22 Mar 2019 02:49:50 +0000 Subject: [PATCH 27/32] The notebook didn't display correctly on the documentation page Attempting to close the notebook will resolve it. --- docs/howto/transforming_inventory_data.ipynb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/howto/transforming_inventory_data.ipynb b/docs/howto/transforming_inventory_data.ipynb index 7f7ff472..8175d98c 100644 --- a/docs/howto/transforming_inventory_data.ipynb +++ b/docs/howto/transforming_inventory_data.ipynb @@ -314,12 +314,13 @@ "Additionally, you might come across the need to include certain `ConnectionOptions` to be able to connect to your devices. \n", "Documentation specific to `nornir` can be found [here](../plugins/connections/index.rst) and for `napalm` can be found [here](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments).\n", "\n", + "\n", "The following example shows how to correctly do so in the `helpers.py`:\n" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -343,12 +344,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You can also specify it as a `default` for your entire inventory in this manner:" + "You can also specify them as a `default` for your entire inventory in this manner:" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ From 69c406aea10369272ac3f766f3d45d3bd84bcd17 Mon Sep 17 00:00:00 2001 From: brandomando <37256678+brandomando@users.noreply.github.com> Date: Wed, 3 Apr 2019 12:33:06 -0500 Subject: [PATCH 28/32] Update inventory.py --- nornir/core/inventory.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nornir/core/inventory.py b/nornir/core/inventory.py index 9343c314..8774a8a1 100644 --- a/nornir/core/inventory.py +++ b/nornir/core/inventory.py @@ -438,3 +438,19 @@ def children_of_group(self, group: Union[str, Group]) -> Set[Host]: if host.has_parent_group(group): hosts.add(host) return hosts + + def add_host(self, name: str, defaults: Optional[Defaults] = None, + **kwargs) -> None: + """ + Add a host to the inventory after initialization + """ + host = {name: Host(name, defaults=self.defaults, **kwargs)} + self.hosts.update(host) + + def add_group(self, name: str, defaults: Optional[Defaults] = None, + **kwargs) -> None: + """ + Add a group to the inventory after initialization + """ + group = {name: Group(name, defaults=self.defaults, **kwargs)} + self.groups.update(group) From 256ddceb9b2ff9932c2cdc1fffeb261e287ebdad Mon Sep 17 00:00:00 2001 From: brandomando <37256678+brandomando@users.noreply.github.com> Date: Wed, 3 Apr 2019 12:45:20 -0500 Subject: [PATCH 29/32] add tests for add_host and add_group --- tests/core/test_inventory.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/core/test_inventory.py b/tests/core/test_inventory.py index f06c552c..52f5b7b1 100644 --- a/tests/core/test_inventory.py +++ b/tests/core/test_inventory.py @@ -236,3 +236,43 @@ def test_children_of_obj(self): inv.hosts["dev4.group_2"], inv.hosts["dev3.group_2"], } + + def test_add_host(self): + data = {"test_var": "test_value"} + defaults = inventory.Defaults(data=data) + g1 = inventory.Group(name="g1") + g2 = inventory.Group(name="g2", groups=inventory.ParentGroups(["g1"])) + h1 = inventory.Host(name="h1", groups=inventory.ParentGroups(["g1", "g2"])) + h2 = inventory.Host(name="h2") + hosts = {"h1": h1, "h2": h2} + groups = {"g1": g1, "g2": g2} + inv = inventory.Inventory(hosts=hosts, groups=groups, defaults=defaults) + inv.add_host(name="h3", groups=["g1"], platform="TestPlatform") + assert "h3" in inv.hosts + assert "g1" in inv.hosts["h3"].groups + assert "test_var" in inv.hosts["h3"].defaults.data.keys() + assert inv.hosts["h3"].defaults.data.get("test_var") == "test_value" + assert inv.hosts["h3"].platform == "TestPlatform" + + def test_add_group(self): + connection_options = {"username": "test_user", "password": "test_pass"} + data = {"test_var": "test_value"} + defaults = inventory.Defaults(data=data, connection_options=connection_options) + g1 = inventory.Group(name="g1") + g2 = inventory.Group(name="g2", groups=inventory.ParentGroups(["g1"])) + h1 = inventory.Host(name="h1", groups=inventory.ParentGroups(["g1", "g2"])) + h2 = inventory.Host(name="h2") + hosts = {"h1": h1, "h2": h2} + groups = {"g1": g1, "g2": g2} + inv = inventory.Inventory(hosts=hosts, groups=groups, defaults=defaults) + inv.add_group(name="g3", username="test_user") + assert "g3" in inv.groups + assert ( + inv.groups["g3"].defaults.connection_options.get("username") == "test_user" + ) + assert ( + inv.groups["g3"].defaults.connection_options.get("password") + == "test_pass" + ) + assert "test_var" in inv.groups["g3"].defaults.data.keys() + assert "test_value" == inv.groups["g3"].defaults.data.get("test_var") From 83f9e5913c4d60116c172c7ca79840057ba28521 Mon Sep 17 00:00:00 2001 From: brandomando <37256678+brandomando@users.noreply.github.com> Date: Thu, 4 Apr 2019 15:11:03 -0500 Subject: [PATCH 30/32] Add connection_options to add_hosts and add_group --- tests/core/test_inventory.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/core/test_inventory.py b/tests/core/test_inventory.py index 52f5b7b1..1c82c391 100644 --- a/tests/core/test_inventory.py +++ b/tests/core/test_inventory.py @@ -247,12 +247,22 @@ def test_add_host(self): hosts = {"h1": h1, "h2": h2} groups = {"g1": g1, "g2": g2} inv = inventory.Inventory(hosts=hosts, groups=groups, defaults=defaults) - inv.add_host(name="h3", groups=["g1"], platform="TestPlatform") + h3_connection_options = {"netmiko": {"extras": {"device_type": "cisco_ios"}}} + inv.add_host( + name="h3", + groups=["g1"], + platform="TestPlatform", + connection_options=h3_connection_options, + ) assert "h3" in inv.hosts assert "g1" in inv.hosts["h3"].groups assert "test_var" in inv.hosts["h3"].defaults.data.keys() assert inv.hosts["h3"].defaults.data.get("test_var") == "test_value" assert inv.hosts["h3"].platform == "TestPlatform" + assert ( + inv.hosts["h3"].connection_options["netmiko"].extras["device_type"] + == "cisco_ios" + ) def test_add_group(self): connection_options = {"username": "test_user", "password": "test_pass"} @@ -265,14 +275,20 @@ def test_add_group(self): hosts = {"h1": h1, "h2": h2} groups = {"g1": g1, "g2": g2} inv = inventory.Inventory(hosts=hosts, groups=groups, defaults=defaults) - inv.add_group(name="g3", username="test_user") + g3_connection_options = {"netmiko": {"extras": {"device_type": "cisco_ios"}}} + inv.add_group( + name="g3", username="test_user", connection_options=g3_connection_options + ) assert "g3" in inv.groups assert ( inv.groups["g3"].defaults.connection_options.get("username") == "test_user" ) assert ( - inv.groups["g3"].defaults.connection_options.get("password") - == "test_pass" + inv.groups["g3"].defaults.connection_options.get("password") == "test_pass" ) assert "test_var" in inv.groups["g3"].defaults.data.keys() assert "test_value" == inv.groups["g3"].defaults.data.get("test_var") + assert ( + inv.groups["g3"].connection_options["netmiko"].extras["device_type"] + == "cisco_ios" + ) From 441e42f90ad890bfa633608116d97e58f7fecd3b Mon Sep 17 00:00:00 2001 From: brandomando <37256678+brandomando@users.noreply.github.com> Date: Thu, 4 Apr 2019 15:20:11 -0500 Subject: [PATCH 31/32] Re-built add_host and add_group with deserializer --- nornir/core/inventory.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/nornir/core/inventory.py b/nornir/core/inventory.py index 8774a8a1..e16aba58 100644 --- a/nornir/core/inventory.py +++ b/nornir/core/inventory.py @@ -439,18 +439,24 @@ def children_of_group(self, group: Union[str, Group]) -> Set[Host]: hosts.add(host) return hosts - def add_host(self, name: str, defaults: Optional[Defaults] = None, - **kwargs) -> None: + def add_host(self, name: str, **kwargs) -> None: """ Add a host to the inventory after initialization """ - host = {name: Host(name, defaults=self.defaults, **kwargs)} + host = { + name: deserializer.inventory.InventoryElement.deserialize_host( + name=name, defaults=self.defaults, **kwargs + ) + } self.hosts.update(host) - def add_group(self, name: str, defaults: Optional[Defaults] = None, - **kwargs) -> None: + def add_group(self, name: str, **kwargs) -> None: """ Add a group to the inventory after initialization """ - group = {name: Group(name, defaults=self.defaults, **kwargs)} + group = { + name: deserializer.inventory.InventoryElement.deserialize_group( + name=name, defaults=self.defaults, **kwargs + ) + } self.groups.update(group) From fa15294c79170aeb9e536f4e5d3fb4ab564277e9 Mon Sep 17 00:00:00 2001 From: Brandon Donohoe Date: Mon, 22 Apr 2019 03:11:31 -0500 Subject: [PATCH 32/32] Get inventory dicts (#375) --- nornir/core/inventory.py | 30 +++++++++++++++++++++++++++ tests/core/test_inventory.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/nornir/core/inventory.py b/nornir/core/inventory.py index e16aba58..2d4c30dd 100644 --- a/nornir/core/inventory.py +++ b/nornir/core/inventory.py @@ -460,3 +460,33 @@ def add_group(self, name: str, **kwargs) -> None: ) } self.groups.update(group) + + def get_inventory_dict(self) -> Dict: + """ + Return serialized dictionary of inventory + """ + return deserializer.inventory.Inventory.serialize(self).dict() + + def get_defaults_dict(self) -> Dict: + """ + Returns serialized dictionary of defaults from inventory + """ + return deserializer.inventory.Defaults.serialize(self.defaults).dict() + + def get_groups_dict(self) -> Dict: + """ + Returns serialized dictionary of groups from inventory + """ + return { + k: deserializer.inventory.InventoryElement.serialize(v).dict() + for k, v in self.groups.items() + } + + def get_hosts_dict(self) -> Dict: + """ + Returns serialized dictionary of hosts from inventory + """ + return { + k: deserializer.inventory.InventoryElement.serialize(v).dict() + for k, v in self.hosts.items() + } diff --git a/tests/core/test_inventory.py b/tests/core/test_inventory.py index 1c82c391..6df21953 100644 --- a/tests/core/test_inventory.py +++ b/tests/core/test_inventory.py @@ -292,3 +292,43 @@ def test_add_group(self): inv.groups["g3"].connection_options["netmiko"].extras["device_type"] == "cisco_ios" ) + + def test_get_inventory_dict(self): + inv = deserializer.Inventory.deserialize(**inv_dict) + inventory_dict = inv.get_inventory_dict() + def_extras = inventory_dict["defaults"]["connection_options"]["dummy"]["extras"] + grp_data = inventory_dict["groups"]["group_1"]["data"] + host_data = inventory_dict["hosts"]["dev1.group_1"]["data"] + assert type(inventory_dict) == dict + assert inventory_dict["defaults"]["username"] == "root" + assert def_extras["blah"] == "from_defaults" + assert "my_var" and "site" in grp_data + assert "www_server" and "role" in host_data + + def test_get_defaults_dict(self): + inv = deserializer.Inventory.deserialize(**inv_dict) + defaults_dict = inv.get_defaults_dict() + con_options = defaults_dict["connection_options"]["dummy"] + assert type(defaults_dict) == dict + assert defaults_dict["username"] == "root" + assert con_options["hostname"] == "dummy_from_defaults" + assert "blah" in con_options["extras"] + + def test_get_groups_dict(self): + inv = deserializer.Inventory.deserialize(**inv_dict) + groups_dict = inv.get_groups_dict() + assert type(groups_dict) == dict + assert groups_dict["group_1"]["password"] == "from_group1" + assert groups_dict["group_2"]["data"]["site"] == "site2" + + def test_get_hosts_dict(self): + inv = deserializer.Inventory.deserialize(**inv_dict) + hosts_dict = inv.get_hosts_dict() + dev1_groups = hosts_dict["dev1.group_1"]["groups"] + dev2_paramiko_opts = hosts_dict["dev2.group_1"]["connection_options"][ + "paramiko" + ] + assert type(hosts_dict) == dict + assert "group_1" in dev1_groups + assert dev2_paramiko_opts["username"] == "root" + assert "dev3.group_2" in hosts_dict