diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..58383906 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +# This is an example .flake8 config, used when developing *Black* itself. +# Keep in sync with setup.cfg which is used for source packages. + +[flake8] +ignore = E266, E501, W503 +max-line-length = 80 +max-complexity = 15 +select = B,C,E,F,W,T4,B9 diff --git a/.gitignore b/.gitignore index 8e0b0df3..209ded0e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +.pytest_cache/ # C extensions *.so @@ -51,6 +52,7 @@ coverage.xml # Django stuff: *.log +*.log.* local_settings.py # Flask stuff: @@ -62,7 +64,7 @@ instance/ # Sphinx documentation docs/_build/ -docs/ref/configuration/generated/*.rst +docs/configuration/generated/*.rst # PyBuilder target/ @@ -97,3 +99,10 @@ tags output/ .DS_Store + +.pytest_cache/ + +.vscode + +# Generated when testing +nsot-docker.sqlite3 diff --git a/.travis.yml b/.travis.yml index 4c2dfcd8..00110003 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,17 @@ python: - 3.5 - 3.6 +matrix: + include: + - python: 3.6 + env: TOXENV=sphinx + - python: 3.6 + env: TOXENV=black + - python: 3.6 + env: TOXENV=pylama + install: - - pip install tox-travis - - pip install coveralls + - pip install tox tox-travis coveralls script: - tox @@ -25,7 +33,10 @@ deploy: provider: pypi user: dbarroso password: - secure: Zq2sLJ1yQzir4rlX9oopchnNqTNkAsbSOApWWPFYZwnLK8swvgMd95PDjLvY07Wzf4NBo1qy+cA1NwXIcAAd2HFr7hmnpj4ZMn2VxJBeznMW9h8OpGRusZs94ilK10A1CEcH77vQsRJYjycLV0hrslbXWfkJh88RJoeJYG4GLML9sDdxSf24ei5+vtA9e/eIQuP/rcoaq3g8CYiliiD6sU7fuIC6zGxzRWs2TSTP9mfvLZommOgVMqWQ9YshB06LtF+pn/UbjFjKnB8gMkSISW72jj9aL/D9Z3/68xPgFovbOmumL91XRnK6/89EwSULV7YzgbLv8PA95eevSuhiqT7Kk1UIHgKrE0v9tv2CJN39h4U74KaTvrssipJYvbGTwaYHScAOpRn4yk1H3Ipd/B6gVfj22ygw76+6yj2pj6U6QLh0JIU83oB5r92xZCZqrwn7/4rLpiLT3TF8IUi1WasTTHLfN7Je+l4InZ7myWXmxq8qqICappgyuFs+IA8d4iN8hAakAuShn2EB3KdnktPPjsd+qJajuMKwTFrogcWPNdALNCTS72W7x3+9P+d2MllEI2G2CF79O69DXSGzXqLrXuvmy0Nf9+q1ozXk1tYww13eAJO93Uzcm3WdTZFGBt8ENpSRIRBtXynkJriJ5PYEYxORzI7uR9A2Oi0UPhk= + secure: N8oS4N3BG8eMTXOxbGvCcZi5c/DSDml4qxux3OVZR2BvFp9X8yiC/9LgpTswp30cDZNgQqrd1cgHnm742hfKUMbcEpRergk65P+5tHcawUwS+jZ47PJJb84KQkne6i63vP5hGFGuN75cPk4U+Lv/q9MfTcWeaVqZfnjmMxwo90o/AqLIvPsduj8wP0wCSbO5DdaR5paTJOmHcTPksVDMyHaxbj5lDYx4kCS04jm4BHWRTBVP6ayyYdmgLDgcHdd54Xphk43EuI3vOMnB0TjU6GmwtsSkXGzsSpws+UNj/gAMZWkUpd5gwPgUThYWOtCiwPSism0DCCYQ9M9GH45210k+o+OgqEIU/HBxVve1rOYaLuApuOGO2gwpwM57WRv0AbnzQbgUVEDbC3XkOFnqxl2sD8ItvdpSTbI24pPj4CY5CdQkAjajorYIqZpmHMo9NVet9yPSTgtNVUqY7NNVIoEhwQzwqr/PtW+1o0f2+EVLyuYxR4Mvtx0B5Nuisrx1fwu0ZaCcpUTItXNkunSXsGmSk1THw06ub+/NgK8AqsKsOtGAwF37Uwgi92JkUmI4LA3UiO28Htu54q3IplhMaaxiMkTUNCj8BVBtykIK956TmDEjp0rJZTX+/64NN+68YYejFwW/Bkjkpra6LOK0AmM1sVoDBm9mNDIHMjiVIyw= on: tags: true branch: master + +after_success: + - coveralls diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..a4d9db54 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,96 @@ +How to contribute to Brigade +============================ + +First of all, thank you for conidering to contribute to this project! + +Several ways to contribute +-------------------------- + +There are several things you can do to help the project. + +- Spread the word about Brigade +- Suggest great features +- Report bugs +- Fix typos +- Write documentation +- Contribute your plugins +- Improve the Brigade core + +Spread the word about Brigade +----------------------------- + +Even if you aren't in the position that you can contribute your time to this project, it still helps us if you spread the word about the project. It could be just a short notice in social media or a discussion you have with your friends. As more people become aware of the project there's a better chance that we reach people who are able to contribute. So, even if you can't directly contribute yourself, someone you refer to us might. + +Suggesting new features +----------------------- + +It could be that you are aware of something that would be great to have in Brigade and we are always welcoming feature requests. Make sure you explain in what scenario your suggested feature would be useful. + +Reporting bugs +-------------- + +When you are `reporting bugs `_, make sure that you give a explaination about the outcome that you expect and what you are seeing. The bugs which are hardest to fix are the ones which we are unable to reproduce. For this reason it's important that you describe what you did and show us how we can reproduce the bug in another environment. + +Fix typos +--------- + +While we try to take care, getting all the works correct can be.. differcult. Typos are the easiest things to fix and if you find any you can help us from looking silly. You can find more typos to fix by looking in the `Brigade source code `_ or by visiting the `Brigade documentation `_. + +Writing documentation +--------------------- + +Documentation is another great way to help if you don't want to contribute actual code. The documentation of Brigade is divided into different sections. + +- Tutorials: Aims to help people learn Brigade with a lot of handholding, the user might not end up with something useful after following the tutorial. The goal is for people to learn how to use Brigade. +- How-to guides: This sections goal is to help people solve a specific task with Brigade +- Reference guides: This section describe the Brigade API and plugins. Most of the content in this area is generated from the source code itself. + +Contributions to the documentation can be small fixes such as changing scentences to make the text more clear, or it could be new guides. + +Contributing plugins +-------------------- + +If you have written your custom plugin for Brigade there's a good chance that it can be useful for others as well. General guidelines when writing plugins are: + +- Make them as generic as possible, it doesn't help others if they only work in your environment +- Make sure that it's possible to have unit tests which automatically test that the plugins are working + + +Contributing to the Brigade core +-------------------------------- + +When you are contributing code to the core of Brigade make sure that the existing tests are passing, and add tests to the code you have added. Having your tests in place ensures that other won't accidentally brake it in the future. + +Before you make any significant code changes to the core it's recommended that you open an issue to discuss your ideas before writing the code. + +Setting up your environment +--------------------------- + +In order to run tests locally you need to have `Docker `_ and `Pandoc `_ installed. Docker is used to test the Brigade plugins and Pandoc is required for building the documentation provided by `Sphinx `_. After those are installed you can go ahead and install the needed Python dependencies. + +.. code-block:: bash + + pip -r requirements-dev.txt + +Running tests +------------- + +While the automated tests will be triggered when you submit a new pull request it can still save you time to run the tests locally first. + +.. code-block:: bash + + make tests + +The test above will run the tests against the Brigade code and documentation. + + +Coding style +------------ + +Brigade uses `Black `_, the the uncompromising Python code formatter. Black makes it easy for you to format your code as you can do so automatically after installing it. Note that Python 3.6 is required to run Black. + +.. code-block:: bash + + make format + +The Black GitHub repo has information about how you can integrate Black in your editor. diff --git a/MANIFEST.in b/MANIFEST.in index 607da8f9..c1ffe8e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include requirements* +include README.md \ No newline at end of file diff --git a/Makefile b/Makefile index 53536625..493bf02b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,17 @@ + +.PHONY: format +format: + black . + .PHONY: start_nsot start_nsot: - docker run -v $(PWD)/tests/inventory_data/nsot/nsot.sqlite3:/nsot.sqlite3 -p 8990:8990 -d --name=nsot nsot/nsot start --noinput + cp $(PWD)/tests/inventory_data/nsot/nsot.sqlite3 $(PWD)/tests/inventory_data/nsot/nsot-docker.sqlite3 + docker run -v $(PWD)/tests/inventory_data/nsot/nsot-docker.sqlite3:/nsot.sqlite3 -p 8990:8990 -d --name=nsot nsot/nsot start --noinput .PHONY: stop_nsot stop_nsot: docker rm -f nsot + +.PHONY: tests +tests: + tox diff --git a/README.md b/README.md index 72344946..5ad6d1df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,48 @@ +[![Build Status](https://travis-ci.org/brigade-automation/brigade.svg?branch=develop)](https://travis-ci.org/brigade-automation/brigade) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![Coverage Status](https://coveralls.io/repos/github/brigade-automation/brigade/badge.svg?branch=develop)](https://coveralls.io/github/brigade-automation/brigade?branch=develop) + + Brigade ======= +Brigade is a pure Python automation framework intented to be used directly from Python. While most automation frameworks use their own DSL which you use to describe what you want to have done, Brigade lets you control everything from Python. + +One of the benefits we want to highlight with this approach is the ease of troubleshooting, if something goes wrong you can just use your existing debug tools directly from Python (just add a line of `import pdb` & `pdb.set_trace()` and you're good to go). Doing the same using a DSL can be quite time consuming. + +What Brigade brings to the table is that it takes care of dealing with your inventory and manages the job of dispatching the tasks you want to run against your nodes and devices. The framework provides a very simple way to write plugins if you aren't happy with the ones we ship. Of course if you have written a plugin you think can be useful to others, please send us your code and test cases as a [pull request](https://github.com/brigade-automation/brigade/pulls). + + +Install +======= + +While Brigade still supports Python 2.7 the recommended version is 3.6. Install it with pip. + +``` +pip install brigade +``` + +Documentation +============= + +Read the [Brigade documentation](https://brigade.readthedocs.io/) online or review it's [code here](https://github.com/brigade-automation/brigade/tree/develop/docs) + +Examples +======== + +You can find some examples and already made tools [here](https://github.com/brigade-automation/brg-tools/) + + +Bugs & New features +=================== + +If you think you have bug or would like to request a new feature, please register a GitHub account and [open an issue](https://github.com/brigade-automation/brigade/issues). + + +Contact & Support +================= + +While we are happy to help, the [GitHub issues]() are intended for bugs and discussions about new features. If are struggling to get something to work but don't believe its due to a bug in Brigade, the place to ask questions is in the #brigade channel in the [networktoCode Slack team](https://networktocode.herokuapp.com/). + + +Contributing to Brigade +======================= -See [docs](https://brigade.readthedocs.io). +If you want to help the project, the [Contribution Guidelines](https://brigade.readthedocs.io/en/develop/contributing/index.html) is the best place to start. diff --git a/brigade/__init__.py b/brigade/__init__.py index 4e5de560..7adb471d 100644 --- a/brigade/__init__.py +++ b/brigade/__init__.py @@ -1,6 +1,6 @@ import pkg_resources try: - __version__ = pkg_resources.get_distribution('brigade').version + __version__ = pkg_resources.get_distribution("brigade").version except pkg_resources.DistributionNotFound: __version__ = "Not installed" diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index a02ea1ce..a78de50f 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -1,11 +1,10 @@ import logging import logging.config import sys -import traceback from multiprocessing.dummy import Pool from brigade.core.configuration import Config -from brigade.core.task import AggregatedResult, Result, Task +from brigade.core.task import AggregatedResult, Task from brigade.plugins.tasks import connections @@ -15,6 +14,7 @@ # multithreading requires objects passed around to be pickable # following methods allow py2 to know how to pickle methods + def _pickle_method(method): func_name = method.im_func.__name__ obj = method.im_self @@ -29,6 +29,7 @@ def _unpickle_method(func_name, obj, cls): pass else: break + return func.__get__(obj, cls) copy_reg.pickle(types.MethodType, _pickle_method, _unpickle_method) @@ -46,6 +47,14 @@ class Data(object): def __init__(self): self.failed_hosts = set() + def recover_host(self, host): + """Remove ``host`` from list of failed hosts.""" + self.failed_hosts.discard(host) + + def reset_failed_hosts(self): + """Reset failed hosts and make all hosts available for future tasks.""" + self.failed_hosts = set() + class Brigade(object): """ @@ -69,15 +78,23 @@ class Brigade(object): available_connections (``dict``): dict of connection types are available """ - def __init__(self, inventory, dry_run, config=None, config_file=None, - available_connections=None, logger=None, data=None): + def __init__( + self, + inventory, + dry_run, + config=None, + config_file=None, + available_connections=None, + logger=None, + data=None, + ): self.logger = logger or logging.getLogger("brigade") self.data = data or Data() self.inventory = inventory self.inventory.brigade = self + self.data.dry_run = dry_run - self.dry_run = dry_run if config_file: self.config = Config(config_file=config_file) else: @@ -90,38 +107,53 @@ def __init__(self, inventory, dry_run, config=None, config_file=None, else: self.available_connections = connections.available_connections + @property + def dry_run(self): + return self.data.dry_run + def configure_logging(self): - format = "%(asctime)s - %(name)s - %(levelname)s" - format += " - %(funcName)10s() - %(message)s" - logging.config.dictConfig({ + dictConfig = self.config.logging_dictConfig or { "version": 1, "disable_existing_loggers": False, - "formatters": { - "simple": {"format": format} - }, - "handlers": { - "info_file_handler": { - "class": "logging.handlers.RotatingFileHandler", - "level": "INFO", - "formatter": "simple", - "filename": "brigade.log", - "maxBytes": 10485760, - "backupCount": 20, - "encoding": "utf8" - }, - }, - "loggers": { - "brigade": { - "level": "INFO", - "handlers": ["info_file_handler"], - "propagate": "no" - }, - }, + "formatters": {"simple": {"format": self.config.logging_format}}, + "handlers": {}, + "loggers": {}, "root": { - "level": "ERROR", - "handlers": ["info_file_handler"] + "level": "CRITICAL" if self.config.logging_loggers else self.config.logging_level.upper(), # noqa + "handlers": [], + "formatter": "simple", + }, + } + handlers_list = [] + if self.config.logging_file: + dictConfig["root"]["handlers"].append("info_file_handler") + handlers_list.append("info_file_handler") + dictConfig["handlers"]["info_file_handler"] = { + "class": "logging.handlers.RotatingFileHandler", + "level": "NOTSET", + "formatter": "simple", + "filename": self.config.logging_file, + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8", + } + if self.config.logging_to_console: + dictConfig["root"]["handlers"].append("info_console") + handlers_list.append("info_console") + dictConfig["handlers"]["info_console"] = { + "class": "logging.StreamHandler", + "level": "NOTSET", + "formatter": "simple", + "stream": "ext://sys.stdout", + } + + for logger in self.config.logging_loggers: + dictConfig["loggers"][logger] = { + "level": self.config.logging_level.upper(), "handlers": handlers_list } - }) + + if dictConfig["root"]["handlers"]: + logging.config.dictConfig(dictConfig) def filter(self, **kwargs): """ @@ -130,23 +162,23 @@ def filter(self, **kwargs): Returns: :obj:`Brigade`: A new object with same configuration as ``self`` but filtered inventory. """ - b = Brigade(**self.__dict__) + b = Brigade(dry_run=self.dry_run, **self.__dict__) b.inventory = self.inventory.filter(**kwargs) return b - def _run_serial(self, task, dry_run, **kwargs): + def _run_serial(self, task, hosts, **kwargs): result = AggregatedResult(kwargs.get("name") or task.__name__) - for host in self.inventory.hosts.values(): - result[host.name] = run_task(host, self, dry_run, Task(task, **kwargs)) + for host in hosts: + result[host.name] = Task(task, **kwargs).start(host, self) return result - def _run_parallel(self, task, num_workers, dry_run, **kwargs): + def _run_parallel(self, task, hosts, num_workers, **kwargs): result = AggregatedResult(kwargs.get("name") or task.__name__) pool = Pool(processes=num_workers) - result_pool = [pool.apply_async(run_task, - args=(h, self, dry_run, Task(task, **kwargs))) - for h in self.inventory.hosts.values()] + result_pool = [ + pool.apply_async(Task(task, **kwargs).start, args=(h, self)) for h in hosts + ] pool.close() pool.join() @@ -155,7 +187,15 @@ def _run_parallel(self, task, num_workers, dry_run, **kwargs): result[r.host.name] = r return result - def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwargs): + def run( + self, + task, + num_workers=None, + raise_on_error=None, + on_good=True, + on_failed=False, + **kwargs + ): """ Run task over all the hosts in the inventory. @@ -163,8 +203,9 @@ def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwarg task (``callable``): function or callable that will be run against each device in the inventory num_workers(``int``): Override for how many hosts to run in paralell for this task - dry_run(``bool``): Whether if we are testing the changes or not raise_on_error (``bool``): Override raise_on_error behavior + on_good(``bool``): Whether to run or not this task on hosts marked as good + on_failed(``bool``): Whether to run or not this task on hosts marked as failed **kwargs: additional argument to pass to ``task`` when calling it Raises: @@ -176,17 +217,29 @@ def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwarg """ num_workers = num_workers or self.config.num_workers - self.logger.info("Running task '{}' with num_workers: {}, dry_run: {}".format( - kwargs.get("name") or task.__name__, num_workers, dry_run)) + run_on = [] + if on_good: + for name, host in self.inventory.hosts.items(): + if name not in self.data.failed_hosts: + run_on.append(host) + if on_failed: + for name, host in self.inventory.hosts.items(): + 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 + ) + ) self.logger.debug(kwargs) if num_workers == 1: - result = self._run_serial(task, dry_run, **kwargs) + result = self._run_serial(task, run_on, **kwargs) else: - result = self._run_parallel(task, num_workers, dry_run, **kwargs) + result = self._run_parallel(task, run_on, num_workers, **kwargs) - raise_on_error = raise_on_error if raise_on_error is not None else \ - self.config.raise_on_error + raise_on_error = raise_on_error if raise_on_error is not None else self.config.raise_on_error # noqa if raise_on_error: result.raise_on_error() else: @@ -194,15 +247,22 @@ def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwarg return result -def run_task(host, brigade, dry_run, task): - logger = logging.getLogger("brigade") - try: - logger.info("{}: {}: running task".format(host.name, task.name)) - r = task._start(host=host, brigade=brigade, dry_run=dry_run) - except Exception as e: - tb = traceback.format_exc() - logger.error("{}: {}".format(host, tb)) - r = Result(host, exception=e, result=tb, failed=True) - task.results.append(r) - r.name = task.name - return task.results +def InitBrigade(config_file="", dry_run=False, **kwargs): + """ + Arguments: + config_file(str): Path to the configuration file (optional) + dry_run(bool): Whether to simulate changes or not + **kwargs: Extra information to pass to the + :obj:`brigade.core.configuration.Config` object + + Returns: + :obj:`brigade.core.Brigade`: fully instantiated and configured + """ + conf = Config(config_file=config_file, **kwargs) + + inv_class = conf.inventory + inv_args = getattr(conf, inv_class.__name__, {}) + transform_function = conf.transform_function + inv = inv_class(transform_function=transform_function, **inv_args) + + return Brigade(inventory=inv, dry_run=dry_run, config=conf) diff --git a/brigade/core/configuration.py b/brigade/core/configuration.py index 61d8c042..f296e978 100644 --- a/brigade/core/configuration.py +++ b/brigade/core/configuration.py @@ -1,4 +1,4 @@ -import ast +import importlib import os @@ -6,31 +6,80 @@ CONF = { - 'num_workers': { - 'description': 'Number of Brigade worker processes that are run at the same time, ' - 'configuration can be overridden on individual tasks by using the ' - '`num_workers` argument to (:obj:`brigade.core.Brigade.run`)', - 'type': 'int', - 'default': 20, + "inventory": { + "description": "Path to inventory modules.", + "type": "str", + "default": "brigade.plugins.inventory.simple.SimpleInventory", }, - 'raise_on_error': { - 'description': "If set to ``True``, (:obj:`brigade.core.Brigade.run`) method of will raise " - "an exception if at least a host failed.", - 'type': 'bool', - 'default': True, + "transform_function": { + "description": "Path to transform function. The transform_function you provide " + "will run against each host in the inventory. This is useful to manipulate host " + "data and make it more consumable. For instance, if your inventory has a 'user' " + "attribute you could use this function to map it to 'brigade_user'", + "type": "str", + "default": {}, }, - 'ssh_config_file': { - 'description': 'User ssh_config_file', - 'type': 'str', - 'default': os.path.join(os.path.expanduser("~"), ".ssh", "config"), - 'default_doc': '~/.ssh/config' + "jinja_filters": { + "description": "Path to callable returning jinja filters to be used.", + "type": "str", + "default": {}, + }, + "num_workers": { + "description": "Number of Brigade worker processes that are run at the same time, " + "configuration can be overridden on individual tasks by using the " + "`num_workers` argument to (:obj:`brigade.core.Brigade.run`)", + "type": "int", + "default": 20, + }, + "raise_on_error": { + "description": "If set to ``True``, (:obj:`brigade.core.Brigade.run`) method of will raise " + "an exception if at least a host failed.", + "type": "bool", + "default": False, + }, + "ssh_config_file": { + "description": "User ssh_config_file", + "type": "str", + "default": os.path.join(os.path.expanduser("~"), ".ssh", "config"), + "default_doc": "~/.ssh/config", + }, + "logging_dictConfig": { + "description": "Configuration dictionary schema supported by the logging subsystem. " + "Overrides rest of logging_* parameters.", + "type": "dict", + "default": {}, + }, + "logging_level": { + "description": "Logging level. Can be any supported level by the logging subsystem", + "type": "str", + "default": "debug", + }, + "logging_file": { + "description": "Logging file. Empty string disables logging to file.", + "type": "str", + "default": "brigade.log", + }, + "logging_format": { + "description": "Logging format", + "type": "str", + "default": "%(asctime)s - %(name)12s - %(levelname)8s - %(funcName)10s() - %(message)s", + }, + "logging_to_console": { + "description": "Whether to log to stdout or not.", + "type": "bool", + "default": False, + }, + "logging_loggers": { + "description": "List of loggers to configure. This allows you to enable logging for " + "multiple loggers. For instance, you could enable logging for both brigade " + "and paramiko or just for paramiko. An empty list will enable logging for " + "all loggers.", + "type": "list", + "default": ["brigade"], }, } -types = { - 'int': int, - 'str': str, -} +types = {"int": int, "str": str} class Config: @@ -42,26 +91,90 @@ class Config: """ def __init__(self, config_file=None, **kwargs): - if config_file: - with open(config_file, 'r') as f: - c = yaml.load(f.read()) + with open(config_file, "r") as f: + data = yaml.load(f.read()) or {} else: - c = {} + data = {} + + for parameter, param_conf in CONF.items(): + self._assign_property(parameter, param_conf, data) - self._assign_properties(c) + for k, v in data.items(): + if k not in CONF: + setattr(self, k, v) for k, v in kwargs.items(): setattr(self, k, v) - def _assign_properties(self, c): + resolve_imports = ["inventory", "transform_function", "jinja_filters"] + for r in resolve_imports: + obj = self._resolve_import_from_string(kwargs.get(r, getattr(self, r))) + setattr(self, r, obj) - for p in CONF: - env = CONF[p].get('env') or 'BRIGADE_' + p.upper() - v = os.environ.get(env) or c.get(p) - v = v if v is not None else CONF[p]['default'] - if CONF[p]['type'] == 'bool': - v = ast.literal_eval(str(v).title()) + callable = ["jinja_filters"] + for c in callable: + func = getattr(self, c) + if func: + setattr(self, c, func()) + + def string_to_bool(self, v): + if v.lower() in ["false", "no", "n", "off", "0"]: + return False + + else: + return True + + def _assign_property(self, parameter, param_conf, data): + v = None + if param_conf["type"] in ("bool", "int", "str"): + env = param_conf.get("env") or "BRIGADE_" + parameter.upper() + v = os.environ.get(env) + if v is None: + v = data.get(parameter, param_conf["default"]) + else: + if param_conf["type"] == "bool": + v = self.string_to_bool(v) + else: + v = types[param_conf["type"]](v) + setattr(self, parameter, v) + + def get(self, parameter, env=None, default=None, parameter_type="str", root=""): + """ + Retrieve a custom parameter from the configuration. + + Arguments: + parameter(str): Name of the parameter to retrieve + env(str): Environment variable name to retrieve the object from + default: default value in case no parameter is found + parameter_type(str): if a value is found cast the variable to this type + root(str): parent key in the configuration file where to look for the parameter + """ + value = os.environ.get(env) if env else None + if value is None: + if root: + d = getattr(self, root, {}) + value = d.get(parameter, default) else: - v = types[CONF[p]['type']](v) - setattr(self, p, v) + value = getattr(self, parameter, default) + if parameter_type in [bool, "bool"]: + if not isinstance(value, bool): + value = self.string_to_bool(value) + else: + value = types[str(parameter_type)](value) + return value + + def _resolve_import_from_string(self, import_path): + """ + Resolves import from a string. Checks if callable or path is given. + + Arguments: + import_path(str): path of the import + """ + if not import_path or callable(import_path): + return import_path + + module_name = ".".join(import_path.split(".")[:-1]) + obj_name = import_path.split(".")[-1] + module = importlib.import_module(module_name) + return getattr(module, obj_name) diff --git a/brigade/core/exceptions.py b/brigade/core/exceptions.py index e19e2532..af7de6d6 100644 --- a/brigade/core/exceptions.py +++ b/brigade/core/exceptions.py @@ -30,6 +30,7 @@ class BrigadeExecutionError(Exception): Attributes: result (:obj:`brigade.core.task.AggregatedResult`): """ + def __init__(self, result): self.result = result @@ -46,5 +47,28 @@ def __str__(self): else: text += "# {} (succeeded)\n".format(k) text += "{}\n".format("#" * 40) - text += "{}\n".format(r.result) + for sub_r in r: + text += "**** {}\n".format(sub_r.name) + text += "{}\n".format(sub_r) return text + + +class BrigadeSubTaskError(Exception): + """ + Raised by brigade when a sub task managed by :meth:`brigade.core.Task.run` + has failed + + Arguments: + task (:obj:`brigade.core.task.Task`): The subtask that failed + result (:obj:`brigade.core.task.Result`): The result of the failed task + Attributes: + task (:obj:`brigade.core.task.Task`): The subtask that failed + result (:obj:`brigade.core.task.Result`): The result of the failed task + """ + + def __init__(self, task, result): + self.task = task + self.result = result + + def __str__(self): + return "Subtask: {} (failed)\n".format(self.task) diff --git a/brigade/core/helpers/__init__.py b/brigade/core/helpers/__init__.py index 67514857..fdc6c302 100644 --- a/brigade/core/helpers/__init__.py +++ b/brigade/core/helpers/__init__.py @@ -5,8 +5,3 @@ def merge_two_dicts(x, y): z = dict(x) z.update(y) return z - - -def format_string(text, task, **kwargs): - return text.format(host=task.host, - **merge_two_dicts(task.host.items(), kwargs)) diff --git a/brigade/core/helpers/jinja_helper.py b/brigade/core/helpers/jinja_helper.py index c584066d..e6f9ee26 100644 --- a/brigade/core/helpers/jinja_helper.py +++ b/brigade/core/helpers/jinja_helper.py @@ -1,17 +1,19 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined -def render_from_file(path, template, **kwargs): - env = Environment(loader=FileSystemLoader(path), - undefined=StrictUndefined, - trim_blocks=True) - # env.filters.update(jinja_filters.filters()) +def render_from_file(path, template, jinja_filters=None, **kwargs): + jinja_filters = jinja_filters or {} + env = Environment( + loader=FileSystemLoader(path), undefined=StrictUndefined, trim_blocks=True + ) + env.filters.update(jinja_filters) template = env.get_template(template) return template.render(**kwargs) -def render_from_string(template, **kwargs): - env = Environment(undefined=StrictUndefined, - trim_blocks=True) +def render_from_string(template, jinja_filters=None, **kwargs): + jinja_filters = jinja_filters or {} + env = Environment(undefined=StrictUndefined, trim_blocks=True) + env.filters.update(jinja_filters) template = env.from_string(template) return template.render(**kwargs) diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index da7af8fe..fcfcf227 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -1,7 +1,5 @@ import getpass -from brigade.core import helpers - class Host(object): """ @@ -15,7 +13,8 @@ class Host(object): Attributes: name (str): Name of the host - group (:obj:`Group`): Group the host belongs to + groups (list of :obj:`Group`): Groups the host belongs to + defaults (``dict``): Default values for host/group data data (dict): data about the device connections (``dict``): Already established connections @@ -36,13 +35,13 @@ class Host(object): # hosts my_host: ip: 1.2.3.4 - group: bma + groups: [bma] --- # groups bma: site: bma - group: all + group: [all] all: domain: acme.com @@ -57,25 +56,40 @@ class Host(object): * ``my_host.group.group.data["domain"]`` will return ``acme.com`` """ - def __init__(self, name, group=None, brigade=None, **kwargs): + def __init__(self, name, groups=None, brigade=None, defaults=None, **kwargs): self.brigade = brigade self.name = name - self.group = group + self.groups = groups or [] self.data = {} self.data["name"] = name self.connections = {} + self.defaults = defaults or {} - if isinstance(group, str): - self.data["group"] = group - else: - self.data["group"] = group.name if group else None + if len(self.groups): + if isinstance(groups[0], str): + self.data["groups"] = groups + else: + self.data["groups"] = [g.name for g in groups] for k, v in kwargs.items(): self.data[k] = v def _resolve_data(self): - d = self.group if self.group else {} - return helpers.merge_two_dicts(d, self.data) + processed = [] + result = {} + for k, v in self.data.items(): + processed.append(k) + result[k] = v + for g in self.groups: + for k, v in g.items(): + if k not in processed: + processed.append(k) + result[k] = v + for k, v in self.defaults.items(): + if k not in processed: + processed.append(k) + result[k] = v + return result def keys(self): """Returns the keys of the attribute ``data`` and of the parent(s) groups.""" @@ -85,12 +99,35 @@ def values(self): """Returns the values of the attribute ``data`` and of the parent(s) groups.""" return self._resolve_data().values() + def items(self): + """ + Returns all the data accessible from a device, including + the one inherited from parent groups + """ + return self._resolve_data().items() + + def has_parent_group(self, group): + """Retuns whether the object is a child of the :obj:`Group` ``group``""" + for g in self.groups: + if g is group or g.has_parent_group(group): + return True + + return False + def __getitem__(self, item): try: return self.data[item] + except KeyError: - if self.group: - return self.group[item] + for g in self.groups: + r = g.get(item) + if r: + return r + + r = self.defaults.get(item) + if r: + return r + raise def __setitem__(self, item, value): @@ -118,16 +155,10 @@ def get(self, item, default=None): """ try: return self.__getitem__(item) + except KeyError: return default - def items(self): - """ - Returns all the data accessible from a device, including - the one inherited from parent groups - """ - return self._resolve_data().items() - @property def brigade(self): """Reference to the parent :obj:`brigade.core.Brigade` object""" @@ -201,8 +232,10 @@ def get_connection(self, connection): try: conn_task = self.brigade.available_connections[connection] except KeyError: - raise AttributeError("not sure how to establish a connection for {}".format( - connection)) + raise AttributeError( + "not sure how to establish a connection for {}".format(connection) + ) + # We use `filter(name=self.name)` to call the connection task for only # the given host. We also have to set `num_workers=1` because chances are # we are already inside a thread @@ -210,12 +243,19 @@ def get_connection(self, connection): r = self.brigade.filter(name=self.name).run(conn_task, num_workers=1) if r[self.name].exception: raise r[self.name].exception + return self.connections[connection] class Group(Host): """Same as :obj:`Host`""" - pass + + def children(self): + return { + n: h + for n, h in self.brigade.inventory.hosts.items() + if h.has_parent_group(self) + } class Inventory(object): @@ -236,32 +276,42 @@ class Inventory(object): groups (dict): keys are group names and the values are :obj:`Group`. """ - def __init__(self, hosts, groups=None, transform_function=None, brigade=None): + def __init__( + self, hosts, groups=None, defaults=None, transform_function=None, brigade=None + ): self._brigade = brigade - groups = groups or {} - self.groups = {} - for n, g in groups.items(): + self.defaults = defaults or {} + + self.groups = groups or {} + for n, g in self.groups.items(): if isinstance(g, dict): g = Group(name=n, brigade=brigade, **g) self.groups[n] = g - for g in self.groups.values(): - if g.group is not None and not isinstance(g.group, Group): - g.group = self.groups[g.group] + for group in self.groups.values(): + group.groups = self._resolve_groups(group.groups) self.hosts = {} for n, h in hosts.items(): if isinstance(h, dict): - h = Host(name=n, brigade=brigade, **h) + h = Host(name=n, brigade=brigade, defaults=self.defaults, **h) if transform_function: transform_function(h) - if h.group is not None and not isinstance(h.group, Group): - h.group = self.groups[h.group] + h.groups = self._resolve_groups(h.groups) self.hosts[n] = h + def _resolve_groups(self, groups): + r = [] + if len(groups): + if not isinstance(groups[0], Group): + r = [self.groups[g] for g in groups] + else: + r = groups + return r + def filter(self, filter_func=None, **kwargs): """ Returns a new inventory after filtering the hosts by matching the data passed to the @@ -286,11 +336,13 @@ def filter(self, filter_func=None, **kwargs): device. If the call returns ``True`` the device will be kept in the inventory """ if filter_func: - filtered = {n: h for n, h in self.hosts.items() - if filter_func(h, **kwargs)} + filtered = {n: h for n, h in self.hosts.items() if filter_func(h, **kwargs)} else: - filtered = {n: h for n, h in self.hosts.items() - if all(h.get(k) == v for k, v in kwargs.items())} + filtered = { + n: h + for n, h in self.hosts.items() + if all(h.get(k) == v for k, v in kwargs.items()) + } return Inventory(hosts=filtered, groups=self.groups, brigade=self.brigade) def __len__(self): diff --git a/brigade/core/task.py b/brigade/core/task.py index baa49813..61265ce0 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -1,6 +1,9 @@ +import logging +import traceback from builtins import super from brigade.core.exceptions import BrigadeExecutionError +from brigade.core.exceptions import BrigadeSubTaskError class Task(object): @@ -12,49 +15,71 @@ class Task(object): Arguments: task (callable): function or callable we will be calling name (``string``): name of task, defaults to ``task.__name__`` - skipped (``bool``): whether to run hosts that should be skipped otherwise or not + severity_level (logging.LEVEL): Severity level associated to the task **kwargs: Parameters that will be passed to the ``task`` Attributes: task (callable): function or callable we will be calling name (``string``): name of task, defaults to ``task.__name__`` - skipped (``bool``): whether to run hosts that should be skipped otherwise or not params: Parameters that will be passed to the ``task``. self.results (:obj:`brigade.core.task.MultiResult`): Intermediate results host (:obj:`brigade.core.inventory.Host`): Host we are operating with. Populated right before calling the ``task`` brigade(:obj:`brigade.core.Brigade`): Populated right before calling the ``task`` - dry_run(``bool``): Populated right before calling the ``task`` + severity_level (logging.LEVEL): Severity level associated to the task """ - def __init__(self, task, name=None, skipped=False, **kwargs): + def __init__(self, task, name=None, severity_level=logging.INFO, **kwargs): self.name = name or task.__name__ self.task = task self.params = kwargs - self.skipped = skipped self.results = MultiResult(self.name) + self.severity_level = severity_level def __repr__(self): return self.name - def _start(self, host, brigade, dry_run, sub_task=False): - if host.name in brigade.data.failed_hosts and not self.skipped: - r = Result(host, skipped=True) - else: - self.host = host - self.brigade = brigade - self.dry_run = dry_run if dry_run is not None else brigade.dry_run - r = self.task(self, **self.params) or Result(host) + def start(self, host, brigade): + """ + Run the task for the given host. + + Arguments: + host (:obj:`brigade.core.inventory.Host`): Host we are operating with. Populated right + before calling the ``task`` + brigade(:obj:`brigade.core.Brigade`): Populated right before calling + the ``task`` + + Returns: + host (:obj:`brigade.core.task.MultiResult`): Results of the task and its subtasks + """ + self.host = host + self.brigade = brigade + + logger = logging.getLogger("brigade") + try: + logger.info("{}: {}: running task".format(self.host.name, self.name)) + r = self.task(self, **self.params) + if not isinstance(r, Result): + r = Result(host=host, result=r) + + except BrigadeSubTaskError as e: + tb = traceback.format_exc() + logger.error("{}: {}".format(self.host, 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)) + r = Result(host, exception=e, result=tb, failed=True) + r.name = self.name + r.severity_level = logging.ERROR if r.failed else self.severity_level - if sub_task: - return r - else: - self.results.insert(0, r) - return self.results + self.results.insert(0, r) + return self.results - def run(self, task, dry_run=None, **kwargs): + def run(self, task, **kwargs): """ This is a utility method to call a task from within a task. For instance: @@ -67,17 +92,33 @@ def grouped_tasks(task): This method will ensure the subtask is run only for the host in the current thread. """ if not self.host or not self.brigade: - msg = ("You have to call this after setting host and brigade attributes. ", - "You probably called this from outside a nested task") + msg = ( + "You have to call this after setting host and brigade attributes. ", + "You probably called this from outside a nested task", + ) raise Exception(msg) - r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True) - if isinstance(r, MultiResult): - self.results.extend(r) - else: - self.results.append(r) + if "severity_level" not in kwargs: + kwargs["severity_level"] = self.severity_level + task = Task(task, **kwargs) + r = task.start(self.host, self.brigade) + self.results.append(r[0] if len(r) == 1 else r) + + if r.failed: + # Without this we will keep running the grouped task + raise BrigadeSubTaskError(task=task, result=r) + return r + def is_dry_run(self, override=None): + """ + Returns whether current task is a dry_run or not. + + Arguments: + override (bool): Override for current task + """ + return override if override is not None else self.brigade.dry_run + class Result(object): """ @@ -89,8 +130,8 @@ class Result(object): result (obj): Result of the task execution, see task's documentation for details host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result failed (bool): Whether the execution failed or not + severity_level (logging.LEVEL): Severity level associated to the result of the excecution exception (Exception): uncaught exception thrown during the exection of the task (if any) - skipped (bool): ``True`` if the host was skipped Attributes: changed (bool): ``True`` if the task is changing the system @@ -98,35 +139,58 @@ class Result(object): result (obj): Result of the task execution, see task's documentation for details host (:obj:`brigade.core.inventory.Host`): Reference to the host that lead ot this result failed (bool): Whether the execution failed or not + severity_level (logging.LEVEL): Severity level associated to the result of the excecution exception (Exception): uncaught exception thrown during the exection of the task (if any) - skipped (bool): ``True`` if the host was skipped """ - def __init__(self, host, result=None, changed=False, diff="", failed=False, exception=None, - skipped=False, **kwargs): + def __init__( + self, + host, + result=None, + changed=False, + diff="", + failed=False, + exception=None, + severity_level=logging.INFO, + **kwargs + ): self.result = result self.host = host self.changed = changed self.diff = diff self.failed = failed self.exception = exception - self.skipped = skipped + self.name = None + self.severity_level = severity_level for k, v in kwargs.items(): setattr(self, k, v) + def __repr__(self): + return '{}: "{}"'.format(self.__class__.__name__, self.name) + + def __str__(self): + if self.exception: + return str(self.exception) + + else: + return str(self.result) + class AggregatedResult(dict): """ It basically is a dict-like object that aggregates the results for all devices. You can access each individual result by doing ``my_aggr_result["hostname_of_device"]``. """ + def __init__(self, name, **kwargs): self.name = name super().__init__(**kwargs) def __repr__(self): - return '{}: {}'.format(self.__class__.__name__, self.name) + return "{} ({}): {}".format( + self.__class__.__name__, self.name, super().__repr__() + ) @property def failed(self): @@ -138,11 +202,6 @@ def failed_hosts(self): """Hosts that failed during the execution of the task.""" return {h: r for h, r in self.items() if r.failed} - @property - def skipped(self): - """If ``True`` at least a host was skipped.""" - return any([h.skipped for h in self.values()]) - def raise_on_error(self): """ Raises: @@ -157,22 +216,21 @@ class MultiResult(list): It is basically is a list-like object that gives you access to the results of all subtasks for a particular device/task. """ + def __init__(self, name): self.name = name def __getattr__(self, name): return getattr(self[0], name) + def __repr__(self): + return "{}: {}".format(self.__class__.__name__, super().__repr__()) + @property def failed(self): """If ``True`` at least a task failed.""" return any([h.failed for h in self]) - @property - def skipped(self): - """If ``True`` at least a host was skipped.""" - return any([h.skipped for h in self]) - @property def changed(self): """If ``True`` at least a task changed the system.""" diff --git a/brigade/easy.py b/brigade/easy.py deleted file mode 100644 index 8726e9a5..00000000 --- a/brigade/easy.py +++ /dev/null @@ -1,23 +0,0 @@ -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory - - -def easy_brigade(host_file="host.yaml", group_file="groups.yaml", dry_run=False, **kwargs): - """ - Helper function to create easily a :obj:`brigade.core.Brigade` object. - - Arguments: - host_file (str): path to the host file that will be passed to - :obj:`brigade.plugins.inventory.simple.SimpleInventory` - group_file (str): path to the group file that will be passed to - :obj:`brigade.plugins.inventory.simple.SimpleInventory` - dry_run (bool): whether if this is a dry run or not - **kwargs: Configuration parameters, see - :doc:`configuration parameters ` - """ - return Brigade( - inventory=SimpleInventory(host_file, group_file), - dry_run=dry_run, - config=Config(**kwargs), - ) diff --git a/brigade/plugins/functions/text/__init__.py b/brigade/plugins/functions/text/__init__.py index 043171f0..e5635c7c 100644 --- a/brigade/plugins/functions/text/__init__.py +++ b/brigade/plugins/functions/text/__init__.py @@ -1,4 +1,12 @@ -from colorama import Fore, Style +import logging +import pprint + +from brigade.core.task import AggregatedResult, MultiResult, Result + +from colorama import Fore, Style, init + + +init(autoreset=True, convert=False, strip=False) def print_title(title): @@ -7,3 +15,84 @@ def print_title(title): """ msg = "**** {} ".format(title) print("{}{}{}{}".format(Style.BRIGHT, Fore.GREEN, msg, "*" * (80 - len(msg)))) + + +def _get_color(result, failed): + if result.failed or failed: + color = Fore.RED + elif result.changed: + color = Fore.YELLOW + else: + color = Fore.GREEN + return color + + +def _print_individual_result( + result, host, vars, failed, severity_level, task_group=False +): + if result.severity_level < severity_level: + return + + color = _get_color(result, failed) + subtitle = "" if result.changed is None else " ** changed : {} ".format( + result.changed + ) + level_name = logging.getLevelName(result.severity_level) + symbol = "v" if task_group else "-" + msg = "{} {}{}".format(symbol * 4, result.name, subtitle) + print( + "{}{}{}{} {}".format( + Style.BRIGHT, color, msg, symbol * (80 - len(msg)), level_name + ) + ) + for v in vars: + x = getattr(result, v, "") + if x and not isinstance(x, str): + pprint.pprint(x, indent=2) + elif x: + print(x) + + +def print_result( + result, host=None, vars=None, failed=None, severity_level=logging.INFO +): + """ + Prints the :obj:`brigade.core.task.Result` from a previous task to screen + + Arguments: + result (:obj:`brigade.core.task.Result`): from a previous task + vars (list of str): Which attributes you want to print + failed (``bool``): if ``True`` assume the task failed + severity_level (int): Print only errors with this severity level or higher + + Returns: + :obj:`brigade.core.task.Result`: + """ + + vars = vars or ["diff", "result", "stdout"] + if isinstance(vars, str): + vars = [vars] + + if isinstance(result, AggregatedResult): + msg = result.name + print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "*" * (80 - len(msg)))) + for host, host_data in sorted(result.items()): + title = "" if host_data.changed is None else " ** changed : {} ".format( + host_data.changed + ) + msg = "* {}{}".format(host, title) + print( + "{}{}{}{}".format(Style.BRIGHT, Fore.BLUE, msg, "*" * (80 - len(msg))) + ) + print_result(host_data, host, vars, failed, severity_level) + elif isinstance(result, MultiResult): + _print_individual_result( + result[0], host, vars, failed, severity_level, task_group=True + ) + for r in result[1:]: + print_result(r, host, vars, failed, severity_level) + color = _get_color(result[0], failed) + msg = "^^^^ END {} ".format(result[0].name) + print("{}{}{}{}".format(Style.BRIGHT, color, msg, "^" * (80 - len(msg)))) + elif isinstance(result, Result): + _print_individual_result(result, host, vars, failed, severity_level) diff --git a/brigade/plugins/inventory/ansible.py b/brigade/plugins/inventory/ansible.py new file mode 100644 index 00000000..8859c9b8 --- /dev/null +++ b/brigade/plugins/inventory/ansible.py @@ -0,0 +1,194 @@ +try: + import configparser as cp +except ImportError: + import ConfigParser as cp +import logging +import os +from builtins import super + +from brigade.core.inventory import Inventory + +import ruamel.yaml + + +logger = logging.getLogger("brigade") + + +class AnsibleParser(object): + + def __init__(self, hostsfile): + self.hostsfile = hostsfile + self.path = os.path.dirname(hostsfile) + self.hosts = {} + self.groups = {} + self.original_data = None + self.load_hosts_file() + + def parse_group(self, group, data, parent=None): + data = data or {} + if group == "defaults": + self.groups[group] = {} + group_file = "all" + else: + self.add(group, self.groups) + group_file = group + + if parent and parent != "defaults": + self.groups[group]["groups"].append(parent) + + self.groups[group].update(data.get("vars", {})) + self.groups[group].update(self.read_vars_file(group_file, self.path, False)) + self.groups[group] = self.map_brigade_vars(self.groups[group]) + + self.parse_hosts(data.get("hosts", {}), parent=group) + + for children, children_data in data.get("children", {}).items(): + self.parse_group(children, children_data, parent=group) + + def parse(self): + self.parse_group("defaults", self.original_data["all"]) + self.sort_groups() + + def parse_hosts(self, hosts, parent=None): + for host, data in hosts.items(): + data = data or {} + self.add(host, self.hosts) + if parent and parent != "defaults": + self.hosts[host]["groups"].append(parent) + self.hosts[host].update(data) + self.hosts[host].update(self.read_vars_file(host, self.path, True)) + self.hosts[host] = self.map_brigade_vars(self.hosts[host]) + + def sort_groups(self): + for host in self.hosts.values(): + host["groups"].sort() + + for name, group in self.groups.items(): + if name == "defaults": + continue + + group["groups"].sort() + + def read_vars_file(self, element, path, is_host=True): + subdir = "host_vars" if is_host else "group_vars" + filepath = os.path.join(path, subdir, element) + + if not os.path.exists(filepath): + logger.debug( + "AnsibleInventory: var file doesn't exist: {}".format(filepath) + ) + return {} + + with open(filepath, "r") as f: + logger.debug("AnsibleInventory: reading var file: {}".format(filepath)) + yml = ruamel.yaml.YAML(typ="rt", pure=True) + return yml.load(f) + + def map_brigade_vars(self, obj): + mappings = { + "ansible_host": "brigade_host", + "ansible_port": "brigade_ssh_port", + "ansible_user": "brigade_username", + "ansible_password": "brigade_password", + } + result = {} + for k, v in obj.items(): + if k in mappings: + result[mappings[k]] = v + else: + result[k] = v + return result + + def add(self, element, element_dict): + if element not in element_dict: + element_dict[element] = {"groups": []} + + +class INIParser(AnsibleParser): + + def normalize_content(self, content): + result = {} + + if not content: + return result + + for option in content.split(): + k, v = option.split("=") + try: + v = int(v) + except Exception: + pass + result[k] = v + + return result + + def normalize(self, data): + result = {} + for k, v in data.items(): + meta = None + if ":" in k: + k, meta = k.split(":") + if k not in result: + result[k] = {} + + if meta not in result[k]: + result[k][meta] = {} + + if meta == "vars": + for data, _ in v.items(): + result[k][meta].update(self.normalize_content(data)) + elif meta == "children": + result[k][meta] = {k: {} for k, _ in v.items()} + else: + result[k]["hosts"] = { + host: self.normalize_content(data) for host, data in v.items() + } + return result + + def load_hosts_file(self): + original_data = cp.ConfigParser( + interpolation=None, allow_no_value=True, delimiters=" " + ) + original_data.read(self.hostsfile) + data = self.normalize(original_data) + data.pop("DEFAULT") + if "all" not in data: + self.original_data = {"all": {"children": data}} + + +class YAMLParser(AnsibleParser): + + def load_hosts_file(self): + with open(self.hostsfile, "r") as f: + yml = ruamel.yaml.YAML(typ="rt", pure=True) + self.original_data = yml.load(f.read()) + + +def parse(hostsfile): + try: + parser = INIParser(hostsfile) + except cp.Error: + try: + parser = YAMLParser(hostsfile) + except ruamel.yaml.scanner.ScannerError: + logger.error( + "couldn't parse '{}' as neither a ini nor yaml file".format(hostsfile) + ) + raise + + parser.parse() + return parser.hosts, parser.groups + + +class AnsibleInventory(Inventory): + """ + Inventory plugin that is capable of reading an ansible inventory. + + Arguments: + hostsfile (string): Ansible inventory file to load + """ + + def __init__(self, hostsfile="hosts", **kwargs): + host_vars, group_vars = parse(hostsfile) + defaults = group_vars.pop("defaults") + super().__init__(host_vars, group_vars, defaults, **kwargs) diff --git a/brigade/plugins/inventory/nsot.py b/brigade/plugins/inventory/nsot.py index b0d2ce3c..54429bb0 100644 --- a/brigade/plugins/inventory/nsot.py +++ b/brigade/plugins/inventory/nsot.py @@ -15,38 +15,70 @@ class NSOTInventory(Inventory): the name of the site the host belongs to. Environment Variables: - * ``NSOT_URL``: URL to nsot's API (defaults to ``http://localhost:8990/api``) - * ``NSOT_EMAIL``: email for authentication (defaults to admin@acme.com) + * ``NSOT_URL``: Corresponds to nsot_url argument + * ``NSOT_EMAIL``: Corresponds to nsot_email argument + * ``NSOT_AUTH_HEADER``: Corresponds to nsot_auth_header argument + * ``NSOT_SECRET_KEY``: Corresponds to nsot_secret_key argument Arguments: flatten_attributes (bool): Assign host attributes to the root object. Useful for filtering hosts. + nsot_url (string): URL to nsot's API (defaults to ``http://localhost:8990/api``) + nsot_email (string): email for authtication (defaults to admin@acme.com) + nsot_auth_header (string): String for auth_header authentication (defaults to X-NSoT-Email) + nsot_secret_key (string): Secret Key for auth_token method. If given auth_token + will be used as auth_method. """ - def __init__(self, flatten_attributes=True, **kwargs): - NSOT_URL = os.environ.get('NSOT_URL', 'http://localhost:8990/api') - NSOT_EMAIL = os.environ.get('NSOT_EMAIL', 'admin@acme.com') + def __init__( + self, + nsot_url="", + nsot_email="", + nsot_auth_method="", + nsot_secret_key="", + nsot_auth_header="", + flatten_attributes=True, + **kwargs + ): + nsot_url = nsot_url or os.environ.get("NSOT_URL", "http://localhost:8990/api") + nsot_email = nsot_email or os.environ.get("NSOT_EMAIL", "admin@acme.com") + nsot_secret_key = nsot_secret_key or os.environ.get("NSOT_SECRET_KEY") - headers = {'X-NSoT-Email': NSOT_EMAIL} - devices = requests.get('{}/devices'.format(NSOT_URL), headers=headers).json() - sites = requests.get('{}/sites'.format(NSOT_URL), headers=headers).json() - interfaces = requests.get('{}/interfaces'.format(NSOT_URL), headers=headers).json() + if nsot_secret_key: + data = {"email": nsot_email, "secret_key": nsot_secret_key} + res = requests.post("{}/authenticate/".format(nsot_url), data=data) + auth_token = res.json().get("auth_token") + headers = { + "Authorization": "AuthToken {}:{}".format(nsot_email, auth_token) + } + + else: + nsot_auth_header = nsot_auth_header or os.environ.get( + "NSOT_AUTH_HEADER", "X-NSoT-Email" + ) + headers = {nsot_auth_header: nsot_email} + + devices = requests.get("{}/devices".format(nsot_url), headers=headers).json() + sites = requests.get("{}/sites".format(nsot_url), headers=headers).json() + interfaces = requests.get( + "{}/interfaces".format(nsot_url), headers=headers + ).json() # We resolve site_id and assign "site" variable with the name of the site for d in devices: - d['site'] = sites[d['site_id'] - 1]['name'] - d['interfaces'] = {} + d["site"] = sites[d["site_id"] - 1]["name"] + d["interfaces"] = {} if flatten_attributes: # We assign attributes to the root - for k, v in d.pop('attributes').items(): + for k, v in d.pop("attributes").items(): d[k] = v # We assign the interfaces to the hosts for i in interfaces: - devices[i['device'] - 1]['interfaces'][i['name']] = i + devices[i["device"] - 1]["interfaces"][i["name"]] = i # Finally the inventory expects a dict of hosts where the key is the hostname - devices = {d['hostname']: d for d in devices} + devices = {d["hostname"]: d for d in devices} super().__init__(devices, None, **kwargs) diff --git a/brigade/plugins/inventory/simple.py b/brigade/plugins/inventory/simple.py index e04e733e..762914c3 100644 --- a/brigade/plugins/inventory/simple.py +++ b/brigade/plugins/inventory/simple.py @@ -1,3 +1,5 @@ +import logging +import os from builtins import super from brigade.core.inventory import Inventory @@ -16,13 +18,15 @@ class SimpleInventory(Inventory): host1.cmh: site: cmh role: host - group: cmh-host + groups: + - cmh-host nos: linux host2.cmh: site: cmh role: host - group: cmh-host + groups: + - cmh-host nos: linux switch00.cmh: @@ -32,7 +36,8 @@ class SimpleInventory(Inventory): napalm_port: 12443 site: cmh role: leaf - group: cmh-leaf + groups: + - cmh-leaf nos: eos switch01.cmh: @@ -42,19 +47,22 @@ class SimpleInventory(Inventory): napalm_port: 12203 site: cmh role: leaf - group: cmh-leaf + groups: + - cmh-leaf nos: junos host1.bma: site: bma role: host - group: bma-host + groups: + - bma-host nos: linux host2.bma: site: bma role: host - group: bma-host + groups: + - bma-host nos: linux switch00.bma: @@ -64,7 +72,8 @@ class SimpleInventory(Inventory): napalm_port: 12443 site: bma role: leaf - group: bma-leaf + groups: + - bma-leaf nos: eos switch01.bma: @@ -74,43 +83,52 @@ class SimpleInventory(Inventory): napalm_port: 12203 site: bma role: leaf - group: bma-leaf + groups: + - bma-leaf nos: junos * group file:: --- - all: - group: null + defaults: domain: acme.com bma-leaf: - group: bma + groups: + - bma bma-host: - group: bma + groups: + - bma bma: - group: all + domain: bma.acme.com cmh-leaf: - group: cmh + groups: + - cmh cmh-host: - group: cmh + groups: + - cmh cmh: - group: all + domain: cmh.acme.com """ - def __init__(self, host_file, group_file=None, **kwargs): + def __init__(self, host_file="hosts.yaml", group_file="groups.yaml", **kwargs): with open(host_file, "r") as f: hosts = yaml.load(f.read()) if group_file: - with open(group_file, "r") as f: - groups = yaml.load(f.read()) + if os.path.exists(group_file): + with open(group_file, "r") as f: + groups = yaml.load(f.read()) + else: + logging.warning("{}: doesn't exist".format(group_file)) + groups = {} else: groups = {} - super().__init__(hosts, groups, **kwargs) + defaults = groups.pop("defaults", {}) + super().__init__(hosts, groups, defaults, **kwargs) diff --git a/brigade/plugins/tasks/apis/__init__.py b/brigade/plugins/tasks/apis/__init__.py new file mode 100644 index 00000000..0395d12a --- /dev/null +++ b/brigade/plugins/tasks/apis/__init__.py @@ -0,0 +1,3 @@ +from .http_method import http_method + +__all__ = ("http_method",) diff --git a/brigade/plugins/tasks/apis/http_method.py b/brigade/plugins/tasks/apis/http_method.py new file mode 100644 index 00000000..55e9fd3e --- /dev/null +++ b/brigade/plugins/tasks/apis/http_method.py @@ -0,0 +1,41 @@ +from brigade.core.task import Result + +import requests + + +def http_method(task=None, method="get", url="", raise_for_status=True, **kwargs): + """ + This is a convenience task that uses `requests `_ to + interact with an HTTP server. + + Arguments: + method (string): HTTP method to call + url (string): URL to connect to + raise_for_status (bool): Whether to call `raise_for_status + `_ + method automatically or not. For quick reference, raise_for_status will consider an + error if the return code is any of 4xx or 5xx + **kwargs: Keyword arguments will be passed to the `request + `_ + method + + Returns: + :obj:`brigade.core.task.Result`: + * result (``string/dict``): Body of the response. Either text or a dict if the + response was a json object + * reponse (object): Original `Response + `_ + """ + r = requests.request(method, url, **kwargs) + + if raise_for_status: + r.raise_for_status() + + try: + content_type = r.headers["Content-type"] + except KeyError: + content_type = "text" + + result = r.json() if "application/json" == content_type else r.text + + return Result(host=task.host if task else None, response=r, result=result) diff --git a/brigade/plugins/tasks/commands/__init__.py b/brigade/plugins/tasks/commands/__init__.py index 26faddc7..967b129b 100644 --- a/brigade/plugins/tasks/commands/__init__.py +++ b/brigade/plugins/tasks/commands/__init__.py @@ -2,7 +2,4 @@ from .remote_command import remote_command -__all__ = ( - "command", - "remote_command", -) +__all__ = ("command", "remote_command") diff --git a/brigade/plugins/tasks/commands/command.py b/brigade/plugins/tasks/commands/command.py index 718bed4e..101b6981 100644 --- a/brigade/plugins/tasks/commands/command.py +++ b/brigade/plugins/tasks/commands/command.py @@ -3,7 +3,6 @@ from brigade.core.exceptions import CommandError -from brigade.core.helpers import format_string from brigade.core.task import Result @@ -22,11 +21,12 @@ def command(task, command): Raises: :obj:`brigade.core.exceptions.CommandError`: when there is a command error """ - command = format_string(command, task, **task.host) - cmd = subprocess.Popen(shlex.split(command), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False) + cmd = subprocess.Popen( + shlex.split(command), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + ) stdout, stderr = cmd.communicate() stdout = stdout.decode() diff --git a/brigade/plugins/tasks/commands/remote_command.py b/brigade/plugins/tasks/commands/remote_command.py index 47a49a53..3d5070f9 100644 --- a/brigade/plugins/tasks/commands/remote_command.py +++ b/brigade/plugins/tasks/commands/remote_command.py @@ -11,6 +11,7 @@ def remote_command(task, command): Returns: :obj:`brigade.core.task.Result`: + * result (``str``): stderr or stdout * stdout (``str``): stdout * stderr (``srr``): stderr @@ -32,4 +33,5 @@ def remote_command(task, command): if exit_status_code: raise CommandError(command, exit_status_code, stdout, stderr) - return Result(host=task.host, stderr=stderr, stdout=stdout) + result = stderr if stderr else stdout + return Result(result=result, host=task.host, stderr=stderr, stdout=stdout) diff --git a/brigade/plugins/tasks/connections/__init__.py b/brigade/plugins/tasks/connections/__init__.py index d504f6e7..b4a8240e 100644 --- a/brigade/plugins/tasks/connections/__init__.py +++ b/brigade/plugins/tasks/connections/__init__.py @@ -9,8 +9,4 @@ "paramiko": paramiko_connection, } -__all__ = ( - "napalm_connection", - "netmiko_connection", - "paramiko_connection", -) +__all__ = ("napalm_connection", "netmiko_connection", "paramiko_connection") diff --git a/brigade/plugins/tasks/connections/netmiko_connection.py b/brigade/plugins/tasks/connections/netmiko_connection.py index e4ba0b46..45639717 100644 --- a/brigade/plugins/tasks/connections/netmiko_connection.py +++ b/brigade/plugins/tasks/connections/netmiko_connection.py @@ -1,31 +1,37 @@ from netmiko import ConnectHandler napalm_to_netmiko_map = { - 'ios': 'cisco_ios', - 'nxos': 'cisco_nxos', - 'eos': 'arista_eos', - 'junos': 'juniper_junos', - 'iosxr': 'cisco_iosxr' + "ios": "cisco_ios", + "nxos": "cisco_nxos", + "eos": "arista_eos", + "junos": "juniper_junos", + "iosxr": "cisco_xr", } -def netmiko_connection(task=None, **netmiko_args): +def netmiko_connection(task, **netmiko_args): """Connect to the host using Netmiko and set the relevant connection in the connection map. + Precedence: ``**netmiko_args`` > discrete inventory attributes > inventory netmiko_options + Arguments: - **netmiko_args: All supported Netmiko ConnectHandler arguments + ``**netmiko_args``: All supported Netmiko ConnectHandler arguments """ host = task.host parameters = { "host": host.host, "username": host.username, "password": host.password, - "port": host.ssh_port + "port": host.ssh_port, } + if host.nos is not None: # Look device_type up in corresponding map, if no entry return the host.nos unmodified device_type = napalm_to_netmiko_map.get(host.nos, host.nos) - parameters['device_type'] = device_type + parameters["device_type"] = device_type - parameters.update(**netmiko_args) - host.connections["netmiko"] = ConnectHandler(**parameters) + # Precedence order: **netmiko_args > discrete inventory attributes > inventory netmiko_options + netmiko_connection_args = host.get("netmiko_options", {}) + netmiko_connection_args.update(parameters) + netmiko_connection_args.update(netmiko_args) + host.connections["netmiko"] = ConnectHandler(**netmiko_connection_args) diff --git a/brigade/plugins/tasks/connections/paramiko_connection.py b/brigade/plugins/tasks/connections/paramiko_connection.py index 6486a1ca..2fcf40b2 100644 --- a/brigade/plugins/tasks/connections/paramiko_connection.py +++ b/brigade/plugins/tasks/connections/paramiko_connection.py @@ -28,18 +28,18 @@ def paramiko_connection(task=None): } user_config = ssh_config.lookup(host.host) - for k in ('hostname', 'username', 'port'): + for k in ("hostname", "username", "port"): if k in user_config: parameters[k] = user_config[k] - if 'proxycommand' in user_config: - parameters['sock'] = paramiko.ProxyCommand(user_config['proxycommand']) + if "proxycommand" in user_config: + parameters["sock"] = paramiko.ProxyCommand(user_config["proxycommand"]) # TODO configurable # if ssh_key_file: # parameters['key_filename'] = ssh_key_file - if 'identityfile' in user_config: - parameters['key_filename'] = user_config['identityfile'] + if "identityfile" in user_config: + parameters["key_filename"] = user_config["identityfile"] client.connect(**parameters) host.connections["paramiko"] = client diff --git a/brigade/plugins/tasks/data/__init__.py b/brigade/plugins/tasks/data/__init__.py index e3c0c62a..2dbc5c33 100644 --- a/brigade/plugins/tasks/data/__init__.py +++ b/brigade/plugins/tasks/data/__init__.py @@ -2,7 +2,4 @@ from .load_yaml import load_yaml -__all__ = ( - "load_json", - "load_yaml", -) +__all__ = ("load_json", "load_yaml") diff --git a/brigade/plugins/tasks/data/load_json.py b/brigade/plugins/tasks/data/load_json.py index c1298bc6..dfc6fc12 100644 --- a/brigade/plugins/tasks/data/load_json.py +++ b/brigade/plugins/tasks/data/load_json.py @@ -1,6 +1,5 @@ import json -from brigade.core.helpers import format_string from brigade.core.task import Result @@ -15,8 +14,7 @@ def load_json(task, file): :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the contents of the file """ - file = format_string(file, task) - with open(file, 'r') as f: + with open(file, "r") as f: data = json.loads(f.read()) return Result(host=task.host, result=data) diff --git a/brigade/plugins/tasks/data/load_yaml.py b/brigade/plugins/tasks/data/load_yaml.py index 7b342a29..058a18c5 100644 --- a/brigade/plugins/tasks/data/load_yaml.py +++ b/brigade/plugins/tasks/data/load_yaml.py @@ -1,4 +1,3 @@ -from brigade.core.helpers import format_string from brigade.core.task import Result @@ -16,8 +15,7 @@ def load_yaml(task, file): :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the contents of the file """ - file = format_string(file, task) - with open(file, 'r') as f: + with open(file, "r") as f: data = yaml.load(f.read()) return Result(host=task.host, result=data) diff --git a/brigade/plugins/tasks/files/__init__.py b/brigade/plugins/tasks/files/__init__.py index 5c393eef..2b8533f2 100644 --- a/brigade/plugins/tasks/files/__init__.py +++ b/brigade/plugins/tasks/files/__init__.py @@ -1,8 +1,5 @@ from .sftp import sftp -from .write import write +from .write_file import write_file -__all__ = ( - "sftp", - "write", -) +__all__ = ("sftp", "write_file") diff --git a/brigade/plugins/tasks/files/sftp.py b/brigade/plugins/tasks/files/sftp.py index 4a51f39d..42d67276 100644 --- a/brigade/plugins/tasks/files/sftp.py +++ b/brigade/plugins/tasks/files/sftp.py @@ -3,7 +3,6 @@ import stat from brigade.core.exceptions import CommandError -from brigade.core.helpers import format_string from brigade.core.task import Result from brigade.plugins.tasks import commands @@ -15,11 +14,11 @@ def get_src_hash(filename): sha1sum = hashlib.sha1() - with open(filename, 'rb') as f: - block = f.read(2**16) + with open(filename, "rb") as f: + block = f.read(2 ** 16) while len(block) != 0: sha1sum.update(block) - block = f.read(2**16) + block = f.read(2 ** 16) return sha1sum.hexdigest() @@ -28,9 +27,11 @@ def get_dst_hash(task, filename): try: result = commands.remote_command(task, command) return result.stdout.split()[0] + except CommandError as e: if "No such file or directory" in e.stderr: - return '' + return "" + raise @@ -38,6 +39,7 @@ def remote_exists(sftp_client, f): try: sftp_client.stat(f) return True + except IOError: return False @@ -49,7 +51,7 @@ def compare_put_files(task, sftp_client, src, dst): try: dst_hash = get_dst_hash(task, dst) except IOError: - dst_hash = '' + dst_hash = "" if src_hash != dst_hash: changed.append(dst) else: @@ -71,7 +73,7 @@ def compare_get_files(task, sftp_client, src, dst): try: dst_hash = get_src_hash(dst) except IOError: - dst_hash = '' + dst_hash = "" if src_hash != dst_hash: changed.append(dst) else: @@ -85,21 +87,21 @@ def compare_get_files(task, sftp_client, src, dst): return changed -def get(task, scp_client, sftp_client, src, dst, *args, **kwargs): +def get(task, scp_client, sftp_client, src, dst, dry_run, *args, **kwargs): changed = compare_get_files(task, sftp_client, src, dst) - if changed and not task.dry_run: + if changed and not dry_run: scp_client.get(src, dst, recursive=True) return changed -def put(task, scp_client, sftp_client, src, dst, *args, **kwargs): +def put(task, scp_client, sftp_client, src, dst, dry_run, *args, **kwargs): changed = compare_put_files(task, sftp_client, src, dst) - if changed and not task.dry_run: + if changed and not dry_run: scp_client.put(src, dst, recursive=True) return changed -def sftp(task, src, dst, action): +def sftp(task, src, dst, action, dry_run=None): """ Transfer files from/to the device using sftp protocol @@ -111,6 +113,7 @@ def sftp(task, src, dst, action): dst="/tmp/README.md") Arguments: + dry_run (bool): Whether to apply changes or not src (``str``): source file dst (``str``): destination action (``str``): ``put``, ``get``. @@ -120,14 +123,12 @@ def sftp(task, src, dst, action): * changed (``bool``): * files_changed (``list``): list of files that changed """ - src = format_string(src, task, **task.host) - dst = format_string(dst, task, **task.host) - actions = { - "put": put, - "get": get, - } + dry_run = task.is_dry_run(dry_run) + actions = {"put": put, "get": get} client = task.host.get_connection("paramiko") scp_client = SCPClient(client.get_transport()) sftp_client = paramiko.SFTPClient.from_transport(client.get_transport()) - files_changed = actions[action](task, scp_client, sftp_client, src, dst) - return Result(host=task.host, changed=bool(files_changed), files_changed=files_changed) + files_changed = actions[action](task, scp_client, sftp_client, src, dst, dry_run) + return Result( + host=task.host, changed=bool(files_changed), files_changed=files_changed + ) diff --git a/brigade/plugins/tasks/files/write.py b/brigade/plugins/tasks/files/write_file.py similarity index 87% rename from brigade/plugins/tasks/files/write.py rename to brigade/plugins/tasks/files/write_file.py index 4bc28d4e..c88646eb 100644 --- a/brigade/plugins/tasks/files/write.py +++ b/brigade/plugins/tasks/files/write_file.py @@ -7,6 +7,7 @@ def _read_file(file): if not os.path.exists(file): return [] + with open(file, "r") as f: return f.read().splitlines() @@ -25,11 +26,12 @@ def _generate_diff(filename, content, append): return "\n".join(diff) -def write(task, filename, content, append=False): +def write_file(task, filename, content, append=False, dry_run=None): """ Write contents to a file (locally) Arguments: + dry_run (bool): Whether to apply changes or not filename (``str``): file you want to write into conteint (``str``): content you want to write append (``bool``): whether you want to replace the contents or append to it @@ -40,7 +42,7 @@ def write(task, filename, content, append=False): """ diff = _generate_diff(filename, content, append) - if not task.dry_run: + if not task.is_dry_run(dry_run): mode = "a+" if append else "w+" with open(filename, mode=mode) as f: f.write(content) diff --git a/brigade/plugins/tasks/networking/__init__.py b/brigade/plugins/tasks/networking/__init__.py index 59e33ed9..63cfd04b 100644 --- a/brigade/plugins/tasks/networking/__init__.py +++ b/brigade/plugins/tasks/networking/__init__.py @@ -2,7 +2,9 @@ from .napalm_configure import napalm_configure from .napalm_get import napalm_get from .napalm_validate import napalm_validate +from .netmiko_file_transfer import netmiko_file_transfer from .netmiko_send_command import netmiko_send_command +from .netmiko_send_config import netmiko_send_config from .tcp_ping import tcp_ping __all__ = ( @@ -10,6 +12,8 @@ "napalm_configure", "napalm_get", "napalm_validate", + "netmiko_file_transfer", "netmiko_send_command", + "netmiko_send_config", "tcp_ping", ) diff --git a/brigade/plugins/tasks/networking/napalm_cli.py b/brigade/plugins/tasks/networking/napalm_cli.py index 59879ac5..94187858 100644 --- a/brigade/plugins/tasks/networking/napalm_cli.py +++ b/brigade/plugins/tasks/networking/napalm_cli.py @@ -5,6 +5,9 @@ def napalm_cli(task, commands): """ Run commands on remote devices using napalm + Arguments: + commands (``list``): List of commands to execute + Returns: :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the result of the commands diff --git a/brigade/plugins/tasks/networking/napalm_configure.py b/brigade/plugins/tasks/networking/napalm_configure.py index 663c8404..eae41e0f 100644 --- a/brigade/plugins/tasks/networking/napalm_configure.py +++ b/brigade/plugins/tasks/networking/napalm_configure.py @@ -1,13 +1,16 @@ -from brigade.core.helpers import format_string from brigade.core.task import Result -def napalm_configure(task, filename=None, configuration=None, replace=False): +def napalm_configure( + task, dry_run=None, filename=None, configuration=None, replace=False +): """ Loads configuration into a network devices using napalm Arguments: + dry_run (bool): Whether to apply changes or not configuration (str): configuration to load into the device + filename (str): filename containing the configuration to load into the device replace (bool): whether to replace or merge the configuration Returns: @@ -16,7 +19,6 @@ def napalm_configure(task, filename=None, configuration=None, replace=False): * diff (``string``): change in the system """ device = task.host.get_connection("napalm") - filename = format_string(filename, task, **task.host) if filename is not None else None if replace: device.load_replace_candidate(filename=filename, config=configuration) @@ -24,7 +26,8 @@ def napalm_configure(task, filename=None, configuration=None, replace=False): device.load_merge_candidate(filename=filename, config=configuration) diff = device.compare_config() - if not task.dry_run and diff: + dry_run = task.is_dry_run(dry_run) + if not dry_run and diff: device.commit_config() else: device.discard_config() diff --git a/brigade/plugins/tasks/networking/napalm_validate.py b/brigade/plugins/tasks/networking/napalm_validate.py index e156b9e8..3e9f4db6 100644 --- a/brigade/plugins/tasks/networking/napalm_validate.py +++ b/brigade/plugins/tasks/networking/napalm_validate.py @@ -17,5 +17,7 @@ def napalm_validate(task, src=None, validation_source=None): * complies (``bool``): Whether the device complies or not """ device = task.host.get_connection("napalm") - r = device.compliance_report(validation_file=src, validation_source=validation_source) + r = device.compliance_report( + validation_file=src, validation_source=validation_source + ) return Result(host=task.host, result=r) diff --git a/brigade/plugins/tasks/networking/netmiko_file_transfer.py b/brigade/plugins/tasks/networking/netmiko_file_transfer.py new file mode 100644 index 00000000..0561e7d1 --- /dev/null +++ b/brigade/plugins/tasks/networking/netmiko_file_transfer.py @@ -0,0 +1,32 @@ +from brigade.core.task import Result + +from netmiko import file_transfer + + +def netmiko_file_transfer(task, source_file, dest_file, **kwargs): + """ + Execute Netmiko file_transfer method + + Arguments: + source_file(str): Source file. + dest_file(str): Destination file. + kwargs (dict, optional): Additional arguments to pass to file_transfer + + Returns: + :obj:`brigade.core.task.Result`: + * result (``bool``): file exists and MD5 is valid + * changed (``bool``): the destination file was changed + + """ + net_connect = task.host.get_connection("netmiko") + kwargs.setdefault("direction", "put") + scp_result = file_transfer( + net_connect, source_file=source_file, dest_file=dest_file, **kwargs + ) + if kwargs.get("disable_md5") is True: + file_valid = scp_result["file_exists"] + else: + file_valid = scp_result["file_exists"] and scp_result["file_verified"] + return Result( + host=task.host, result=file_valid, changed=scp_result["file_transferred"] + ) diff --git a/brigade/plugins/tasks/networking/netmiko_send_command.py b/brigade/plugins/tasks/networking/netmiko_send_command.py index 7bcb7328..ebd5c915 100644 --- a/brigade/plugins/tasks/networking/netmiko_send_command.py +++ b/brigade/plugins/tasks/networking/netmiko_send_command.py @@ -12,7 +12,7 @@ def netmiko_send_command(task, command_string, use_timing=False, **kwargs): Returns: :obj:`brigade.core.task.Result`: - * result (``dict``): dictionary with the result of the getter + * result (``dict``): dictionary with the result of the show command. """ net_connect = task.host.get_connection("netmiko") if use_timing: diff --git a/brigade/plugins/tasks/networking/netmiko_send_config.py b/brigade/plugins/tasks/networking/netmiko_send_config.py new file mode 100644 index 00000000..63c9366a --- /dev/null +++ b/brigade/plugins/tasks/networking/netmiko_send_config.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals + +from brigade.core.task import Result + + +def netmiko_send_config(task, config_commands=None, config_file=None, **kwargs): + """ + Execute Netmiko send_config_set method (or send_config_from_file) + Arguments: + config_commands(list, optional): Commands to configure on the remote network device. + config_file(str, optional): File to read configuration commands from. + kwargs (dict, optional): Additional arguments to pass to method. + + Returns: + :obj:`brigade.core.task.Result`: + * result (``dict``): dictionary showing the CLI from the configuration changes. + """ + net_connect = task.host.get_connection("netmiko") + if config_commands: + result = net_connect.send_config_set(config_commands=config_commands, **kwargs) + elif config_file: + result = net_connect.send_config_from_file(config_file=config_file, **kwargs) + else: + raise ValueError("Must specify either config_commands or config_file") + + return Result(host=task.host, result=result, changed=True) diff --git a/brigade/plugins/tasks/networking/tcp_ping.py b/brigade/plugins/tasks/networking/tcp_ping.py index 9cf68dfc..4f4d47aa 100644 --- a/brigade/plugins/tasks/networking/tcp_ping.py +++ b/brigade/plugins/tasks/networking/tcp_ping.py @@ -17,7 +17,7 @@ def tcp_ping(task, ports, timeout=2, host=None): Returns: :obj:`brigade.core.task.Result`: - * tcp_port (``dict``): Contains port numbers as keys with True/False as values + * result (``dict``): Contains port numbers as keys with True/False as values """ if isinstance(ports, int): @@ -26,6 +26,7 @@ def tcp_ping(task, ports, timeout=2, host=None): if isinstance(ports, list): if not all(isinstance(port, int) for port in ports): raise ValueError("Invalid value for 'ports'") + else: raise ValueError("Invalid value for 'ports'") @@ -47,4 +48,4 @@ def tcp_ping(task, ports, timeout=2, host=None): s.close() result[port] = connection - return Result(host=task.host, tcp_port=result) + return Result(host=task.host, result=result) diff --git a/brigade/plugins/tasks/text/__init__.py b/brigade/plugins/tasks/text/__init__.py index 3a41db10..0c8d088a 100644 --- a/brigade/plugins/tasks/text/__init__.py +++ b/brigade/plugins/tasks/text/__init__.py @@ -1,9 +1,4 @@ -from .print_result import print_result from .template_file import template_file from .template_string import template_string -__all__ = ( - "print_result", - "template_file", - "template_string", -) +__all__ = ("template_file", "template_string") diff --git a/brigade/plugins/tasks/text/print_result.py b/brigade/plugins/tasks/text/print_result.py deleted file mode 100644 index 60801c59..00000000 --- a/brigade/plugins/tasks/text/print_result.py +++ /dev/null @@ -1,58 +0,0 @@ -import pprint - -from brigade.core.task import AggregatedResult, MultiResult, Result - -from colorama import Fore, Style, init - - -init(autoreset=True, convert=False, strip=False) - - -def print_result(task, data, vars=None, failed=None, task_id=None): - """ - Prints on screen the :obj:`brigade.core.task.Result` from a previous task - - Arguments: - data (:obj:`brigade.core.task.Result`): from a previous task - vars (list of str): Which attributes you want to print - failed (``bool``): if ``True`` assume the task failed - task_id (``int``): if we have a :obj:`brigade.core.task.MultiResult` print - only task in this position - - Returns: - :obj:`brigade.core.task.Result`: - """ - vars = vars or ["diff", "result", "stdout"] - if isinstance(vars, str): - vars = [vars] - - if isinstance(data, AggregatedResult): - data = data[task.host.name] - - if task_id is not None: - r = data[task_id] - data = MultiResult(data.name) - data.append(r) - - if data.failed or failed: - color = Fore.RED - elif data.changed: - color = Fore.YELLOW - else: - color = Fore.BLUE - title = "" if data.changed is None else " ** changed : {} ".format(data.changed) - msg = "* {}{}".format(task.host.name, title) - print("{}{}{}{}".format(Style.BRIGHT, color, msg, "*" * (80 - len(msg)))) - for r in data: - subtitle = "" if r.changed is None else " ** changed : {} ".format(r.changed) - msg = "---- {}{} ".format(r.name, subtitle) - print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg)))) - for v in vars: - x = getattr(r, v, "") - if x and not isinstance(x, str): - pprint.pprint(x, indent=2) - elif x: - print(x) - print() - - return Result(task.host) diff --git a/brigade/plugins/tasks/text/template_file.py b/brigade/plugins/tasks/text/template_file.py index 9b8d8a78..5e5b4112 100644 --- a/brigade/plugins/tasks/text/template_file.py +++ b/brigade/plugins/tasks/text/template_file.py @@ -1,23 +1,28 @@ -from brigade.core.helpers import format_string, jinja_helper, merge_two_dicts +from brigade.core.helpers import jinja_helper, merge_two_dicts from brigade.core.task import Result -def template_file(task, template, path, **kwargs): +def template_file(task, template, path, jinja_filters=None, **kwargs): """ Renders contants of a file with jinja2. All the host data is available in the tempalte Arguments: template (string): filename - path (string): path to dir with templates (will be rendered with format) + path (string): path to dir with templates + jinja_filters (dict): jinja filters to enable. Defaults to brigade.config.jinja_filters **kwargs: additional data to pass to the template Returns: :obj:`brigade.core.task.Result`: * result (``string``): rendered string """ + jinja_filters = jinja_filters or {} or task.brigade.config.jinja_filters merged = merge_two_dicts(task.host, kwargs) - path = format_string(path, task, **kwargs) - template = format_string(template, task, **kwargs) - text = jinja_helper.render_from_file(template=template, path=path, - host=task.host, **merged) + text = jinja_helper.render_from_file( + template=template, + path=path, + host=task.host, + jinja_filters=jinja_filters, + **merged + ) return Result(host=task.host, result=text) diff --git a/brigade/plugins/tasks/text/template_string.py b/brigade/plugins/tasks/text/template_string.py index cb4b842f..80d3f83b 100644 --- a/brigade/plugins/tasks/text/template_string.py +++ b/brigade/plugins/tasks/text/template_string.py @@ -2,19 +2,22 @@ from brigade.core.task import Result -def template_string(task, template, **kwargs): +def template_string(task, template, jinja_filters=None, **kwargs): """ Renders a string with jinja2. All the host data is available in the tempalte Arguments: template (string): template string + jinja_filters (dict): jinja filters to enable. Defaults to brigade.config.jinja_filters **kwargs: additional data to pass to the template Returns: :obj:`brigade.core.task.Result`: * result (``string``): rendered string """ + jinja_filters = jinja_filters or {} or task.brigade.config.jinja_filters merged = merge_two_dicts(task.host, kwargs) - text = jinja_helper.render_from_string(template=template, - host=task.host, **merged) + text = jinja_helper.render_from_string( + template=template, host=task.host, jinja_filters=jinja_filters, **merged + ) return Result(host=task.host, result=text) diff --git a/docs/_data_templates/configuration-parameters.j2 b/docs/_data_templates/configuration-parameters.j2 index 0f06931a..842b919f 100644 --- a/docs/_data_templates/configuration-parameters.j2 +++ b/docs/_data_templates/configuration-parameters.j2 @@ -14,7 +14,11 @@ The configuration parameters will be set by the :doc:`Brigade.core.configuration Type Default + {% if v['type'] in ('str', 'int', 'bool') %} {{ v['env'] or 'BRIGADE_' + k|upper }} + {% else %} + N/A + {% endif %} {{ v['type'] }} {{ v['default_doc'] or v['default'] }} diff --git a/docs/conf.py b/docs/conf.py index 77bde96b..b38aa7f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,10 +23,10 @@ from jinja2 import Environment from jinja2 import FileSystemLoader -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) -from brigade.core.configuration import CONF # noqa +from brigade.core.configuration import CONF # noqa # -- General configuration ------------------------------------------------ BASEPATH = os.path.dirname(__file__) @@ -38,33 +38,33 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'nbsphinx'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "nbsphinx"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'brigade' -copyright = '2017, David Barroso' -author = 'David Barroso' +project = "brigade" +copyright = "2017, David Barroso" +author = "David Barroso" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.0.1' +version = "0.0.1" # The full version, including alpha/beta/rc tags. -release = '0.0.1' +release = "0.0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -76,10 +76,10 @@ # 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"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -90,7 +90,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -101,7 +101,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -120,7 +120,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'brigadedoc' +htmlhelp_basename = "brigadedoc" # -- Options for LaTeX output --------------------------------------------- @@ -129,15 +129,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -147,8 +144,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'brigade.tex', 'brigade Documentation', - 'David Barroso', 'manual'), + (master_doc, "brigade.tex", "brigade Documentation", "David Barroso", "manual") ] @@ -156,10 +152,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'brigade', 'brigade Documentation', - [author], 1) -] +man_pages = [(master_doc, "brigade", "brigade Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -168,9 +161,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'brigade', 'brigade Documentation', - author, 'brigade', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "brigade", + "brigade Documentation", + author, + "brigade", + "One line description of project.", + "Miscellaneous", + ) ] @@ -180,17 +179,17 @@ def build_configuration_parameters(app): env = Environment(loader=FileSystemLoader("{0}/_data_templates".format(BASEPATH))) template_file = env.get_template("configuration-parameters.j2") data = {} - data['params'] = CONF + data["params"] = CONF rendered_template = template_file.render(**data) - output_dir = '{0}/ref/configuration/generated'.format(BASEPATH) - with open('{}/parameters.rst'.format(output_dir), 'w') as f: + output_dir = "{0}/configuration/generated".format(BASEPATH) + with open("{}/parameters.rst".format(output_dir), "w") as f: f.write(rendered_template) def setup(app): """Map methods to states of the documentation build.""" - app.connect('builder-inited', build_configuration_parameters) - app.add_stylesheet('css/custom.css') + app.connect("builder-inited", build_configuration_parameters) + app.add_stylesheet("css/custom.css") build_configuration_parameters(None) diff --git a/docs/ref/configuration/generated/.placeholder b/docs/configuration/generated/.placeholder similarity index 100% rename from docs/ref/configuration/generated/.placeholder rename to docs/configuration/generated/.placeholder diff --git a/docs/ref/configuration/index.rst b/docs/configuration/index.rst similarity index 65% rename from docs/ref/configuration/index.rst rename to docs/configuration/index.rst index 4b724985..7f5f84ec 100644 --- a/docs/ref/configuration/index.rst +++ b/docs/configuration/index.rst @@ -1,5 +1,5 @@ -Brigade Configuration -===================== +Configuration +============= Each configuration parameter are applied in the following order: @@ -7,4 +7,4 @@ Each configuration parameter are applied in the following order: 2. Parameter in configuration file / object 3. Default value -.. include:: generated/parameters.rst \ No newline at end of file +.. include:: generated/parameters.rst diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst new file mode 120000 index 00000000..e9a8ba64 --- /dev/null +++ b/docs/contributing/index.rst @@ -0,0 +1 @@ +../../CONTRIBUTING.rst \ No newline at end of file diff --git a/examples/highlighter.py b/docs/highlighter.py similarity index 66% rename from examples/highlighter.py rename to docs/highlighter.py index ebe31b33..26a46377 100644 --- a/examples/highlighter.py +++ b/docs/highlighter.py @@ -5,7 +5,7 @@ from pygments import highlight from pygments.formatters import HtmlFormatter -from pygments.lexers import get_lexer_by_name +from pygments.lexers import get_lexer_for_filename HTML_TEMPLATE = """\n", + "
1 ---\n",
+       "2 num_workers: 100\n",
+       "3 inventory: brigade.plugins.inventory.simple.SimpleInventory\n",
+       "4 SimpleInventory:\n",
+       "5     host_file: "inventory/hosts.yaml"\n",
+       "6     group_file: "inventory/groups.yaml"\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%highlight_file config.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "**Note:** To pass options to the inventory plugin add a top-level dictionary named after the inventory class name; `SimpleInventory` in this example.\n", + "
\n", + "\n", + "Now to create the [brigade](../../ref/api/brigade.rst#brigade) object:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from brigade.core import InitBrigade\n", + "brg = InitBrigade(config_file=\"config.yaml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also initialize brigade programmatically without a configuration file:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from brigade.core import InitBrigade\n", + "brg = InitBrigade(num_workers=100,\n", + " inventory=\"brigade.plugins.inventory.simple.SimpleInventory\",\n", + " SimpleInventory={\"host_file\": \"inventory/hosts.yaml\",\n", + " \"group_file\": \"inventory/groups.yaml\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or with a combination of both methods:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from brigade.core import InitBrigade\n", + "brg = InitBrigade(num_workers=50, config_file=\"config.yaml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.config.num_workers" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/intro/install.rst b/docs/tutorials/intro/install.rst index 7d1b042d..c2586301 100644 --- a/docs/tutorials/intro/install.rst +++ b/docs/tutorials/intro/install.rst @@ -13,42 +13,22 @@ Brigade is published to `PyPI `_ and can be i pip --version - pip 1.5.4 from /home/vagrant/brigade/local/lib/python2.7/site-packages (python 2.7) + pip 9.0.3 from /Users/patrick/brigade/lib/python3.6/site-packages (python 3.6) -It could be that you need to use the pip3 binary instead of pip as pip3 is for Python 3 on some systems. +It could be that you need to use the pip3 binary instead of pip as pip3 is for Python 3 on some systems. Speaking of Python 3, this tutorial is written with Python 3.6 in mind. This has mostly to do with the use of f-strings, you should however be able to follow along even if you are still at Python 2.7. However, if you are starting something new don't use Python 2.7. If you have to make sure that you're pip is up to date as you might have trouble installing some of the Brigade dependencies if you have a very old pip if you are on version 9.0 or later you should be find. -So above we can see that we have pip installed. However the version 1.5.4 is pretty old. To save ourselves from trouble when installing packages we start by upgrading pip. - -.. code-block:: bash - - pip install "pip>=9.0.1" - - pip --version - - pip 9.0.1 from /home/vagrant/brigade/local/lib/python2.7/site-packages (python 2.7) - -That's more like it! The next step is to install Brigade. +As you would assume, the installation is then very easy. .. code-block:: bash pip install brigade Collecting brigade + Collecting colorama (from brigade) [...] - Successfully installed MarkupSafe-1.0 - asn1crypto-0.23.0 bcrypt-3.1.4 brigade-0.0.5 - certifi-2017.11.5 cffi-1.11.2 chardet-3.0.4 - cryptography-2.1.4 enum34-1.1.6 future-0.16.0 - idna-2.6 ipaddress-1.0.18 jinja2-2.10 - jtextfsm-0.3.1 junos-eznc-2.1.7 lxml-4.1.1 - napalm-2.2.0 ncclient-0.5.3 netaddr-0.7.19 - netmiko-1.4.3 paramiko-2.4.0 pyIOSXR-0.52 - pyasn1-0.4.2 pycparser-2.18 pyeapi-0.8.1 - pynacl-1.2.1 pynxos-0.0.3 pyserial-3.4 - pyyaml-3.12 requests-2.18.4 scp-0.10.2 - six-1.11.0 urllib3-1.22 - -Please note that the above output has been abbreviated for readability. Your output will probably be longer. You should see that `brigade` is successfully installed. + Successfully installed MarkupSafe-1.0 asn1crypto-0.24.0 bcrypt-3.1.4 brigade-0.0.6 + +Please note that the above output has been abbreviated for readability. Your output will be quite a bit longer. You should see that `brigade` is successfully installed. Now we can verify that Brigade is installed and that you are able to import the package from Python. diff --git a/docs/tutorials/intro/inventory.ipynb b/docs/tutorials/intro/inventory.ipynb new file mode 100644 index 00000000..d3d1b665 --- /dev/null +++ b/docs/tutorials/intro/inventory.ipynb @@ -0,0 +1,1059 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", + "%run ../../highlighter.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inventory\n", + "\n", + "The Inventory is arguably the most important piece of brigade. Let's see how it works. To begin with the [inventory](../../ref/api/inventory.rst#brigade.core.inventory.Inventory) is comprised of [hosts](../../ref/api/inventory.rst#brigade.core.inventory.Host) and [groups](../../ref/api/inventory.rst#brigade.core.inventory.Group).\n", + "\n", + "In this tutorial we are using the [SimpleInventory](../../plugins/inventory/simple.rst#brigade.plugins.inventory.simple.SimpleInventory) plugin. This inventory plugin stores all the relevant data in two files. Let's start by checking them:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
  1 ---\n",
+       "  2 host1.cmh:\n",
+       "  3     brigade_host: 127.0.0.1\n",
+       "  4     brigade_ssh_port: 2201\n",
+       "  5     brigade_username: vagrant\n",
+       "  6     brigade_password: vagrant\n",
+       "  7     site: cmh\n",
+       "  8     role: host\n",
+       "  9     groups:\n",
+       " 10         - cmh\n",
+       " 11     brigade_nos: linux\n",
+       " 12     type: host\n",
+       " 13 \n",
+       " 14 host2.cmh:\n",
+       " 15     brigade_host: 127.0.0.1\n",
+       " 16     brigade_ssh_port: 2202\n",
+       " 17     brigade_username: vagrant\n",
+       " 18     brigade_password: vagrant\n",
+       " 19     site: cmh\n",
+       " 20     role: host\n",
+       " 21     groups:\n",
+       " 22         - cmh\n",
+       " 23     brigade_nos: linux\n",
+       " 24     type: host\n",
+       " 25 \n",
+       " 26 spine00.cmh:\n",
+       " 27     brigade_host: 127.0.0.1\n",
+       " 28     brigade_username: vagrant\n",
+       " 29     brigade_password: vagrant\n",
+       " 30     brigade_network_api_port: 12444\n",
+       " 31     site: cmh\n",
+       " 32     role: spine\n",
+       " 33     groups:\n",
+       " 34         - cmh\n",
+       " 35     brigade_nos: eos\n",
+       " 36     type: network_device\n",
+       " 37 \n",
+       " 38 spine01.cmh:\n",
+       " 39     brigade_host: 127.0.0.1\n",
+       " 40     brigade_username: vagrant\n",
+       " 41     brigade_password: ""\n",
+       " 42     brigade_network_api_port: 12204\n",
+       " 43     site: cmh\n",
+       " 44     role: spine\n",
+       " 45     groups:\n",
+       " 46         - cmh\n",
+       " 47     brigade_nos: junos\n",
+       " 48     type: network_device\n",
+       " 49 \n",
+       " 50 leaf00.cmh:\n",
+       " 51     brigade_host: 127.0.0.1\n",
+       " 52     brigade_username: vagrant\n",
+       " 53     brigade_password: vagrant\n",
+       " 54     brigade_network_api_port: 12443\n",
+       " 55     site: cmh\n",
+       " 56     role: leaf\n",
+       " 57     groups:\n",
+       " 58         - cmh\n",
+       " 59     brigade_nos: eos\n",
+       " 60     type: network_device\n",
+       " 61     asn: 65100\n",
+       " 62 \n",
+       " 63 leaf01.cmh:\n",
+       " 64     brigade_host: 127.0.0.1\n",
+       " 65     brigade_username: vagrant\n",
+       " 66     brigade_password: ""\n",
+       " 67     brigade_network_api_port: 12203\n",
+       " 68     site: cmh\n",
+       " 69     role: leaf\n",
+       " 70     groups:\n",
+       " 71         - cmh\n",
+       " 72     brigade_nos: junos\n",
+       " 73     type: network_device\n",
+       " 74     asn: 65101\n",
+       " 75 \n",
+       " 76 host1.bma:\n",
+       " 77     site: bma\n",
+       " 78     role: host\n",
+       " 79     groups:\n",
+       " 80         - bma\n",
+       " 81     brigade_nos: linux\n",
+       " 82     type: host\n",
+       " 83 \n",
+       " 84 host2.bma:\n",
+       " 85     site: bma\n",
+       " 86     role: host\n",
+       " 87     groups:\n",
+       " 88         - bma\n",
+       " 89     brigade_nos: linux\n",
+       " 90     type: host\n",
+       " 91 \n",
+       " 92 spine00.bma:\n",
+       " 93     brigade_host: 127.0.0.1\n",
+       " 94     brigade_username: vagrant\n",
+       " 95     brigade_password: vagrant\n",
+       " 96     brigade_network_api_port: 12444\n",
+       " 97     site: bma\n",
+       " 98     role: spine\n",
+       " 99     groups:\n",
+       "100         - bma\n",
+       "101     brigade_nos: eos\n",
+       "102     type: network_device\n",
+       "103 \n",
+       "104 spine01.bma:\n",
+       "105     brigade_host: 127.0.0.1\n",
+       "106     brigade_username: vagrant\n",
+       "107     brigade_password: ""\n",
+       "108     brigade_network_api_port: 12204\n",
+       "109     site: bma\n",
+       "110     role: spine\n",
+       "111     groups:\n",
+       "112         - bma\n",
+       "113     brigade_nos: junos\n",
+       "114     type: network_device\n",
+       "115 \n",
+       "116 leaf00.bma:\n",
+       "117     brigade_host: 127.0.0.1\n",
+       "118     brigade_username: vagrant\n",
+       "119     brigade_password: vagrant\n",
+       "120     brigade_network_api_port: 12443\n",
+       "121     site: bma\n",
+       "122     role: leaf\n",
+       "123     groups:\n",
+       "124         - bma\n",
+       "125     brigade_nos: eos\n",
+       "126     type: network_device\n",
+       "127 \n",
+       "128 leaf01.bma:\n",
+       "129     brigade_host: 127.0.0.1\n",
+       "130     brigade_username: vagrant\n",
+       "131     brigade_password: wrong_password\n",
+       "132     brigade_network_api_port: 12203\n",
+       "133     site: bma\n",
+       "134     role: leaf\n",
+       "135     groups:\n",
+       "136         - bma\n",
+       "137     brigade_nos: junos\n",
+       "138     type: network_device\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# hosts file\n", + "%highlight_file inventory/hosts.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The hosts file is basically a map where the outermost key is the hostname and then any arbitrary `` pair you want inside. Usually `brigade_*` keys have special meaning, you can investigate the [hosts](../../ref/api/inventory.rst#brigade.core.inventory.Host) class for details on those. In addition, the `groups` key is a list of groups you can inherite data from. We will inspect soon how the inheritance model works.\n", + "\n", + "Now, let's look at the groups file:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 ---\n",
+       " 2 defaults:\n",
+       " 3     domain: acme.local\n",
+       " 4 \n",
+       " 5 global:\n",
+       " 6     domain: global.local\n",
+       " 7     asn: 1\n",
+       " 8 \n",
+       " 9 eu:\n",
+       "10     asn: 65100\n",
+       "11 \n",
+       "12 bma:\n",
+       "13     groups:\n",
+       "14         - eu\n",
+       "15         - global\n",
+       "16 \n",
+       "17 cmh:\n",
+       "18     asn: 65000\n",
+       "19     vlans:\n",
+       "20       100: frontend\n",
+       "21       200: backend\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# groups file\n", + "%highlight_file inventory/groups.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pretty similar to the hosts file.\n", + "\n", + "### Accessing the inventory\n", + "\n", + "You can access the [inventory](../../ref/api/inventory.rst#brigade.core.inventory.Inventory) with the `inventory` attribute:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from brigade.core import InitBrigade\n", + "brg = InitBrigade(config_file=\"config.yaml\")\n", + "\n", + "brg.inventory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The inventory has two dict-like attributes `hosts` and `groups` that you can use to access the hosts and groups respectively:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'host1.bma': Host: host1.bma,\n", + " 'host1.cmh': Host: host1.cmh,\n", + " 'host2.bma': Host: host2.bma,\n", + " 'host2.cmh': Host: host2.cmh,\n", + " 'leaf00.bma': Host: leaf00.bma,\n", + " 'leaf00.cmh': Host: leaf00.cmh,\n", + " 'leaf01.bma': Host: leaf01.bma,\n", + " 'leaf01.cmh': Host: leaf01.cmh,\n", + " 'spine00.bma': Host: spine00.bma,\n", + " 'spine00.cmh': Host: spine00.cmh,\n", + " 'spine01.bma': Host: spine01.bma,\n", + " 'spine01.cmh': Host: spine01.cmh}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.inventory.hosts" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'bma': Group: bma,\n", + " 'cmh': Group: cmh,\n", + " 'eu': Group: eu,\n", + " 'global': Group: global}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.inventory.groups" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Host: leaf01.bma" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.inventory.hosts[\"leaf01.bma\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hosts and groups are also dict-like objects:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['name', 'groups', 'brigade_host', 'brigade_username', 'brigade_password', 'brigade_network_api_port', 'site', 'role', 'brigade_nos', 'type', 'asn', 'domain'])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "host = brg.inventory.hosts[\"leaf01.bma\"]\n", + "host.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'bma'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "host[\"site\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inheritance model\n", + "\n", + "Let's see how the inheritance models works by example. Let's start by looking again at the groups file:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
 1 ---\n",
+       " 2 defaults:\n",
+       " 3     domain: acme.local\n",
+       " 4 \n",
+       " 5 global:\n",
+       " 6     domain: global.local\n",
+       " 7     asn: 1\n",
+       " 8 \n",
+       " 9 eu:\n",
+       "10     asn: 65100\n",
+       "11 \n",
+       "12 bma:\n",
+       "13     groups:\n",
+       "14         - eu\n",
+       "15         - global\n",
+       "16 \n",
+       "17 cmh:\n",
+       "18     asn: 65000\n",
+       "19     vlans:\n",
+       "20       100: frontend\n",
+       "21       200: backend\n",
+       "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# groups file\n", + "%highlight_file inventory/groups.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The host `leaf01.bma` belongs to the group `bma` which in turn belongs to the groups `eu` and `global`. The host `spine00.cmh` belongs to the group `cmh` which doesn't belong to any other group.\n", + "\n", + "Data resolution works by iterating recursively over all the parent groups and trying to see if that parent group (or any of it's parents) contains the data. For instance:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'global.local'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "leaf01_bma = brg.inventory.hosts[\"leaf01.bma\"]\n", + "leaf01_bma[\"domain\"] # comes from the group `global`" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "65100" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "leaf01_bma[\"asn\"] # comes from group `eu`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The group `defaults` is special. This group contains data that will be returned if neither the host nor the parents have a specific value for it." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'acme.local'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "leaf01_cmh = brg.inventory.hosts[\"leaf01.cmh\"]\n", + "leaf01_cmh[\"domain\"] # comes from defaults" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If brigade can't resolve the data you should get a KeyError as usual:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Couldn't find key: 'non_existent'\n" + ] + } + ], + "source": [ + "try:\n", + " leaf01_cmh[\"non_existent\"]\n", + "except KeyError as e:\n", + " print(f\"Couldn't find key: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also try to access data without recursive resolution by using the `data` attribute. For example, if we try to access `leaf01_cmh.data[\"domain\"]` we should get an error as the host itself doesn't have that data:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Couldn't find key: 'domain'\n" + ] + } + ], + "source": [ + "try:\n", + " leaf01_cmh.data[\"domain\"]\n", + "except KeyError as e:\n", + " print(f\"Couldn't find key: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering the inventory\n", + "\n", + "So far we have seen that `brg.inventory.hosts` and `brg.inventory.groups` are dict-like objects that we can use to iterate over all the hosts and groups or to access any particular one directly. Now we are going to see how we can do some fancy filtering that will enable us to operate on groups of hosts based on their properties.\n", + "\n", + "The simpler way of filtering hosts is by `` pairs. For instance:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.filter(site=\"cmh\").inventory.hosts.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also filter using multiple `` pairs:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['spine00.cmh', 'spine01.cmh'])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.filter(site=\"cmh\", role=\"spine\").inventory.hosts.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Filter is cumulative:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['spine00.cmh', 'spine01.cmh'])" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.filter(site=\"cmh\").filter(role=\"spine\").inventory.hosts.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['spine00.cmh', 'spine01.cmh'])" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cmh = brg.filter(site=\"cmh\")\n", + "cmh.filter(role=\"spine\").inventory.hosts.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['leaf00.cmh', 'leaf01.cmh'])" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cmh.filter(role=\"leaf\").inventory.hosts.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also grab the children of a group:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'host1.bma': Host: host1.bma,\n", + " 'host2.bma': Host: host2.bma,\n", + " 'leaf00.bma': Host: leaf00.bma,\n", + " 'leaf01.bma': Host: leaf01.bma,\n", + " 'spine00.bma': Host: spine00.bma,\n", + " 'spine01.bma': Host: spine01.bma}" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brg.inventory.groups[\"eu\"].children()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Advanced filtering\n", + "\n", + "Sometimes you need more fancy filtering. For those cases you can use a filter function:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['spine00.cmh', 'spine01.cmh', 'spine00.bma', 'spine01.bma'])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def has_long_name(host):\n", + " return len(host.name) == 11\n", + "\n", + "brg.filter(filter_func=has_long_name).inventory.hosts.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['host1.cmh', 'host2.cmh', 'host1.bma', 'host2.bma'])" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Or a lambda function\n", + "brg.filter(filter_func=lambda h: len(h.name) == 9).inventory.hosts.keys()" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/intro/inventory.rst b/docs/tutorials/intro/inventory.rst deleted file mode 100644 index a8f089cb..00000000 --- a/docs/tutorials/intro/inventory.rst +++ /dev/null @@ -1,135 +0,0 @@ -The Inventory -============= - -The inventory is arguably the most important piece of Brigade. The inventory organizes hosts and makes sure tasks have the correct data for each host. - - -Inventory data --------------- - -Before we start let's take a look at the inventory data: - -* ``hosts.yaml`` - -.. literalinclude:: ../../../examples/inventory/hosts.yaml - -* ``groups.yaml`` - -.. literalinclude:: ../../../examples/inventory/groups.yaml - -Loading the inventory ---------------------- - -You can create the inventory in different ways, depending on your data source. To see the available plugins you can use go to the :ref:`ref-inventory` reference guide. - -.. note:: For this and the subsequent sections of this tutorial we are going to use the :obj:`SimpleInventory ` with the data located in ``/examples/inventory/``. We will also use the ``Vagrantfile`` located there so you should be able to reproduce everything. - -First, let's load the inventory:: - - >>> from brigade.plugins.inventory.simple import SimpleInventory - >>> inventory = SimpleInventory(host_file="hosts.yaml", group_file="groups.yaml") - -Now let's inspect the hosts and groups we have:: - - >>> inventory.hosts - {'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} - >>> inventory.groups - {'all': Group: all, 'bma': Group: bma, 'cmh': Group: cmh} - -As you probably noticed both ``hosts`` and ``groups`` are dictionaries so you can iterate over them if you want to. - -Data ----- - -Let's start by grabbing a host: - - >>> h = inventory.hosts['host1.cmh'] - >>> print(h) - host1.cmh - -Now, let's check some attributes:: - - >>> h["site"] - 'cmh' - >>> h.data["role"] - 'host' - >>> h["domain"] - 'acme.com' - >>> h.data["domain"] - Traceback (most recent call last): - File "", line 1, in - KeyError: 'domain' - >>> h.group["domain"] - 'acme.com' - -What does this mean? You can access host data in two ways: - -1. As if the host was a dictionary, i.e., ``h["domain"]`` in which case the inventory will resolve the groups and use data inherited from them (in our example ``domain`` is coming from the parent group). -2. Via the ``data`` attribute in which case there is no group resolution going on so ``h["domain"]`` fails is that piece of data is not directly assigned to the host. - -Most of the time you will care about the first option but if you ever need to get data only from the host you can do it without a hassle. - -Finally, the host behaves like a python dictionary so you can iterate over the data as such:: - - >>> h.keys() - dict_keys(['name', 'group', 'asn', 'vlans', 'site', 'role', 'brigade_nos', 'type']) - >>> h.values() - dict_values(['host1.cmh', 'cmh', 65000, {100: 'frontend', 200: 'backend'}, 'cmh', 'host', 'linux', 'host']) - >>> h.items() - dict_items([('name', 'host1.cmh'), ('group', 'cmh'), ('asn', 65000), ('vlans', {100: 'frontend', 200: 'backend'}), ('site', 'cmh'), ('role', 'host'), ('brigade_nos', 'linux'), ('type', 'host')]) - >>> for k, v in h.items(): - ... print(k, v) - ... - name host1.cmh - group cmh - asn 65000 - vlans {100: 'frontend', 200: 'backend'} - site cmh - role host - brigade_nos linux - type host - >>> - -.. note:: You can head to :obj:`brigade.core.inventory.Host` and :obj:`brigade.core.inventory.Group` for details on all the available attributes and functions for each ``host`` and ``group``. - -Filtering the inventory ------------------------ - -You won't always want to operate over all hosts, sometimes you will want to operate over some of them based on some attributes. In order to do so the inventory can help you filtering based on it's attributes. For instance:: - - >>> inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh', 'host1.bma', 'host2.bma', 'spine00.bma', 'spine01.bma', 'leaf00.bma', 'leaf01.bma']) - >>> inventory.filter(site="bma").hosts.keys() - dict_keys(['host1.bma', 'host2.bma', 'spine00.bma', 'spine01.bma', 'leaf00.bma', 'leaf01.bma']) - >>> inventory.filter(site="bma", role="spine").hosts.keys() - dict_keys(['spine00.bma', 'spine01.bma']) - >>> inventory.filter(site="bma").filter(role="spine").hosts.keys() - dict_keys(['spine00.bma', 'spine01.bma']) - -Note in the last line that the filter is cumulative so you can do things like this: - - >>> cmh = inventory.filter(site="cmh") - >>> cmh.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh']) - >>> cmh_eos = cmh.filter(brigade_nos="eos") - >>> cmh_eos.hosts.keys() - dict_keys(['spine00.cmh', 'leaf00.cmh']) - >>> cmh_eos.filter(role="spine").hosts.keys() - dict_keys(['spine00.cmh']) - -This should give you enough room to build groups in any way you want. - -Advanced filtering -__________________ - -You can also do more complex filtering by using functions or lambdas:: - - >>> def has_long_name(host): - ... return len(host.name) == 11 - ... - >>> inventory.filter(filter_func=has_long_name).hosts.keys() - dict_keys(['spine00.cmh', 'spine01.cmh', 'spine00.bma', 'spine01.bma']) - >>> inventory.filter(filter_func=lambda h: len(h.name) == 9).hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'host1.bma', 'host2.bma']) - -Not the most useful example but it should be enough to illustrate how it works. diff --git a/examples/inventory/Vagrantfile b/docs/tutorials/intro/inventory/Vagrantfile similarity index 76% rename from examples/inventory/Vagrantfile rename to docs/tutorials/intro/inventory/Vagrantfile index d576a0d7..b0c2f21a 100644 --- a/examples/inventory/Vagrantfile +++ b/docs/tutorials/intro/inventory/Vagrantfile @@ -37,6 +37,7 @@ Vagrant.configure(2) do |config| leaf00.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false leaf00.vm.network "private_network", virtualbox__intnet: "link_3", ip: "169.254.1.11", auto_config: false + leaf00.vm.network "private_network", virtualbox__intnet: "link_5", ip: "169.254.1.11", auto_config: false end config.vm.define "leaf01" do |leaf01| @@ -46,6 +47,17 @@ Vagrant.configure(2) do |config| leaf01.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false leaf01.vm.network "private_network", virtualbox__intnet: "link_4", ip: "169.254.1.11", auto_config: false + leaf01.vm.network "private_network", virtualbox__intnet: "link_5", ip: "169.254.1.11", auto_config: false + end + + config.vm.define "host00" do |host00| + host00.vm.box = "hashicorp/precise64" + host00.vm.network "private_network", virtualbox__intnet: "link_5", ip: "10.123.231.100", auto_config: false + end + + config.vm.define "host01" do |host01| + host01.vm.box = "hashicorp/precise64" + host01.vm.network "private_network", virtualbox__intnet: "link_5", ip: "10.123.231.101", auto_config: false end end diff --git a/docs/tutorials/intro/inventory/groups.yaml b/docs/tutorials/intro/inventory/groups.yaml new file mode 100644 index 00000000..451d6bae --- /dev/null +++ b/docs/tutorials/intro/inventory/groups.yaml @@ -0,0 +1,21 @@ +--- +defaults: + domain: acme.local + +global: + domain: global.local + asn: 1 + +eu: + asn: 65100 + +bma: + groups: + - eu + - global + +cmh: + asn: 65000 + vlans: + 100: frontend + 200: backend diff --git a/examples/inventory/hosts.yaml b/docs/tutorials/intro/inventory/hosts.yaml similarity index 79% rename from examples/inventory/hosts.yaml rename to docs/tutorials/intro/inventory/hosts.yaml index 15ccd489..d59f8f33 100644 --- a/examples/inventory/hosts.yaml +++ b/docs/tutorials/intro/inventory/hosts.yaml @@ -1,15 +1,25 @@ --- host1.cmh: + brigade_host: 127.0.0.1 + brigade_ssh_port: 2201 + brigade_username: vagrant + brigade_password: vagrant site: cmh role: host - group: cmh + groups: + - cmh brigade_nos: linux type: host host2.cmh: + brigade_host: 127.0.0.1 + brigade_ssh_port: 2202 + brigade_username: vagrant + brigade_password: vagrant site: cmh role: host - group: cmh + groups: + - cmh brigade_nos: linux type: host @@ -20,7 +30,8 @@ spine00.cmh: brigade_network_api_port: 12444 site: cmh role: spine - group: cmh + groups: + - cmh brigade_nos: eos type: network_device @@ -31,7 +42,8 @@ spine01.cmh: brigade_network_api_port: 12204 site: cmh role: spine - group: cmh + groups: + - cmh brigade_nos: junos type: network_device @@ -42,7 +54,8 @@ leaf00.cmh: brigade_network_api_port: 12443 site: cmh role: leaf - group: cmh + groups: + - cmh brigade_nos: eos type: network_device asn: 65100 @@ -54,7 +67,8 @@ leaf01.cmh: brigade_network_api_port: 12203 site: cmh role: leaf - group: cmh + groups: + - cmh brigade_nos: junos type: network_device asn: 65101 @@ -62,14 +76,16 @@ leaf01.cmh: host1.bma: site: bma role: host - group: bma + groups: + - bma brigade_nos: linux type: host host2.bma: site: bma role: host - group: bma + groups: + - bma brigade_nos: linux type: host @@ -80,7 +96,8 @@ spine00.bma: brigade_network_api_port: 12444 site: bma role: spine - group: bma + groups: + - bma brigade_nos: eos type: network_device @@ -91,7 +108,8 @@ spine01.bma: brigade_network_api_port: 12204 site: bma role: spine - group: bma + groups: + - bma brigade_nos: junos type: network_device @@ -102,7 +120,8 @@ leaf00.bma: brigade_network_api_port: 12443 site: bma role: leaf - group: bma + groups: + - bma brigade_nos: eos type: network_device @@ -113,6 +132,7 @@ leaf01.bma: brigade_network_api_port: 12203 site: bma role: leaf - group: bma + groups: + - bma brigade_nos: junos type: network_device diff --git a/docs/tutorials/intro/running_tasks.rst b/docs/tutorials/intro/running_tasks.rst deleted file mode 100644 index 63edcbf9..00000000 --- a/docs/tutorials/intro/running_tasks.rst +++ /dev/null @@ -1,89 +0,0 @@ -Running tasks -============= - -Once you have your brigade objects you can start running :doc:`tasks `. The first thing you have to do is import the task you want to use:: - - >>> from brigade.plugins.tasks.commands import command - -Now you should be able to run that task for all devices:: - - >>> result = brigade.run(command, - ... command="echo hi! I am {host} and I am a {host.nos} device") - -.. note:: Note you can format strings using host data. - -This should give us a :obj:`brigade.core.task.AggregatedResult` object, which is a dictionary-like object where the key is the name of ``Host`` and the value a :obj:`brigade.core.task.Result`. - -Now, we can iterate over the object:: - - >>> for host, res in result.items(): - ... print(host + ": " + res.stdout) - ... - host1.cmh: hi! I am host1.cmh and I am a linux device - host2.cmh: hi! I am host2.cmh and I am a linux device - spine00.cmh: hi! I am spine00.cmh and I am a eos device - spine01.cmh: hi! I am spine01.cmh and I am a junos device - leaf00.cmh: hi! I am leaf00.cmh and I am a eos device - leaf01.cmh: hi! I am leaf01.cmh and I am a junos device - host1.bma: hi! I am host1.bma and I am a linux device - host2.bma: hi! I am host2.bma and I am a linux device - spine00.bma: hi! I am spine00.bma and I am a eos device - spine01.bma: hi! I am spine01.bma and I am a junos device - leaf00.bma: hi! I am leaf00.bma and I am a eos device - leaf01.bma: hi! I am leaf01.bma and I am a junos device - -Or we can use a task that knows how to operate on the :obj:`brigade.core.task.AggregatedResult` object like the task :obj:`brigade.plugins.tasks.text.print_result`:: - - >>> b.run(print_result, - ... num_workers=1, - ... data=result, - ... vars=["stdout"]) - * host1.cmh ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - hi! I am host1.cmh and I am a linux device - - * host2.cmh ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - hi! I am host2.cmh and I am a linux device - - * spine00.cmh ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - hi! I am spine00.cmh and I am a eos device - - * spine01.cmh ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - hi! I am spine01.cmh and I am a junos device - - * leaf00.cmh ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf00.cmh and I am a eos device - - * leaf01.cmh ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf01.cmh and I am a junos device - - * host1.bma ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - hi! I am host1.bma and I am a linux device - - * host2.bma ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - hi! I am host2.bma and I am a linux device - - * spine00.bma ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - hi! I am spine00.bma and I am a eos device - - * spine01.bma ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - hi! I am spine01.bma and I am a junos device - - * leaf00.bma ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf00.bma and I am a eos device - - * leaf01.bma ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf01.bma and I am a junos device - -.. note:: We need to pass ``num_workers=1`` to the ``print_result`` task because otherwise brigade will run each host at the same time using multithreading mangling the output. diff --git a/docs/tutorials/intro/running_tasks_different_hosts.rst b/docs/tutorials/intro/running_tasks_different_hosts.rst deleted file mode 100644 index 864d5b00..00000000 --- a/docs/tutorials/intro/running_tasks_different_hosts.rst +++ /dev/null @@ -1,72 +0,0 @@ -Running tasks on different groups of hosts -========================================== - -Below you can see an example where we use the ``filtering`` capabilities of ``brigade`` to run different tasks on different devices:: - - >>> switches = brigade.filter(type="network_device") - >>> hosts = brigade.filter(type="host") - >>> - >>> rs = switches.run(command, - ... command="echo I am a switch") - >>> - >>> rh = hosts.run(command, - ... command="echo I am a host") - -Because :obj:`brigade.core.task.AggregatedResult` objects behave like dictionaries you can add the results of the second task to the result of the first one:: - - >>> rs.update(rh) - -And then just print the result for all the devices:: - - >>> brigade.run(print_result, - ... num_workers=1, - ... data=rs, - ... vars=["stdout"]) - * host1.cmh ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - I am a host - - * host2.cmh ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - I am a host - - * spine00.cmh ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * spine01.cmh ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * leaf00.cmh ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * leaf01.cmh ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * host1.bma ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - I am a host - - * host2.bma ** changed : False ************************************************* - ---- command ** changed : False ----------------------------------------------- - I am a host - - * spine00.bma ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * spine01.bma ** changed : False *********************************************** - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * leaf00.bma ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - I am a switch - - * leaf01.bma ** changed : False ************************************************ - ---- command ** changed : False ----------------------------------------------- - I am a switch - diff --git a/docs/tutorials/intro/running_tasks_errors.rst b/docs/tutorials/intro/running_tasks_errors.rst deleted file mode 100644 index 7e34e1cc..00000000 --- a/docs/tutorials/intro/running_tasks_errors.rst +++ /dev/null @@ -1,390 +0,0 @@ -Dealing with task errors -======================== - -Tasks can fail due to many reasons. As we continue we will see how to deal with errors effectively with brigade. - -Failing on error by default ---------------------------- - -Brigade can raise a :obj:`brigade.core.exceptions.BrigadeExecutionError` exception automatically as soon as an error occurs. For instance:: - - >>> brigade = easy_brigade( - ... host_file="hosts.yaml", group_file="groups.yaml", - ... dry_run=True, - ... raise_on_error=True, - ... ) - >>> - >>> - >>> def task_that_sometimes_fails(task): - ... if task.host.name == "leaf00.cmh": - ... raise Exception("an uncontrolled exception happened") - ... elif task.host.name == "leaf01.cmh": - ... return Result(host=task.host, result="yikes", failed=True) - ... else: - ... return Result(host=task.host, result="swoosh") - ... - >>> - >>> b = brigade.filter(site="cmh") - >>> r = b.run(task_that_sometimes_fails) - Traceback (most recent call last): - File "", line 1, in - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run - result.raise_on_error() - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error - raise BrigadeExecutionError(self) - brigade.core.exceptions.BrigadeExecutionError: - ######################################## - # host1.cmh (succeeded) - ######################################## - swoosh - ######################################## - # host2.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine00.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine01.cmh (succeeded) - ######################################## - swoosh - ######################################## - # leaf00.cmh (failed) - ######################################## - Traceback (most recent call last): - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task - r = task._start(host=host, brigade=brigade, dry_run=dry_run) - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start - r = self.task(self, **self.params) or Result(host) - File "", line 3, in task_that_sometimes_fails - Exception: an uncontrolled exception happened - - ######################################## - # leaf01.cmh (failed) - ######################################## - yikes - -Ok, let's see what happened there. First, we configured the default behavior to raise an Exception as soon as an error occurs:: - - >>> brigade = easy_brigade( - ... host_file="hosts.yaml", group_file="groups.yaml", - ... dry_run=True, - ... raise_on_error=True, - ... ) - >>> - -Then, the following task fails with an exception for ``leaf00.cmh`` and with a controlled error on ``leaf01.cmh``. It doesn't matter if the error is controlled or not, both cases will trigger brigade to raise an Exception. - - >>> def task_that_sometimes_fails(task): - ... if task.host.name == "leaf00.cmh": - ... raise Exception("an uncontrolled exception happened") - ... elif task.host.name == "leaf01.cmh": - ... return Result(host=task.host, result="yikes", failed=True) - ... else: - ... return Result(host=task.host, result="swoosh") - ... - -Finally, when we run the task brigade fails immediately and the traceback is shown on the screen:: - - >>> b = brigade.filter(site="cmh") - >>> r = b.run(task_that_sometimes_fails) - Traceback (most recent call last): - File "", line 1, in - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run - result.raise_on_error() - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error - raise BrigadeExecutionError(self) - brigade.core.exceptions.BrigadeExecutionError: - ######################################## - # host1.cmh (succeeded) - ######################################## - swoosh - ######################################## - # host2.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine00.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine01.cmh (succeeded) - ######################################## - swoosh - ######################################## - # leaf00.cmh (failed) - ######################################## - Traceback (most recent call last): - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task - r = task._start(host=host, brigade=brigade, dry_run=dry_run) - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start - r = self.task(self, **self.params) or Result(host) - File "", line 3, in task_that_sometimes_fails - Exception: an uncontrolled exception happened - - ######################################## - # leaf01.cmh (failed) - ######################################## - yikes - -As with any other exception you can capture it:: - - >>> try: - ... r = b.run(task_that_sometimes_fails) - ... except BrigadeExecutionError as e: - ... error = e - ... - >>> - -Let's inspect the object. You can easily identify the tasks that failed:: - - >>> error.failed_hosts - {'leaf00.cmh': [], 'leaf01.cmh': []} - >>> error.failed_hosts['leaf00.cmh'][0].failed - True - >>> error.failed_hosts['leaf00.cmh'][0].result - 'Traceback (most recent call last):\n File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task\n r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start\n r = self.task(self, **self.params) or Result(host)\n File "", line 3, in task_that_sometimes_fails\nException: an uncontrolled exception happened\n' - >>> error.failed_hosts['leaf00.cmh'][0].exception - Exception('an uncontrolled exception happened',) - >>> error.failed_hosts['leaf01.cmh'][0].failed - True - >>> error.failed_hosts['leaf01.cmh'][0].result - 'yikes' - >>> error.failed_hosts['leaf01.cmh'][0].exception - >>> - -Or you can just grab the :obj:`brigade.core.task.AggregatedResult` inside the exception and do something useful with it:: - - >>> error.result.items() - dict_items([('host1.cmh', []), ('host2.cmh', []), ('spine00.cmh', []), ('spine01.cmh', []), ('leaf00.cmh', []), ('leaf01.cmh', [])]) - -Not failing by default ----------------------- - -Now, let's repeat the previous example but setting ``raise_on_error=False``:: - - >>> from brigade.core.task import Result - >>> from brigade.easy import easy_brigade - >>> from brigade.plugins.tasks.text import print_result - >>> - >>> brigade = easy_brigade( - ... host_file="hosts.yaml", group_file="groups.yaml", - ... dry_run=True, - ... raise_on_error=False, - ... ) - >>> - >>> - >>> def task_that_sometimes_fails(task): - ... if task.host.name == "leaf00.cmh": - ... raise Exception("an uncontrolled exception happened") - ... elif task.host.name == "leaf01.cmh": - ... return Result(host=task.host, result="yikes", failed=True) - ... else: - ... return Result(host=task.host, result="swoosh") - ... - >>> - >>> b = brigade.filter(site="cmh") - >>> - >>> r = b.run(task_that_sometimes_fails) - >>> - -If ``raise_on_error=False`` the result of the task will contain a :obj:`brigade.core.task.AggregatedResult` object describing what happened:: - - >>> r["leaf00.cmh"].failed - True - >>> r["leaf00.cmh"].result - 'Traceback (most recent call last):\n File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task\n r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start\n r = self.task(self, **self.params) or Result(host)\n File "", line 3, in task_that_sometimes_fails\nException: an uncontrolled exception happened\n' - >>> r["leaf00.cmh"].exception - Exception('an uncontrolled exception happened',) - >>> r["leaf01.cmh"].failed - True - >>> r["leaf01.cmh"].result - 'yikes' - >>> r["leaf01.cmh"].exception - >>> r["host1.cmh"].failed - False - >>> r["host1.cmh"].result - 'swoosh' - -Skipping Hosts --------------- - -If you set ``raise_on_error=False`` and a task fails ``brigade`` will keep track of the failing hosts and will skip the host in following tasks:: - - >>> r = b.run(task_that_sometimes_fails) - >>> r.failed - True - >>> r.failed - False - -What did just happen? Let's inspect the result:: - - >>> r.skipped - True - >>> r['leaf00.cmh'].failed - False - >>> r['leaf00.cmh'].skipped - True - >>> r['leaf00.cmh'].result - >>> r['leaf01.cmh'].failed - False - >>> r['leaf01.cmh'].skipped - True - >>> r['leaf01.cmh'].result - >>> - -As you can see the second time we ran the same tasks didn't trigger any error because the hosts that failed the first time were skipped. You can inspect which devices are on the "blacklist":: - - >>> b.data.failed_hosts - {'leaf00.cmh', 'leaf01.cmh'} - -And even whitelist them: - - >>> r = b.run(task_that_sometimes_fails) - >>> r['leaf00.cmh'].skipped - True - >>> r['leaf01.cmh'].skipped - False - >>> r['leaf01.cmh'].failed - True - -You can also reset the list of blacklisted hosts:: - - >>> b.data.failed_hosts = set() - >>> r = b.run(task_that_sometimes_fails) - >>> r['leaf00.cmh'].skipped - False - >>> r['leaf00.cmh'].failed - True - >>> r['leaf01.cmh'].skipped - False - >>> r['leaf01.cmh'].failed - True - -``AggreggatedResult`` ---------------------- - -Regardless of if you had ``raise_on_error`` set to ``True`` or ``False`` you will have access to the very same :obj:`brigade.core.task.AggregatedResult` object. The only difference is that in the former case you will have the object in the ``result`` attribute of a :obj:`brigade.core.exceptions.BrigadeExecutionError` object and on the latter you will get it in the assigned variable. - -Let's see a few things you can do with an :obj:`brigade.core.task.AggregatedResult` object:: - - >>> r - AggregatedResult: task_that_sometimes_fails - >>> r.failed - True - >>> r.failed_hosts - {'leaf00.cmh': [], 'leaf01.cmh': []} - >>> r.raise_on_error() - Traceback (most recent call last): - File "", line 1, in - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error - raise BrigadeExecutionError(self) - brigade.core.exceptions.BrigadeExecutionError: - ######################################## - # host1.cmh (succeeded) - ######################################## - swoosh - ######################################## - # host2.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine00.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine01.cmh (succeeded) - ######################################## - swoosh - ######################################## - # leaf00.cmh (failed) - ######################################## - Traceback (most recent call last): - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task - r = task._start(host=host, brigade=brigade, dry_run=dry_run) - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start - r = self.task(self, **self.params) or Result(host) - File "", line 3, in task_that_sometimes_fails - Exception: an uncontrolled exception happened - - ######################################## - # leaf01.cmh (failed) - ######################################## - yikes - -As you can see you can quickly discern if the execution failed and you can even trigger the exception automatically if needed (if no host failed ``r.raise_on_error`` will just return ``None``) - -Overriding default behavior ---------------------------- - -Regardless of the default behavior you can force ``raise_on_error`` on a per task basis:: - - >>> r = b.run(task_that_sometimes_fails, - ... raise_on_error=True) - Traceback (most recent call last): - File "", line 2, in - r = b.run(task_that_sometimes_fails, - raise_on_error=False) - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 191, in run - result.raise_on_error() - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 145, in raise_on_error - raise BrigadeExecutionError(self) - brigade.core.exceptions.BrigadeExecutionError: - ######################################## - # host1.cmh (succeeded) - ######################################## - swoosh - ######################################## - # host2.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine00.cmh (succeeded) - ######################################## - swoosh - ######################################## - # spine01.cmh (succeeded) - ######################################## - swoosh - ######################################## - # leaf00.cmh (failed) - ######################################## - Traceback (most recent call last): - File "/Users/dbarroso/workspace/brigade/brigade/core/__init__.py", line 201, in run_task - r = task._start(host=host, brigade=brigade, dry_run=dry_run) - File "/Users/dbarroso/workspace/brigade/brigade/core/task.py", line 41, in _start - r = self.task(self, **self.params) or Result(host) - File "", line 3, in task_that_sometimes_fails - Exception: an uncontrolled exception happened - - ######################################## - # leaf01.cmh (failed) - ######################################## - yikes - - >>> r = b.run(task_that_sometimes_fails, - ... raise_on_error=False) - >>> - -As you can see, regardless of what ``brigade`` had been configured to do, the task failed on the first case but didn't on the second one. - -Which one to use ----------------- - -It dependsâ„¢. As a rule of thumb it's probably safer to fail by default and capture errors explicitly. For instance, a continuation you can see an example where we run a task that can change the system and if it fails we try to run a cleanup operation and if it doesn't succeed either we blacklist the host so further tasks are skipped for that host:: - - try: - brigade.run(task_that_attempts_to_change_the_system) - except BrigadeExecutionError as e: - for host in e.failed_hosts.keys(): - r = brigade.filter(name=host).run(task_that_reverts_changes, - raise_on_error=True) - if r.failed: - brigade.data.failed_hosts.add(host) - -In other simpler cases it might be just simpler and completely safe to ignore errors:: - - r = brigade.run(a_task_that_is_safe_if_it_fails) - brigade.run(print_result, - data=result) diff --git a/docs/tutorials/intro/running_tasks_grouping.rst b/docs/tutorials/intro/running_tasks_grouping.rst deleted file mode 100644 index f3330cf7..00000000 --- a/docs/tutorials/intro/running_tasks_grouping.rst +++ /dev/null @@ -1,227 +0,0 @@ -Grouping tasks -============== - -Sometimes it is useful to group tasks either for reusability purposes or to speed up the execution (see :doc:`execution model `). Creating groups of tasks is very easy, for instance:: - - def group_of_tasks(task): - task.run(command, - command="echo hi! I am {host} and I am a {host.nos} device") - task.run(command, - command="echo hi! I am a {host[type]}") - -Groups of tasks are called as regular tasks:: - - >>> b = brigade.filter(site="cmh") - >>> result = b.run(group_of_tasks) - >>> - >>> - >>> b.run(print_result, - ... num_workers=1, - ... data=result, - ... vars=["stdout"]) - * host1.cmh ** changed : False ************************************************* - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am host1.cmh and I am a linux device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a host - - * host2.cmh ** changed : False ************************************************* - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am host2.cmh and I am a linux device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a host - - * spine00.cmh ** changed : False *********************************************** - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am spine00.cmh and I am a eos device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a network_device - - * spine01.cmh ** changed : False *********************************************** - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am spine01.cmh and I am a junos device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a network_device - - * leaf00.cmh ** changed : False ************************************************ - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf00.cmh and I am a eos device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a network_device - - * leaf01.cmh ** changed : False ************************************************ - ---- group_of_tasks ** changed : False ---------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - hi! I am leaf01.cmh and I am a junos device - - ---- command ** changed : False ----------------------------------------------- - hi! I am a network_device - -Groups of tasks return for each host a :obj:`brigade.core.task.MultiResult` object which is a list-like object of :obj:`brigade.core.task.Result`. The object will contain the result for each individual task within the group of tasks:: - - >>> result["leaf01.cmh"].__class__ - - >>> result["leaf01.cmh"][0].name - 'group_of_tasks' - >>> result["leaf01.cmh"][1].name - 'command' - >>> result["leaf01.cmh"][1].result - 'hi! I am leaf01.cmh and I am a junos device\n' - -.. note:: Position ``0`` will be the result for the grouping itself while the rest will be the results for the task inside in the same order as defined in there. - -Groups of tasks can also return their own result if needed:: - - >>> from brigade.core.task import Result - >>> - >>> - >>> def group_of_tasks_with_result(task): - ... task.run(command, - ... command="echo hi! I am {host} and I am a {host.nos} device") - ... task.run(command, - ... command="echo hi! I am a {host[type]}") - ... return Result(host=task.host, result="Yippee ki-yay") - ... - >>> result = b.run(group_of_tasks_with_result) - >>> - >>> result["leaf01.cmh"][0].name - 'group_of_tasks_with_result' - >>> result["leaf01.cmh"][0].result - 'Yippee ki-yay' - -Accessing host data -------------------- - -Something interesting about groupings is that you can access host data from them. For instance:: - - >>> def access_host_data(task): - ... if task.host.nos == "eos": - ... task.host["my-new-var"] = "setting a new var for eos" - ... elif task.host.nos == "junos": - ... task.host["my-new-var"] = "setting a new var for junos" - ... - >>> - >>> b.run(access_host_data) - >>> - >>> b.inventory.hosts["leaf00.cmh"]["my-new-var"] - 'setting a new var for eos' - >>> b.inventory.hosts["leaf01.cmh"]["my-new-var"] - 'setting a new var for junos' - -Reusability ------------ - -We mentioned earlier that groups of tasks where also useful for reusability purposes. Let's see it with an example:: - - >>> def count(task, to): - ... task.run(command, - ... command="echo {}".format(list(range(0, to)))) - ... - -Great, we created a super complex task that can count up to an arbitrary number. Let's count to 10:: - - >>> result = b.run(count, - ... to=10) - >>> - >>> - >>> b.run(print_result, - ... num_workers=1, - ... data=result, - ... vars=["stdout"]) - * host1.cmh ** changed : False ************************************************* - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - * host2.cmh ** changed : False ************************************************* - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - * spine00.cmh ** changed : False *********************************************** - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - * spine01.cmh ** changed : False *********************************************** - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - * leaf00.cmh ** changed : False ************************************************ - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - * leaf01.cmh ** changed : False ************************************************ - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - -And now to 20:: - - >>> result = b.run(count, - ... to=20) - >>> - >>> b.run(print_result, - ... num_workers=1, - ... data=result, - ... vars=["stdout"]) - * host1.cmh ** changed : False ************************************************* - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - - * host2.cmh ** changed : False ************************************************* - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - - * spine00.cmh ** changed : False *********************************************** - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - - * spine01.cmh ** changed : False *********************************************** - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - - * leaf00.cmh ** changed : False ************************************************ - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - - * leaf01.cmh ** changed : False ************************************************ - ---- count ** changed : False ------------------------------------------------- - - ---- command ** changed : False ----------------------------------------------- - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] - diff --git a/docs/tutorials/intro/task_results.ipynb b/docs/tutorials/intro/task_results.ipynb new file mode 100644 index 00000000..e9cbd0cb --- /dev/null +++ b/docs/tutorials/intro/task_results.ipynb @@ -0,0 +1,501 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Processing results\n", + "\n", + "In this tutorial we are going to see how we can process the results of running tasks.\n", + "\n", + "Let's start with some code we have seen already:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from brigade.core import InitBrigade\n", + "from brigade.plugins.tasks import networking, text\n", + "from brigade.plugins.functions.text import print_title\n", + "import logging\n", + "\n", + "brg = InitBrigade(config_file=\"config.yaml\", dry_run=True)\n", + "cmh = brg.filter(site=\"cmh\", type=\"network_device\")\n", + "\n", + "def basic_configuration(task):\n", + " # Transform inventory data to configuration via a template file\n", + " r = task.run(task=text.template_file,\n", + " name=\"Base Configuration\",\n", + " template=\"base.j2\",\n", + " path=f\"templates/{task.host.nos}\",\n", + " severity_level=logging.DEBUG)\n", + "\n", + " # Save the compiled configuration into a host variable\n", + " task.host[\"config\"] = r.result\n", + "\n", + " # Deploy that configuration to the device using NAPALM\n", + " task.run(task=networking.napalm_configure,\n", + " name=\"Loading Configuration on the device\",\n", + " replace=False,\n", + " configuration=task.host[\"config\"],\n", + " severity_level=logging.INFO)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's call the task group so we can start inspecting the result object:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "result = cmh.run(task=basic_configuration)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The easy way\n", + "\n", + "Most of the time you will just want to provide some feedback on what's going on. For that you can use [print_result](../../plugins/functions/text.rst#brigade.plugins.functions.text.print_result) function:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[36mbasic_configuration*************************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.local;\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.local;\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "from brigade.plugins.functions.text import print_result\n", + "\n", + "print_result(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should also be able to print a single host:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print_result(result[\"spine00.cmh\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or even a single task:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print_result(result[\"spine00.cmh\"][2])" + ] + }, + { + "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", + "\n", + "Now let's tell `print_result` to print tasks marked as `DEBUG`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[36mbasic_configuration*************************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m---- Base Configuration ** changed : False ------------------------------------- DEBUG\u001b[0m\n", + "\u001b[0mhostname spine00.cmh\n", + "ip domain-name cmh.acme.local\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m---- Base Configuration ** changed : False ------------------------------------- DEBUG\u001b[0m\n", + "\u001b[0msystem {\n", + " host-name spine01.cmh;\n", + " domain-name cmh.acme.local;\n", + "}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name spine01.cmh;\n", + "+ domain-name cmh.acme.local;\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m---- Base Configuration ** changed : False ------------------------------------- DEBUG\u001b[0m\n", + "\u001b[0mhostname leaf00.cmh\n", + "ip domain-name cmh.acme.local\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname leaf00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32mvvvv basic_configuration ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m---- Base Configuration ** changed : False ------------------------------------- DEBUG\u001b[0m\n", + "\u001b[0msystem {\n", + " host-name leaf01.cmh;\n", + " domain-name cmh.acme.local;\n", + "}\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[33m---- Loading Configuration on the device ** changed : True --------------------- INFO\u001b[0m\n", + "\u001b[0m[edit system]\n", + "- host-name vsrx;\n", + "+ host-name leaf01.cmh;\n", + "+ domain-name cmh.acme.local;\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[32m^^^^ END basic_configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print_result(result, severity_level=logging.DEBUG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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#brigade.core.task.AggregatedResult). This object is a dict-like object you can use to iterate over or access directly hosts:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AggregatedResult (basic_configuration): {'spine00.cmh': MultiResult: [Result: \"basic_configuration\", Result: \"Base Configuration\", Result: \"Loading Configuration on the device\"], 'spine01.cmh': MultiResult: [Result: \"basic_configuration\", Result: \"Base Configuration\", Result: \"Loading Configuration on the device\"], 'leaf00.cmh': MultiResult: [Result: \"basic_configuration\", Result: \"Base Configuration\", Result: \"Loading Configuration on the device\"], 'leaf01.cmh': MultiResult: [Result: \"basic_configuration\", Result: \"Base Configuration\", Result: \"Loading Configuration on the device\"]}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MultiResult: [Result: \"basic_configuration\", Result: \"Base Configuration\", Result: \"Loading Configuration on the device\"]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[\"spine00.cmh\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You probably noticed that inside each key in [AggregatedResult](../../ref/api/task.rst#brigade.core.task.AggregatedResult) there is a [MultiResult](../../ref/api/task.rst#brigade.core.task.MultiResult) object. This object is a list-like object you can use to iterate over or access any [Result](../../ref/api/task.rst#brigade.core.task.Result) you want:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result: \"basic_configuration\"" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[\"spine00.cmh\"][0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both `MultiResult` and `Result` should clearly indicate if there was some error or change in the system:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "changed: \u001b[0m \u001b[0mTrue\u001b[0m\n", + "\u001b[0mfailed: \u001b[0m \u001b[0mFalse\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print(\"changed: \", result[\"spine00.cmh\"].changed)\n", + "print(\"failed: \", result[\"spine00.cmh\"].failed)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "changed: \u001b[0m \u001b[0mFalse\u001b[0m\n", + "\u001b[0mfailed: \u001b[0m \u001b[0mFalse\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print(\"changed: \", result[\"spine00.cmh\"][0].changed)\n", + "print(\"failed: \", result[\"spine00.cmh\"][0].failed)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should be able to access any extra data a particular task might have set:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "@@ -7,6 +7,9 @@\n", + " action bash sudo /mnt/flash/initialize_ma1.sh\n", + " !\n", + " transceiver qsfp default-mode 4x10G\n", + "+!\n", + "+hostname spine00.cmh\n", + "+ip domain-name cmh.acme.local\n", + " !\n", + " spanning-tree mode mstp\n", + " !\u001b[0m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "print(result[\"spine00.cmh\"][2].diff)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This latter will depend on the task executed so you will have to refer to the documentation of the task to see what might have been populated by it." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/templates/eos/base.j2 b/docs/tutorials/intro/templates/eos/base.j2 similarity index 98% rename from examples/templates/eos/base.j2 rename to docs/tutorials/intro/templates/eos/base.j2 index d9261d07..692a00cd 100644 --- a/examples/templates/eos/base.j2 +++ b/docs/tutorials/intro/templates/eos/base.j2 @@ -1,3 +1,2 @@ hostname {{ host }} ip domain-name {{ site }}.{{ domain }} - diff --git a/examples/templates/junos/base.j2 b/docs/tutorials/intro/templates/junos/base.j2 similarity index 98% rename from examples/templates/junos/base.j2 rename to docs/tutorials/intro/templates/junos/base.j2 index 518bb1ae..775270c8 100644 --- a/examples/templates/junos/base.j2 +++ b/docs/tutorials/intro/templates/junos/base.j2 @@ -2,4 +2,3 @@ system { host-name {{ host }}; domain-name {{ site }}.{{ domain }}; } - diff --git a/examples/1_simple_runbooks/backup.ipynb b/examples/1_simple_runbooks/backup.ipynb deleted file mode 100644 index 8f5956ec..00000000 --- a/examples/1_simple_runbooks/backup.ipynb +++ /dev/null @@ -1,557 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Backup\n", - "\n", - "This runbook is going to download the configuration of the devices and save it under `./backup/$hostname`. It also reports changes as we will a continuation.\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Runbook that downloads the configuration from the devices and\n",
-       " 4 stores them on disk.\n",
-       " 5 """\n",
-       " 6 from brigade.easy import easy_brigade\n",
-       " 7 from brigade.plugins.tasks import files, networking, text\n",
-       " 8 \n",
-       " 9 \n",
-       "10 def backup(task):\n",
-       "11     """\n",
-       "12     This function groups two tasks:\n",
-       "13         1. Download configuration from the device\n",
-       "14         2. Store to disk\n",
-       "15     """\n",
-       "16     result = task.run(networking.napalm_get,\n",
-       "17                       name="Gathering configuration",\n",
-       "18                       getters="config")\n",
-       "19 \n",
-       "20     task.run(files.write,\n",
-       "21              name="Saving Configuration to disk",\n",
-       "22              content=result.result["config"]["running"],\n",
-       "23              filename="./backups/{}".format(task.host))\n",
-       "24 \n",
-       "25 \n",
-       "26 brg = easy_brigade(\n",
-       "27         host_file="../inventory/hosts.yaml",\n",
-       "28         group_file="../inventory/groups.yaml",\n",
-       "29         dry_run=False,\n",
-       "30         raise_on_error=True,\n",
-       "31 )\n",
-       "32 \n",
-       "33 # select which devices we want to work with\n",
-       "34 filtered = brg.filter(type="network_device", site="cmh")\n",
-       "35 \n",
-       "36 # Run the ``backup`` function that groups the tasks to\n",
-       "37 # download/store devices' configuration\n",
-       "38 results = filtered.run(backup,\n",
-       "39                        name="Backing up configurations")\n",
-       "40 \n",
-       "41 # Let's print the result on screen\n",
-       "42 filtered.run(text.print_result,\n",
-       "43              num_workers=1,  # task should be done synchronously\n",
-       "44              data=results,\n",
-       "45              task_id=-1,  # we only want to print the last task\n",
-       "46              )\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file backup.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Let's run the command for the first time (note we are cleaning first `./backups/` folder to pretend each run of the following cell is the first one):" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- ./backups/spine00.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,34 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: localhost (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - "+!\n", - "+interface Ethernet1\n", - "+!\n", - "+interface Ethernet2\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+no ip routing\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- ./backups/spine01.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,70 @@\n", - "\n", - "+\n", - "+## Last commit: 2018-01-14 14:33:48 UTC by vagrant\n", - "+version 12.1X47-D20.7;\n", - "+system {\n", - "+ host-name vsrx;\n", - "+ root-authentication {\n", - "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", - "+ }\n", - "+ login {\n", - "+ user vagrant {\n", - "+ uid 2000;\n", - "+ class super-user;\n", - "+ authentication {\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ services {\n", - "+ ssh {\n", - "+ root-login allow;\n", - "+ }\n", - "+ netconf {\n", - "+ ssh;\n", - "+ }\n", - "+ web-management {\n", - "+ http {\n", - "+ interface ge-0/0/0.0;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ syslog {\n", - "+ user * {\n", - "+ any emergency;\n", - "+ }\n", - "+ file messages {\n", - "+ any any;\n", - "+ authorization info;\n", - "+ }\n", - "+ file interactive-commands {\n", - "+ interactive-commands any;\n", - "+ }\n", - "+ }\n", - "+ license {\n", - "+ autoupdate {\n", - "+ url https://ae1.juniper.net/junos/key_retrieval;\n", - "+ }\n", - "+ }\n", - "+}\n", - "+interfaces {\n", - "+ ge-0/0/0 {\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ dhcp;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+security {\n", - "+ forwarding-options {\n", - "+ family {\n", - "+ inet6 {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ mpls {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- ./backups/leaf00.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,34 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: localhost (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - "+!\n", - "+interface Ethernet1\n", - "+!\n", - "+interface Ethernet2\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+no ip routing\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- ./backups/leaf01.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,70 @@\n", - "\n", - "+\n", - "+## Last commit: 2018-01-14 14:33:48 UTC by vagrant\n", - "+version 12.1X47-D20.7;\n", - "+system {\n", - "+ host-name vsrx;\n", - "+ root-authentication {\n", - "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\";\n", - "+ }\n", - "+ login {\n", - "+ user vagrant {\n", - "+ uid 2000;\n", - "+ class super-user;\n", - "+ authentication {\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ services {\n", - "+ ssh {\n", - "+ root-login allow;\n", - "+ }\n", - "+ netconf {\n", - "+ ssh;\n", - "+ }\n", - "+ web-management {\n", - "+ http {\n", - "+ interface ge-0/0/0.0;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ syslog {\n", - "+ user * {\n", - "+ any emergency;\n", - "+ }\n", - "+ file messages {\n", - "+ any any;\n", - "+ authorization info;\n", - "+ }\n", - "+ file interactive-commands {\n", - "+ interactive-commands any;\n", - "+ }\n", - "+ }\n", - "+ license {\n", - "+ autoupdate {\n", - "+ url https://ae1.juniper.net/junos/key_retrieval;\n", - "+ }\n", - "+ }\n", - "+}\n", - "+interfaces {\n", - "+ ge-0/0/0 {\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ dhcp;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+security {\n", - "+ forwarding-options {\n", - "+ family {\n", - "+ inet6 {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ mpls {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%rm backups/* > /dev/null\n", - "%run backup.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's run it again to see how ``brigade`` detects no changes in the backup:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run backup.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's change the device's hostname and run the backup tool again:\n", - "\n", - " localhost(config)#hostname blah\n", - " blah(config)# end" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- ./backups/spine00.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -1,5 +1,5 @@\n", - "\n", - " ! Command: show running-config\n", - "-! device: localhost (vEOS, EOS-4.17.5M)\n", - "+! device: blah (vEOS, EOS-4.17.5M)\n", - " !\n", - " ! boot system flash:/vEOS-lab.swi\n", - " !\n", - "@@ -8,6 +8,8 @@\n", - "\n", - " action bash sudo /mnt/flash/initialize_ma1.sh\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname blah\n", - " !\n", - " spanning-tree mode mstp\n", - " !\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : False --------------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run backup.py" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/1_simple_runbooks/backup.py b/examples/1_simple_runbooks/backup.py deleted file mode 100755 index baec0417..00000000 --- a/examples/1_simple_runbooks/backup.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -""" -Runbook that downloads the configuration from the devices and -stores them on disk. -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import files, networking, text - - -def backup(task): - """ - This function groups two tasks: - 1. Download configuration from the device - 2. Store to disk - """ - result = task.run(networking.napalm_get, - name="Gathering configuration", - getters="config") - - task.run(files.write, - name="Saving Configuration to disk", - content=result.result["config"]["running"], - filename="./backups/{}".format(task.host)) - - -brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=True, -) - -# select which devices we want to work with -filtered = brg.filter(type="network_device", site="cmh") - -# Run the ``backup`` function that groups the tasks to -# download/store devices' configuration -results = filtered.run(backup, - name="Backing up configurations") - -# Let's print the result on screen -filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - ) diff --git a/examples/1_simple_runbooks/configure.ipynb b/examples/1_simple_runbooks/configure.ipynb deleted file mode 100644 index 2f409b64..00000000 --- a/examples/1_simple_runbooks/configure.ipynb +++ /dev/null @@ -1,673 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Configure\n", - "\n", - "This is a runbook to configure the network. To do so we are going to load first some data from the directory `../extra_data/` and then a bunch of templates to generate, based on that extra data, the configuration for the devices.\n", - "\n", - "## Extra data\n", - "\n", - "Let's first look at the extra data we are going to use:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../extra_data/leaf00.cmh:\r\n", - "l3.yaml\r\n", - "\r\n", - "../extra_data/leaf01.cmh:\r\n", - "l3.yaml\r\n", - "\r\n", - "../extra_data/spine00.cmh:\r\n", - "l3.yaml\r\n", - "\r\n", - "../extra_data/spine01.cmh:\r\n", - "l3.yaml\r\n" - ] - } - ], - "source": [ - "%ls ../extra_data/*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's look at one of the files for reference:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---\r\n", - "interfaces:\r\n", - " Ethernet1:\r\n", - " connects_to: spine00.cmh\r\n", - " ipv4: 10.0.0.1/31\r\n", - " enabled: false\r\n", - " Ethernet2:\r\n", - " connects_to: spine01.cmh\r\n", - " ipv4: 10.0.1.1/31\r\n", - " enabled: true\r\n", - "\r\n", - "sessions:\r\n", - " - ipv4: 10.0.0.0\r\n", - " peer_as: 65000\r\n", - " - ipv4: 10.0.1.0\r\n", - " peer_as: 65000\r\n" - ] - } - ], - "source": [ - "% cat ../extra_data/leaf00.cmh/l3.yaml" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Templates\n", - "\n", - "To configure the network we will transform the data we saw before into actual configurationusing jinja2 templates. The templates are:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../templates/eos:\r\n", - "base.j2 interfaces.j2 leaf.j2 routing.j2 spine.j2\r\n", - "\r\n", - "../templates/junos:\r\n", - "base.j2 interfaces.j2 leaf.j2 routing.j2 spine.j2\r\n" - ] - } - ], - "source": [ - "%ls ../templates/*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As an example, let's look how the ``interfaces.j2`` template will consume the extra data we saw before to configure the interfaces:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{% for interface, data in l3.interfaces.items() %}\r\n", - "interface {{ interface }}\r\n", - " no switchport\r\n", - " ip address {{ data.ipv4 }}\r\n", - " description link to {{ data.connects_to }}\r\n", - " {{ \"no\" if data.enabled else \"\" }} shutdown\r\n", - "{% endfor %}\r\n", - "\r\n" - ] - } - ], - "source": [ - "%cat ../templates/eos/interfaces.j2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Code\n", - "\n", - "Now let's look at the code that will sticth everything together:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Runbook to configure datacenter\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.functions.text import print_title\n",
-       " 7 from brigade.plugins.tasks import data, networking, text\n",
-       " 8 \n",
-       " 9 \n",
-       "10 def configure(task):\n",
-       "11     """\n",
-       "12     This function groups all the tasks needed to configure the\n",
-       "13     network:\n",
-       "14 \n",
-       "15         1. Loading extra data\n",
-       "16         2. Templates to build configuration\n",
-       "17         3. Deploy configuration on the device\n",
-       "18     """\n",
-       "19     r = task.run(text.template_file,\n",
-       "20                  name="Base Configuration",\n",
-       "21                  template="base.j2",\n",
-       "22                  path="../templates/{brigade_nos}")\n",
-       "23     # r.result holds the result of rendering the template\n",
-       "24     # we store in the host itself so we can keep updating\n",
-       "25     # it as we render other templates\n",
-       "26     task.host["config"] = r.result\n",
-       "27 \n",
-       "28     r = task.run(data.load_yaml,\n",
-       "29                  name="Loading extra data",\n",
-       "30                  file="../extra_data/{host}/l3.yaml")\n",
-       "31     # r.result holds the data contained in the yaml files\n",
-       "32     # we load the data inside the host itself for further use\n",
-       "33     task.host["l3"] = r.result\n",
-       "34 \n",
-       "35     r = task.run(text.template_file,\n",
-       "36                  name="Interfaces Configuration",\n",
-       "37                  template="interfaces.j2",\n",
-       "38                  path="../templates/{brigade_nos}")\n",
-       "39     # we update our hosts' config\n",
-       "40     task.host["config"] += r.result\n",
-       "41 \n",
-       "42     r = task.run(text.template_file,\n",
-       "43                  name="Routing Configuration",\n",
-       "44                  template="routing.j2",\n",
-       "45                  path="../templates/{brigade_nos}")\n",
-       "46     # we update our hosts' config\n",
-       "47     task.host["config"] += r.result\n",
-       "48 \n",
-       "49     r = task.run(text.template_file,\n",
-       "50                  name="Role-specific Configuration",\n",
-       "51                  template="{role}.j2",\n",
-       "52                  path="../templates/{brigade_nos}")\n",
-       "53     # we update our hosts' config\n",
-       "54     task.host["config"] += r.result\n",
-       "55 \n",
-       "56     task.run(networking.napalm_configure,\n",
-       "57              name="Loading Configuration on the device",\n",
-       "58              replace=False,\n",
-       "59              configuration=task.host["config"])\n",
-       "60 \n",
-       "61 \n",
-       "62 brg = easy_brigade(\n",
-       "63         host_file="../inventory/hosts.yaml",\n",
-       "64         group_file="../inventory/groups.yaml",\n",
-       "65         dry_run=False,\n",
-       "66         raise_on_error=True,\n",
-       "67 )\n",
-       "68 \n",
-       "69 \n",
-       "70 # select which devices we want to work with\n",
-       "71 filtered = brg.filter(type="network_device", site="cmh")\n",
-       "72 \n",
-       "73 results = filtered.run(task=configure)\n",
-       "74 \n",
-       "75 print_title("Playbook to configure the network")\n",
-       "76 filtered.run(text.print_result,\n",
-       "77              name="Configure device",\n",
-       "78              num_workers=1,  # task should be done synchronously\n",
-       "79              data=results,\n",
-       "80              task_id=-1,  # we only want to print the last task\n",
-       "81              )\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file configure.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Finally let's see everything in action:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,7 +8,8 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "-hostname localhost\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - " !\n", - " spanning-tree mode mstp\n", - " !\n", - "@@ -20,13 +21,28 @@\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - " !\n", - " interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65000;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,6 +8,9 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - " spanning-tree mode mstp\n", - " !\n", - " aaa authorization exec default local\n", - "@@ -17,14 +20,36 @@\n", - " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - " !\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - " interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name leaf01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to spine00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.0.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to spine01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65101;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.0.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ neighbor 10.0.1.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\n", - "+ vlans {\n", - "+ backend {\n", - "+ vlan-id 200;\n", - "+ }\n", - "+ frontend {\n", - "+ vlan-id 100;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The tool also detects unwanted changes and corrects them. For instance, let's change the hostname manually:\n", - "\n", - " spine00.cmh((config)#hostname localhost\n", - " localhost(config)#\n", - "\n", - "And run the runbook again:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,7 +8,7 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "-hostname localhost\n", - "+hostname spine00.cmh\n", - " ip domain-name cmh.acme.com\n", - " !\n", - " spanning-tree mode mstp\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/1_simple_runbooks/configure.py b/examples/1_simple_runbooks/configure.py deleted file mode 100755 index d120a723..00000000 --- a/examples/1_simple_runbooks/configure.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -""" -Runbook to configure datacenter -""" -from brigade.easy import easy_brigade -from brigade.plugins.functions.text import print_title -from brigade.plugins.tasks import data, networking, text - - -def configure(task): - """ - This function groups all the tasks needed to configure the - network: - - 1. Loading extra data - 2. Templates to build configuration - 3. Deploy configuration on the device - """ - r = task.run(text.template_file, - name="Base Configuration", - template="base.j2", - path="../templates/{brigade_nos}") - # r.result holds the result of rendering the template - # we store in the host itself so we can keep updating - # it as we render other templates - task.host["config"] = r.result - - r = task.run(data.load_yaml, - name="Loading extra data", - file="../extra_data/{host}/l3.yaml") - # r.result holds the data contained in the yaml files - # we load the data inside the host itself for further use - task.host["l3"] = r.result - - r = task.run(text.template_file, - name="Interfaces Configuration", - template="interfaces.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - r = task.run(text.template_file, - name="Routing Configuration", - template="routing.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - r = task.run(text.template_file, - name="Role-specific Configuration", - template="{role}.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - task.run(networking.napalm_configure, - name="Loading Configuration on the device", - replace=False, - configuration=task.host["config"]) - - -brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=True, -) - - -# select which devices we want to work with -filtered = brg.filter(type="network_device", site="cmh") - -results = filtered.run(task=configure) - -print_title("Playbook to configure the network") -filtered.run(text.print_result, - name="Configure device", - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - ) diff --git a/examples/1_simple_runbooks/get_facts.ipynb b/examples/1_simple_runbooks/get_facts.ipynb deleted file mode 100644 index 920b6eba..00000000 --- a/examples/1_simple_runbooks/get_facts.ipynb +++ /dev/null @@ -1,662 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "hide_input": true - }, - "source": [ - "# Get Facts" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Following runbook will connect to devices in the site \"cmh\" and gather information about basic facts and interfaces.\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Very simple runbook to get facts and print them on the screen.\n",
-       " 4 """\n",
-       " 5 \n",
-       " 6 from brigade.easy import easy_brigade\n",
-       " 7 from brigade.plugins.functions.text import print_title\n",
-       " 8 from brigade.plugins.tasks import networking, text\n",
-       " 9 \n",
-       "10 \n",
-       "11 brg = easy_brigade(\n",
-       "12         host_file="../inventory/hosts.yaml",\n",
-       "13         group_file="../inventory/groups.yaml",\n",
-       "14         dry_run=False,\n",
-       "15         raise_on_error=False,\n",
-       "16 )\n",
-       "17 \n",
-       "18 print_title("Getting interfaces and facts")\n",
-       "19 \n",
-       "20 # select which devices we want to work with\n",
-       "21 filtered = brg.filter(type="network_device", site="cmh")\n",
-       "22 \n",
-       "23 # we are going to gather "interfaces" and "facts"\n",
-       "24 # information with napalm\n",
-       "25 results = filtered.run(networking.napalm_get,\n",
-       "26                        getters=["interfaces", "facts"])\n",
-       "27 \n",
-       "28 # Let's print the result on screen\n",
-       "29 filtered.run(text.print_result,\n",
-       "30              num_workers=1,  # task should be done synchronously\n",
-       "31              data=results,\n",
-       "32              )\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file get_facts.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Let's run it:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[32m**** Getting interfaces and facts **********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine00.cmh.cmh.acme.com'\u001b[0m,\n", - " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", - " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", - " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", - " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'uptime'\u001b[0m: \u001b[0m76742\u001b[0m,\n", - " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516010990.2331386\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939602.295958\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939616.808321\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine01.cmh.cmh.acme.com'\u001b[0m,\n", - " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m'ge-0/0/0'\u001b[0m,\n", - " \u001b[0m'gr-0/0/0'\u001b[0m,\n", - " \u001b[0m'ip-0/0/0'\u001b[0m,\n", - " \u001b[0m'lsq-0/0/0'\u001b[0m,\n", - " \u001b[0m'lt-0/0/0'\u001b[0m,\n", - " \u001b[0m'mt-0/0/0'\u001b[0m,\n", - " \u001b[0m'sp-0/0/0'\u001b[0m,\n", - " \u001b[0m'ge-0/0/1'\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m,\n", - " \u001b[0m'.local.'\u001b[0m,\n", - " \u001b[0m'dsc'\u001b[0m,\n", - " \u001b[0m'gre'\u001b[0m,\n", - " \u001b[0m'ipip'\u001b[0m,\n", - " \u001b[0m'irb'\u001b[0m,\n", - " \u001b[0m'lo0'\u001b[0m,\n", - " \u001b[0m'lsi'\u001b[0m,\n", - " \u001b[0m'mtun'\u001b[0m,\n", - " \u001b[0m'pimd'\u001b[0m,\n", - " \u001b[0m'pime'\u001b[0m,\n", - " \u001b[0m'pp0'\u001b[0m,\n", - " \u001b[0m'ppd0'\u001b[0m,\n", - " \u001b[0m'ppe0'\u001b[0m,\n", - " \u001b[0m'st0'\u001b[0m,\n", - " \u001b[0m'tap'\u001b[0m,\n", - " \u001b[0m'vlan'\u001b[0m]\u001b[0m,\n", - " \u001b[0m'model'\u001b[0m: \u001b[0m'FIREFLY-PERIMETER'\u001b[0m,\n", - " \u001b[0m'os_version'\u001b[0m: \u001b[0m'12.1X47-D20.7'\u001b[0m,\n", - " \u001b[0m'serial_number'\u001b[0m: \u001b[0m'5efd44465d10'\u001b[0m,\n", - " \u001b[0m'uptime'\u001b[0m: \u001b[0m76648\u001b[0m,\n", - " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Juniper'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'.local.'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'dsc'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:AA:8C:76'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:1A:7F:BF'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76597.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:70:E5:81'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'gr-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'gre'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ip-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ipip'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'irb'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'4C:96:14:8C:76:B0'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lo0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lsi'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lsq-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76598.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'02:96:14:8C:76:B3'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'mt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'mtun'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pimd'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pime'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pp0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ppd0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ppe0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sp-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76598.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'st0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'tap'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'vlan'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76607.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'00:00:00:00:00:00'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'leaf00.cmh.cmh.acme.com'\u001b[0m,\n", - " \u001b[0m'hostname'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", - " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", - " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", - " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'uptime'\u001b[0m: \u001b[0m76556\u001b[0m,\n", - " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine00.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516010957.639385\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine01.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939788.3633773\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1515939804.0736248\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'leaf01.cmh.cmh.acme.com'\u001b[0m,\n", - " \u001b[0m'hostname'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'interface_list'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m'ge-0/0/0'\u001b[0m,\n", - " \u001b[0m'gr-0/0/0'\u001b[0m,\n", - " \u001b[0m'ip-0/0/0'\u001b[0m,\n", - " \u001b[0m'lsq-0/0/0'\u001b[0m,\n", - " \u001b[0m'lt-0/0/0'\u001b[0m,\n", - " \u001b[0m'mt-0/0/0'\u001b[0m,\n", - " \u001b[0m'sp-0/0/0'\u001b[0m,\n", - " \u001b[0m'ge-0/0/1'\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m,\n", - " \u001b[0m'.local.'\u001b[0m,\n", - " \u001b[0m'dsc'\u001b[0m,\n", - " \u001b[0m'gre'\u001b[0m,\n", - " \u001b[0m'ipip'\u001b[0m,\n", - " \u001b[0m'irb'\u001b[0m,\n", - " \u001b[0m'lo0'\u001b[0m,\n", - " \u001b[0m'lsi'\u001b[0m,\n", - " \u001b[0m'mtun'\u001b[0m,\n", - " \u001b[0m'pimd'\u001b[0m,\n", - " \u001b[0m'pime'\u001b[0m,\n", - " \u001b[0m'pp0'\u001b[0m,\n", - " \u001b[0m'ppd0'\u001b[0m,\n", - " \u001b[0m'ppe0'\u001b[0m,\n", - " \u001b[0m'st0'\u001b[0m,\n", - " \u001b[0m'tap'\u001b[0m,\n", - " \u001b[0m'vlan'\u001b[0m]\u001b[0m,\n", - " \u001b[0m'model'\u001b[0m: \u001b[0m'FIREFLY-PERIMETER'\u001b[0m,\n", - " \u001b[0m'os_version'\u001b[0m: \u001b[0m'12.1X47-D20.7'\u001b[0m,\n", - " \u001b[0m'serial_number'\u001b[0m: \u001b[0m'9d842799f666'\u001b[0m,\n", - " \u001b[0m'uptime'\u001b[0m: \u001b[0m76460\u001b[0m,\n", - " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Juniper'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'.local.'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'dsc'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:AA:8C:76'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine00.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:A0:42:60'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to spine01.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76407.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:00:6D:5A'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m,\n", - " \u001b[0m'gr-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'gre'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ip-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ipip'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'irb'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'4C:96:14:8C:76:B0'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lo0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lsi'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lsq-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76408.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'lt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'02:96:14:8C:76:B3'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[0m'mt-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'mtun'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pimd'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pime'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'pp0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ppd0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ppe0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sp-0/0/0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76408.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m800\u001b[0m}\u001b[0m,\n", - " \u001b[0m'st0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'None'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'tap'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m-1.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'Unspecified'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m-1\u001b[0m}\u001b[0m,\n", - " \u001b[0m'vlan'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m76417.0\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'00:00:00:00:00:00'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run get_facts.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/1_simple_runbooks/get_facts.py b/examples/1_simple_runbooks/get_facts.py deleted file mode 100755 index 5b308998..00000000 --- a/examples/1_simple_runbooks/get_facts.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -""" -Very simple runbook to get facts and print them on the screen. -""" - -from brigade.easy import easy_brigade -from brigade.plugins.functions.text import print_title -from brigade.plugins.tasks import networking, text - - -brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=False, -) - -print_title("Getting interfaces and facts") - -# select which devices we want to work with -filtered = brg.filter(type="network_device", site="cmh") - -# we are going to gather "interfaces" and "facts" -# information with napalm -results = filtered.run(networking.napalm_get, - getters=["interfaces", "facts"]) - -# Let's print the result on screen -filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - ) diff --git a/examples/1_simple_runbooks/rollback.ipynb b/examples/1_simple_runbooks/rollback.ipynb deleted file mode 100644 index e99f693f..00000000 --- a/examples/1_simple_runbooks/rollback.ipynb +++ /dev/null @@ -1,438 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Rollback\n", - "\n", - "This runbook plays well with the ``backup.py`` one. You can basically backup the configuration, and then roll it back with this runbook if things don't go as expected.\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Runbook to rollback configuration from a saved configuration\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import networking, text\n",
-       " 7 \n",
-       " 8 \n",
-       " 9 def rollback(task):\n",
-       "10     """\n",
-       "11     This function loads the backup from ./backups/$hostname and\n",
-       "12     deploys it.\n",
-       "13     """\n",
-       "14     task.run(networking.napalm_configure,\n",
-       "15              name="Loading Configuration on the device",\n",
-       "16              replace=True,\n",
-       "17              filename="backups/{host}")\n",
-       "18 \n",
-       "19 \n",
-       "20 brg = easy_brigade(\n",
-       "21         host_file="../inventory/hosts.yaml",\n",
-       "22         group_file="../inventory/groups.yaml",\n",
-       "23         dry_run=False,\n",
-       "24         raise_on_error=True,\n",
-       "25 )\n",
-       "26 \n",
-       "27 \n",
-       "28 # select which devices we want to work with\n",
-       "29 filtered = brg.filter(type="network_device", site="cmh")\n",
-       "30 \n",
-       "31 results = filtered.run(task=rollback)\n",
-       "32 \n",
-       "33 filtered.run(text.print_result,\n",
-       "34              num_workers=1,  # task should be done synchronously\n",
-       "35              data=results,\n",
-       "36              task_id=-1,  # we only want to print the last task\n",
-       "37              )\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file rollback.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "So let's rollback to the backup configuration we took before configuring the network early on:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,8 +8,7 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "-hostname spine00.cmh\n", - "-ip domain-name cmh.acme.com\n", - "+hostname blah\n", - " !\n", - " spanning-tree mode mstp\n", - " !\n", - "@@ -21,28 +20,13 @@\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - " !\n", - " interface Ethernet1\n", - "- description link to leaf00.cmh\n", - "- no switchport\n", - "- ip address 10.0.0.0/31\n", - " !\n", - " interface Ethernet2\n", - "- description link to leaf01.cmh\n", - "- no switchport\n", - "- ip address 10.0.0.2/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-ip routing\n", - "-!\n", - "-router bgp 65000\n", - "- neighbor 10.0.0.1 remote-as 65100\n", - "- neighbor 10.0.0.1 maximum-routes 12000 \n", - "- neighbor 10.0.0.3 remote-as 65101\n", - "- neighbor 10.0.0.3 maximum-routes 12000 \n", - "- address-family ipv4\n", - "- neighbor 10.0.0.1 activate\n", - "- neighbor 10.0.0.3 activate\n", - "+no ip routing\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name spine01.cmh;\n", - "+ host-name vsrx;\n", - "- domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "- ge-0/0/1 {\n", - "- description \"link to leaf00.cmh\";\n", - "- unit 0 {\n", - "- family inet {\n", - "- address 10.0.1.0/31;\n", - "- }\n", - "- }\n", - "- }\n", - "- ge-0/0/2 {\n", - "- description \"link to leaf01.cmh\";\n", - "- unit 0 {\n", - "- family inet {\n", - "- address 10.0.1.2/31;\n", - "- }\n", - "- }\n", - "- }\n", - "[edit]\n", - "- routing-options {\n", - "- autonomous-system 65000;\n", - "- }\n", - "- protocols {\n", - "- bgp {\n", - "- import PERMIT_ALL;\n", - "- export PERMIT_ALL;\n", - "- group peers {\n", - "- neighbor 10.0.1.1 {\n", - "- peer-as 65100;\n", - "- }\n", - "- neighbor 10.0.1.3 {\n", - "- peer-as 65101;\n", - "- }\n", - "- }\n", - "- }\n", - "- }\n", - "- policy-options {\n", - "- policy-statement PERMIT_ALL {\n", - "- from protocol bgp;\n", - "- then accept;\n", - "- }\n", - "- }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,9 +8,6 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "-hostname leaf00.cmh\n", - "-ip domain-name cmh.acme.com\n", - "-!\n", - " spanning-tree mode mstp\n", - " !\n", - " aaa authorization exec default local\n", - "@@ -20,36 +17,14 @@\n", - " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - " !\n", - "-vlan 100\n", - "- name frontend\n", - "-!\n", - "-vlan 200\n", - "- name backend\n", - "-!\n", - " interface Ethernet1\n", - "- description link to spine00.cmh\n", - "- shutdown\n", - "- no switchport\n", - "- ip address 10.0.0.1/31\n", - " !\n", - " interface Ethernet2\n", - "- description link to spine01.cmh\n", - "- no switchport\n", - "- ip address 10.0.1.1/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-ip routing\n", - "-!\n", - "-router bgp 65100\n", - "- neighbor 10.0.0.0 remote-as 65000\n", - "- neighbor 10.0.0.0 maximum-routes 12000 \n", - "- neighbor 10.0.1.0 remote-as 65000\n", - "- neighbor 10.0.1.0 maximum-routes 12000 \n", - "- address-family ipv4\n", - "- neighbor 10.0.0.0 activate\n", - "- neighbor 10.0.1.0 activate\n", - "+no ip routing\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name leaf01.cmh;\n", - "+ host-name vsrx;\n", - "- domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "- ge-0/0/1 {\n", - "- description \"link to spine00.cmh\";\n", - "- unit 0 {\n", - "- family inet {\n", - "- address 10.0.0.3/31;\n", - "- }\n", - "- }\n", - "- }\n", - "- ge-0/0/2 {\n", - "- description \"link to spine01.cmh\";\n", - "- unit 0 {\n", - "- family inet {\n", - "- address 10.0.1.3/31;\n", - "- }\n", - "- }\n", - "- }\n", - "[edit]\n", - "- routing-options {\n", - "- autonomous-system 65101;\n", - "- }\n", - "- protocols {\n", - "- bgp {\n", - "- import PERMIT_ALL;\n", - "- export PERMIT_ALL;\n", - "- group peers {\n", - "- neighbor 10.0.0.2 {\n", - "- peer-as 65000;\n", - "- }\n", - "- neighbor 10.0.1.2 {\n", - "- peer-as 65000;\n", - "- }\n", - "- }\n", - "- }\n", - "- }\n", - "- policy-options {\n", - "- policy-statement PERMIT_ALL {\n", - "- from protocol bgp;\n", - "- then accept;\n", - "- }\n", - "- }\n", - "- vlans {\n", - "- backend {\n", - "- vlan-id 200;\n", - "- }\n", - "- frontend {\n", - "- vlan-id 100;\n", - "- }\n", - "- }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As with other tasks, changes are detected and only when needed are applied:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/1_simple_runbooks/rollback.py b/examples/1_simple_runbooks/rollback.py deleted file mode 100755 index 2c219cc7..00000000 --- a/examples/1_simple_runbooks/rollback.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -""" -Runbook to rollback configuration from a saved configuration -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import networking, text - -import click - - -def rollback(task): - """ - This function loads the backup from ./backups/$hostname and - deploys it. - """ - task.run(networking.napalm_configure, - name="Loading Configuration on the device", - replace=True, - filename="backups/{host}") - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="k=v pairs to filter the devices") -@click.option('--get', '-g', multiple=True, - help="getters you want to use") -def main(filter, get): - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=True, - ) - - # select which devices we want to work with - filtered = brg.filter(type="network_device", site="cmh") - - results = filtered.run(task=rollback) - - filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - ) - - -if __name__ == "__main__": - main() diff --git a/examples/1_simple_runbooks/validate.ipynb b/examples/1_simple_runbooks/validate.ipynb deleted file mode 100644 index 8af4499e..00000000 --- a/examples/1_simple_runbooks/validate.ipynb +++ /dev/null @@ -1,689 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Validate\n", - "\n", - "This playbook uses [napalm validation](http://napalm.readthedocs.io/en/latest/validate/index.html) functionality to verify correctness of the network.\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Runbook that verifies that BGP sessions are configured and up.\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import data, networking, text\n",
-       " 7 \n",
-       " 8 \n",
-       " 9 def validate(task):\n",
-       "10     task.host["config"] = ""\n",
-       "11 \n",
-       "12     r = task.run(name="read data",\n",
-       "13                  task=data.load_yaml,\n",
-       "14                  file="../extra_data/{host}/l3.yaml")\n",
-       "15 \n",
-       "16     validation_rules = [{\n",
-       "17         'get_bgp_neighbors': {\n",
-       "18             'global': {\n",
-       "19                 'peers': {\n",
-       "20                     '_mode': 'strict',\n",
-       "21                 }\n",
-       "22             }\n",
-       "23         }\n",
-       "24     }]\n",
-       "25     peers = validation_rules[0]['get_bgp_neighbors']['global']['peers']\n",
-       "26     for session in r.result['sessions']:\n",
-       "27         peers[session['ipv4']] = {'is_up': True}\n",
-       "28 \n",
-       "29     task.run(name="validating data",\n",
-       "30              task=networking.napalm_validate,\n",
-       "31              validation_source=validation_rules)\n",
-       "32 \n",
-       "33 \n",
-       "34 def print_compliance(task, results):\n",
-       "35     """\n",
-       "36     We use this task so we can access directly the result\n",
-       "37     for each specific host and see if the task complies or not\n",
-       "38     and pass it to print_result.\n",
-       "39     """\n",
-       "40     task.run(text.print_result,\n",
-       "41              name="print result",\n",
-       "42              data=results[task.host.name],\n",
-       "43              failed=not results[task.host.name][2].result['complies'],\n",
-       "44              )\n",
-       "45 \n",
-       "46 \n",
-       "47 brg = easy_brigade(\n",
-       "48         host_file="../inventory/hosts.yaml",\n",
-       "49         group_file="../inventory/groups.yaml",\n",
-       "50         dry_run=False,\n",
-       "51         raise_on_error=True,\n",
-       "52 )\n",
-       "53 \n",
-       "54 \n",
-       "55 filtered = brg.filter(type="network_device", site="cmh")\n",
-       "56 \n",
-       "57 results = filtered.run(task=validate)\n",
-       "58 \n",
-       "59 filtered.run(print_compliance,\n",
-       "60              results=results,\n",
-       "61              num_workers=1)\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file validate.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Let's start by running the script on an unconfigured network:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m['global']\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run validate.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For each host we get the data we are using for validation and the result. What the report is saying is that we don't even have the BGP instance 'global' (default instance) configured so let's do it:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[32m**** Playbook to configure the network *****************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,7 +8,8 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "-hostname blah\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - " !\n", - " spanning-tree mode mstp\n", - " !\n", - "@@ -20,13 +21,28 @@\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - " !\n", - " interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65000;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,6 +8,9 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - " spanning-tree mode mstp\n", - " !\n", - " aaa authorization exec default local\n", - "@@ -17,14 +20,36 @@\n", - " username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - " !\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - " interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name leaf01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to spine00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.0.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to spine01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65101;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.0.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ neighbor 10.0.1.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\n", - "+ vlans {\n", - "+ backend {\n", - "+ vlan-id 200;\n", - "+ }\n", - "+ frontend {\n", - "+ vlan-id 100;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the network is configured let's validate the deployment again:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", - " \u001b[0m'10.0.0.3'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", - " \u001b[0m'10.0.1.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run validate.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "What the report is basically telling us is that ``spina01`` and ``leaf01`` are pssing our tests, however, ``spine00`` and ``leaf00`` as one of their BGP sessions that should be ``up`` is actually ``down``." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/1_simple_runbooks/validate.py b/examples/1_simple_runbooks/validate.py deleted file mode 100755 index 4453babd..00000000 --- a/examples/1_simple_runbooks/validate.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -""" -Runbook that verifies that BGP sessions are configured and up. -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import data, networking, text - - -def validate(task): - task.host["config"] = "" - - r = task.run(name="read data", - task=data.load_yaml, - file="../extra_data/{host}/l3.yaml") - - validation_rules = [{ - 'get_bgp_neighbors': { - 'global': { - 'peers': { - '_mode': 'strict', - } - } - } - }] - peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] - for session in r.result['sessions']: - peers[session['ipv4']] = {'is_up': True} - - task.run(name="validating data", - task=networking.napalm_validate, - validation_source=validation_rules) - - -def print_compliance(task, results): - """ - We use this task so we can access directly the result - for each specific host and see if the task complies or not - and pass it to print_result. - """ - task.run(text.print_result, - name="print result", - data=results[task.host.name], - failed=not results[task.host.name][2].result['complies'], - ) - - -brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=True, -) - - -filtered = brg.filter(type="network_device", site="cmh") - -results = filtered.run(task=validate) - -filtered.run(print_compliance, - results=results, - num_workers=1) diff --git a/examples/2_simple_tooling/backup.ipynb b/examples/2_simple_tooling/backup.ipynb deleted file mode 100644 index 7df81f0d..00000000 --- a/examples/2_simple_tooling/backup.ipynb +++ /dev/null @@ -1,1062 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Backup\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Tool that downloads the configuration from the devices and\n",
-       " 4 stores them on disk.\n",
-       " 5 """\n",
-       " 6 from brigade.easy import easy_brigade\n",
-       " 7 from brigade.plugins.tasks import files, networking, text\n",
-       " 8 \n",
-       " 9 import click\n",
-       "10 \n",
-       "11 \n",
-       "12 def backup(task, path):\n",
-       "13     """\n",
-       "14     This function groups two tasks:\n",
-       "15         1. Download configuration from the device\n",
-       "16         2. Store to disk\n",
-       "17     """\n",
-       "18     result = task.run(networking.napalm_get,\n",
-       "19                       name="Gathering configuration from the device",\n",
-       "20                       getters="config")\n",
-       "21 \n",
-       "22     task.run(files.write,\n",
-       "23              name="Saving Configuration to disk",\n",
-       "24              content=result.result["config"]["running"],\n",
-       "25              filename="{}/{}".format(path, task.host))\n",
-       "26 \n",
-       "27 \n",
-       "28 @click.command()\n",
-       "29 @click.option('--filter', '-f', multiple=True,\n",
-       "30               help="filters to apply. For instance site=cmh")\n",
-       "31 @click.option('--path', '-p', default=".",\n",
-       "32               help="Where to save the backup files")\n",
-       "33 def main(filter, path):\n",
-       "34     """\n",
-       "35     Backups running configuration of devices into a file\n",
-       "36     """\n",
-       "37     brg = easy_brigade(\n",
-       "38             host_file="../inventory/hosts.yaml",\n",
-       "39             group_file="../inventory/groups.yaml",\n",
-       "40             dry_run=False,\n",
-       "41             raise_on_error=False,\n",
-       "42     )\n",
-       "43 \n",
-       "44     # filter is going to be a list of key=value so we clean that first\n",
-       "45     filter_dict = {"type": "network_device"}\n",
-       "46     for f in filter:\n",
-       "47         k, v = f.split("=")\n",
-       "48         filter_dict[k] = v\n",
-       "49 \n",
-       "50     # let's filter the devices\n",
-       "51     filtered = brg.filter(**filter_dict)\n",
-       "52 \n",
-       "53     # Run the ``backup`` function that groups the tasks to\n",
-       "54     # download/store devices' configuration\n",
-       "55     results = filtered.run(backup,\n",
-       "56                            name="Backing up configurations",\n",
-       "57                            path=path)\n",
-       "58 \n",
-       "59     # Let's print the result on screen\n",
-       "60     filtered.run(text.print_result,\n",
-       "61                  num_workers=1,  # task should be done synchronously\n",
-       "62                  data=results,\n",
-       "63                  task_id=-1,  # we only want to print the last task\n",
-       "64                  skipped=True,\n",
-       "65                  )\n",
-       "66 \n",
-       "67 \n",
-       "68 if __name__ == "__main__":\n",
-       "69     main()\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file backup.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Let's start with the help so we can see what we can do." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: backup.py [OPTIONS]\n", - "\n", - " Backups running configuration of devices into a file\n", - "\n", - "Options:\n", - " -f, --filter TEXT filters to apply. For instance site=cmh\n", - " -p, --path TEXT Where to save the backup files\n", - " --help Show this message and exit.\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run backup.py --help" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With those options it should be easy to backup devices at different sites in different paths:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mkdir: backups/cmh: File exists\n", - "\u001b[0m\u001b[0m\u001b[0m\u001b[0m\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/cmh//spine00.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,52 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: spine00.cmh (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - "+!\n", - "+interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - "+!\n", - "+interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/cmh//spine01.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,110 @@\n", - "\n", - "+\n", - "+## Last commit: 2018-01-15 12:02:22 UTC by vagrant\n", - "+version 12.1X47-D20.7;\n", - "+system {\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "+ root-authentication {\n", - "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", - "+ }\n", - "+ login {\n", - "+ user vagrant {\n", - "+ uid 2000;\n", - "+ class super-user;\n", - "+ authentication {\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ services {\n", - "+ ssh {\n", - "+ root-login allow;\n", - "+ }\n", - "+ netconf {\n", - "+ ssh;\n", - "+ }\n", - "+ web-management {\n", - "+ http {\n", - "+ interface ge-0/0/0.0;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ syslog {\n", - "+ user * {\n", - "+ any emergency;\n", - "+ }\n", - "+ file messages {\n", - "+ any any;\n", - "+ authorization info;\n", - "+ }\n", - "+ file interactive-commands {\n", - "+ interactive-commands any;\n", - "+ }\n", - "+ }\n", - "+ license {\n", - "+ autoupdate {\n", - "+ url https://ae1.juniper.net/junos/key_retrieval;\n", - "+ }\n", - "+ }\n", - "+}\n", - "+interfaces {\n", - "+ ge-0/0/0 {\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ dhcp;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+routing-options {\n", - "+ autonomous-system 65000;\n", - "+}\n", - "+protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+}\n", - "+security {\n", - "+ forwarding-options {\n", - "+ family {\n", - "+ inet6 {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ mpls {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/cmh//leaf00.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,59 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: leaf00.cmh (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - "+!\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - "+interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - "+!\n", - "+interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/cmh//leaf01.cmh\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,118 @@\n", - "\n", - "+\n", - "+## Last commit: 2018-01-15 12:02:23 UTC by vagrant\n", - "+version 12.1X47-D20.7;\n", - "+system {\n", - "+ host-name leaf01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "+ root-authentication {\n", - "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\";\n", - "+ }\n", - "+ login {\n", - "+ user vagrant {\n", - "+ uid 2000;\n", - "+ class super-user;\n", - "+ authentication {\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ services {\n", - "+ ssh {\n", - "+ root-login allow;\n", - "+ }\n", - "+ netconf {\n", - "+ ssh;\n", - "+ }\n", - "+ web-management {\n", - "+ http {\n", - "+ interface ge-0/0/0.0;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ syslog {\n", - "+ user * {\n", - "+ any emergency;\n", - "+ }\n", - "+ file messages {\n", - "+ any any;\n", - "+ authorization info;\n", - "+ }\n", - "+ file interactive-commands {\n", - "+ interactive-commands any;\n", - "+ }\n", - "+ }\n", - "+ license {\n", - "+ autoupdate {\n", - "+ url https://ae1.juniper.net/junos/key_retrieval;\n", - "+ }\n", - "+ }\n", - "+}\n", - "+interfaces {\n", - "+ ge-0/0/0 {\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ dhcp;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/1 {\n", - "+ description \"link to spine00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.0.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to spine01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+routing-options {\n", - "+ autonomous-system 65101;\n", - "+}\n", - "+protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.0.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ neighbor 10.0.1.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+}\n", - "+security {\n", - "+ forwarding-options {\n", - "+ family {\n", - "+ inet6 {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ mpls {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+vlans {\n", - "+ backend {\n", - "+ vlan-id 200;\n", - "+ }\n", - "+ frontend {\n", - "+ vlan-id 100;\n", - "+ }\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%mkdir backups/cmh\n", - "%rm backups/cmh/*\n", - "%run backup.py --filter site=cmh --path backups/cmh/" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mkdir: backups/bma: File exists\n", - "\u001b[0m\u001b[0m\u001b[0m\u001b[0m\u001b[1m\u001b[33m* spine00.bma ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/bma//spine00.bma\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,52 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: spine00.cmh (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - "+!\n", - "+interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - "+!\n", - "+interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.bma ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/bma//spine01.bma\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,110 @@\n", - "\n", - "+\n", - "+## Last commit: 2018-01-15 12:02:22 UTC by vagrant\n", - "+version 12.1X47-D20.7;\n", - "+system {\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "+ root-authentication {\n", - "+ encrypted-password \"$1$5MhDFyrI$NBBMndW1POqbN.0QEA4z0.\";\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\";\n", - "+ }\n", - "+ login {\n", - "+ user vagrant {\n", - "+ uid 2000;\n", - "+ class super-user;\n", - "+ authentication {\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key\";\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ services {\n", - "+ ssh {\n", - "+ root-login allow;\n", - "+ }\n", - "+ netconf {\n", - "+ ssh;\n", - "+ }\n", - "+ web-management {\n", - "+ http {\n", - "+ interface ge-0/0/0.0;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ syslog {\n", - "+ user * {\n", - "+ any emergency;\n", - "+ }\n", - "+ file messages {\n", - "+ any any;\n", - "+ authorization info;\n", - "+ }\n", - "+ file interactive-commands {\n", - "+ interactive-commands any;\n", - "+ }\n", - "+ }\n", - "+ license {\n", - "+ autoupdate {\n", - "+ url https://ae1.juniper.net/junos/key_retrieval;\n", - "+ }\n", - "+ }\n", - "+}\n", - "+interfaces {\n", - "+ ge-0/0/0 {\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ dhcp;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+routing-options {\n", - "+ autonomous-system 65000;\n", - "+}\n", - "+protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\n", - "+policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+}\n", - "+security {\n", - "+ forwarding-options {\n", - "+ family {\n", - "+ inet6 {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ mpls {\n", - "+ mode packet-based;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.bma ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Saving Configuration to disk ** changed : True ---------------------------\u001b[0m\n", - "\u001b[0m--- backups/bma//leaf00.bma\n", - "\n", - "+++ new\n", - "\n", - "@@ -0,0 +1,59 @@\n", - "\n", - "+! Command: show running-config\n", - "+! device: leaf00.cmh (vEOS, EOS-4.17.5M)\n", - "+!\n", - "+! boot system flash:/vEOS-lab.swi\n", - "+!\n", - "+event-handler dhclient\n", - "+ trigger on-boot\n", - "+ action bash sudo /mnt/flash/initialize_ma1.sh\n", - "+!\n", - "+transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - "+spanning-tree mode mstp\n", - "+!\n", - "+aaa authorization exec default local\n", - "+!\n", - "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", - "+!\n", - "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - "+!\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - "+interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - "+!\n", - "+interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - "+!\n", - "+interface Management1\n", - "+ ip address 10.0.2.15/24\n", - "+!\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - "+!\n", - "+management api http-commands\n", - "+ no shutdown\n", - "+!\n", - "+!\n", - "+end\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* leaf01.bma ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Backing up configurations ** changed : False -----------------------------\u001b[0m\n", - "\u001b[0mTraceback (most recent call last):\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1250, in open\n", - " device_params={'name': 'junos', 'local': False})\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 154, in connect\n", - " return connect_ssh(*args, **kwds)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 119, in connect_ssh\n", - " session.connect(*args, **kwds)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 412, in connect\n", - " self._auth(username, password, key_filenames, allow_agent, look_for_keys)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 508, in _auth\n", - " raise AuthenticationError(repr(saved_exception))\n", - "ncclient.transport.errors.AuthenticationError: AuthenticationException('Authentication failed.',)\n", - "\n", - "During handling of the above exception, another exception occurred:\n", - "\n", - "Traceback (most recent call last):\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", - " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/examples/2_simple_tooling/backup.py\", line 20, in backup\n", - " getters=\"config\")\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 67, in run\n", - " r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/networking/napalm_get.py\", line 16, in napalm_get\n", - " device = task.host.get_connection(\"napalm\")\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/inventory.py\", line 212, in get_connection\n", - " raise r[self.name].exception\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", - " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/connections/napalm_connection.py\", line 31, in napalm_connection\n", - " host.connections[\"napalm\"].open()\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/napalm/junos/junos.py\", line 106, in open\n", - " self.device.open()\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1254, in open\n", - " raise EzErrors.ConnectAuthError(self)\n", - "jnpr.junos.exception.ConnectAuthError: ConnectAuthError(127.0.0.1)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%mkdir backups/bma\n", - "%rm backups/bma/*\n", - "%run backup.py --filter site=bma --path backups/bma/" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[31m* leaf01.bma ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Backing up configurations ** changed : False -----------------------------\u001b[0m\n", - "\u001b[0mTraceback (most recent call last):\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1250, in open\n", - " device_params={'name': 'junos', 'local': False})\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 154, in connect\n", - " return connect_ssh(*args, **kwds)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/manager.py\", line 119, in connect_ssh\n", - " session.connect(*args, **kwds)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 412, in connect\n", - " self._auth(username, password, key_filenames, allow_agent, look_for_keys)\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/ncclient/transport/ssh.py\", line 508, in _auth\n", - " raise AuthenticationError(repr(saved_exception))\n", - "ncclient.transport.errors.AuthenticationError: AuthenticationException('Authentication failed.',)\n", - "\n", - "During handling of the above exception, another exception occurred:\n", - "\n", - "Traceback (most recent call last):\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", - " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/examples/2_simple_tooling/backup.py\", line 20, in backup\n", - " getters=\"config\")\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 67, in run\n", - " r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/networking/napalm_get.py\", line 16, in napalm_get\n", - " device = task.host.get_connection(\"napalm\")\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/inventory.py\", line 212, in get_connection\n", - " raise r[self.name].exception\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/__init__.py\", line 201, in run_task\n", - " r = task._start(host=host, brigade=brigade, dry_run=dry_run)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/core/task.py\", line 42, in _start\n", - " r = self.task(self, **self.params) or Result(host)\n", - " File \"/Users/dbarroso/workspace/brigade/brigade/plugins/tasks/connections/napalm_connection.py\", line 31, in napalm_connection\n", - " host.connections[\"napalm\"].open()\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/napalm/junos/junos.py\", line 106, in open\n", - " self.device.open()\n", - " File \"/Users/dbarroso/.virtualenvs/brigade/lib/python3.6/site-packages/jnpr/junos/device.py\", line 1254, in open\n", - " raise EzErrors.ConnectAuthError(self)\n", - "jnpr.junos.exception.ConnectAuthError: ConnectAuthError(127.0.0.1)\n", - "\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run backup.py --filter name=leaf01.bma --path backups/bma/" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "> Note that brigade detected and reported that we failed to authenticate to one of the devices.\n", - "\n", - "Now we can check we have the backups in the right place:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "backups/bma:\n", - "leaf00.bma spine00.bma spine01.bma\n", - "\n", - "backups/cmh:\n", - "leaf00.cmh leaf01.cmh spine00.cmh spine01.cmh\n", - "\u001b[0m\u001b[0m" - ] - } - ], - "source": [ - "% ls backups/*" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/2_simple_tooling/backup.py b/examples/2_simple_tooling/backup.py deleted file mode 100755 index ba3d79b3..00000000 --- a/examples/2_simple_tooling/backup.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -""" -Tool that downloads the configuration from the devices and -stores them on disk. -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import files, networking, text - -import click - - -def backup(task, path): - """ - This function groups two tasks: - 1. Download configuration from the device - 2. Store to disk - """ - result = task.run(networking.napalm_get, - name="Gathering configuration from the device", - getters="config") - - task.run(files.write, - name="Saving Configuration to disk", - content=result.result["config"]["running"], - filename="{}/{}".format(path, task.host)) - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="filters to apply. For instance site=cmh") -@click.option('--path', '-p', default=".", - help="Where to save the backup files") -def main(filter, path): - """ - Backups running configuration of devices into a file - """ - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=False, - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - # let's filter the devices - filtered = brg.filter(**filter_dict) - - # Run the ``backup`` function that groups the tasks to - # download/store devices' configuration - results = filtered.run(backup, - name="Backing up configurations", - path=path) - - # Let's print the result on screen - filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - skipped=True, - ) - - -if __name__ == "__main__": - main() diff --git a/examples/2_simple_tooling/configure.ipynb b/examples/2_simple_tooling/configure.ipynb deleted file mode 100644 index fd28dd6c..00000000 --- a/examples/2_simple_tooling/configure.ipynb +++ /dev/null @@ -1,747 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Configure\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Tool to configure datacenter\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import data, networking, text\n",
-       " 7 \n",
-       " 8 import click\n",
-       " 9 \n",
-       "10 \n",
-       "11 def configure(task):\n",
-       "12     """\n",
-       "13     This function groups all the tasks needed to configure the\n",
-       "14     network:\n",
-       "15 \n",
-       "16         1. Loading extra data\n",
-       "17         2. Templates to build configuration\n",
-       "18         3. Deploy configuration on the device\n",
-       "19     """\n",
-       "20     r = task.run(text.template_file,\n",
-       "21                  name="Base Configuration",\n",
-       "22                  template="base.j2",\n",
-       "23                  path="../templates/{brigade_nos}")\n",
-       "24     # r.result holds the result of rendering the template\n",
-       "25     # we store in the host itself so we can keep updating\n",
-       "26     # it as we render other templates\n",
-       "27     task.host["config"] = r.result\n",
-       "28 \n",
-       "29     r = task.run(data.load_yaml,\n",
-       "30                  name="Loading extra data",\n",
-       "31                  file="../extra_data/{host}/l3.yaml")\n",
-       "32     # r.result holds the data contained in the yaml files\n",
-       "33     # we load the data inside the host itself for further use\n",
-       "34     task.host["l3"] = r.result\n",
-       "35 \n",
-       "36     r = task.run(text.template_file,\n",
-       "37                  name="Interfaces Configuration",\n",
-       "38                  template="interfaces.j2",\n",
-       "39                  path="../templates/{brigade_nos}")\n",
-       "40     # we update our hosts' config\n",
-       "41     task.host["config"] += r.result\n",
-       "42 \n",
-       "43     r = task.run(text.template_file,\n",
-       "44                  name="Routing Configuration",\n",
-       "45                  template="routing.j2",\n",
-       "46                  path="../templates/{brigade_nos}")\n",
-       "47     # we update our hosts' config\n",
-       "48     task.host["config"] += r.result\n",
-       "49 \n",
-       "50     r = task.run(text.template_file,\n",
-       "51                  name="Role-specific Configuration",\n",
-       "52                  template="{role}.j2",\n",
-       "53                  path="../templates/{brigade_nos}")\n",
-       "54     # we update our hosts' config\n",
-       "55     task.host["config"] += r.result\n",
-       "56 \n",
-       "57     task.run(networking.napalm_configure,\n",
-       "58              name="Loading Configuration on the device",\n",
-       "59              replace=False,\n",
-       "60              configuration=task.host["config"])\n",
-       "61 \n",
-       "62 \n",
-       "63 @click.command()\n",
-       "64 @click.option('--filter', '-f', multiple=True,\n",
-       "65               help="k=v pairs to filter the devices")\n",
-       "66 @click.option('--commit/--no-commit', '-c', default=False,\n",
-       "67               help="whether you want to commit the changes or not")\n",
-       "68 def main(filter, commit):\n",
-       "69     brg = easy_brigade(\n",
-       "70             host_file="../inventory/hosts.yaml",\n",
-       "71             group_file="../inventory/groups.yaml",\n",
-       "72             dry_run=not commit,\n",
-       "73             raise_on_error=False,\n",
-       "74     )\n",
-       "75 \n",
-       "76     # filter is going to be a list of key=value so we clean that first\n",
-       "77     filter_dict = {"type": "network_device"}\n",
-       "78     for f in filter:\n",
-       "79         k, v = f.split("=")\n",
-       "80         filter_dict[k] = v\n",
-       "81 \n",
-       "82     # let's filter the devices\n",
-       "83     filtered = brg.filter(**filter_dict)\n",
-       "84 \n",
-       "85     results = filtered.run(task=configure)\n",
-       "86 \n",
-       "87     filtered.run(text.print_result,\n",
-       "88                  num_workers=1,  # task should be done synchronously\n",
-       "89                  data=results,\n",
-       "90                  task_id=-1,  # we only want to print the last task\n",
-       "91                  skipped=True,\n",
-       "92                  )\n",
-       "93 \n",
-       "94 \n",
-       "95 if __name__ == "__main__":\n",
-       "96     main()\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file configure.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "Let's start with the help so we can see what we can do." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: configure.py [OPTIONS]\n", - "\n", - "Options:\n", - " -f, --filter TEXT k=v pairs to filter the devices\n", - " -c, --commit / --no-commit whether you want to commit the changes or not\n", - " --help Show this message and exit.\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py --help" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With those options it should be easy to check which changes are to be applied before even applying them." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -7,6 +7,9 @@\n", - " action bash sudo /mnt/flash/initialize_ma1.sh\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - " !\n", - " spanning-tree mode mstp\n", - " !\n", - "@@ -18,13 +21,28 @@\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", - " !\n", - " interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65000;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,6 +8,9 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - " spanning-tree mode mstp\n", - " !\n", - " aaa authorization exec default local\n", - "@@ -17,14 +20,36 @@\n", - " username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", - " !\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - " interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name leaf01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to spine00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.0.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to spine01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65101;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.0.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ neighbor 10.0.1.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\n", - "+ vlans {\n", - "+ backend {\n", - "+ vlan-id 200;\n", - "+ }\n", - "+ frontend {\n", - "+ vlan-id 100;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py --filter site=cmh --no-commit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can review the changes and commit them if we are happy:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -7,6 +7,9 @@\n", - " action bash sudo /mnt/flash/initialize_ma1.sh\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - "+!\n", - "+hostname spine00.cmh\n", - "+ip domain-name cmh.acme.com\n", - " !\n", - " spanning-tree mode mstp\n", - " !\n", - "@@ -18,13 +21,28 @@\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", - " !\n", - " interface Ethernet1\n", - "+ description link to leaf00.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.0/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to leaf01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.0.2/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65000\n", - "+ neighbor 10.0.0.1 remote-as 65100\n", - "+ neighbor 10.0.0.1 maximum-routes 12000 \n", - "+ neighbor 10.0.0.3 remote-as 65101\n", - "+ neighbor 10.0.0.3 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.1 activate\n", - "+ neighbor 10.0.0.3 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name spine01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to leaf00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.0/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to leaf01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.2/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65000;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.1.1 {\n", - "+ peer-as 65100;\n", - "+ }\n", - "+ neighbor 10.0.1.3 {\n", - "+ peer-as 65101;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -8,6 +8,9 @@\n", - " !\n", - " transceiver qsfp default-mode 4x10G\n", - " !\n", - "+hostname leaf00.cmh\n", - "+ip domain-name cmh.acme.com\n", - "+!\n", - " spanning-tree mode mstp\n", - " !\n", - " aaa authorization exec default local\n", - "@@ -17,14 +20,36 @@\n", - " username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", - " username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", - " !\n", - "+vlan 100\n", - "+ name frontend\n", - "+!\n", - "+vlan 200\n", - "+ name backend\n", - "+!\n", - " interface Ethernet1\n", - "+ description link to spine00.cmh\n", - "+ shutdown\n", - "+ no switchport\n", - "+ ip address 10.0.0.1/31\n", - " !\n", - " interface Ethernet2\n", - "+ description link to spine01.cmh\n", - "+ no switchport\n", - "+ ip address 10.0.1.1/31\n", - " !\n", - " interface Management1\n", - " ip address 10.0.2.15/24\n", - " !\n", - "-no ip routing\n", - "+ip routing\n", - "+!\n", - "+router bgp 65100\n", - "+ neighbor 10.0.0.0 remote-as 65000\n", - "+ neighbor 10.0.0.0 maximum-routes 12000 \n", - "+ neighbor 10.0.1.0 remote-as 65000\n", - "+ neighbor 10.0.1.0 maximum-routes 12000 \n", - "+ address-family ipv4\n", - "+ neighbor 10.0.0.0 activate\n", - "+ neighbor 10.0.1.0 activate\n", - " !\n", - " management api http-commands\n", - " no shutdown\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system]\n", - "- host-name vsrx;\n", - "+ host-name leaf01.cmh;\n", - "+ domain-name cmh.acme.com;\n", - "[edit interfaces]\n", - "+ ge-0/0/1 {\n", - "+ description \"link to spine00.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.0.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ ge-0/0/2 {\n", - "+ description \"link to spine01.cmh\";\n", - "+ unit 0 {\n", - "+ family inet {\n", - "+ address 10.0.1.3/31;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "[edit]\n", - "+ routing-options {\n", - "+ autonomous-system 65101;\n", - "+ }\n", - "+ protocols {\n", - "+ bgp {\n", - "+ import PERMIT_ALL;\n", - "+ export PERMIT_ALL;\n", - "+ group peers {\n", - "+ neighbor 10.0.0.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ neighbor 10.0.1.2 {\n", - "+ peer-as 65000;\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ }\n", - "+ policy-options {\n", - "+ policy-statement PERMIT_ALL {\n", - "+ from protocol bgp;\n", - "+ then accept;\n", - "+ }\n", - "+ }\n", - "+ vlans {\n", - "+ backend {\n", - "+ vlan-id 200;\n", - "+ }\n", - "+ frontend {\n", - "+ vlan-id 100;\n", - "+ }\n", - "+ }\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py --filter site=cmh --commit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we run the tool again it should report no changes:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run configure.py --filter site=cmh --no-commit" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/2_simple_tooling/configure.py b/examples/2_simple_tooling/configure.py deleted file mode 100755 index 82b75744..00000000 --- a/examples/2_simple_tooling/configure.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python -""" -Tool to configure datacenter -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import data, networking, text - -import click - - -def configure(task): - """ - This function groups all the tasks needed to configure the - network: - - 1. Loading extra data - 2. Templates to build configuration - 3. Deploy configuration on the device - """ - r = task.run(text.template_file, - name="Base Configuration", - template="base.j2", - path="../templates/{brigade_nos}") - # r.result holds the result of rendering the template - # we store in the host itself so we can keep updating - # it as we render other templates - task.host["config"] = r.result - - r = task.run(data.load_yaml, - name="Loading extra data", - file="../extra_data/{host}/l3.yaml") - # r.result holds the data contained in the yaml files - # we load the data inside the host itself for further use - task.host["l3"] = r.result - - r = task.run(text.template_file, - name="Interfaces Configuration", - template="interfaces.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - r = task.run(text.template_file, - name="Routing Configuration", - template="routing.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - r = task.run(text.template_file, - name="Role-specific Configuration", - template="{role}.j2", - path="../templates/{brigade_nos}") - # we update our hosts' config - task.host["config"] += r.result - - task.run(networking.napalm_configure, - name="Loading Configuration on the device", - replace=False, - configuration=task.host["config"]) - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="k=v pairs to filter the devices") -@click.option('--commit/--no-commit', '-c', default=False, - help="whether you want to commit the changes or not") -def main(filter, commit): - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=not commit, - raise_on_error=False, - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - # let's filter the devices - filtered = brg.filter(**filter_dict) - - results = filtered.run(task=configure) - - filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - skipped=True, - ) - - -if __name__ == "__main__": - main() diff --git a/examples/2_simple_tooling/get_facts.ipynb b/examples/2_simple_tooling/get_facts.ipynb deleted file mode 100644 index 87049422..00000000 --- a/examples/2_simple_tooling/get_facts.ipynb +++ /dev/null @@ -1,306 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Get facts\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Very simple tool to get facts and print them on the screen.\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import networking, text\n",
-       " 7 \n",
-       " 8 import click\n",
-       " 9 \n",
-       "10 \n",
-       "11 @click.command()\n",
-       "12 @click.option('--filter', '-f', multiple=True,\n",
-       "13               help="k=v pairs to filter the devices")\n",
-       "14 @click.option('--get', '-g', multiple=True,\n",
-       "15               help="getters you want to use")\n",
-       "16 def main(filter, get):\n",
-       "17     """\n",
-       "18     Retrieve information from network devices using napalm\n",
-       "19     """\n",
-       "20     brg = easy_brigade(\n",
-       "21             host_file="../inventory/hosts.yaml",\n",
-       "22             group_file="../inventory/groups.yaml",\n",
-       "23             dry_run=False,\n",
-       "24             raise_on_error=False,\n",
-       "25     )\n",
-       "26 \n",
-       "27     # filter is going to be a list of key=value so we clean that first\n",
-       "28     filter_dict = {"type": "network_device"}\n",
-       "29     for f in filter:\n",
-       "30         k, v = f.split("=")\n",
-       "31         filter_dict[k] = v\n",
-       "32 \n",
-       "33     # select which devices we want to work with\n",
-       "34     filtered = brg.filter(**filter_dict)\n",
-       "35     results = filtered.run(networking.napalm_get,\n",
-       "36                            getters=get)\n",
-       "37 \n",
-       "38     # Let's print the result on screen\n",
-       "39     filtered.run(text.print_result,\n",
-       "40                  num_workers=1,  # task should be done synchronously\n",
-       "41                  data=results)\n",
-       "42 \n",
-       "43 \n",
-       "44 if __name__ == "__main__":\n",
-       "45     main()\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file get_facts.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "As usual, let's start with the help:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: get_facts.py [OPTIONS]\n", - "\n", - " Retrieve information from network devices using napalm\n", - "\n", - "Options:\n", - " -f, --filter TEXT k=v pairs to filter the devices\n", - " -g, --get TEXT getters you want to use\n", - " --help Show this message and exit.\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run get_facts.py --help" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Looks like we can use any getter. Let's see:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516038050.9974556\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m'link to leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516037549.5002303\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:0C:31:79'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m0\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Management1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'description'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'is_enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'is_up'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'last_flapped'\u001b[0m: \u001b[0m1516037563.4058475\u001b[0m,\n", - " \u001b[0m'mac_address'\u001b[0m: \u001b[0m'08:00:27:47:87:83'\u001b[0m,\n", - " \u001b[0m'speed'\u001b[0m: \u001b[0m1000\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run get_facts.py --filter name=spine00.cmh -g interfaces" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- napalm_get ** changed : False --------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'facts'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'fqdn'\u001b[0m: \u001b[0m'spine00.cmh.cmh.acme.com'\u001b[0m,\n", - " \u001b[0m'hostname'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'interface_list'\u001b[0m: \u001b[0m['Ethernet1', 'Ethernet2', 'Management1']\u001b[0m,\n", - " \u001b[0m'model'\u001b[0m: \u001b[0m'vEOS'\u001b[0m,\n", - " \u001b[0m'os_version'\u001b[0m: \u001b[0m'4.17.5M-4414219.4175M'\u001b[0m,\n", - " \u001b[0m'serial_number'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'uptime'\u001b[0m: \u001b[0m1156\u001b[0m,\n", - " \u001b[0m'vendor'\u001b[0m: \u001b[0m'Arista'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'users'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'admin'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'level'\u001b[0m: \u001b[0m15\u001b[0m,\n", - " \u001b[0m'password'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'role'\u001b[0m: \u001b[0m'network-admin'\u001b[0m,\n", - " \u001b[0m'sshkeys'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m,\n", - " \u001b[0m'vagrant'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'level'\u001b[0m: \u001b[0m15\u001b[0m,\n", - " \u001b[0m'password'\u001b[0m: \u001b[0m''\u001b[0m,\n", - " \u001b[0m'role'\u001b[0m: \u001b[0m'network-admin'\u001b[0m,\n", - " \u001b[0m'sshkeys'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run get_facts.py --filter name=spine00.cmh -g facts -g users" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/2_simple_tooling/get_facts.py b/examples/2_simple_tooling/get_facts.py deleted file mode 100755 index 8bbe60e0..00000000 --- a/examples/2_simple_tooling/get_facts.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -""" -Very simple tool to get facts and print them on the screen. -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import networking, text - -import click - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="k=v pairs to filter the devices") -@click.option('--get', '-g', multiple=True, - help="getters you want to use") -def main(filter, get): - """ - Retrieve information from network devices using napalm - """ - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=False, - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - # select which devices we want to work with - filtered = brg.filter(**filter_dict) - results = filtered.run(networking.napalm_get, - getters=get) - - # Let's print the result on screen - filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results) - - -if __name__ == "__main__": - main() diff --git a/examples/2_simple_tooling/rollback.ipynb b/examples/2_simple_tooling/rollback.ipynb deleted file mode 100644 index e09fc848..00000000 --- a/examples/2_simple_tooling/rollback.ipynb +++ /dev/null @@ -1,411 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Rollback\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Tool to rollback configuration from a saved configuration\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import networking, text\n",
-       " 7 \n",
-       " 8 import click\n",
-       " 9 \n",
-       "10 \n",
-       "11 def rollback(task, path):\n",
-       "12     """\n",
-       "13     This function loads the backup from ./$path/$hostname and\n",
-       "14     deploys it.\n",
-       "15     """\n",
-       "16     task.run(networking.napalm_configure,\n",
-       "17              name="Loading Configuration on the device",\n",
-       "18              replace=True,\n",
-       "19              filename="{}/{}".format(path, task.host))\n",
-       "20 \n",
-       "21 \n",
-       "22 @click.command()\n",
-       "23 @click.option('--filter', '-f', multiple=True,\n",
-       "24               help="k=v pairs to filter the devices")\n",
-       "25 @click.option('--commit/--no-commit', '-c', default=False,\n",
-       "26               help="whether you want to commit the changes or not")\n",
-       "27 @click.option('--path', '-p', default=".",\n",
-       "28               help="Where to save the backup files")\n",
-       "29 def main(filter, commit, path):\n",
-       "30     brg = easy_brigade(\n",
-       "31             host_file="../inventory/hosts.yaml",\n",
-       "32             group_file="../inventory/groups.yaml",\n",
-       "33             dry_run=not commit,\n",
-       "34             raise_on_error=True,\n",
-       "35     )\n",
-       "36 \n",
-       "37     # filter is going to be a list of key=value so we clean that first\n",
-       "38     filter_dict = {"type": "network_device"}\n",
-       "39     for f in filter:\n",
-       "40         k, v = f.split("=")\n",
-       "41         filter_dict[k] = v\n",
-       "42 \n",
-       "43     # let's filter the devices\n",
-       "44     filtered = brg.filter(**filter_dict)\n",
-       "45 \n",
-       "46     results = filtered.run(task=rollback, path=path)\n",
-       "47 \n",
-       "48     filtered.run(text.print_result,\n",
-       "49                  num_workers=1,  # task should be done synchronously\n",
-       "50                  data=results,\n",
-       "51                  task_id=-1,  # we only want to print the last task\n",
-       "52                  )\n",
-       "53 \n",
-       "54 \n",
-       "55 if __name__ == "__main__":\n",
-       "56     main()\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file rollback.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "As usual, let's start with the help:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: rollback.py [OPTIONS]\n", - "\n", - "Options:\n", - " -f, --filter TEXT k=v pairs to filter the devices\n", - " -c, --commit / --no-commit whether you want to commit the changes or not\n", - " -p, --path TEXT Where to save the backup files\n", - " --help Show this message and exit.\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py --help" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Looks like we can filter devices as usual, we can test changes as with the ``configure.py`` tool and that we can even choose the path where to look for the configurations. Let's try it:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -15,10 +15,10 @@\n", - " !\n", - " aaa authorization exec default local\n", - " !\n", - "-aaa root secret sha512 $6$DInx3sLBwR.ikKs8$ThNQ/uVPDT9YXcreGqYZ53IZA.abXsMPsV4jdylCVl5Nu6GMcVcFgvE9L3hvM/vZDJS7.1xs9ZwphFdP1BhNP1\n", - "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", - " !\n", - "-username admin privilege 15 role network-admin secret sha512 $6$JHsj9QpFAuo8TKNS$98Wmnr/.L2CCHtd6peL2q4fGOnt39C/XtBPJms6J/u1qBX9xWvf99FIYuQPSoqCTYBrN0ZNjzVKeIkNnV.Gez.\n", - "-username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", - "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - " !\n", - " interface Ethernet1\n", - " description link to leaf00.cmh\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system root-authentication]\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\"; ## SECRET-DATA\n", - "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcn03wv4SesN3JeAdwiOzgEdY5f+u4yptRK8OEjkHLuJg/6lR6MoD2BkdvWLShtx97/kVbxbTWOu9XM1mZ+E/YDR0mt7eHWwiy/OlgP9i0MzSj+XhtMUzRp7Ow+34VrrW7yQmuIkigq/QkDPv3b6O0u0y6azCQVrg5pvwRdZU2xTyKt/aM6/TL+glVh508XqG7RzlsmIRnrSa0WfHzcbQKPTJXlAjLGoYk53SltxW//e5HMQnTAJop0ic7FniXrVhS8F9iKxfLfFqzB5JJ2gaQX3y3cPr1MIg60aoSprI/8297wjE6fnQGcp1H1fD5rJx96m+3ViwydbtElhljcreB vagrant\"; ## SECRET-DATA\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -15,10 +15,10 @@\n", - " !\n", - " aaa authorization exec default local\n", - " !\n", - "-aaa root secret sha512 $6$8FLl5NfSda1Wh6d8$3SO4hP73eJnMf5kHrKUoMJ4jMtLU7iRrj/FJwAgAdk4GImfqy6WYLqMgmfpYOe6v/T4rjIFpOX..LmhCnbgbO0\n", - "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", - " !\n", - "-username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", - "-username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", - "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - " !\n", - " vlan 100\n", - " name frontend\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system root-authentication]\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\"; ## SECRET-DATA\n", - "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDuoXCl14JaKfWnyKSp1c4wjBv6XiCMsIRT7w0+BQxvaS7D1AoxNksYCTTjAJ8HVaMcLD7MI4bajS3/oEwtmCVpNJBG91UCi0P3tN2GjQwCwzrZG0eNpP2Gy51sKcq2lM1sxi+9QKYAtK5gmqV2Y8UeOuo4jKVNxCrPLYXO2BQBGCBUPayDjiPDir0H2BCKGpuwgegHgpkFKw+tWqo0IFsQmnvOQX+mjGDV8PVghCnzLO2ZbZrZPu5rRSgZm+CFGK1DGDsPBgdElxnu6ytjVIKkDzHrZ6HEm7yFgneb0WDGEmVl8MvBS9VPXXv8NzHJTUnedbxWKcqJ+xurpAGAYm6n vagrant\"; ## SECRET-DATA\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py --path backups/cmh --no-commit --filter site=cmh" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Looks legit, let's commit the changes:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[33m* spine00.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -15,10 +15,10 @@\n", - " !\n", - " aaa authorization exec default local\n", - " !\n", - "-aaa root secret sha512 $6$DInx3sLBwR.ikKs8$ThNQ/uVPDT9YXcreGqYZ53IZA.abXsMPsV4jdylCVl5Nu6GMcVcFgvE9L3hvM/vZDJS7.1xs9ZwphFdP1BhNP1\n", - "+aaa root secret sha512 $6$5stn7z2imBLV6iO0$w0ZnOhy8SwNdELdO2da9q8wDKerYTyY8evY052UoyRJ2Wo6liaUneuTFGphL8JQD9gtESOipCBb6PYmSMuUjs.\n", - " !\n", - "-username admin privilege 15 role network-admin secret sha512 $6$JHsj9QpFAuo8TKNS$98Wmnr/.L2CCHtd6peL2q4fGOnt39C/XtBPJms6J/u1qBX9xWvf99FIYuQPSoqCTYBrN0ZNjzVKeIkNnV.Gez.\n", - "-username vagrant privilege 15 role network-admin secret sha512 $6$Ga4ejrWPFsycSHFN$IJoLAEfCFHqiOwZX/PHlcx5vZ.Hpfx3NxHQXXEuf.Ni3QKlYL108fHruK86rzCjh9aYvBzoQ/ljLSy09.p6Z6/\n", - "+username admin privilege 15 role network-admin secret sha512 $6$qkXlQpatVlanYe9v$aHTbPaGTaqDRCp5WSC3DPpDfblYSE24.OHeKgGOOTf0.Ol2lDpivTvHByx5tU41sVOGcHqc4U4LgrKv8AjbKQ/\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$kRQZJTqx69hOW5ag$Y6VX8Kk37TWEsriKdr6ixqvMuUSSbuFu2Eh/5SIet2TCeXP3bdlwikIAruPp6lHB5HdC.t6tPsZVctHMU7H590\n", - " !\n", - " interface Ethernet1\n", - " description link to leaf00.cmh\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* spine01.cmh ** changed : True ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system root-authentication]\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDi/i8iiAZsXC5qdmJZpTxKjUyyoMgEGoHXl/TMFdJjSV+XAZ18OXAEsvPO0AlXJ6RZTwK8Zcr6TLq4l1Kssd+kVN02shFkgDo3wWf3I2BXKKdog6/6fbhiD1SgCeafzWBlUQvREgDQDy1XSFjNjSJ39vtOa8ikqGdbf4XH0hjoLHYDV0H0VNZLboULCNFPF0PHQfPrsp2AXHU+p7sl61GhZgfw6WuLIzXWqJyq9B0Q5XgdmvnvdjZeTOShoPTPbaRYVVFOMGTqJQOZsl5P3wTIJT8JG7iEz1Tiar8nmltON83sy/lEODhZkJPXe3zw3fwUIS9yQ53z0t1UGHm7KGNX vagrant\"; ## SECRET-DATA\n", - "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcn03wv4SesN3JeAdwiOzgEdY5f+u4yptRK8OEjkHLuJg/6lR6MoD2BkdvWLShtx97/kVbxbTWOu9XM1mZ+E/YDR0mt7eHWwiy/OlgP9i0MzSj+XhtMUzRp7Ow+34VrrW7yQmuIkigq/QkDPv3b6O0u0y6azCQVrg5pvwRdZU2xTyKt/aM6/TL+glVh508XqG7RzlsmIRnrSa0WfHzcbQKPTJXlAjLGoYk53SltxW//e5HMQnTAJop0ic7FniXrVhS8F9iKxfLfFqzB5JJ2gaQX3y3cPr1MIg60aoSprI/8297wjE6fnQGcp1H1fD5rJx96m+3ViwydbtElhljcreB vagrant\"; ## SECRET-DATA\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf00.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m@@ -15,10 +15,10 @@\n", - " !\n", - " aaa authorization exec default local\n", - " !\n", - "-aaa root secret sha512 $6$8FLl5NfSda1Wh6d8$3SO4hP73eJnMf5kHrKUoMJ4jMtLU7iRrj/FJwAgAdk4GImfqy6WYLqMgmfpYOe6v/T4rjIFpOX..LmhCnbgbO0\n", - "+aaa root secret sha512 $6$sRifRAo/DXihW7sG$3r4MMTsslNCCWdD/FFIw3lvnnkI4SWO0bvhEzvWSurrOBgUsxjrmgN5kywH5Ta7LNNXiWjFfjwoyefn9nqeB2/\n", - " !\n", - "-username admin privilege 15 role network-admin secret sha512 $6$JA1pT7X2JMgpNLbD$mqA6evjEvg1wN09FOs9zniHg63Q.t7DEGEE5mxjXbmzLn5BI4H0OYjramSH5TTwsIyrBbTVbEv49dzeHqpYD4/\n", - "-username vagrant privilege 15 role network-admin secret sha512 $6$MZz3VvL4.drK.FFg$lgXW.Fcb9rxxhAoYPg/GxFKAKVxrDEsPmeVNxxn8IH7RnRDRgZltqjPdpq53XYPaeGQO51MZ1qt30ziPwKbDl0\n", - "+username admin privilege 15 role network-admin secret sha512 $6$/K1M3ENrC/xALAOm$1vCB5TfaI8ih5GQRCwhRE7KGzmc.EGuQZ7dEuwhP7AJC0/A97u88miINH/7GtrBpRZ.Inn5JY9tuymMcmyyKc.\n", - "+username vagrant privilege 15 role network-admin secret sha512 $6$9CGTCvCiiJK3lDMp$kU9ncPDBkw0w09.h9wIhQtMAkZ/1zD1ds/wlAZAtmSQf5ntNMjDgvmZpBcXWAPAETlk4.kA9niLTVmQwaLBV/.\n", - " !\n", - " vlan 100\n", - " name frontend\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[33m* leaf01.cmh ** changed : True *************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : True --------------------\u001b[0m\n", - "\u001b[0m[edit system root-authentication]\n", - "+ ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsfGpEhGi8CbjIHJkMju/CJH6IuQiIzZyDt+AVieDfXKWDuBSOfc7YV8xNdYMqQqpDOWmEVZ7dhfD6IWDI3aa6WLkEXORD+zScjQo+5iHty6VlI61ImHQkWhWX6pZi3Cq/JsH8oldIC2xvzFNWB2p1suu+rzuGtJjbDq5NMlp1bNSiBgV0dHZR6Lt1UuK/rVBl7FbBN8HpInM+a37SkkwIrKMK8z42Ax9ufd17P3SqZP8oo+Ql4Y3aeCz2t4CfZNh9YRLZSiUYF16VN+31mzKEqT7+0rFlyfv/CaPwyfAv2BPFljUEsyFsWU923EGYQsfOIKVnd+zzHDHIHapVMQbh vagrant\"; ## SECRET-DATA\n", - "- ssh-rsa \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDuoXCl14JaKfWnyKSp1c4wjBv6XiCMsIRT7w0+BQxvaS7D1AoxNksYCTTjAJ8HVaMcLD7MI4bajS3/oEwtmCVpNJBG91UCi0P3tN2GjQwCwzrZG0eNpP2Gy51sKcq2lM1sxi+9QKYAtK5gmqV2Y8UeOuo4jKVNxCrPLYXO2BQBGCBUPayDjiPDir0H2BCKGpuwgegHgpkFKw+tWqo0IFsQmnvOQX+mjGDV8PVghCnzLO2ZbZrZPu5rRSgZm+CFGK1DGDsPBgdElxnu6ytjVIKkDzHrZ6HEm7yFgneb0WDGEmVl8MvBS9VPXXv8NzHJTUnedbxWKcqJ+xurpAGAYm6n vagrant\"; ## SECRET-DATA\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py --path backups/cmh --commit --filter site=cmh" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And let's verify all changes were applied correctly:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[34m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- Loading Configuration on the device ** changed : False -------------------\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run rollback.py --path backups/cmh --no-commit --filter site=cmh" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/2_simple_tooling/rollback.py b/examples/2_simple_tooling/rollback.py deleted file mode 100755 index 175d7dd5..00000000 --- a/examples/2_simple_tooling/rollback.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -""" -Tool to rollback configuration from a saved configuration -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import networking, text - -import click - - -def rollback(task, path): - """ - This function loads the backup from ./$path/$hostname and - deploys it. - """ - task.run(networking.napalm_configure, - name="Loading Configuration on the device", - replace=True, - filename="{}/{}".format(path, task.host)) - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="k=v pairs to filter the devices") -@click.option('--commit/--no-commit', '-c', default=False, - help="whether you want to commit the changes or not") -@click.option('--path', '-p', default=".", - help="Where to save the backup files") -def main(filter, commit, path): - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=not commit, - raise_on_error=True, - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - # let's filter the devices - filtered = brg.filter(**filter_dict) - - results = filtered.run(task=rollback, path=path) - - filtered.run(text.print_result, - num_workers=1, # task should be done synchronously - data=results, - task_id=-1, # we only want to print the last task - ) - - -if __name__ == "__main__": - main() diff --git a/examples/2_simple_tooling/validate.ipynb b/examples/2_simple_tooling/validate.ipynb deleted file mode 100644 index 75711ffc..00000000 --- a/examples/2_simple_tooling/validate.ipynb +++ /dev/null @@ -1,404 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# ignore this cell, this is just a helper cell to provide the magic %highlight_file\n", - "%run ../highlighter.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Validate\n", - "\n", - "## Code" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
 1 #!/usr/bin/env python\n",
-       " 2 """\n",
-       " 3 Runbook that verifies that BGP sessions are configured and up.\n",
-       " 4 """\n",
-       " 5 from brigade.easy import easy_brigade\n",
-       " 6 from brigade.plugins.tasks import data, networking, text\n",
-       " 7 \n",
-       " 8 import click\n",
-       " 9 \n",
-       "10 \n",
-       "11 def validate(task):\n",
-       "12     task.host["config"] = ""\n",
-       "13 \n",
-       "14     r = task.run(name="read data",\n",
-       "15                  task=data.load_yaml,\n",
-       "16                  file="../extra_data/{host}/l3.yaml")\n",
-       "17 \n",
-       "18     validation_rules = [{\n",
-       "19         'get_bgp_neighbors': {\n",
-       "20             'global': {\n",
-       "21                 'peers': {\n",
-       "22                     '_mode': 'strict',\n",
-       "23                 }\n",
-       "24             }\n",
-       "25         }\n",
-       "26     }]\n",
-       "27     peers = validation_rules[0]['get_bgp_neighbors']['global']['peers']\n",
-       "28     for session in r.result['sessions']:\n",
-       "29         peers[session['ipv4']] = {'is_up': True}\n",
-       "30 \n",
-       "31     task.run(name="validating data",\n",
-       "32              task=networking.napalm_validate,\n",
-       "33              validation_source=validation_rules)\n",
-       "34 \n",
-       "35 \n",
-       "36 def print_compliance(task, results):\n",
-       "37     """\n",
-       "38     We use this task so we can access directly the result\n",
-       "39     for each specific host and see if the task complies or not\n",
-       "40     and pass it to print_result.\n",
-       "41     """\n",
-       "42     task.run(name="print result",\n",
-       "43              task=text.print_result,\n",
-       "44              data=results[task.host.name],\n",
-       "45              failed=not results[task.host.name][2].result['complies'],\n",
-       "46              )\n",
-       "47 \n",
-       "48 \n",
-       "49 @click.command()\n",
-       "50 @click.option('--filter', '-f', multiple=True,\n",
-       "51               help="k=v pairs to filter the devices")\n",
-       "52 def main(filter):\n",
-       "53     brg = easy_brigade(\n",
-       "54             host_file="../inventory/hosts.yaml",\n",
-       "55             group_file="../inventory/groups.yaml",\n",
-       "56             dry_run=False,\n",
-       "57             raise_on_error=True,\n",
-       "58     )\n",
-       "59 \n",
-       "60     # filter is going to be a list of key=value so we clean that first\n",
-       "61     filter_dict = {"type": "network_device"}\n",
-       "62     for f in filter:\n",
-       "63         k, v = f.split("=")\n",
-       "64         filter_dict[k] = v\n",
-       "65 \n",
-       "66     # select which devices we want to work with\n",
-       "67     filtered = brg.filter(**filter_dict)\n",
-       "68 \n",
-       "69     results = filtered.run(task=validate)\n",
-       "70 \n",
-       "71     filtered.run(print_compliance,\n",
-       "72                  results=results,\n",
-       "73                  num_workers=1)\n",
-       "74 \n",
-       "75 \n",
-       "76 if __name__ == "__main__":\n",
-       "77     main()\n",
-       "
\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%highlight_file validate.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Demo\n", - "\n", - "As usual, let's start with the help:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Usage: validate.py [OPTIONS]\n", - "\n", - "Options:\n", - " -f, --filter TEXT k=v pairs to filter the devices\n", - " --help Show this message and exit.\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run validate.py --help" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Not much to it, very similar to its runbook counterpart:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m\u001b[31m* spine00.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.0.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", - " \u001b[0m'10.0.0.3'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* spine01.cmh ** changed : False ***********************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.0/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'leaf01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.2/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.1.1', 'peer_as': 65100}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.3', 'peer_as': 65101}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[31m* leaf00.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'Ethernet1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.1/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'Ethernet2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.1/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.0', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.0', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'peers'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'10.0.0.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'diff'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'is_up'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'actual_value'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'complies'\u001b[0m: \u001b[0mFalse\u001b[0m,\n", - " \u001b[0m'expected_value'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mFalse\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m,\n", - " \u001b[0m'10.0.1.0'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[34m* leaf01.cmh ** changed : False ************************************************\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validate ** changed : False ----------------------------------------------\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- read data ** changed : False ---------------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'interfaces'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'ge-0/0/1'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine00.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.0.3/31'\u001b[0m}\u001b[0m,\n", - " \u001b[0m'ge-0/0/2'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'connects_to'\u001b[0m: \u001b[0m'spine01.cmh'\u001b[0m,\n", - " \u001b[0m'enabled'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'ipv4'\u001b[0m: \u001b[0m'10.0.1.3/31'\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'sessions'\u001b[0m: \u001b[0m[\u001b[0m \u001b[0m\u001b[0m{'ipv4': '10.0.0.2', 'peer_as': 65000}\u001b[0m,\n", - " \u001b[0m{'ipv4': '10.0.1.2', 'peer_as': 65000}\u001b[0m]\u001b[0m}\u001b[0m\n", - "\u001b[0m\u001b[1m\u001b[36m---- validating data ** changed : False ---------------------------------------\u001b[0m\n", - "\u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'get_bgp_neighbors'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'extra'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'missing'\u001b[0m: \u001b[0m[]\u001b[0m,\n", - " \u001b[0m'present'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'global'\u001b[0m: \u001b[0m{\u001b[0m \u001b[0m'complies'\u001b[0m: \u001b[0mTrue\u001b[0m,\n", - " \u001b[0m'nested'\u001b[0m: \u001b[0mTrue\u001b[0m}\u001b[0m}\u001b[0m}\u001b[0m,\n", - " \u001b[0m'skipped'\u001b[0m: \u001b[0m[]\u001b[0m}\u001b[0m\n", - "\u001b[0m\n", - "\u001b[0m" - ] - } - ], - "source": [ - "%run validate.py --filter site=cmh" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/2_simple_tooling/validate.py b/examples/2_simple_tooling/validate.py deleted file mode 100755 index 04597074..00000000 --- a/examples/2_simple_tooling/validate.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -""" -Runbook that verifies that BGP sessions are configured and up. -""" -from brigade.easy import easy_brigade -from brigade.plugins.tasks import data, networking, text - -import click - - -def validate(task): - task.host["config"] = "" - - r = task.run(name="read data", - task=data.load_yaml, - file="../extra_data/{host}/l3.yaml") - - validation_rules = [{ - 'get_bgp_neighbors': { - 'global': { - 'peers': { - '_mode': 'strict', - } - } - } - }] - peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] - for session in r.result['sessions']: - peers[session['ipv4']] = {'is_up': True} - - task.run(name="validating data", - task=networking.napalm_validate, - validation_source=validation_rules) - - -def print_compliance(task, results): - """ - We use this task so we can access directly the result - for each specific host and see if the task complies or not - and pass it to print_result. - """ - task.run(name="print result", - task=text.print_result, - data=results[task.host.name], - failed=not results[task.host.name][2].result['complies'], - ) - - -@click.command() -@click.option('--filter', '-f', multiple=True, - help="k=v pairs to filter the devices") -def main(filter): - brg = easy_brigade( - host_file="../inventory/hosts.yaml", - group_file="../inventory/groups.yaml", - dry_run=False, - raise_on_error=True, - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - # select which devices we want to work with - filtered = brg.filter(**filter_dict) - - results = filtered.run(task=validate) - - filtered.run(print_compliance, - results=results, - num_workers=1) - - -if __name__ == "__main__": - main() diff --git a/examples/3_advanced_tooling/mate.py b/examples/3_advanced_tooling/mate.py deleted file mode 100755 index e78acd91..00000000 --- a/examples/3_advanced_tooling/mate.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -""" -In this example we write a CLI tool with brigade and click to deploy configuration. -""" - -from brigade.core import Brigade -from brigade.core.configuration import Config -from brigade.plugins.inventory.simple import SimpleInventory - -import click - -from tasks import backup, configure, get_facts, validate - - -@click.group() -@click.option('--filter', '-f', multiple=True) -@click.option('--commit/--no-commit', '-c', default=False) -@click.pass_context -def run(ctx, filter, commit): - brigade = Brigade( - inventory=SimpleInventory("../hosts.yaml", "../groups.yaml"), - dry_run=not commit, - config=Config(raise_on_error=False), - ) - - # filter is going to be a list of key=value so we clean that first - filter_dict = {"type": "network_device"} - for f in filter: - k, v = f.split("=") - filter_dict[k] = v - - filtered = brigade.filter(**filter_dict) # let's filter the devices - ctx.obj["filtered"] = filtered - - -run.add_command(backup.backup) -run.add_command(configure.configure) -run.add_command(get_facts.get) -run.add_command(validate.validate) - - -if __name__ == "__main__": - run(obj={}) diff --git a/examples/3_advanced_tooling/tasks/backup.py b/examples/3_advanced_tooling/tasks/backup.py deleted file mode 100755 index 67262c38..00000000 --- a/examples/3_advanced_tooling/tasks/backup.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -""" -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. - -The main difference with get_facts_simple.py is that instead of calling a plugin directly -we wrap it in a function. It is not very useful or necessary here but illustrates how -tasks can be grouped. -""" -from brigade.plugins import functions -from brigade.plugins.tasks import files, networking, text - -import click - - -def backup_task(task, path): - result = task.run(networking.napalm_get, - getters="config") - - return task.run(files.write, - content=result.result["config"]["running"], - filename="{}/{}".format(path, task.host)) - - -@click.command() -@click.option('--backup-path', default=".") -@click.pass_context -def backup(ctx, backup_path, **kwargs): - functions.text.print_title("Backing up configurations") - filtered = ctx.obj["filtered"] - results = filtered.run(backup_task, - path=backup_path) - - # Let's print the result on screen - return filtered.run(text.print_result, - num_workers=1, - data=results) diff --git a/examples/3_advanced_tooling/tasks/configure.py b/examples/3_advanced_tooling/tasks/configure.py deleted file mode 100755 index 6ef6c512..00000000 --- a/examples/3_advanced_tooling/tasks/configure.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -""" -In this example we write a CLI tool with brigade and click to deploy configuration. -""" -import time - -from brigade.plugins import functions -from brigade.plugins.tasks import data, networking, text - -import click - -from . import backup as backup_ -from . import validate as validate_ - - -def configure_task(task): - r = task.run(text.template_file, - template="base.j2", - path="../templates/{brigade_nos}") - task.host["config"] = r.result - - r = task.run(data.load_yaml, - file="../extra_data/{host}/l3.yaml") - task.host["l3"] = r.result - - r = task.run(text.template_file, - template="interfaces.j2", - path="../templates/{brigade_nos}") - task.host["config"] += r.result - - r = task.run(text.template_file, - template="routing.j2", - path="../templates/{brigade_nos}") - task.host["config"] += r.result - - r = task.run(text.template_file, - template="{role}.j2", - path="../templates/{brigade_nos}") - task.host["config"] += r.result - - return task.run(networking.napalm_configure, - replace=False, - configuration=task.host["config"]) - - -@click.command() -@click.option("--validate/--no-validate", default=False) -@click.option("--rollback/--no-rollback", default=False) -@click.option("--backup/--no-backup", default=False) -@click.option('--backup-path', default=".") -@click.pass_context -def configure(ctx, validate, backup, backup_path, rollback): - filtered = ctx.obj["filtered"] - - if backup: - backup_.backup.invoke(ctx) - - functions.text.print_title("Configure Network") - results = filtered.run(task=configure_task) - - filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) - - if validate: - time.sleep(10) - r = validate_.validate.invoke(ctx) - - if r.failed and rollback: - functions.text.print_title("Rolling back configuration!!!") - r = filtered.run(networking.napalm_configure, - replace=True, - filename=backup_path + "/{host}") - filtered.run(text.print_result, - num_workers=1, - data=r) - import pdb; pdb.set_trace() # noqa diff --git a/examples/3_advanced_tooling/tasks/get_facts.py b/examples/3_advanced_tooling/tasks/get_facts.py deleted file mode 100755 index e31dcdd9..00000000 --- a/examples/3_advanced_tooling/tasks/get_facts.py +++ /dev/null @@ -1,21 +0,0 @@ -from brigade.plugins.tasks import networking, text - -import click - - -@click.command() -@click.option('--get', '-g', multiple=True, - help="getters you want to use") -@click.pass_context -def get(ctx, get): - """ - Retrieve information from network devices using napalm - """ - filtered = ctx.obj["filtered"] - results = filtered.run(networking.napalm_get, - getters=get) - - # Let's print the result on screen - filtered.run(text.print_result, - num_workers=1, # we are printing on screen so we want to do this synchronously - data=results) diff --git a/examples/3_advanced_tooling/tasks/validate.py b/examples/3_advanced_tooling/tasks/validate.py deleted file mode 100755 index a22bf30b..00000000 --- a/examples/3_advanced_tooling/tasks/validate.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -""" -In this example we write a CLI tool with brigade and click to deploy configuration. -""" -from brigade.plugins import functions -from brigade.plugins.tasks import data, networking, text - -import click - - -def validate_task(task): - task.host["config"] = "" - - r = task.run(name="read data", - task=data.load_yaml, - file="../extra_data/{host}/l3.yaml") - - validation_rules = [{ - 'get_bgp_neighbors': { - 'global': { - 'peers': { - '_mode': 'strict', - } - } - } - }] - peers = validation_rules[0]['get_bgp_neighbors']['global']['peers'] - for session in r.result['sessions']: - peers[session['ipv4']] = {'is_up': True} - - return task.run(name="validating data", - task=networking.napalm_validate, - validation_source=validation_rules) - - -@click.command() -@click.pass_context -def validate(ctx, **kwargs): - functions.text.print_title("Make sure BGP sessions are UP") - filtered = ctx.obj["filtered"] - - results = filtered.run(task=validate_task) - - filtered.run(name="print validate result", - num_workers=1, - task=text.print_result, - data=results) - - return results diff --git a/examples/extra_data/leaf00.cmh/l3.yaml b/examples/extra_data/leaf00.cmh/l3.yaml deleted file mode 100644 index f5fb9854..00000000 --- a/examples/extra_data/leaf00.cmh/l3.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -interfaces: - Ethernet1: - connects_to: spine00.cmh - ipv4: 10.0.0.1/31 - enabled: false - Ethernet2: - connects_to: spine01.cmh - ipv4: 10.0.1.1/31 - enabled: true - -sessions: - - ipv4: 10.0.0.0 - peer_as: 65000 - - ipv4: 10.0.1.0 - peer_as: 65000 diff --git a/examples/extra_data/leaf01.cmh/l3.yaml b/examples/extra_data/leaf01.cmh/l3.yaml deleted file mode 100644 index f3fb1c5c..00000000 --- a/examples/extra_data/leaf01.cmh/l3.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -interfaces: - ge-0/0/1: - connects_to: spine00.cmh - ipv4: 10.0.0.3/31 - enabled: true - ge-0/0/2: - connects_to: spine01.cmh - ipv4: 10.0.1.3/31 - enabled: true - -sessions: - - ipv4: 10.0.0.2 - peer_as: 65000 - - ipv4: 10.0.1.2 - peer_as: 65000 diff --git a/examples/extra_data/spine00.cmh/l3.yaml b/examples/extra_data/spine00.cmh/l3.yaml deleted file mode 100644 index 19c58537..00000000 --- a/examples/extra_data/spine00.cmh/l3.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -interfaces: - Ethernet1: - connects_to: leaf00.cmh - ipv4: 10.0.0.0/31 - enabled: true - Ethernet2: - connects_to: leaf01.cmh - ipv4: 10.0.0.2/31 - enabled: true - -sessions: - - ipv4: 10.0.0.1 - peer_as: 65100 - - ipv4: 10.0.0.3 - peer_as: 65101 diff --git a/examples/extra_data/spine01.cmh/l3.yaml b/examples/extra_data/spine01.cmh/l3.yaml deleted file mode 100644 index 7bdd33cc..00000000 --- a/examples/extra_data/spine01.cmh/l3.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -interfaces: - ge-0/0/1: - connects_to: leaf00.cmh - ipv4: 10.0.1.0/31 - enabled: true - ge-0/0/2: - connects_to: leaf01.cmh - ipv4: 10.0.1.2/31 - enabled: true - -sessions: - - ipv4: 10.0.1.1 - peer_as: 65100 - - ipv4: 10.0.1.3 - peer_as: 65101 diff --git a/examples/inventory/groups.yaml b/examples/inventory/groups.yaml deleted file mode 100644 index 8cbbb15c..00000000 --- a/examples/inventory/groups.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -all: - domain: acme.com - -bma: - group: all - -cmh: - group: all - asn: 65000 - vlans: - 100: frontend - 200: backend diff --git a/examples/inventory/network_diagram.graffle b/examples/inventory/network_diagram.graffle deleted file mode 100644 index 871fb479..00000000 Binary files a/examples/inventory/network_diagram.graffle and /dev/null differ diff --git a/examples/inventory/network_diagram.png b/examples/inventory/network_diagram.png deleted file mode 100644 index 064b4a05..00000000 Binary files a/examples/inventory/network_diagram.png and /dev/null differ diff --git a/examples/requirements.txt b/examples/requirements.txt deleted file mode 100644 index dca9a909..00000000 --- a/examples/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -click diff --git a/examples/templates/eos/interfaces.j2 b/examples/templates/eos/interfaces.j2 deleted file mode 100644 index 6aea0d99..00000000 --- a/examples/templates/eos/interfaces.j2 +++ /dev/null @@ -1,8 +0,0 @@ -{% for interface, data in l3.interfaces.items() %} -interface {{ interface }} - no switchport - ip address {{ data.ipv4 }} - description link to {{ data.connects_to }} - {{ "no" if data.enabled else "" }} shutdown -{% endfor %} - diff --git a/examples/templates/eos/leaf.j2 b/examples/templates/eos/leaf.j2 deleted file mode 100644 index 4d6c9506..00000000 --- a/examples/templates/eos/leaf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% for vlan_id, name in vlans.items() %} -vlan {{ vlan_id }} - name {{ name }} -{% endfor %} diff --git a/examples/templates/eos/routing.j2 b/examples/templates/eos/routing.j2 deleted file mode 100644 index dea73518..00000000 --- a/examples/templates/eos/routing.j2 +++ /dev/null @@ -1,12 +0,0 @@ -ip routing - -default router bgp -router bgp {{ asn }} - {% for session in l3.sessions %} - neighbor {{ session.ipv4 }} remote-as {{ session.peer_as }} - - address-family ipv4 - neighbor {{ session.ipv4 }} activate - {% endfor %} -exit - diff --git a/examples/templates/junos/interfaces.j2 b/examples/templates/junos/interfaces.j2 deleted file mode 100644 index 1c794330..00000000 --- a/examples/templates/junos/interfaces.j2 +++ /dev/null @@ -1,15 +0,0 @@ -interfaces { -{% for interface, data in l3.interfaces.items() %} - replace: - {{ interface }} { - description "link to {{ data.connects_to }}"; - {{ "disable;" if not data.enabled else "" }} - unit 0 { - family inet { - address {{ data.ipv4 }}; - } - } - } -{% endfor %} -} - diff --git a/examples/templates/junos/leaf.j2 b/examples/templates/junos/leaf.j2 deleted file mode 100644 index f2e40819..00000000 --- a/examples/templates/junos/leaf.j2 +++ /dev/null @@ -1,8 +0,0 @@ -replace: -vlans { -{% for vlan_id, name in vlans.items() %} - {{ name }} { - vlan-id {{ vlan_id }}; - } -{% endfor %} -} diff --git a/examples/templates/junos/routing.j2 b/examples/templates/junos/routing.j2 deleted file mode 100644 index cc231986..00000000 --- a/examples/templates/junos/routing.j2 +++ /dev/null @@ -1,30 +0,0 @@ -routing-options { - autonomous-system {{ asn }}; -} - -policy-options { - policy-statement PERMIT_ALL { - from protocol bgp; - then accept; - } -} - -protocols { - replace: - bgp { - import PERMIT_ALL; - export PERMIT_ALL; - } -} - -{% for session in l3.sessions %} -protocols { - bgp { - group peers { - neighbor {{ session.ipv4 }} { - peer-as {{ session.peer_as }}; - } - } - } -} -{% endfor %} diff --git a/requirements-dev.txt b/requirements-dev.txt index cb638680..8d6f3c01 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ +decorator pytest pytest-cov pylama flake8-import-order tox +black==18.4a1; python_version >= '3.6' -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 2efafbfc..12396b0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ colorama pyyaml jinja2 -napalm>=2.2.0 -netmiko>=2.0.0 +napalm>=2.3.0 +netmiko>=2.1.1 paramiko future requests +ruamel.yaml diff --git a/setup.cfg b/setup.cfg index 10aae871..579d83f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,5 +18,5 @@ skip = .tox/* max_line_length = 100 [tool:pytest] -addopts = --cov=brigade --cov-report=term-missing -vs --pylama +addopts = --cov=brigade --cov-report=term-missing -vs python_paths = ./ diff --git a/setup.py b/setup.py index 2f383cf0..7a76b253 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,37 @@ #!/usr/bin/env python -import uuid +from setuptools import find_packages, setup -try: - from setuptools import find_packages, setup -except ImportError: - from distutils.core import setup +with open("requirements.txt", "r") as fs: + reqs = [r for r in fs.read().splitlines() if (len(r) > 0 and not r.startswith("#"))] -from pip.req import parse_requirements +with open("README.md", "r") as fs: + long_description = fs.read() -install_reqs = parse_requirements('requirements.txt', session=uuid.uuid1()) -reqs = [str(ir.req) for ir in install_reqs] +__author__ = "dbarrosop@dravetech.com" +__license__ = "Apache License, version 2" +__version__ = "1.0.0" -__author__ = 'dbarrosop@dravetech.com' -__license__ = 'Apache License, version 2' - -__version__ = '0.0.6' - -setup(name='brigade', - version=__version__, - description="Fighting fire with fire", - author=__author__, - url='https://github.com/napalm-automation/brigade', - include_package_data=True, - install_requires=reqs, - packages=find_packages(exclude=("test*", )), - license=__license__, - test_suite='tests', - platforms='any', - classifiers=['Development Status :: 4 - Beta', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - ]) +setup( + name="brigade", + version=__version__, + description="Fighting fire with fire", + long_description=long_description, + long_description_content_type="text/markdown", + author=__author__, + url="https://github.com/brigade-automation/brigade", + include_package_data=True, + install_requires=reqs, + packages=find_packages(exclude=("test*",)), + license=__license__, + test_suite="tests", + platforms="any", + classifiers=[ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + ], +) diff --git a/tests/conftest.py b/tests/conftest.py index abe65897..f47fade2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,19 +10,24 @@ logging.basicConfig( filename="tests.log", - filemode='w', + filemode="w", level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(funcName)20s() - %(message)s', + format="%(asctime)s - %(name)s - %(levelname)s - %(funcName)20s() - %(message)s", ) @pytest.fixture(scope="session", autouse=True) def containers(request): """Start/Stop containers needed for the tests.""" + def fin(): logging.info("Stopping containers") - subprocess.check_call(["./tests/inventory_data/containers.sh", "stop"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE) + subprocess.check_call( + ["./tests/inventory_data/containers.sh", "stop"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + request.addfinalizer(fin) try: @@ -30,8 +35,9 @@ def fin(): except Exception: pass logging.info("Starting containers") - subprocess.check_call(["./tests/inventory_data/containers.sh", "start"], - stdout=subprocess.PIPE) + subprocess.check_call( + ["./tests/inventory_data/containers.sh", "start"], stdout=subprocess.PIPE + ) @pytest.fixture(scope="session", autouse=True) @@ -40,8 +46,10 @@ def brigade(request): dir_path = os.path.dirname(os.path.realpath(__file__)) brigade = Brigade( - inventory=SimpleInventory("{}/inventory_data/hosts.yaml".format(dir_path), - "{}/inventory_data/groups.yaml".format(dir_path)), + inventory=SimpleInventory( + "{}/inventory_data/hosts.yaml".format(dir_path), + "{}/inventory_data/groups.yaml".format(dir_path), + ), dry_run=True, ) return brigade diff --git a/tests/core/inventory_data/groups.yaml b/tests/core/inventory_data/groups.yaml deleted file mode 100644 index 4d48fc78..00000000 --- a/tests/core/inventory_data/groups.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -all: - my_var: comes_from_all - -group_1: - group: all - my_var: comes_from_group_1 - site: site1 - -group_2: - group: all - site: site2 diff --git a/tests/core/inventory_data/hosts.yaml b/tests/core/inventory_data/hosts.yaml deleted file mode 100644 index 4b20c208..00000000 --- a/tests/core/inventory_data/hosts.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -host1.group_1: - group: group_1 - my_var: comes_from_host1.group_1 - www_server: nginx - role: www - -host2.group_1: - group: group_1 - role: db - -host3.group_2: - group: group_2 - www_server: apache - role: www - -host4.group_2: - group: group_2 - my_var: comes_from_host4.group_2 - role: db diff --git a/tests/core/test_InitBrigade.py b/tests/core/test_InitBrigade.py new file mode 100644 index 00000000..d6a3d9fc --- /dev/null +++ b/tests/core/test_InitBrigade.py @@ -0,0 +1,94 @@ +import os +from builtins import super + + +from brigade.core import InitBrigade +from brigade.core.inventory import Inventory + + +dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_InitBrigade") + + +def transform_func(host): + host.processed_by_transform_function = True + + +class StringInventory(Inventory): + + def __init__(self, *args, **kwargs): + hosts = {"host1": {}, "host2": {}} + super().__init__(hosts, *args, **kwargs) + + +class Test(object): + + def test_InitBrigade_defaults(self): + os.chdir("tests/inventory_data/") + try: + brg = InitBrigade() + finally: + os.chdir("../../") + assert not brg.dry_run + assert brg.config.num_workers == 20 + assert len(brg.inventory.hosts) + assert len(brg.inventory.groups) + + def test_InitBrigade_file(self): + brg = InitBrigade(config_file=os.path.join(dir_path, "a_config.yaml")) + assert not brg.dry_run + assert brg.config.num_workers == 100 + assert len(brg.inventory.hosts) + assert len(brg.inventory.groups) + + def test_InitBrigade_programmatically(self): + brg = InitBrigade( + num_workers=100, + inventory="brigade.plugins.inventory.simple.SimpleInventory", + SimpleInventory={ + "host_file": "tests/inventory_data/hosts.yaml", + "group_file": "tests/inventory_data/groups.yaml", + }, + ) + assert not brg.dry_run + assert brg.config.num_workers == 100 + assert len(brg.inventory.hosts) + assert len(brg.inventory.groups) + + def test_InitBrigade_combined(self): + brg = InitBrigade( + config_file=os.path.join(dir_path, "a_config.yaml"), num_workers=200 + ) + assert not brg.dry_run + assert brg.config.num_workers == 200 + assert len(brg.inventory.hosts) + assert len(brg.inventory.groups) + + def test_InitBrigade_different_inventory_by_string(self): + brg = InitBrigade( + config_file=os.path.join(dir_path, "a_config.yaml"), + inventory="tests.core.test_InitBrigade.StringInventory", + ) + assert isinstance(brg.inventory, StringInventory) + + def test_InitBrigade_different_inventory_imported(self): + brg = InitBrigade( + config_file=os.path.join(dir_path, "a_config.yaml"), + inventory=StringInventory, + ) + assert isinstance(brg.inventory, StringInventory) + + def test_InitBrigade_different_transform_function_by_string(self): + brg = InitBrigade( + config_file=os.path.join(dir_path, "a_config.yaml"), + transform_function="tests.core.test_InitBrigade.transform_func", + ) + for value in brg.inventory.hosts.values(): + assert value.processed_by_transform_function + + def test_InitBrigade_different_transform_function_imported(self): + brg = InitBrigade( + config_file=os.path.join(dir_path, "a_config.yaml"), + transform_function=transform_func, + ) + for value in brg.inventory.hosts.values(): + assert value.processed_by_transform_function diff --git a/tests/core/test_InitBrigade/a_config.yaml b/tests/core/test_InitBrigade/a_config.yaml new file mode 100644 index 00000000..9047b4a9 --- /dev/null +++ b/tests/core/test_InitBrigade/a_config.yaml @@ -0,0 +1,6 @@ +--- +num_workers: 100 +inventory: brigade.plugins.inventory.simple.SimpleInventory +SimpleInventory: + host_file: "tests/inventory_data/hosts.yaml" + group_file: "tests/inventory_data/groups.yaml" diff --git a/tests/core/test_configuration.py b/tests/core/test_configuration.py new file mode 100644 index 00000000..d65f50c9 --- /dev/null +++ b/tests/core/test_configuration.py @@ -0,0 +1,97 @@ +import os + +from brigade.core.configuration import Config + +# import pytest + + +dir_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "test_configuration" +) + + +class Test(object): + + def test_configuration_empty(self): + config = Config( + config_file=os.path.join(dir_path, "empty.yaml"), + arg1=1, + arg2=False, + arg3=None, + arg4="asd", + ) + assert config.num_workers == 20 + assert not config.raise_on_error + assert config.arg1 == 1 + assert config.arg2 is False + assert config.arg3 is None + assert config.arg4 == "asd" + + def test_configuration_normal(self): + config = Config( + config_file=os.path.join(dir_path, "config.yaml"), + arg1=1, + arg2=False, + arg3=None, + arg4="asd", + ) + assert config.num_workers == 10 + assert not config.raise_on_error + assert config.arg1 == 1 + assert config.arg2 is False + assert config.arg3 is None + assert config.arg4 == "asd" + + def test_configuration_normal_override_argument(self): + config = Config( + config_file=os.path.join(dir_path, "config.yaml"), + num_workers=20, + raise_on_error=True, + ) + assert config.num_workers == 20 + assert config.raise_on_error + + def test_configuration_normal_override_env(self): + os.environ["BRIGADE_NUM_WORKERS"] = "30" + os.environ["BRIGADE_RAISE_ON_ERROR"] = "1" + config = Config(config_file=os.path.join(dir_path, "config.yaml")) + assert config.num_workers == 30 + assert config.raise_on_error + os.environ.pop("BRIGADE_NUM_WORKERS") + os.environ.pop("BRIGADE_RAISE_ON_ERROR") + + def test_configuration_bool_env(self): + os.environ["BRIGADE_RAISE_ON_ERROR"] = "0" + config = Config() + assert config.num_workers == 20 + assert not config.raise_on_error + + def test_get_user_defined_from_file(self): + config = Config(config_file=os.path.join(dir_path, "config.yaml")) + assert ( + config.get("user_defined", env="USER_DEFINED", default="qweqwe") == "asdasd" + ) + + def test_get_user_defined_from_default(self): + config = Config() + assert ( + config.get("user_defined", env="USER_DEFINED", default="qweqwe") == "qweqwe" + ) + + def test_get_user_defined_from_env(self): + os.environ["USER_DEFINED"] = "zxczxc" + config = Config(config_file=os.path.join(dir_path, "config.yaml")) + assert ( + config.get("user_defined", env="USER_DEFINED", default="qweqwe") == "zxczxc" + ) + os.environ.pop("USER_DEFINED") + + def test_get_user_defined_from_env_bool(self): + os.environ["USER_DEFINED"] = "0" + config = Config() + assert not config.get("user_defined", env="USER_DEFINED", parameter_type="bool") + os.environ.pop("USER_DEFINED") + + def test_get_user_defined_nested(self): + config = Config(config_file=os.path.join(dir_path, "config.yaml")) + assert config.get("user_defined", root="my_root") == "i am nested" diff --git a/tests/core/test_configuration/config.yaml b/tests/core/test_configuration/config.yaml new file mode 100644 index 00000000..164c4f87 --- /dev/null +++ b/tests/core/test_configuration/config.yaml @@ -0,0 +1,6 @@ +--- +num_workers: 10 +raise_on_error: no +user_defined: "asdasd" +my_root: + user_defined: "i am nested" diff --git a/examples/3_advanced_tooling/tasks/__init__.py b/tests/core/test_configuration/empty.yaml similarity index 100% rename from examples/3_advanced_tooling/tasks/__init__.py rename to tests/core/test_configuration/empty.yaml diff --git a/tests/core/test_inventory.py b/tests/core/test_inventory.py index 267dd6ac..62a7a02b 100644 --- a/tests/core/test_inventory.py +++ b/tests/core/test_inventory.py @@ -1,55 +1,206 @@ import os +from brigade.core.inventory import Group, Host from brigade.plugins.inventory.simple import SimpleInventory import pytest dir_path = os.path.dirname(os.path.realpath(__file__)) -inventory = SimpleInventory("{}/../inventory_data/hosts.yaml".format(dir_path), - "{}/../inventory_data/groups.yaml".format(dir_path)) +inventory = SimpleInventory( + "{}/../inventory_data/hosts.yaml".format(dir_path), + "{}/../inventory_data/groups.yaml".format(dir_path), +) + + +def compare_lists(left, right): + result = len(left) == len(right) + if not result: + return result + + def to_sets(l): + if isinstance(l, str): + return l + + r = set() + for x in l: + if isinstance(x, list) or isinstance(x, tuple): + x = frozenset(to_sets(x)) + r.add(x) + return r + + left = to_sets(left) + right = to_sets(right) + return left == right class Test(object): + def test_hosts(self): + defaults = {"var4": "ALL"} + g1 = Group(name="g1", var1="1", var2="2", var3="3") + g2 = Group(name="g2", var1="a", var2="b") + g3 = Group(name="g3", var1="A", var4="Z") + g4 = Group(name="g4", groups=[g2, g1], var3="q") + + h1 = Host(name="host1", groups=[g1, g2], defaults=defaults) + assert h1["var1"] == "1" + assert h1["var2"] == "2" + assert h1["var3"] == "3" + assert h1["var4"] == "ALL" + assert compare_lists( + list(h1.keys()), ["name", "groups", "var1", "var2", "var3", "var4"] + ) + assert compare_lists( + list(h1.values()), ["host1", ["g1", "g2"], "1", "2", "3", "ALL"] + ) + assert compare_lists( + list(h1.items()), + [ + ("name", "host1"), + ("groups", ["g1", "g2"]), + ("var1", "1"), + ("var2", "2"), + ("var3", "3"), + ("var4", "ALL"), + ], + ) + + h2 = Host(name="host2", groups=[g2, g1], defaults=defaults) + assert h2["var1"] == "a" + assert h2["var2"] == "b" + assert h2["var3"] == "3" + assert h2["var4"] == "ALL" + assert compare_lists( + list(h2.keys()), ["name", "groups", "var1", "var2", "var3", "var4"] + ) + assert compare_lists( + list(h2.values()), ["host2", ["g2", "g1"], "a", "b", "3", "ALL"] + ) + assert compare_lists( + list(h2.items()), + [ + ("name", "host2"), + ("groups", ["g2", "g1"]), + ("var1", "a"), + ("var2", "b"), + ("var3", "3"), + ("var4", "ALL"), + ], + ) + + h3 = Host(name="host3", groups=[g4, g3], defaults=defaults) + assert h3["var1"] == "a" + assert h3["var2"] == "b" + assert h3["var3"] == "q" + assert h3["var4"] == "Z" + assert compare_lists( + list(h3.keys()), ["name", "groups", "var3", "var1", "var2", "var4"] + ) + assert compare_lists( + list(h3.values()), ["host3", ["g4", "g3"], "q", "a", "b", "Z"] + ) + assert compare_lists( + list(h3.items()), + [ + ("name", "host3"), + ("groups", ["g4", "g3"]), + ("var3", "q"), + ("var1", "a"), + ("var2", "b"), + ("var4", "Z"), + ], + ) + + h4 = Host(name="host4", groups=[g3, g4], defaults=defaults) + assert h4["var1"] == "A" + assert h4["var2"] == "b" + assert h4["var3"] == "q" + assert h4["var4"] == "Z" + assert compare_lists( + list(h4.keys()), ["name", "groups", "var1", "var4", "var3", "var2"] + ) + assert compare_lists( + list(h4.values()), ["host4", ["g3", "g4"], "A", "Z", "q", "b"] + ) + assert compare_lists( + list(h4.items()), + [ + ("name", "host4"), + ("groups", ["g3", "g4"]), + ("var1", "A"), + ("var4", "Z"), + ("var3", "q"), + ("var2", "b"), + ], + ) + + with pytest.raises(KeyError): + assert h2["asdasd"] + def test_filtering(self): unfiltered = sorted(list(inventory.hosts.keys())) - assert unfiltered == ['dev1.group_1', 'dev2.group_1', 'dev3.group_2', 'dev4.group_2'] + assert ( + unfiltered + == ["dev1.group_1", "dev2.group_1", "dev3.group_2", "dev4.group_2"] + ) www = sorted(list(inventory.filter(role="www").hosts.keys())) - assert www == ['dev1.group_1', 'dev3.group_2'] + assert www == ["dev1.group_1", "dev3.group_2"] - www_site1 = sorted(list(inventory.filter(role="www", site="site1").hosts.keys())) - assert www_site1 == ['dev1.group_1'] + www_site1 = sorted( + list(inventory.filter(role="www", site="site1").hosts.keys()) + ) + assert www_site1 == ["dev1.group_1"] - www_site1 = sorted(list(inventory.filter(role="www").filter(site="site1").hosts.keys())) - assert www_site1 == ['dev1.group_1'] + www_site1 = sorted( + list(inventory.filter(role="www").filter(site="site1").hosts.keys()) + ) + assert www_site1 == ["dev1.group_1"] def test_filtering_func(self): - long_names = sorted(list(inventory.filter( - filter_func=lambda x: len(x["my_var"]) > 20).hosts.keys())) - assert long_names == ['dev1.group_1', 'dev4.group_2'] + long_names = sorted( + list( + inventory.filter( + filter_func=lambda x: len(x["my_var"]) > 20 + ).hosts.keys() + ) + ) + assert long_names == ["dev1.group_1", "dev4.group_2"] def longer_than(dev, length): return len(dev["my_var"]) > length - long_names = sorted(list(inventory.filter( - filter_func=longer_than, length=20).hosts.keys())) - assert long_names == ['dev1.group_1', 'dev4.group_2'] + long_names = sorted( + list(inventory.filter(filter_func=longer_than, length=20).hosts.keys()) + ) + assert long_names == ["dev1.group_1", "dev4.group_2"] def test_filter_unique_keys(self): - filtered = sorted(list(inventory.filter(www_server='nginx').hosts.keys())) - assert filtered == ['dev1.group_1'] + filtered = sorted(list(inventory.filter(www_server="nginx").hosts.keys())) + assert filtered == ["dev1.group_1"] def test_var_resolution(self): assert inventory.hosts["dev1.group_1"]["my_var"] == "comes_from_dev1.group_1" assert inventory.hosts["dev2.group_1"]["my_var"] == "comes_from_group_1" - assert inventory.hosts["dev3.group_2"]["my_var"] == "comes_from_all" + assert inventory.hosts["dev3.group_2"]["my_var"] == "comes_from_defaults" assert inventory.hosts["dev4.group_2"]["my_var"] == "comes_from_dev4.group_2" - assert inventory.hosts["dev1.group_1"].data["my_var"] == "comes_from_dev1.group_1" + assert ( + inventory.hosts["dev1.group_1"].data["my_var"] == "comes_from_dev1.group_1" + ) with pytest.raises(KeyError): inventory.hosts["dev2.group_1"].data["my_var"] with pytest.raises(KeyError): inventory.hosts["dev3.group_2"].data["my_var"] - assert inventory.hosts["dev4.group_2"].data["my_var"] == "comes_from_dev4.group_2" + assert ( + inventory.hosts["dev4.group_2"].data["my_var"] == "comes_from_dev4.group_2" + ) + + def test_has_parents(self): + assert inventory.hosts["dev1.group_1"].has_parent_group( + inventory.groups["group_1"] + ) + assert not inventory.hosts["dev1.group_1"].has_parent_group( + inventory.groups["group_2"] + ) diff --git a/tests/core/test_multithreading.py b/tests/core/test_multithreading.py index 5c334d34..2c9f1bcf 100644 --- a/tests/core/test_multithreading.py +++ b/tests/core/test_multithreading.py @@ -47,29 +47,50 @@ def test_blocking_task_multithreading(self, brigade): assert delta.seconds == 2, delta def test_failing_task_simple_singlethread(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(failing_task_simple, num_workers=1) - for k, v in e.value.result.items(): + result = brigade.run(failing_task_simple, num_workers=1) + processed = False + for k, v in result.items(): + processed = True assert isinstance(k, str), k assert isinstance(v.exception, Exception), v + assert processed + brigade.data.reset_failed_hosts() def test_failing_task_simple_multithread(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(failing_task_simple, num_workers=NUM_WORKERS) - for k, v in e.value.result.items(): + result = brigade.run(failing_task_simple, num_workers=NUM_WORKERS) + processed = False + for k, v in result.items(): + processed = True assert isinstance(k, str), k assert isinstance(v.exception, Exception), v + assert processed + brigade.data.reset_failed_hosts() def test_failing_task_complex_singlethread(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(failing_task_complex, num_workers=1) - for k, v in e.value.result.items(): + result = brigade.run(failing_task_complex, num_workers=1) + processed = False + for k, v in result.items(): + processed = True assert isinstance(k, str), k assert isinstance(v.exception, CommandError), v + assert processed + brigade.data.reset_failed_hosts() def test_failing_task_complex_multithread(self, brigade): + result = brigade.run(failing_task_complex, num_workers=NUM_WORKERS) + processed = False + for k, v in result.items(): + processed = True + assert isinstance(k, str), k + assert isinstance(v.exception, CommandError), v + assert processed + brigade.data.reset_failed_hosts() + + def test_failing_task_complex_multithread_raise_on_error(self, brigade): with pytest.raises(BrigadeExecutionError) as e: - brigade.run(failing_task_complex, num_workers=NUM_WORKERS) + brigade.run( + failing_task_complex, num_workers=NUM_WORKERS, raise_on_error=True + ) for k, v in e.value.result.items(): assert isinstance(k, str), k assert isinstance(v.exception, CommandError), v diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index a3ac2ede..7aa06065 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -1,26 +1,28 @@ +import logging + from brigade.plugins.tasks import commands def task_fails_for_some(task): if task.host.name == "dev3.group_2": # let's hardcode a failure - task.run(commands.command, - command="sasdasdasd") + task.run(commands.command, command="sasdasdasd") else: - task.run(commands.command, - command="echo {}".format(task.host)) + task.run( + commands.command, + command="echo {}".format(task.host), + severity_level=logging.DEBUG, + ) def sub_task(task): - task.run(commands.command, - command="echo {}".format(task.host)) + task.run(commands.command, command="echo {}".format(task.host)) class Test(object): def test_task(self, brigade): - result = brigade.run(commands.command, - command="echo hi") + result = brigade.run(commands.command, command="echo hi") assert result for h, r in result.items(): assert r.stdout.strip() == "hi" @@ -34,9 +36,10 @@ def test_sub_task(self, brigade): assert h == r[1].stdout.strip() def test_skip_failed_host(self, brigade): - result = brigade.run(task_fails_for_some, raise_on_error=False) + result = brigade.run(task_fails_for_some) assert result.failed - assert not result.skipped + assert "dev3.group_2" in result + for h, r in result.items(): if h == "dev3.group_2": assert r.failed @@ -46,13 +49,55 @@ def test_skip_failed_host(self, brigade): result = brigade.run(task_fails_for_some) assert not result.failed - assert result.skipped - for h, r in result.items(): - if h == "dev3.group_2": - assert r.skipped + assert "dev3.group_2" not in result + + brigade.data.reset_failed_hosts() + + def test_run_on(self, brigade): + result = brigade.run(task_fails_for_some) + assert result.failed + assert "dev3.group_2" in result + assert "dev1.group_1" in result + + result = brigade.run(task_fails_for_some, on_failed=True) + assert result.failed + assert "dev3.group_2" in result + assert "dev1.group_1" in result + + result = brigade.run(task_fails_for_some, on_failed=True, on_good=False) + assert result.failed + assert "dev3.group_2" in result + assert "dev1.group_1" not in result + + result = brigade.run(task_fails_for_some, on_failed=False, on_good=True) + assert not result.failed + assert "dev3.group_2" not in result + assert "dev1.group_1" in result + + brigade.data.reset_failed_hosts() + + def test_severity(self, brigade): + r = brigade.run(commands.command, command="echo blah") + for host, result in r.items(): + assert result[0].severity_level == logging.INFO + + r = brigade.run( + commands.command, command="echo blah", severity_level=logging.WARN + ) + for host, result in r.items(): + assert result[0].severity_level == logging.WARN + + r = brigade.run(sub_task, severity_level=logging.WARN) + for host, result in r.items(): + for sr in result: + assert sr.severity_level == logging.WARN + + r = brigade.run(task_fails_for_some, severity_level=logging.WARN, num_workers=1) + for host, result in r.items(): + if host == "dev3.group_2": + assert result[0].severity_level == logging.ERROR else: - assert not r.skipped - assert h == r[1].stdout.strip() + assert result[0].severity_level == logging.WARN + assert result[1].severity_level == logging.DEBUG - # let's reset it - brigade.data.failed_hosts = set() + brigade.data.reset_failed_hosts() diff --git a/tests/inventory_data/containers.sh b/tests/inventory_data/containers.sh index 93d98edc..0498be87 100755 --- a/tests/inventory_data/containers.sh +++ b/tests/inventory_data/containers.sh @@ -5,6 +5,8 @@ start () { docker run -d -p 65002:22 --name dev2.group_1 --hostname=dev2.group_1 dbarroso/stupid_ssh_container docker run -d -p 65003:22 --name dev3.group_2 --hostname=dev3.group_2 dbarroso/stupid_ssh_container docker run -d -p 65004:22 --name dev4.group_2 --hostname=dev4.group_2 dbarroso/stupid_ssh_container + docker run -d -p 65080:80 --name httpbin bungoume/httpbin-container + sleep 3 } stop () { @@ -12,6 +14,7 @@ stop () { docker rm -f dev2.group_1 docker rm -f dev3.group_2 docker rm -f dev4.group_2 + docker rm -f httpbin } if [ "$1" == "start" ]; then diff --git a/tests/inventory_data/groups.yaml b/tests/inventory_data/groups.yaml index c43ef530..f10df87a 100644 --- a/tests/inventory_data/groups.yaml +++ b/tests/inventory_data/groups.yaml @@ -1,16 +1,14 @@ --- -all: - my_var: comes_from_all +defaults: + my_var: comes_from_defaults brigade_host: 127.0.0.1 brigade_username: root brigade_password: docker brigade_os: linux group_1: - group: all my_var: comes_from_group_1 site: site1 group_2: - group: all site: site2 diff --git a/tests/inventory_data/hosts.yaml b/tests/inventory_data/hosts.yaml index 53f24116..76126566 100644 --- a/tests/inventory_data/hosts.yaml +++ b/tests/inventory_data/hosts.yaml @@ -1,6 +1,7 @@ --- dev1.group_1: - group: group_1 + groups: + - group_1 my_var: comes_from_dev1.group_1 www_server: nginx role: www @@ -8,13 +9,15 @@ dev1.group_1: brigade_nos: eos dev2.group_1: - group: group_1 + groups: + - group_1 role: db brigade_ssh_port: 65002 brigade_nos: junos dev3.group_2: - group: group_2 + groups: + - group_2 www_server: apache role: www brigade_ssh_port: 65003 @@ -25,7 +28,8 @@ dev3.group_2: # brigade_password: vagrant dev4.group_2: - group: group_2 + groups: + - group_2 my_var: comes_from_dev4.group_2 role: db brigade_ssh_port: 65004 diff --git a/tests/inventory_data/nsot/nsot.sqlite3 b/tests/inventory_data/nsot/nsot.sqlite3 index 43db53f9..716df7a4 100644 Binary files a/tests/inventory_data/nsot/nsot.sqlite3 and b/tests/inventory_data/nsot/nsot.sqlite3 differ diff --git a/examples/templates/eos/spine.j2 b/tests/plugins/functions/__init__.py similarity index 100% rename from examples/templates/eos/spine.j2 rename to tests/plugins/functions/__init__.py diff --git a/examples/templates/junos/spine.j2 b/tests/plugins/functions/text/__init__.py similarity index 100% rename from examples/templates/junos/spine.j2 rename to tests/plugins/functions/text/__init__.py diff --git a/tests/plugins/functions/text/output_data/basic_inventory.stderr b/tests/plugins/functions/text/output_data/basic_inventory.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory.stdout b/tests/plugins/functions/text/output_data/basic_inventory.stdout new file mode 100644 index 00000000..400491f8 --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory.stdout @@ -0,0 +1,17 @@ +echo_task*********************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_host.stderr b/tests/plugins/functions/text/output_data/basic_inventory_one_host.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_host.stdout b/tests/plugins/functions/text/output_data/basic_inventory_one_host.stdout new file mode 100644 index 00000000..ba300d7a --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory_one_host.stdout @@ -0,0 +1,6 @@ +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_host_python27.stderr b/tests/plugins/functions/text/output_data/basic_inventory_one_host_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_host_python27.stdout b/tests/plugins/functions/text/output_data/basic_inventory_one_host_python27.stdout new file mode 100644 index 00000000..424afece --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory_one_host_python27.stdout @@ -0,0 +1,6 @@ +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{ 'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_task.stderr b/tests/plugins/functions/text/output_data/basic_inventory_one_task.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_task.stdout b/tests/plugins/functions/text/output_data/basic_inventory_one_task.stdout new file mode 100644 index 00000000..5700ec74 --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory_one_task.stdout @@ -0,0 +1,2 @@ +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_task_python27.stderr b/tests/plugins/functions/text/output_data/basic_inventory_one_task_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory_one_task_python27.stdout b/tests/plugins/functions/text/output_data/basic_inventory_one_task_python27.stdout new file mode 100644 index 00000000..5700ec74 --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory_one_task_python27.stdout @@ -0,0 +1,2 @@ +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade diff --git a/tests/plugins/functions/text/output_data/basic_inventory_python27.stderr b/tests/plugins/functions/text/output_data/basic_inventory_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_inventory_python27.stdout b/tests/plugins/functions/text/output_data/basic_inventory_python27.stdout new file mode 100644 index 00000000..400491f8 --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_inventory_python27.stdout @@ -0,0 +1,17 @@ +echo_task*********************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/basic_single.stderr b/tests/plugins/functions/text/output_data/basic_single.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_single.stdout b/tests/plugins/functions/text/output_data/basic_single.stdout new file mode 100644 index 00000000..842de17d --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_single.stdout @@ -0,0 +1,5 @@ +echo_task*********************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/basic_single_python27.stderr b/tests/plugins/functions/text/output_data/basic_single_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/basic_single_python27.stdout b/tests/plugins/functions/text/output_data/basic_single_python27.stdout new file mode 100644 index 00000000..842de17d --- /dev/null +++ b/tests/plugins/functions/text/output_data/basic_single_python27.stdout @@ -0,0 +1,5 @@ +echo_task*********************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv echo_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +Hello from Brigade +^^^^ END echo_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/changed_host.stderr b/tests/plugins/functions/text/output_data/changed_host.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/changed_host.stdout b/tests/plugins/functions/text/output_data/changed_host.stdout new file mode 100644 index 00000000..7c043e9a --- /dev/null +++ b/tests/plugins/functions/text/output_data/changed_host.stdout @@ -0,0 +1,15 @@ +read_data*********************************************************************** +* dev1.group_1 ** changed : True *********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv WARNING +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : True ---------------------------------------------- WARNING +[1, 2, 3] +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv WARNING +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : False --------------------------------------------- WARNING +[4, 5, 6] +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/changed_host_python27.stderr b/tests/plugins/functions/text/output_data/changed_host_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/changed_host_python27.stdout b/tests/plugins/functions/text/output_data/changed_host_python27.stdout new file mode 100644 index 00000000..7c043e9a --- /dev/null +++ b/tests/plugins/functions/text/output_data/changed_host_python27.stdout @@ -0,0 +1,15 @@ +read_data*********************************************************************** +* dev1.group_1 ** changed : True *********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv WARNING +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : True ---------------------------------------------- WARNING +[1, 2, 3] +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv WARNING +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : False --------------------------------------------- WARNING +[4, 5, 6] +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/failed_with_severity.stderr b/tests/plugins/functions/text/output_data/failed_with_severity.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/failed_with_severity.stdout b/tests/plugins/functions/text/output_data/failed_with_severity.stdout new file mode 100644 index 00000000..c6fcf161 --- /dev/null +++ b/tests/plugins/functions/text/output_data/failed_with_severity.stdout @@ -0,0 +1,21 @@ +read_data*********************************************************************** +* dev1.group_1 ** changed : True *********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR +BrigadeSubTaskError() +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : False --------------------------------------------- ERROR +Exception('Unknown Error -> Contact your system administrator',) +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/failed_with_severity_python27.stderr b/tests/plugins/functions/text/output_data/failed_with_severity_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/failed_with_severity_python27.stdout b/tests/plugins/functions/text/output_data/failed_with_severity_python27.stdout new file mode 100644 index 00000000..c6fcf161 --- /dev/null +++ b/tests/plugins/functions/text/output_data/failed_with_severity_python27.stdout @@ -0,0 +1,21 @@ +read_data*********************************************************************** +* dev1.group_1 ** changed : True *********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv read_data ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR +BrigadeSubTaskError() +---- echo_task ** changed : False ---------------------------------------------- CRITICAL +Hello from CRITICAL +---- parse_data ** changed : False --------------------------------------------- ERROR +Exception('Unknown Error -> Contact your system administrator',) +^^^^ END read_data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/multiple_tasks.stderr b/tests/plugins/functions/text/output_data/multiple_tasks.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/multiple_tasks.stdout b/tests/plugins/functions/text/output_data/multiple_tasks.stdout new file mode 100644 index 00000000..0a6892a6 --- /dev/null +++ b/tests/plugins/functions/text/output_data/multiple_tasks.stdout @@ -0,0 +1,30 @@ +**** Behold the data! ********************************************************** +data_with_greeting************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/output_data/multiple_tasks_python27.stderr b/tests/plugins/functions/text/output_data/multiple_tasks_python27.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/functions/text/output_data/multiple_tasks_python27.stdout b/tests/plugins/functions/text/output_data/multiple_tasks_python27.stdout new file mode 100644 index 00000000..2ee93878 --- /dev/null +++ b/tests/plugins/functions/text/output_data/multiple_tasks_python27.stdout @@ -0,0 +1,30 @@ +**** Behold the data! ********************************************************** +data_with_greeting************************************************************** +* dev1.group_1 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{ 'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev2.group_1 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{ 'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev3.group_2 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{ 'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* dev4.group_2 ** changed : False ********************************************** +vvvv data_with_greeting ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +---- echo_task ** changed : False ---------------------------------------------- INFO +Hello from Brigade +---- load_data ** changed : False ---------------------------------------------- INFO +{ 'os': 'Linux', 'services': ['http', 'smtp', 'dns']} +^^^^ END data_with_greeting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/plugins/functions/text/test_print_result.py b/tests/plugins/functions/text/test_print_result.py new file mode 100644 index 00000000..c7f690fd --- /dev/null +++ b/tests/plugins/functions/text/test_print_result.py @@ -0,0 +1,101 @@ +import os +import logging + +from brigade.plugins.functions.text import print_result +from brigade.plugins.functions.text import print_title +from brigade.core.task import Result +from tests.wrapper import wrap_cli_test + +output_dir = "{}/output_data".format(os.path.dirname(os.path.realpath(__file__))) + + +def echo_task(task, msg="Brigade"): + return Result( + host=task.host, + result="Hello from {}".format(msg), + output="Hello from {}".format(msg), + ) + + +def load_data(task): + data = {"os": "Linux", "services": ["http", "smtp", "dns"]} + return Result(host=task.host, result=data) + + +def data_with_greeting(task): + task.run(task=echo_task) + task.run(task=load_data) + + +def parse_data(task): + + data = {} + data["failed"] = False + data["changed"] = False + + if "dev1.group_1" == task.host.name: + data["values"] = [1, 2, 3] + data["changed"] = True + + elif "dev2.group_1" == task.host.name: + data["values"] = [4, 5, 6] + + elif "dev3.group_2" == task.host.name: + data["values"] = [7, 8, 9] + + elif "dev4.group_2" == task.host.name: + data["values"] = [10, 11, 12] + data["changed"] = False + data["failed"] = True + + if data["failed"]: + raise Exception("Unknown Error -> Contact your system administrator") + + return Result(host=task.host, changed=data["changed"], result=data["values"]) + + +def read_data(task): + task.run(task=echo_task, severity_level=logging.DEBUG) + task.run(task=echo_task, msg="CRITICAL", severity_level=logging.CRITICAL) + task.run(task=parse_data, severity_level=logging.WARN) + + +class Test(object): + + @wrap_cli_test(output="{}/basic_single".format(output_dir)) + def test_print_basic(self, brigade): + filter = brigade.filter(name="dev1.group_1") + result = filter.run(echo_task) + print_result(result, vars="result") + + @wrap_cli_test(output="{}/basic_inventory".format(output_dir)) + def test_print_basic_inventory(self, brigade): + result = brigade.run(echo_task) + print_result(result) + + @wrap_cli_test(output="{}/basic_inventory_one_host".format(output_dir)) + def test_print_basic_inventory_one_host(self, brigade): + result = brigade.run(data_with_greeting) + print_result(result["dev2.group_1"]) + + @wrap_cli_test(output="{}/basic_inventory_one_task".format(output_dir)) + def test_print_basic_inventory_one_task(self, brigade): + result = brigade.run(data_with_greeting) + print_result(result["dev2.group_1"][1]) + + @wrap_cli_test(output="{}/multiple_tasks".format(output_dir)) + def test_print_multiple_tasks(self, brigade): + result = brigade.run(data_with_greeting) + print_title("Behold the data!") + print_result(result) + + @wrap_cli_test(output="{}/changed_host".format(output_dir)) + def test_print_changed_host(self, brigade): + filter = brigade.filter(site="site1") + result = filter.run(read_data, severity_level=logging.WARN) + print_result(result) + + @wrap_cli_test(output="{}/failed_with_severity".format(output_dir)) + def test_print_failed_with_severity(self, brigade): + result = brigade.run(read_data) + print_result(result, vars=["exception", "output"], severity_level=logging.ERROR) diff --git a/tests/plugins/inventory/ansible/ini/expected/groups.yaml b/tests/plugins/inventory/ansible/ini/expected/groups.yaml new file mode 100644 index 00000000..c0d2d5e4 --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/expected/groups.yaml @@ -0,0 +1,19 @@ +dbservers: + groups: + - servers + my_var: from_dbservers + my_yet_another_var: from_dbservers +defaults: + my_other_var: from_all + my_var: from_all +frontend: + groups: [] +servers: + escape_pods: 2 + groups: [] + halon_system_timeout: 30 + self_destruct_countdown: 60 + some_server: foo.southeast.example.com +webservers: + groups: + - servers diff --git a/tests/plugins/inventory/ansible/ini/expected/hosts.yaml b/tests/plugins/inventory/ansible/ini/expected/hosts.yaml new file mode 100644 index 00000000..cf9682ef --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/expected/hosts.yaml @@ -0,0 +1,21 @@ +bar.example.com: + groups: + - webservers +foo.example.com: + groups: + - frontend + - webservers +one.example.com: + groups: + - dbservers + my_var: from_one.example.com +three.example.com: + brigade_host: 192.0.2.50 + brigade_ssh_port: 5555 + groups: + - dbservers +two.example.com: + groups: + - dbservers + my_var: from_hostfile + whatever: asdasd diff --git a/tests/plugins/inventory/ansible/ini/source/group_vars/all b/tests/plugins/inventory/ansible/ini/source/group_vars/all new file mode 100644 index 00000000..13cb84ea --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/source/group_vars/all @@ -0,0 +1,3 @@ +--- +my_var: from_all +my_other_var: from_all diff --git a/tests/plugins/inventory/ansible/ini/source/group_vars/dbservers b/tests/plugins/inventory/ansible/ini/source/group_vars/dbservers new file mode 100644 index 00000000..443b50c8 --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/source/group_vars/dbservers @@ -0,0 +1,3 @@ +--- +my_var: from_dbservers +my_yet_another_var: from_dbservers diff --git a/tests/plugins/inventory/ansible/ini/source/host_vars/one.example.com b/tests/plugins/inventory/ansible/ini/source/host_vars/one.example.com new file mode 100644 index 00000000..a011cd08 --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/source/host_vars/one.example.com @@ -0,0 +1,2 @@ +--- +my_var: from_one.example.com diff --git a/tests/plugins/inventory/ansible/ini/source/host_vars/two.example.com b/tests/plugins/inventory/ansible/ini/source/host_vars/two.example.com new file mode 100644 index 00000000..9d27cf13 --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/source/host_vars/two.example.com @@ -0,0 +1,2 @@ +--- +whatever: asdasd diff --git a/tests/plugins/inventory/ansible/ini/source/hosts b/tests/plugins/inventory/ansible/ini/source/hosts new file mode 100644 index 00000000..4eeea252 --- /dev/null +++ b/tests/plugins/inventory/ansible/ini/source/hosts @@ -0,0 +1,21 @@ +[webservers] +foo.example.com +bar.example.com + +[dbservers] +one.example.com +two.example.com my_var=from_hostfile +three.example.com ansible_port=5555 ansible_host=192.0.2.50 + +[frontend] +foo.example.com + +[servers:children] +webservers +dbservers + +[servers:vars] +some_server=foo.southeast.example.com +halon_system_timeout=30 +self_destruct_countdown=60 +escape_pods=2 diff --git a/tests/plugins/inventory/ansible/parse_error/source/hosts b/tests/plugins/inventory/ansible/parse_error/source/hosts new file mode 100644 index 00000000..d2e91f22 --- /dev/null +++ b/tests/plugins/inventory/ansible/parse_error/source/hosts @@ -0,0 +1,6 @@ +asdas +das +das +das +dasdasdasdasd=asd adadsad +asdasdasd: qqwewqe diff --git a/tests/plugins/inventory/ansible/yaml/expected/groups.yaml b/tests/plugins/inventory/ansible/yaml/expected/groups.yaml new file mode 100644 index 00000000..c0d2d5e4 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/expected/groups.yaml @@ -0,0 +1,19 @@ +dbservers: + groups: + - servers + my_var: from_dbservers + my_yet_another_var: from_dbservers +defaults: + my_other_var: from_all + my_var: from_all +frontend: + groups: [] +servers: + escape_pods: 2 + groups: [] + halon_system_timeout: 30 + self_destruct_countdown: 60 + some_server: foo.southeast.example.com +webservers: + groups: + - servers diff --git a/tests/plugins/inventory/ansible/yaml/expected/hosts.yaml b/tests/plugins/inventory/ansible/yaml/expected/hosts.yaml new file mode 100644 index 00000000..cf9682ef --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/expected/hosts.yaml @@ -0,0 +1,21 @@ +bar.example.com: + groups: + - webservers +foo.example.com: + groups: + - frontend + - webservers +one.example.com: + groups: + - dbservers + my_var: from_one.example.com +three.example.com: + brigade_host: 192.0.2.50 + brigade_ssh_port: 5555 + groups: + - dbservers +two.example.com: + groups: + - dbservers + my_var: from_hostfile + whatever: asdasd diff --git a/tests/plugins/inventory/ansible/yaml/source/group_vars/all b/tests/plugins/inventory/ansible/yaml/source/group_vars/all new file mode 100644 index 00000000..13cb84ea --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/source/group_vars/all @@ -0,0 +1,3 @@ +--- +my_var: from_all +my_other_var: from_all diff --git a/tests/plugins/inventory/ansible/yaml/source/group_vars/dbservers b/tests/plugins/inventory/ansible/yaml/source/group_vars/dbservers new file mode 100644 index 00000000..443b50c8 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/source/group_vars/dbservers @@ -0,0 +1,3 @@ +--- +my_var: from_dbservers +my_yet_another_var: from_dbservers diff --git a/tests/plugins/inventory/ansible/yaml/source/host_vars/one.example.com b/tests/plugins/inventory/ansible/yaml/source/host_vars/one.example.com new file mode 100644 index 00000000..a011cd08 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/source/host_vars/one.example.com @@ -0,0 +1,2 @@ +--- +my_var: from_one.example.com diff --git a/tests/plugins/inventory/ansible/yaml/source/host_vars/two.example.com b/tests/plugins/inventory/ansible/yaml/source/host_vars/two.example.com new file mode 100644 index 00000000..9d27cf13 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/source/host_vars/two.example.com @@ -0,0 +1,2 @@ +--- +whatever: asdasd diff --git a/tests/plugins/inventory/ansible/yaml/source/hosts b/tests/plugins/inventory/ansible/yaml/source/hosts new file mode 100644 index 00000000..9fca5c31 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml/source/hosts @@ -0,0 +1,24 @@ +all: + children: + servers: + children: + webservers: + hosts: + foo.example.com: + bar.example.com: + dbservers: + hosts: + one.example.com: + two.example.com: + my_var: from_hostfile + three.example.com: + ansible_port: 5555 + ansible_host: 192.0.2.50 + vars: + some_server: foo.southeast.example.com + halon_system_timeout: 30 + self_destruct_countdown: 60 + escape_pods: 2 + frontend: + hosts: + foo.example.com: diff --git a/tests/plugins/inventory/ansible/yaml2/expected/groups.yaml b/tests/plugins/inventory/ansible/yaml2/expected/groups.yaml new file mode 100644 index 00000000..914dee95 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml2/expected/groups.yaml @@ -0,0 +1 @@ +defaults: {} diff --git a/tests/plugins/inventory/ansible/yaml2/expected/hosts.yaml b/tests/plugins/inventory/ansible/yaml2/expected/hosts.yaml new file mode 100644 index 00000000..5e746bb5 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml2/expected/hosts.yaml @@ -0,0 +1,9 @@ +one.example.com: + groups: [] +three.example.com: + brigade_host: 192.0.2.50 + brigade_ssh_port: 5555 + groups: [] +two.example.com: + groups: [] + my_var: from_hostfile diff --git a/tests/plugins/inventory/ansible/yaml2/source/hosts b/tests/plugins/inventory/ansible/yaml2/source/hosts new file mode 100644 index 00000000..671ff524 --- /dev/null +++ b/tests/plugins/inventory/ansible/yaml2/source/hosts @@ -0,0 +1,8 @@ +all: + hosts: + one.example.com: + two.example.com: + my_var: from_hostfile + three.example.com: + ansible_port: 5555 + ansible_host: 192.0.2.50 diff --git a/tests/plugins/inventory/test_ansible.py b/tests/plugins/inventory/test_ansible.py new file mode 100644 index 00000000..78ed75dc --- /dev/null +++ b/tests/plugins/inventory/test_ansible.py @@ -0,0 +1,50 @@ +import os + +from brigade.plugins.inventory import ansible + +import pytest + +import ruamel.yaml + + +BASE_PATH = os.path.join(os.path.dirname(__file__), "ansible") + + +def save(hosts, groups, hosts_file, groups_file): + yml = ruamel.yaml.YAML(typ="safe", pure=True) + with open(hosts_file, "w+") as f: + f.write(yml.dump(hosts, default_flow_style=False)) + with open(groups_file, "w+") as f: + f.write(yml.dump(groups, default_flow_style=False)) + + +def read(hosts_file, groups_file): + yml = ruamel.yaml.YAML(typ="safe") + with open(hosts_file, "r") as f: + hosts = yml.load(f.read()) + with open(groups_file, "r") as f: + groups = yml.load(f.read()) + return hosts, groups + + +class Test(object): + + @pytest.mark.parametrize("case", ["ini", "yaml", "yaml2"]) + def test_inventory(self, case): + base_path = os.path.join(BASE_PATH, case) + hosts_file = os.path.join(base_path, "expected", "hosts.yaml") + groups_file = os.path.join(base_path, "expected", "groups.yaml") + + hosts, groups = ansible.parse( + hostsfile=os.path.join(base_path, "source", "hosts") + ) + # save(hosts, groups, hosts_file, groups_file) + + expected_hosts, expected_groups = read(hosts_file, groups_file) + assert hosts == expected_hosts + assert groups == expected_groups + + def test_parse_error(self): + base_path = os.path.join(BASE_PATH, "parse_error") + with pytest.raises(ruamel.yaml.scanner.ScannerError): + ansible.parse(hostsfile=os.path.join(base_path, "source", "hosts")) diff --git a/tests/plugins/inventory/test_nsot.py b/tests/plugins/inventory/test_nsot.py index 0c1cbc11..ce1740e0 100644 --- a/tests/plugins/inventory/test_nsot.py +++ b/tests/plugins/inventory/test_nsot.py @@ -17,14 +17,15 @@ def transform_function(host): @pytest.fixture(scope="module") def inv(request): """Start/Stop containers needed for the tests.""" + def fin(): - subprocess.check_call(["make", "stop_nsot"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE) + subprocess.check_call( + ["make", "stop_nsot"], stderr=subprocess.PIPE, stdout=subprocess.PIPE + ) request.addfinalizer(fin) - subprocess.check_call(["make", "start_nsot"], - stdout=subprocess.PIPE) + subprocess.check_call(["make", "start_nsot"], stdout=subprocess.PIPE) if os.getenv("TRAVIS"): time.sleep(10) diff --git a/tests/plugins/tasks/apis/__init__.py b/tests/plugins/tasks/apis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/tasks/apis/test_http_method.py b/tests/plugins/tasks/apis/test_http_method.py new file mode 100644 index 00000000..a2c5ceaa --- /dev/null +++ b/tests/plugins/tasks/apis/test_http_method.py @@ -0,0 +1,73 @@ +import json + +from brigade.plugins.tasks.apis import http_method + +import pytest + +from requests.exceptions import HTTPError + + +BASE_URL = "http://localhost:65080" + + +class Test(object): + + def test_simple_get_text(self): + url = "{}/encoding/utf8".format(BASE_URL) + result = http_method(method="get", url=url) + assert result.response.status_code + assert result.result.startswith("

Unicode Demo

") + assert result.result == result.response.text + + def test_simple_get_json(self): + url = "{}/get".format(BASE_URL) + result = http_method(method="get", url=url) + assert result.response.status_code + assert result.response.json()["args"] == {} + assert json.loads(result.response.text)["args"] == {} + assert result.result == result.response.json() + + def test_headers(self): + url = "{}/headers".format(BASE_URL) + headers = {"X-Test": "a test"} + result = http_method(method="get", url=url, headers=headers) + assert result.result["headers"]["X-Test"] == "a test" + + def test_params(self): + url = "{}/get".format(BASE_URL) + params = {"my_param": "my_value"} + result = http_method(method="get", url=url, params=params) + assert result.result["args"]["my_param"] == "my_value" + + def test_post_data(self): + url = "{}/post".format(BASE_URL) + data = {"my_param": "my_value"} + result = http_method(method="post", url=url, data=data) + assert result.result["form"]["my_param"] == "my_value" + + def test_post_json(self): + url = "{}/post".format(BASE_URL) + json = {"my_param": "my_value"} + result = http_method(method="post", url=url, json=json) + assert result.result["data"] == '{"my_param": "my_value"}' + + def test_raise_for_status(self): + url = "{}/status/418".format(BASE_URL) + + with pytest.raises(HTTPError): + http_method(method="post", url=url) + + result = http_method(method="post", url=url, raise_for_status=False) + assert result.response.status_code + + def test_with_brigade(self, brigade): + url = "{}/get".format(BASE_URL) + params = {"my_param": "my_value"} + + r = brigade.run(http_method, method="get", url=url, params=params) + + processed = False + for host, result in r.items(): + processed = True + assert result[0].result["args"]["my_param"] == "my_value" + assert processed diff --git a/tests/plugins/tasks/commands/test_command.py b/tests/plugins/tasks/commands/test_command.py index 7ed7e6cd..215d7d91 100644 --- a/tests/plugins/tasks/commands/test_command.py +++ b/tests/plugins/tasks/commands/test_command.py @@ -1,30 +1,34 @@ -from brigade.core.exceptions import BrigadeExecutionError, CommandError +from brigade.core.exceptions import CommandError from brigade.plugins.tasks import commands -import pytest + +def echo_hostname(task): + command = "echo {host.name}".format(host=task.host) + task.run(task=commands.command, command=command) class Test(object): def test_command(self, brigade): - result = brigade.run(commands.command, - command="echo {host.name}") + result = brigade.run(echo_hostname) assert result for h, r in result.items(): - assert h == r.stdout.strip() + assert h == r[1].stdout.strip() def test_command_error(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(commands.command, - command="ech") - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): - assert isinstance(result.exception, OSError) + result = brigade.run(commands.command, command="ech") + processed = False + for r in result.values(): + processed = True + assert isinstance(r.exception, OSError) + assert processed + brigade.data.reset_failed_hosts() def test_command_error_generic(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(commands.command, - command="ls /asdadsd") - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): - assert isinstance(result.exception, CommandError) + result = brigade.run(commands.command, command="ls /asdadsd") + processed = False + for r in result.values(): + processed = True + assert isinstance(r.exception, CommandError) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/commands/test_remote_command.py b/tests/plugins/tasks/commands/test_remote_command.py index 9486745f..7f2b89d8 100644 --- a/tests/plugins/tasks/commands/test_remote_command.py +++ b/tests/plugins/tasks/commands/test_remote_command.py @@ -1,22 +1,20 @@ -from brigade.core.exceptions import BrigadeExecutionError, CommandError +from brigade.core.exceptions import CommandError from brigade.plugins.tasks import commands -import pytest - class Test(object): def test_remote_command(self, brigade): - result = brigade.run(commands.remote_command, - command="hostname") + result = brigade.run(commands.remote_command, command="hostname") assert result for h, r in result.items(): assert h == r.stdout.strip() def test_remote_command_error_generic(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(commands.remote_command, - command="ls /asdadsd") - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): - assert isinstance(result.exception, CommandError) + result = brigade.run(commands.remote_command, command="ls /asdadsd") + processed = False + for r in result.values(): + processed = True + assert isinstance(r.exception, CommandError) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/data/test_load_json.py b/tests/plugins/tasks/data/test_load_json.py index e53d778b..a381f25e 100644 --- a/tests/plugins/tasks/data/test_load_json.py +++ b/tests/plugins/tasks/data/test_load_json.py @@ -1,47 +1,44 @@ import os import sys -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import data -import pytest - - -data_dir = '{}/test_data'.format(os.path.dirname(os.path.realpath(__file__))) +data_dir = "{}/test_data".format(os.path.dirname(os.path.realpath(__file__))) class Test(object): def test_load_json(self, brigade): - test_file = '{}/simple.json'.format(data_dir) - result = brigade.run(data.load_json, - file=test_file) + test_file = "{}/simple.json".format(data_dir) + result = brigade.run(data.load_json, file=test_file) for h, r in result.items(): d = r.result - assert d['env'] == 'test' - assert d['services'] == ['dhcp', 'dns'] + assert d["env"] == "test" + assert d["services"] == ["dhcp", "dns"] def test_load_json_error_broken_file(self, brigade): - test_file = '{}/broken.json'.format(data_dir) - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(data.load_json, - file=test_file) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + test_file = "{}/broken.json".format(data_dir) + results = brigade.run(data.load_json, file=test_file) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, ValueError) + assert processed + brigade.data.reset_failed_hosts() def test_load_json_error_missing_file(self, brigade): - test_file = '{}/missing.json'.format(data_dir) + test_file = "{}/missing.json".format(data_dir) if sys.version_info.major == 2: not_found = IOError else: - not_found = FileNotFoundError # noqa + not_found = FileNotFoundError # noqa - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(data.load_json, - file=test_file) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(data.load_json, file=test_file) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, not_found) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/data/test_load_yaml.py b/tests/plugins/tasks/data/test_load_yaml.py index 4421cb96..d6c931d6 100644 --- a/tests/plugins/tasks/data/test_load_yaml.py +++ b/tests/plugins/tasks/data/test_load_yaml.py @@ -2,51 +2,48 @@ import sys -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import data -import pytest - - from yaml.scanner import ScannerError -data_dir = '{}/test_data'.format(os.path.dirname(os.path.realpath(__file__))) +data_dir = "{}/test_data".format(os.path.dirname(os.path.realpath(__file__))) class Test(object): def test_load_yaml(self, brigade): - test_file = '{}/simple.yaml'.format(data_dir) - result = brigade.run(data.load_yaml, - file=test_file) + test_file = "{}/simple.yaml".format(data_dir) + result = brigade.run(data.load_yaml, file=test_file) for h, r in result.items(): d = r.result - assert d['env'] == 'test' - assert d['services'] == ['dhcp', 'dns'] + assert d["env"] == "test" + assert d["services"] == ["dhcp", "dns"] def test_load_yaml_error_broken_file(self, brigade): - test_file = '{}/broken.yaml'.format(data_dir) - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(data.load_yaml, - file=test_file) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + test_file = "{}/broken.yaml".format(data_dir) + results = brigade.run(data.load_yaml, file=test_file) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, ScannerError) + assert processed + brigade.data.reset_failed_hosts() def test_load_yaml_error_missing_file(self, brigade): - test_file = '{}/missing.yaml'.format(data_dir) + test_file = "{}/missing.yaml".format(data_dir) if sys.version_info.major == 2: not_found = IOError else: - not_found = FileNotFoundError # noqa + not_found = FileNotFoundError # noqa - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(data.load_yaml, - file=test_file) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(data.load_yaml, file=test_file) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, not_found) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/files/test_sftp.py b/tests/plugins/tasks/files/test_sftp.py index 0e7e4450..b1677fc4 100644 --- a/tests/plugins/tasks/files/test_sftp.py +++ b/tests/plugins/tasks/files/test_sftp.py @@ -6,130 +6,116 @@ # import pytest +def get_file(task): + filename = "/tmp/{uuid}-{host.name}".format(uuid=uuid.uuid4(), host=task.host) + r = task.run( + task=files.sftp, dry_run=True, action="get", src="/etc/hostname", dst=filename + ) + assert r + assert r.changed, r.files_changed + + r = task.run( + task=files.sftp, dry_run=False, action="get", src="/etc/hostname", dst=filename + ) + assert r + assert r.changed, r.files_changed + + r = task.run( + task=files.sftp, dry_run=False, action="get", src="/etc/hostname", dst=filename + ) + assert r + assert not r.changed + + +def get_directory(task): + filename = "/tmp/{uuid}-{host.name}".format(uuid=uuid.uuid4(), host=task.host) + r = task.run( + task=files.sftp, dry_run=True, action="get", src="/etc/terminfo/", dst=filename + ) + assert r + assert r.changed, r.files_changed + + r = task.run( + task=files.sftp, dry_run=False, action="get", src="/etc/terminfo/", dst=filename + ) + assert r + assert r.changed, r.files_changed + + r = task.run( + task=files.sftp, dry_run=True, action="get", src="/etc/terminfo/", dst=filename + ) + assert r + assert not r.changed + + class Test(object): def test_sftp_put(self, brigade): - result = brigade.run(files.sftp, - dry_run=True, - action="put", - src="README.md", - dst="/tmp/README.md") + result = brigade.run( + files.sftp, + dry_run=True, + action="put", + src="README.md", + dst="/tmp/README.md", + ) assert result for h, r in result.items(): assert r.changed, r.files_changed - result = brigade.run(files.sftp, - dry_run=False, - action="put", - src="README.md", - dst="/tmp/README.md") + result = brigade.run( + files.sftp, + dry_run=False, + action="put", + src="README.md", + dst="/tmp/README.md", + ) assert result for h, r in result.items(): assert r.changed, r.files_changed - result = brigade.run(files.sftp, - dry_run=True, - action="put", - src="README.md", - dst="/tmp/README.md") + result = brigade.run( + files.sftp, + dry_run=True, + action="put", + src="README.md", + dst="/tmp/README.md", + ) assert result for h, r in result.items(): assert not r.changed def test_sftp_get(self, brigade): - filename = "/tmp/" + str(uuid.uuid4()) + "-{host}" - result = brigade.run(files.sftp, - dry_run=True, - action="get", - src="/etc/hostname", - dst=filename) - - assert result - for h, r in result.items(): - assert r.changed, r.files_changed - - result = brigade.run(files.sftp, - dry_run=False, - action="get", - src="/etc/hostname", - dst=filename) - - assert result - for h, r in result.items(): - assert r.changed, r.files_changed - - result = brigade.run(files.sftp, - dry_run=False, - action="get", - src="/etc/hostname", - dst=filename) - - assert result - for h, r in result.items(): - assert not r.changed + result = brigade.run(get_file) + assert not result.failed def test_sftp_put_directory(self, brigade): - result = brigade.run(files.sftp, - dry_run=True, - action="put", - src="./brigade", - dst="/tmp/asd") + result = brigade.run( + files.sftp, dry_run=True, action="put", src="./brigade", dst="/tmp/asd" + ) assert result for h, r in result.items(): assert r.changed, r.files_changed - result = brigade.run(files.sftp, - dry_run=False, - action="put", - src="./brigade", - dst="/tmp/asd") + result = brigade.run( + files.sftp, dry_run=False, action="put", src="./brigade", dst="/tmp/asd" + ) assert result for h, r in result.items(): assert r.changed, r.files_changed - result = brigade.run(files.sftp, - dry_run=True, - action="put", - src="./brigade", - dst="/tmp/asd") + result = brigade.run( + files.sftp, dry_run=True, action="put", src="./brigade", dst="/tmp/asd" + ) assert result for h, r in result.items(): assert not r.changed def test_sftp_get_directory(self, brigade): - filename = "/tmp/" + str(uuid.uuid4()) + "-{host}" - result = brigade.run(files.sftp, - dry_run=True, - action="get", - src="/etc/terminfo/", - dst=filename) - - assert result - for h, r in result.items(): - assert r.changed, r.files_changed - - result = brigade.run(files.sftp, - dry_run=False, - action="get", - src="/etc/terminfo/", - dst=filename) - - assert result - for h, r in result.items(): - assert r.changed, r.files_changed - - result = brigade.run(files.sftp, - dry_run=True, - action="get", - src="/etc/terminfo/", - dst=filename) - - assert result - for h, r in result.items(): - assert not r.changed + result = brigade.run(get_directory) + assert not result.failed diff --git a/tests/plugins/tasks/files/test_write.py b/tests/plugins/tasks/files/test_write.py deleted file mode 100644 index 79584369..00000000 --- a/tests/plugins/tasks/files/test_write.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -import uuid - -from brigade.plugins.tasks import files - - -content_a = """ -BLAH -BLEH -BLIH -BLOH -BLUH -""" - -content_b = """ -BLAH -BLOH -BLUH BLUH -BLIH -""" - - -diff_new = """--- /tmp/brigade-write/dev3.group_2-f66d9331-3eeb-4912-98b9-37f55ac48deb - -+++ new - -@@ -0,0 +1,6 @@ - -+ -+BLAH -+BLEH -+BLIH -+BLOH -+BLUH""" - -diff_overwrite = """--- /tmp/brigade-write/dev4.group_2-e63969eb-2261-4200-8913-196a12f4d791 - -+++ new - -@@ -1,6 +1,5 @@ - - - BLAH --BLEH -+BLOH -+BLUH BLUH - BLIH --BLOH --BLUH""" # noqa - - -diff_append = """--- /tmp/brigade-write/dev4.group_2-36ea350d-6623-4098-a961-fc143504eb42 - -+++ new - -@@ -4,3 +4,8 @@ - - BLIH - BLOH - BLUH -+ -+BLAH -+BLOH -+BLUH BLUH -+BLIH""" # noqa - - -BASEPATH = "/tmp/brigade-write" -if not os.path.exists(BASEPATH): - os.makedirs(BASEPATH) - - -def _test_write(task): - filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) - r = task.run(files.write, - dry_run=True, - filename=filename, - content=content_a) - - assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] - assert r.changed - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_a) - - assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] - assert r.changed - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_a) - - assert not r.diff - assert not r.changed - - -def _test_overwrite(task): - filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_a) - - assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] - assert r.changed - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_b) - - assert r.diff.splitlines()[1:] == diff_overwrite.splitlines()[1:] - assert r.changed - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_b) - - assert not r.diff - assert not r.changed - - -def _test_append(task): - filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_a) - - assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] - assert r.changed - - r = task.run(files.write, - dry_run=False, - filename=filename, - content=content_b, - append=True) - - assert r.diff.splitlines()[1:] == diff_append.splitlines()[1:] - assert r.changed - - -class Test(object): - - def test_write(self, brigade): - brigade.run(_test_write) - - def test_overwrite(self, brigade): - brigade.run(_test_overwrite) - - def test_append(self, brigade): - brigade.run(_test_append) diff --git a/tests/plugins/tasks/files/test_write_file.py b/tests/plugins/tasks/files/test_write_file.py new file mode 100644 index 00000000..b2251cf8 --- /dev/null +++ b/tests/plugins/tasks/files/test_write_file.py @@ -0,0 +1,139 @@ +import os +import uuid + +from brigade.plugins.tasks import files + + +content_a = """ +BLAH +BLEH +BLIH +BLOH +BLUH +""" + +content_b = """ +BLAH +BLOH +BLUH BLUH +BLIH +""" + + +diff_new = """--- /tmp/brigade-write_file/dev3.group_2-f66d9331-3eeb-4912-98b9-37f55ac48deb + ++++ new + +@@ -0,0 +1,6 @@ + ++ ++BLAH ++BLEH ++BLIH ++BLOH ++BLUH""" + +diff_overwrite_file = """--- /tmp/brigade-write_file/dev4.group_2-e63969eb-2261-4200-8913-196a12f4d791 + ++++ new + +@@ -1,6 +1,5 @@ + + + BLAH +-BLEH ++BLOH ++BLUH BLUH + BLIH +-BLOH +-BLUH""" # noqa + + +diff_append = """--- /tmp/brigade-write_file/dev4.group_2-36ea350d-6623-4098-a961-fc143504eb42 + ++++ new + +@@ -4,3 +4,8 @@ + + BLIH + BLOH + BLUH ++ ++BLAH ++BLOH ++BLUH BLUH ++BLIH""" # noqa + + +BASEPATH = "/tmp/brigade-write_file" +if not os.path.exists(BASEPATH): + os.makedirs(BASEPATH) + + +def _test_write_file(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + r = task.run(files.write_file, dry_run=True, filename=filename, content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_a) + + assert not r.diff + assert not r.changed + + +def _test_overwrite_file(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_b) + + assert r.diff.splitlines()[1:] == diff_overwrite_file.splitlines()[1:] + assert r.changed + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_b) + + assert not r.diff + assert not r.changed + + +def _test_append(task): + filename = "{}/{}-{}".format(BASEPATH, task.host, str(uuid.uuid4())) + + r = task.run(files.write_file, dry_run=False, filename=filename, content=content_a) + + assert r.diff.splitlines()[1:] == diff_new.splitlines()[1:] + assert r.changed + + r = task.run( + files.write_file, + dry_run=False, + filename=filename, + content=content_b, + append=True, + ) + + assert r.diff.splitlines()[1:] == diff_append.splitlines()[1:] + assert r.changed + + +class Test(object): + + def test_write_file(self, brigade): + brigade.run(_test_write_file) + + def test_overwrite_file(self, brigade): + brigade.run(_test_overwrite_file) + + def test_append(self, brigade): + brigade.run(_test_append) diff --git a/tests/plugins/tasks/networking/data/test_file.txt b/tests/plugins/tasks/networking/data/test_file.txt new file mode 100644 index 00000000..6fbcd24f --- /dev/null +++ b/tests/plugins/tasks/networking/data/test_file.txt @@ -0,0 +1 @@ +Testing secure copy file for brigade diff --git a/tests/plugins/tasks/networking/test_napalm_cli.py b/tests/plugins/tasks/networking/test_napalm_cli.py index 7bc712ff..98a28128 100644 --- a/tests/plugins/tasks/networking/test_napalm_cli.py +++ b/tests/plugins/tasks/networking/test_napalm_cli.py @@ -17,23 +17,24 @@ def test_napalm_cli(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_cli"} d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_cli, - commands=["show version", - "show interfaces"]) + result = d.run( + networking.napalm_cli, commands=["show version", "show interfaces"] + ) assert result for h, r in result.items(): assert r.result["show version"] assert r.result["show interfaces"] - # def test_napalm_cli_error(self, brigade): - # opt = {"path": THIS_DIR + "/test_napalm_cli_error"} - # with pytest.raises(BrigadeExecutionError) as e: - # brigade.filter(name="dev3.group_2").run(networking.napalm_cli, - # num_workers=1, - # commands=["show version", - # "show interfacesa"], - # optional_args=opt) - # assert len(e.value.failed_hosts) - # for result in e.value.failed_hosts.values(): - # assert isinstance(result.exception, exceptions.CommandErrorException) - # print(exc) + +# def test_napalm_cli_error(self, brigade): +# opt = {"path": THIS_DIR + "/test_napalm_cli_error"} +# with pytest.raises(BrigadeExecutionError) as e: +# brigade.filter(name="dev3.group_2").run(networking.napalm_cli, +# num_workers=1, +# commands=["show version", +# "show interfacesa"], +# optional_args=opt) +# assert len(e.value.failed_hosts) +# for result in e.value.failed_hosts.values(): +# assert isinstance(result.exception, exceptions.CommandErrorException) +# print(exc) diff --git a/tests/plugins/tasks/networking/test_napalm_configure.py b/tests/plugins/tasks/networking/test_napalm_configure.py index fcbe0e9d..8ab1bc75 100644 --- a/tests/plugins/tasks/networking/test_napalm_configure.py +++ b/tests/plugins/tasks/networking/test_napalm_configure.py @@ -1,12 +1,9 @@ import os -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import connections, networking from napalm.base import exceptions -import pytest - THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + "/mocked/napalm_configure" @@ -29,18 +26,18 @@ def test_napalm_configure_change_commit(self, brigade): configuration = "hostname changed-hostname" d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_configure, - dry_run=False, - configuration=configuration) + result = d.run( + networking.napalm_configure, dry_run=False, configuration=configuration + ) assert result for h, r in result.items(): assert "+hostname changed-hostname" in r.diff assert r.changed opt = {"path": THIS_DIR + "/test_napalm_configure_change_commit/step2"} d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_configure, - dry_run=True, - configuration=configuration) + result = d.run( + networking.napalm_configure, dry_run=True, configuration=configuration + ) assert result for h, r in result.items(): assert "+hostname changed-hostname" not in r.diff @@ -52,8 +49,10 @@ def test_napalm_configure_change_error(self, brigade): d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - with pytest.raises(BrigadeExecutionError) as e: - d.run(networking.napalm_configure, configuration=configuration) - assert len(e.value.failed_hosts) - for result in e.value.failed_hosts.values(): + results = d.run(networking.napalm_configure, configuration=configuration) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, exceptions.MergeConfigException) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/networking/test_napalm_get.py b/tests/plugins/tasks/networking/test_napalm_get.py index 459e3933..e8dea302 100644 --- a/tests/plugins/tasks/networking/test_napalm_get.py +++ b/tests/plugins/tasks/networking/test_napalm_get.py @@ -1,10 +1,7 @@ import os -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import connections, networking -import pytest - THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + "/mocked/napalm_get" @@ -15,9 +12,7 @@ def test_napalm_getters(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_getters"} d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_get, - getters=["facts", - "interfaces"]) + result = d.run(networking.napalm_get, getters=["facts", "interfaces"]) assert result for h, r in result.items(): assert r.result["facts"] @@ -28,10 +23,10 @@ def test_napalm_getters_error(self, brigade): d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - with pytest.raises(BrigadeExecutionError) as e: - d.run(networking.napalm_get, - getters=["facts", - "interfaces"]) - assert len(e.value.failed_hosts) - for result in e.value.failed_hosts.values(): + results = d.run(networking.napalm_get, getters=["facts", "interfaces"]) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, KeyError) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/networking/test_napalm_validate.py b/tests/plugins/tasks/networking/test_napalm_validate.py index 5a6c3b0a..e85953a3 100644 --- a/tests/plugins/tasks/networking/test_napalm_validate.py +++ b/tests/plugins/tasks/networking/test_napalm_validate.py @@ -10,23 +10,23 @@ class Test(object): def test_napalm_validate_src_ok(self, brigade): opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} - print(opt["path"]) d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_validate, - src=THIS_DIR + "/data/validate_ok.yaml") + result = d.run( + networking.napalm_validate, src=THIS_DIR + "/data/validate_ok.yaml" + ) assert result for h, r in result.items(): assert not r.failed def test_napalm_validate_src_error(self, brigade): opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} - print(opt["path"]) d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - result = d.run(networking.napalm_validate, - src=THIS_DIR + "/data/validate_error.yaml") + result = d.run( + networking.napalm_validate, src=THIS_DIR + "/data/validate_error.yaml" + ) assert result for h, r in result.items(): assert not r.failed @@ -34,16 +34,12 @@ def test_napalm_validate_src_error(self, brigade): def test_napalm_validate_src_validate_source(self, brigade): opt = {"path": THIS_DIR + "/mocked/napalm_get/test_napalm_getters"} - print(opt["path"]) d = brigade.filter(name="dev3.group_2") d.run(connections.napalm_connection, optional_args=opt) - validation_dict = [ - {"get_interfaces": {"Ethernet1": {"description": ""}}} - ] + validation_dict = [{"get_interfaces": {"Ethernet1": {"description": ""}}}] - result = d.run(networking.napalm_validate, - validation_source=validation_dict) + result = d.run(networking.napalm_validate, validation_source=validation_dict) assert result for h, r in result.items(): diff --git a/tests/plugins/tasks/networking/test_netmiko_file_transfer.py b/tests/plugins/tasks/networking/test_netmiko_file_transfer.py new file mode 100644 index 00000000..ef67c407 --- /dev/null +++ b/tests/plugins/tasks/networking/test_netmiko_file_transfer.py @@ -0,0 +1,22 @@ +import os + +from brigade.plugins.tasks import networking + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + + +class Test(object): + + def test_netmiko_file_transfer(self, brigade): + source_file = os.path.join(THIS_DIR, "data", "test_file.txt") + dest_file = "test_file.txt" + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_file_transfer, + source_file=source_file, + dest_file=dest_file, + direction="put", + ) + assert result + for h, r in result.items(): + assert r.result + assert r.changed diff --git a/tests/plugins/tasks/networking/test_netmiko_send_command.py b/tests/plugins/tasks/networking/test_netmiko_send_command.py index e3fd69ca..8c0021ae 100644 --- a/tests/plugins/tasks/networking/test_netmiko_send_command.py +++ b/tests/plugins/tasks/networking/test_netmiko_send_command.py @@ -3,17 +3,26 @@ class Test(object): - def test_netmiko_send_command(self, brigade): + def test_explicit_netmiko_connection(self, brigade): brigade.filter(name="dev4.group_2").run(task=connections.netmiko_connection) - result = brigade.filter(name="dev4.group_2").run(networking.netmiko_send_command, - command_string="hostname") + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_send_command, command_string="hostname" + ) + assert result + for h, r in result.items(): + assert h == r.result.strip() + + def test_netmiko_send_command(self, brigade): + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_send_command, command_string="hostname" + ) assert result for h, r in result.items(): assert h == r.result.strip() - result = brigade.filter(name="dev4.group_2").run(networking.netmiko_send_command, - command_string="hostname", - use_timing=True) + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_send_command, command_string="hostname", use_timing=True + ) assert result for h, r in result.items(): assert h == r.result.strip() diff --git a/tests/plugins/tasks/networking/test_netmiko_send_config.py b/tests/plugins/tasks/networking/test_netmiko_send_config.py new file mode 100644 index 00000000..42be8e53 --- /dev/null +++ b/tests/plugins/tasks/networking/test_netmiko_send_config.py @@ -0,0 +1,21 @@ +from brigade.plugins.tasks import connections, networking + + +class Test(object): + + def test_explicit_netmiko_connection(self, brigade): + brigade.filter(name="dev4.group_2").run(task=connections.netmiko_connection) + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_send_config, config_commands="hostname" + ) + assert result + for h, r in result.items(): + assert h in r.result.strip() + + def test_netmiko_send_command(self, brigade): + result = brigade.filter(name="dev4.group_2").run( + networking.netmiko_send_config, config_commands="hostname" + ) + assert result + for h, r in result.items(): + assert h in r.result.strip() diff --git a/tests/plugins/tasks/networking/test_tcp_ping.py b/tests/plugins/tasks/networking/test_tcp_ping.py index eb42993a..26e9ea42 100644 --- a/tests/plugins/tasks/networking/test_tcp_ping.py +++ b/tests/plugins/tasks/networking/test_tcp_ping.py @@ -2,15 +2,12 @@ from brigade.core import Brigade -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.inventory.simple import SimpleInventory from brigade.plugins.tasks import networking -import pytest - cur_dir = os.path.dirname(os.path.realpath(__file__)) -ext_inv_file = '{}/../../../inventory_data/external_hosts.yaml'.format(cur_dir) +ext_inv_file = "{}/../../../inventory_data/external_hosts.yaml".format(cur_dir) class Test(object): @@ -21,7 +18,7 @@ def test_tcp_ping_port(self, brigade): assert result for h, r in result.items(): - assert r.tcp_port[65004] + assert r.result[65004] def test_tcp_ping_ports(self, brigade): filter = brigade.filter(name="dev4.group_2") @@ -29,35 +26,37 @@ def test_tcp_ping_ports(self, brigade): assert result for h, r in result.items(): - assert r.tcp_port[35004] is False - assert r.tcp_port[65004] + assert r.result[35004] is False + assert r.result[65004] def test_tcp_ping_invalid_port(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(networking.tcp_ping, - ports='web') - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(networking.tcp_ping, ports="web") + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, ValueError) + assert processed + brigade.data.reset_failed_hosts() def test_tcp_ping_invalid_ports(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(networking.tcp_ping, - ports=[22, 'web', 443]) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(networking.tcp_ping, ports=[22, "web", 443]) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, ValueError) + assert processed + brigade.data.reset_failed_hosts() def test_tcp_ping_external_hosts(): - external = Brigade(inventory=SimpleInventory(ext_inv_file), dry_run=True) + external = Brigade(inventory=SimpleInventory(ext_inv_file, ""), dry_run=True) result = external.run(networking.tcp_ping, ports=[23, 443]) assert result for h, r in result.items(): - if h == 'www.github.com': - assert r.tcp_port[23] is False - assert r.tcp_port[443] + if h == "www.github.com": + assert r.result[23] is False + assert r.result[443] else: - assert r.tcp_port[23] is False - assert r.tcp_port[443] is False + assert r.result[23] is False + assert r.result[443] is False diff --git a/tests/plugins/tasks/text/test_template_file.py b/tests/plugins/tasks/text/test_template_file.py index b7aa0070..83384ddf 100644 --- a/tests/plugins/tasks/text/test_template_file.py +++ b/tests/plugins/tasks/text/test_template_file.py @@ -1,37 +1,32 @@ import os -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import text from jinja2 import TemplateSyntaxError -import pytest - -data_dir = '{}/test_data'.format(os.path.dirname(os.path.realpath(__file__))) +data_dir = "{}/test_data".format(os.path.dirname(os.path.realpath(__file__))) class Test(object): def test_template_file(self, brigade): - result = brigade.run(text.template_file, - template='simple.j2', - path=data_dir) + result = brigade.run(text.template_file, template="simple.j2", path=data_dir) assert result for h, r in result.items(): assert h in r.result - if h == 'host3.group_2': - assert 'my_var: comes_from_all' in r.result - if h == 'host4.group_2': - assert 'my_var: comes_from_host4.group_2' in r.result + if h == "host3.group_2": + assert "my_var: comes_from_all" in r.result + if h == "host4.group_2": + assert "my_var: comes_from_host4.group_2" in r.result def test_template_file_error_broken_file(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(text.template_file, - template='broken.j2', - path=data_dir) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(text.template_file, template="broken.j2", path=data_dir) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, TemplateSyntaxError) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/plugins/tasks/text/test_template_string.py b/tests/plugins/tasks/text/test_template_string.py index 6919517a..9716d7ef 100644 --- a/tests/plugins/tasks/text/test_template_string.py +++ b/tests/plugins/tasks/text/test_template_string.py @@ -1,53 +1,48 @@ import os - -from brigade.core.exceptions import BrigadeExecutionError from brigade.plugins.tasks import text from jinja2 import TemplateSyntaxError -import pytest - - -data_dir = '{}/test_data'.format(os.path.dirname(os.path.realpath(__file__))) +data_dir = "{}/test_data".format(os.path.dirname(os.path.realpath(__file__))) -simple_j2 = ''' +simple_j2 = """ host-name: {{ host }} my_var: {{ my_var}} -''' +""" -broken_j2 = ''' +broken_j2 = """ #Broken template host-name {{ host my_var: {{ my_var}} -''' +""" class Test(object): def test_template_string(self, brigade): - result = brigade.run(text.template_string, - template=simple_j2) + result = brigade.run(text.template_string, template=simple_j2) assert result for h, r in result.items(): - assert 'host-name: {}'.format(h) in r.result - if h == 'host1.group_1': - assert 'my_var: comes_from_host1.group_1' in r.result - if h == 'host2.group_1': - assert 'my_var: comes_from_group_1' in r.result + assert "host-name: {}".format(h) in r.result + if h == "host1.group_1": + assert "my_var: comes_from_host1.group_1" in r.result + if h == "host2.group_1": + assert "my_var: comes_from_group_1" in r.result def test_template_string_error_broken_string(self, brigade): - with pytest.raises(BrigadeExecutionError) as e: - brigade.run(text.template_string, - template=broken_j2) - assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for result in e.value.failed_hosts.values(): + results = brigade.run(text.template_string, template=broken_j2) + processed = False + for result in results.values(): + processed = True assert isinstance(result.exception, TemplateSyntaxError) + assert processed + brigade.data.reset_failed_hosts() diff --git a/tests/wrapper.py b/tests/wrapper.py new file mode 100644 index 00000000..66b83da1 --- /dev/null +++ b/tests/wrapper.py @@ -0,0 +1,57 @@ +import sys +from decorator import decorator + +if sys.version_info.major == 2: + from StringIO import StringIO +else: + from io import StringIO + + +def wrap_cli_test(output, save_output=False): + """ + This decorator captures the stdout and stder and compare it + with the contects of the specified files. + + Arguments: + output (string): Path to the output. stdout and stderr prefixes will be added automatically + save_output (bool): Whether to save the output or not. Useful when creating the tests + """ + + @decorator + def run_test(func, *args, **kwargs): + + stdout = StringIO() + backup_stdout = sys.stdout + sys.stdout = stdout + + stderr = StringIO() + backup_stderr = sys.stderr + sys.stderr = stderr + + func(*args, **kwargs) + sys.stdout = backup_stdout + sys.stderr = backup_stderr + + output_file = output + if sys.version_info.major == 2: + output_file += "_python27" + + if save_output: + with open("{}.stdout".format(output_file), "w+") as f: + f.write(stdout.getvalue()) + with open("{}.stderr".format(output_file), "w+") as f: + f.write(stderr.getvalue()) + + with open("{}.stdout".format(output_file), "r") as f: + screen_output = stdout.getvalue() + reference_output = f.read() + if screen_output != reference_output: + raise Exception(screen_output, reference_output) + + with open("{}.stderr".format(output_file), "r") as f: + screen_output = stderr.getvalue() + reference_output = f.read() + if screen_output != reference_output: + raise Exception(screen_output, reference_output) + + return run_test diff --git a/tox.ini b/tox.ini index 6a4e96fc..b26595fb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,30 @@ envlist = py27,py34,py35,py36 deps = -rrequirements.txt -rrequirements-dev.txt - -rdocs/requirements.txt passenv = * commands = py.test + +[testenv:black] +deps = black==18.4a1 + +basepython = python3.6 +commands = + black --check . + +[testenv:sphinx] +deps = + -rdocs/requirements.txt + +basepython = python3.6 +commands = sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html + +[testenv:pylama] +deps = + -rrequirements-dev.txt + +basepython = python3.6 +commands = + pylama .