diff --git a/.gitignore b/.gitignore index 1bde5d19..8e0b0df3 100644 --- a/.gitignore +++ b/.gitignore @@ -96,8 +96,4 @@ tags .vars output/ -demo/log - -tests.log - .DS_Store diff --git a/.travis.yml b/.travis.yml index 8952416b..4c2dfcd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ sudo: required services: - docker +addons: + apt_packages: + - pandoc + language: python python: - 2.7 diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 2a2dfbfc..a02ea1ce 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -1,10 +1,11 @@ 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, Task +from brigade.core.task import AggregatedResult, Result, Task from brigade.plugins.tasks import connections @@ -33,7 +34,17 @@ def _unpickle_method(func_name, obj, cls): copy_reg.pickle(types.MethodType, _pickle_method, _unpickle_method) -logger = logging.getLogger("brigade") +class Data(object): + """ + This class is just a placeholder to share data amongsts different + versions of Brigade after running ``filter`` multiple times. + + Attributes: + failed_hosts (list): Hosts that have failed to run a task properly + """ + + def __init__(self): + self.failed_hosts = set() class Brigade(object): @@ -43,6 +54,7 @@ class Brigade(object): Arguments: inventory (:obj:`brigade.core.inventory.Inventory`): Inventory to work with + data(:obj:`brigade.core.Data`): shared data amongst different iterations of brigade dry_run(``bool``): Whether if we are testing the changes or not config (:obj:`brigade.core.configuration.Config`): Configuration object config_file (``str``): Path to Yaml configuration file @@ -51,13 +63,17 @@ class Brigade(object): Attributes: inventory (:obj:`brigade.core.inventory.Inventory`): Inventory to work with + data(:obj:`brigade.core.Data`): shared data amongst different iterations of brigade dry_run(``bool``): Whether if we are testing the changes or not config (:obj:`brigade.core.configuration.Config`): Configuration parameters available_connections (``dict``): dict of connection types are available """ def __init__(self, inventory, dry_run, config=None, config_file=None, - available_connections=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 @@ -67,17 +83,46 @@ def __init__(self, inventory, dry_run, config=None, config_file=None, else: self.config = config or Config() - format = "\033[31m%(asctime)s - %(name)s - %(levelname)s" - format += " - %(funcName)20s() - %(message)s\033[0m" - logging.basicConfig( - level=logging.ERROR, - format=format, - ) + self.configure_logging() + if available_connections is not None: self.available_connections = available_connections else: self.available_connections = connections.available_connections + def configure_logging(self): + format = "%(asctime)s - %(name)s - %(levelname)s" + format += " - %(funcName)10s() - %(message)s" + logging.config.dictConfig({ + "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" + }, + }, + "root": { + "level": "ERROR", + "handlers": ["info_file_handler"] + } + }) + def filter(self, **kwargs): """ See :py:meth:`brigade.core.inventory.Inventory.filter` @@ -90,38 +135,27 @@ def filter(self, **kwargs): return b def _run_serial(self, task, dry_run, **kwargs): - t = Task(task, **kwargs) - result = AggregatedResult() + result = AggregatedResult(kwargs.get("name") or task.__name__) for host in self.inventory.hosts.values(): - try: - logger.debug("{}: running task {}".format(host.name, t)) - r = t._start(host=host, brigade=self, dry_run=dry_run) - result[host.name] = r - except Exception as e: - logger.error("{}: {}".format(host, e)) - result.failed_hosts[host.name] = e - result.tracebacks[host.name] = traceback.format_exc() + result[host.name] = run_task(host, self, dry_run, Task(task, **kwargs)) return result def _run_parallel(self, task, num_workers, dry_run, **kwargs): - result = AggregatedResult() + 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))) + result_pool = [pool.apply_async(run_task, + args=(h, self, dry_run, Task(task, **kwargs))) for h in self.inventory.hosts.values()] pool.close() pool.join() - for r in result_pool: - host, res, exc, traceback = r.get() - if exc: - result.failed_hosts[host] = exc - result.tracebacks[host] = traceback - else: - result[host] = res + for rp in result_pool: + r = rp.get() + result[r.host.name] = r return result - def run(self, task, num_workers=None, dry_run=None, **kwargs): + def run(self, task, num_workers=None, dry_run=None, raise_on_error=None, **kwargs): """ Run task over all the hosts in the inventory. @@ -130,6 +164,7 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): 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 **kwargs: additional argument to pass to ``task`` when calling it Raises: @@ -141,21 +176,33 @@ def run(self, task, num_workers=None, dry_run=None, **kwargs): """ 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)) + self.logger.debug(kwargs) + if num_workers == 1: result = self._run_serial(task, dry_run, **kwargs) else: result = self._run_parallel(task, num_workers, dry_run, **kwargs) - if self.config.raise_on_error: + raise_on_error = raise_on_error if raise_on_error is not None else \ + self.config.raise_on_error + if raise_on_error: result.raise_on_error() + else: + self.data.failed_hosts.update(result.failed_hosts.keys()) return result def run_task(host, brigade, dry_run, task): + logger = logging.getLogger("brigade") try: - logger.debug("{}: running task {}".format(host.name, task)) + logger.info("{}: {}: running task".format(host.name, task.name)) r = task._start(host=host, brigade=brigade, dry_run=dry_run) - return host.name, r, None, None except Exception as e: - logger.error("{}: {}".format(host, e)) - return host.name, None, e, traceback.format_exc() + 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 diff --git a/brigade/core/exceptions.py b/brigade/core/exceptions.py index d16efacd..e19e2532 100644 --- a/brigade/core/exceptions.py +++ b/brigade/core/exceptions.py @@ -32,19 +32,19 @@ class BrigadeExecutionError(Exception): """ def __init__(self, result): self.result = result - self.failed_hosts = result.failed_hosts - self.tracebacks = result.tracebacks + + @property + def failed_hosts(self): + return {k: v for k, v in self.result.items() if v.failed} def __str__(self): text = "\n" for k, r in self.result.items(): text += "{}\n".format("#" * 40) - text += "# {} (succeeded) \n".format(k) - text += "{}\n".format("#" * 40) - text += "{}\n".format(r) - for k, r in self.tracebacks.items(): - text += "{}\n".format("#" * 40) - text += "# {} (failed) \n".format(k) + if r.failed: + text += "# {} (failed)\n".format(k) + else: + text += "# {} (succeeded)\n".format(k) text += "{}\n".format("#" * 40) - text += "{}\n".format(r) + text += "{}\n".format(r.result) return text diff --git a/brigade/core/helpers/__init__.py b/brigade/core/helpers/__init__.py index 652792b4..67514857 100644 --- a/brigade/core/helpers/__init__.py +++ b/brigade/core/helpers/__init__.py @@ -2,7 +2,7 @@ def merge_two_dicts(x, y): try: z = x.copy() except AttributeError: - z = x.items() + z = dict(x) z.update(y) return z diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index 52c5caf9..da7af8fe 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -1,12 +1,8 @@ import getpass -import logging from brigade.core import helpers -logger = logging.getLogger("brigade") - - class Host(object): """ Represents a host. @@ -77,19 +73,17 @@ def __init__(self, name, group=None, brigade=None, **kwargs): 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) + def keys(self): """Returns the keys of the attribute ``data`` and of the parent(s) groups.""" - k = list(self.data.keys()) - if self.group: - k.extend(list(self.group.keys())) - return k + return self._resolve_data().keys() def values(self): """Returns the values of the attribute ``data`` and of the parent(s) groups.""" - v = list(self.data.values()) - if self.group: - v.extend(list(self.group.values())) - return v + return self._resolve_data().values() def __getitem__(self, item): try: @@ -132,11 +126,7 @@ def items(self): Returns all the data accessible from a device, including the one inherited from parent groups """ - if self.group: - d = self.group.items() - else: - d = {} - return helpers.merge_two_dicts(d, self.data) + return self._resolve_data().items() @property def brigade(self): @@ -217,7 +207,9 @@ def get_connection(self, connection): # the given host. We also have to set `num_workers=1` because chances are # we are already inside a thread # Task should establish a connection and populate self.connection[connection] - self.brigade.filter(name=self.name).run(conn_task, num_workers=1) + 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] diff --git a/brigade/core/task.py b/brigade/core/task.py index 72a1e694..baa49813 100644 --- a/brigade/core/task.py +++ b/brigade/core/task.py @@ -1,10 +1,7 @@ -import logging from builtins import super from brigade.core.exceptions import BrigadeExecutionError -logger = logging.getLogger("brigade") - class Task(object): """ @@ -14,10 +11,16 @@ 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 **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 @@ -25,20 +28,33 @@ class Task(object): dry_run(``bool``): Populated right before calling the ``task`` """ - def __init__(self, task, **kwargs): + def __init__(self, task, name=None, skipped=False, **kwargs): + self.name = name or task.__name__ self.task = task self.params = kwargs + self.skipped = skipped + self.results = MultiResult(self.name) def __repr__(self): - return self.task.__name__ - - def _start(self, host, brigade, dry_run): - self.host = host - self.brigade = brigade - self.dry_run = dry_run if dry_run is not None else brigade.dry_run - return self.task(self, **self.params) - - def run(self, task, **kwargs): + 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) + r.name = self.name + + if sub_task: + return r + else: + self.results.insert(0, r) + return self.results + + def run(self, task, dry_run=None, **kwargs): """ This is a utility method to call a task from within a task. For instance: @@ -54,32 +70,47 @@ def grouped_tasks(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) - aggr = self.brigade.filter(name=self.host.name).run(task, num_workers=1, **kwargs) - return aggr[self.host.name] + 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) + return r class Result(object): """ - Returned by tasks. + Result of running individual tasks. Arguments: changed (bool): ``True`` if the task is changing the system diff (obj): Diff between state of the system before/after running this task 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 + 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 diff (obj): Diff between state of the system before/after running this task 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 + 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="", **kwargs): + def __init__(self, host, result=None, changed=False, diff="", failed=False, exception=None, + skipped=False, **kwargs): self.result = result self.host = host self.changed = changed self.diff = diff + self.failed = failed + self.exception = exception + self.skipped = skipped for k, v in kwargs.items(): setattr(self, k, v) @@ -89,19 +120,63 @@ 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"]``. - - Attributes: - failed_hosts (list): list of hosts that failed """ - def __init__(self, **kwargs): + def __init__(self, name, **kwargs): + self.name = name super().__init__(**kwargs) - self.failed_hosts = {} - self.tracebacks = {} + + def __repr__(self): + return '{}: {}'.format(self.__class__.__name__, self.name) @property def failed(self): """If ``True`` at least a host failed.""" - return bool(self.failed_hosts) + return any([h.failed for h in self.values()]) + + @property + 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: + :obj:`brigade.core.exceptions.BrigadeExecutionError`: When at least a task failed + """ + if self.failed: + raise BrigadeExecutionError(self) + + +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) + + @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.""" + return any([h.changed for h in self]) def raise_on_error(self): """ diff --git a/brigade/easy.py b/brigade/easy.py new file mode 100644 index 00000000..8726e9a5 --- /dev/null +++ b/brigade/easy.py @@ -0,0 +1,23 @@ +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/__init__.py b/brigade/plugins/functions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/brigade/plugins/functions/text/__init__.py b/brigade/plugins/functions/text/__init__.py new file mode 100644 index 00000000..043171f0 --- /dev/null +++ b/brigade/plugins/functions/text/__init__.py @@ -0,0 +1,9 @@ +from colorama import Fore, Style + + +def print_title(title): + """ + Helper function to print a title. + """ + msg = "**** {} ".format(title) + print("{}{}{}{}".format(Style.BRIGHT, Fore.GREEN, msg, "*" * (80 - len(msg)))) diff --git a/brigade/plugins/tasks/commands/command.py b/brigade/plugins/tasks/commands/command.py index 3efc587a..718bed4e 100644 --- a/brigade/plugins/tasks/commands/command.py +++ b/brigade/plugins/tasks/commands/command.py @@ -1,4 +1,3 @@ -import logging import shlex import subprocess @@ -8,9 +7,6 @@ from brigade.core.task import Result -logger = logging.getLogger("brigade") - - def command(task, command): """ Executes a command locally @@ -27,7 +23,6 @@ def command(task, command): :obj:`brigade.core.exceptions.CommandError`: when there is a command error """ command = format_string(command, task, **task.host) - logger.debug("{}:local_cmd:{}".format(task.host, command)) cmd = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/brigade/plugins/tasks/commands/remote_command.py b/brigade/plugins/tasks/commands/remote_command.py index f7330ae6..47a49a53 100644 --- a/brigade/plugins/tasks/commands/remote_command.py +++ b/brigade/plugins/tasks/commands/remote_command.py @@ -1,12 +1,7 @@ -import logging - from brigade.core.exceptions import CommandError from brigade.core.task import Result -logger = logging.getLogger("brigade") - - def remote_command(task, command): """ Executes a command locally diff --git a/brigade/plugins/tasks/files/__init__.py b/brigade/plugins/tasks/files/__init__.py index 1a430fa2..5c393eef 100644 --- a/brigade/plugins/tasks/files/__init__.py +++ b/brigade/plugins/tasks/files/__init__.py @@ -1,6 +1,8 @@ from .sftp import sftp +from .write import write __all__ = ( "sftp", + "write", ) diff --git a/brigade/plugins/tasks/files/sftp.py b/brigade/plugins/tasks/files/sftp.py index c010c5e1..4a51f39d 100644 --- a/brigade/plugins/tasks/files/sftp.py +++ b/brigade/plugins/tasks/files/sftp.py @@ -1,5 +1,4 @@ import hashlib -import logging import os import stat @@ -13,9 +12,6 @@ from scp import SCPClient -logger = logging.getLogger("brigade") - - def get_src_hash(filename): sha1sum = hashlib.sha1() diff --git a/brigade/plugins/tasks/files/write.py b/brigade/plugins/tasks/files/write.py new file mode 100644 index 00000000..4bc28d4e --- /dev/null +++ b/brigade/plugins/tasks/files/write.py @@ -0,0 +1,48 @@ +import difflib +import os + +from brigade.core.task import Result + + +def _read_file(file): + if not os.path.exists(file): + return [] + with open(file, "r") as f: + return f.read().splitlines() + + +def _generate_diff(filename, content, append): + original = _read_file(filename) + if append: + c = list(original) + c.extend(content.splitlines()) + content = c + else: + content = content.splitlines() + + diff = difflib.unified_diff(original, content, fromfile=filename, tofile="new") + + return "\n".join(diff) + + +def write(task, filename, content, append=False): + """ + Write contents to a file (locally) + + Arguments: + 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 + + Returns: + * changed (``bool``): + * diff (``str``): unified diff + """ + diff = _generate_diff(filename, content, append) + + if not task.dry_run: + mode = "a+" if append else "w+" + with open(filename, mode=mode) as f: + f.write(content) + + return Result(host=task.host, diff=diff, changed=bool(diff)) diff --git a/brigade/plugins/tasks/networking/__init__.py b/brigade/plugins/tasks/networking/__init__.py index 2b62194e..59e33ed9 100644 --- a/brigade/plugins/tasks/networking/__init__.py +++ b/brigade/plugins/tasks/networking/__init__.py @@ -1,6 +1,7 @@ from .napalm_cli import napalm_cli from .napalm_configure import napalm_configure from .napalm_get import napalm_get +from .napalm_validate import napalm_validate from .netmiko_send_command import netmiko_send_command from .tcp_ping import tcp_ping @@ -8,6 +9,7 @@ "napalm_cli", "napalm_configure", "napalm_get", + "napalm_validate", "netmiko_send_command", "tcp_ping", ) diff --git a/brigade/plugins/tasks/networking/napalm_configure.py b/brigade/plugins/tasks/networking/napalm_configure.py index 895d06b4..663c8404 100644 --- a/brigade/plugins/tasks/networking/napalm_configure.py +++ b/brigade/plugins/tasks/networking/napalm_configure.py @@ -1,7 +1,8 @@ +from brigade.core.helpers import format_string from brigade.core.task import Result -def napalm_configure(task, configuration, replace=False): +def napalm_configure(task, filename=None, configuration=None, replace=False): """ Loads configuration into a network devices using napalm @@ -15,15 +16,16 @@ def napalm_configure(task, configuration, 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(config=configuration) + device.load_replace_candidate(filename=filename, config=configuration) else: - device.load_merge_candidate(config=configuration) + device.load_merge_candidate(filename=filename, config=configuration) diff = device.compare_config() - if task.dry_run: - device.discard_config() - else: + if not task.dry_run and diff: device.commit_config() + else: + device.discard_config() return Result(host=task.host, diff=diff, changed=len(diff) > 0) diff --git a/brigade/plugins/tasks/networking/napalm_validate.py b/brigade/plugins/tasks/networking/napalm_validate.py new file mode 100644 index 00000000..e156b9e8 --- /dev/null +++ b/brigade/plugins/tasks/networking/napalm_validate.py @@ -0,0 +1,21 @@ +from brigade.core.task import Result + + +def napalm_validate(task, src=None, validation_source=None): + """ + Gather information with napalm and validate it: + + http://napalm.readthedocs.io/en/develop/validate/index.html + + Arguments: + src: file to use as validation source + validation_source (list): instead of a file data needed to validate device's state + + Returns: + :obj:`brigade.core.task.Result`: + * result (``dict``): dictionary with the result of the validation + * 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) + return Result(host=task.host, result=r) diff --git a/brigade/plugins/tasks/text/__init__.py b/brigade/plugins/tasks/text/__init__.py index dfc5efa3..3a41db10 100644 --- a/brigade/plugins/tasks/text/__init__.py +++ b/brigade/plugins/tasks/text/__init__.py @@ -1,7 +1,9 @@ +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", ) diff --git a/brigade/plugins/tasks/text/print_result.py b/brigade/plugins/tasks/text/print_result.py new file mode 100644 index 00000000..60801c59 --- /dev/null +++ b/brigade/plugins/tasks/text/print_result.py @@ -0,0 +1,58 @@ +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 66207780..9b8d8a78 100644 --- a/brigade/plugins/tasks/text/template_file.py +++ b/brigade/plugins/tasks/text/template_file.py @@ -17,6 +17,7 @@ def template_file(task, template, path, **kwargs): """ 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) return Result(host=task.host, result=text) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 00000000..2da34a19 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,12 @@ +div.pygments pre { + font-size: 0.8em; + padding: 0.5em 0.5em 0.5em 0.5em; +} + +span.lineno { + color: gray; +} + +span.lineno::after { + content: "|" +} diff --git a/docs/conf.py b/docs/conf.py index ba2321d7..77bde96b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,7 +38,7 @@ # 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'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'nbsphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -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. @@ -190,6 +190,7 @@ def build_configuration_parameters(app): def setup(app): """Map methods to states of the documentation build.""" app.connect('builder-inited', build_configuration_parameters) + app.add_stylesheet('css/custom.css') build_configuration_parameters(None) diff --git a/docs/howto/basic-napalm-getters.rst b/docs/howto/basic-napalm-getters.rst deleted file mode 100644 index d72e0c6f..00000000 --- a/docs/howto/basic-napalm-getters.rst +++ /dev/null @@ -1,146 +0,0 @@ -Gathering information with NAPALM -################################# - -Inventory -========= - -Let's start by seeing how to work with the inventory. Let's assume the following files: - -* Hosts file: - -.. literalinclude:: ../../examples/hosts.yaml - :name: hosts.yaml - :language: yaml - -* Groups file: - -.. literalinclude:: ../../examples/groups.yaml - :name: groups.yaml - :language: yaml - -We can instantiate Brigade as follows:: - - >>> from brigade.core import Brigade - >>> from brigade.plugins.inventory.simple import SimpleInventory - >>> brigade = Brigade( - ... inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - ... dry_run=True) - >>> brigade.inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh', 'host1.bma', 'host2.bma', 'switch00.bma', 'switch01.bma']) - >>> brigade.inventory.groups.keys() - dict_keys(['all', 'bma-leaf', 'bma-host', 'bma', 'cmh-leaf', 'cmh-host', 'cmh']) - -As you can see instantiating brigade and providing inventory information is very easy. Now let's see how we can filter hosts. This will be useful when we want to apply certain tasks to only certain devices:: - - >>> brigade.filter(site="cmh").inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh']) - >>> brigade.filter(site="cmh", role="leaf").inventory.hosts.keys() - dict_keys(['switch00.cmh', 'switch01.cmh']) - -You can basically filter by any attribute the device has. The filter is also cumulative:: - - >>> cmh = brigade.filter(site="cmh") - >>> cmh.inventory.hosts.keys() - dict_keys(['host1.cmh', 'host2.cmh', 'switch00.cmh', 'switch01.cmh']) - >>> cmh.filter(role="leaf").inventory.hosts.keys() - dict_keys(['switch00.cmh', 'switch01.cmh']) - -Data -==== - -Now let's see how to access data. Let's start by grabbing a host:: - - >>> host = brigade.inventory.hosts["switch00.cmh"] - -Now, you can access host data either via the host itself, as it behaves like a dict, or via it's ``data`` attribute. The difference is that if access data via the host itself the information will be resolved and data inherited by parent groups will be accessible while if you access the data via the ``data`` attribute only data belonging to the host will be accessible. Let's see a few examples, refer to the files on top of this document for reference:: - - >>> host["nos"] - 'eos' - >>> host.data["nos"] - 'eos' - >>> host["domain"] - 'acme.com' - >>> host.domain["domain"] - Traceback (most recent call last): - File "", line 1, in - AttributeError: 'Host' object has no attribute 'domain' - -You can access the parent group via the ``group`` attribute and :obj:`brigade.core.inventory.Group` behave in the same exact way as :obj:`brigade.core.inventory.Host`:: - - >>> host.group - Group: cmh-leaf - >>> host.group["domain"] - 'acme.com' - >>> host.group.data["domain"] - Traceback (most recent call last): - File "", line 1, in - KeyError: 'domain' - -Tasks -===== - -Now we know how to deal with the inventory let's try to use plugin to gather device information:: - - >>> from brigade.plugins import tasks - >>> cmh_leaf = brigade.filter(site="cmh", role="leaf") - >>> result = cmh_leaf.run(task=tasks.napalm_get_facts, - ... facts="facts") - >>> print(result) - {'switch00.cmh': {'result': {'hostname': 'switch00.cmh', 'fqdn': 'switch00.cmh.cmh.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83187, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']}}, 'switch01.cmh': {'result': {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.cmh', 'fqdn': 'switch01.cmh.cmh.acme.com', 'uptime': 83084, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']}}} - -You can also group multiple tasks into a single block:: - - >>> def get_info(task): - ... # Grouping multiple tasks that go together - ... r = tasks.napalm_get_facts(task, "facts") - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... r = tasks.napalm_get_facts(task, "interfaces") - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... print() - ... - >>> cmh_leaf.run(task=get_info) - switch00.cmh - ============ - {'hostname': 'switch00.bma', 'fqdn': 'switch00.bma.bma.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83424, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']} - switch00.cmh - ============ - {'Ethernet2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 1511034159.0399787, 'speed': 0, 'mac_address': '08:00:27:AB:42:B6'}, 'Management1': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 1511033376.7964435, 'speed': 1000, 'mac_address': '08:00:27:47:87:83'}, 'Ethernet1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 1511033362.0302556, 'speed': 0, 'mac_address': '08:00:27:2D:F4:5A'}} - - switch01.cmh - ============ - {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.bma', 'fqdn': 'switch01.bma.bma.acme.com', 'uptime': 83320, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']} - switch01.cmh - ============ - {'ge-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83272.0, 'mac_address': '08:00:27:AA:8C:76', 'speed': 1000}, 'gr-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ip-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'lsq-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83273.0, 'mac_address': 'None', 'speed': -1}, 'lt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '02:96:14:8C:76:B3', 'speed': 800}, 'mt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'sp-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83273.0, 'mac_address': 'Unspecified', 'speed': 800}, 'ge-0/0/1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 83272.0, 'mac_address': '08:00:27:FB:F0:FC', 'speed': 1000}, 'ge-0/0/2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 82560.0, 'mac_address': '08:00:27:32:60:54', 'speed': 1000}, '.local.': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'dsc': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'gre': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'ipip': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'irb': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '4C:96:14:8C:76:B0', 'speed': -1}, 'lo0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'lsi': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'mtun': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pimd': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pime': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pp0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'ppd0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ppe0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'st0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'tap': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'vlan': {'is_up': False, 'is_enabled': True, 'description': '', 'last_flapped': 83282.0, 'mac_address': '00:00:00:00:00:00', 'speed': 1000}} - -Or even reuse:: - - >>> def get_facts(task, facts): - ... # variable "facts" will let us reuse this for multiple purposes - ... r = tasks.napalm_get_facts(task, facts) - ... print(task.host.name) - ... print("============") - ... print(r["result"]) - ... print() - ... - >>> cmh_leaf.run(task=get_facts, facts="facts") - switch00.cmh - ============ - {'hostname': 'switch00.bma', 'fqdn': 'switch00.bma.bma.acme.com', 'vendor': 'Arista', 'model': 'vEOS', 'serial_number': '', 'os_version': '4.17.5M-4414219.4175M', 'uptime': 83534, 'interface_list': ['Ethernet1', 'Ethernet2', 'Management1']} - - switch01.cmh - ============ - {'vendor': 'Juniper', 'model': 'FIREFLY-PERIMETER', 'serial_number': 'a7defdc362ff', 'os_version': '12.1X47-D20.7', 'hostname': 'switch01.bma', 'fqdn': 'switch01.bma.bma.acme.com', 'uptime': 83431, 'interface_list': ['ge-0/0/0', 'gr-0/0/0', 'ip-0/0/0', 'lsq-0/0/0', 'lt-0/0/0', 'mt-0/0/0', 'sp-0/0/0', 'ge-0/0/1', 'ge-0/0/2', '.local.', 'dsc', 'gre', 'ipip', 'irb', 'lo0', 'lsi', 'mtun', 'pimd', 'pime', 'pp0', 'ppd0', 'ppe0', 'st0', 'tap', 'vlan']} - - >>> cmh_leaf.run(task=get_facts, facts="interfaces") - switch00.cmh - ============ - {'Ethernet2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 1511034159.0400095, 'speed': 0, 'mac_address': '08:00:27:AB:42:B6'}, 'Management1': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 1511033376.7963786, 'speed': 1000, 'mac_address': '08:00:27:47:87:83'}, 'Ethernet1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 1511033362.0302918, 'speed': 0, 'mac_address': '08:00:27:2D:F4:5A'}} - - switch01.cmh - ============ - {'ge-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83387.0, 'mac_address': '08:00:27:AA:8C:76', 'speed': 1000}, 'gr-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ip-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'lsq-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83388.0, 'mac_address': 'None', 'speed': -1}, 'lt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '02:96:14:8C:76:B3', 'speed': 800}, 'mt-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'sp-0/0/0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': 83388.0, 'mac_address': 'Unspecified', 'speed': 800}, 'ge-0/0/1': {'is_up': True, 'is_enabled': True, 'description': 'An Interface in bma', 'last_flapped': 83387.0, 'mac_address': '08:00:27:FB:F0:FC', 'speed': 1000}, 'ge-0/0/2': {'is_up': False, 'is_enabled': False, 'description': 'Another interface in bma', 'last_flapped': 82675.0, 'mac_address': '08:00:27:32:60:54', 'speed': 1000}, '.local.': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'dsc': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'gre': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'ipip': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'irb': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': '4C:96:14:8C:76:B0', 'speed': -1}, 'lo0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'lsi': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'mtun': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pimd': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pime': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'pp0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'ppd0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'ppe0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': 800}, 'st0': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'None', 'speed': -1}, 'tap': {'is_up': True, 'is_enabled': True, 'description': '', 'last_flapped': -1.0, 'mac_address': 'Unspecified', 'speed': -1}, 'vlan': {'is_up': False, 'is_enabled': True, 'description': '', 'last_flapped': 83397.0, 'mac_address': '00:00:00:00:00:00', 'speed': 1000}} diff --git a/docs/howto/from_runbooks_to_complex_tooling.rst b/docs/howto/from_runbooks_to_complex_tooling.rst new file mode 100644 index 00000000..3f56fe61 --- /dev/null +++ b/docs/howto/from_runbooks_to_complex_tooling.rst @@ -0,0 +1,11 @@ +From Runbooks to Advanced Tooling +================================= + +In this section we are going to build advanced tooling in a series of baby steps. We will start writing very simple runbooks, then we will slightly rewrite those runbooks to turn them into flexible cli tools. Once we are done with that we will turn those isolated cli tools into an advanced tool that can accommodate different workflows. + +.. toctree:: + :maxdepth: 1 + :glob: + + simple_runbooks/index + simple_tooling/index diff --git a/docs/howto/index.rst b/docs/howto/index.rst index 69843994..dfadc113 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -5,4 +5,4 @@ How to use Brigade :maxdepth: 1 :glob: - * \ No newline at end of file + * diff --git a/docs/howto/simple_runbooks/backup.ipynb b/docs/howto/simple_runbooks/backup.ipynb new file mode 120000 index 00000000..f6561d19 --- /dev/null +++ b/docs/howto/simple_runbooks/backup.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/backup.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/configure.ipynb b/docs/howto/simple_runbooks/configure.ipynb new file mode 120000 index 00000000..9fc2fc70 --- /dev/null +++ b/docs/howto/simple_runbooks/configure.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/configure.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/get_facts.ipynb b/docs/howto/simple_runbooks/get_facts.ipynb new file mode 120000 index 00000000..cdf882f1 --- /dev/null +++ b/docs/howto/simple_runbooks/get_facts.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/get_facts.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/index.rst b/docs/howto/simple_runbooks/index.rst new file mode 100644 index 00000000..f63c4e42 --- /dev/null +++ b/docs/howto/simple_runbooks/index.rst @@ -0,0 +1,11 @@ +Simple Runbooks +=============== + +In this series we are going to build a few simple runbooks to do various tasks on the network. Each runbook is going to do one specific tasks and is going to make certain assumptions to simplify the logic as possible; like which devices are involved, where is the data located, etc. In following series we will build on these runbooks to build more flexible and complex tooling. + +.. toctree:: + :maxdepth: 1 + :glob: + + * + diff --git a/docs/howto/simple_runbooks/rollback.ipynb b/docs/howto/simple_runbooks/rollback.ipynb new file mode 120000 index 00000000..4081ad20 --- /dev/null +++ b/docs/howto/simple_runbooks/rollback.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/rollback.ipynb \ No newline at end of file diff --git a/docs/howto/simple_runbooks/validate.ipynb b/docs/howto/simple_runbooks/validate.ipynb new file mode 120000 index 00000000..91119d86 --- /dev/null +++ b/docs/howto/simple_runbooks/validate.ipynb @@ -0,0 +1 @@ +../../../examples/1_simple_runbooks/validate.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/backup.ipynb b/docs/howto/simple_tooling/backup.ipynb new file mode 120000 index 00000000..4cd21f71 --- /dev/null +++ b/docs/howto/simple_tooling/backup.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/backup.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/configure.ipynb b/docs/howto/simple_tooling/configure.ipynb new file mode 120000 index 00000000..71c86d66 --- /dev/null +++ b/docs/howto/simple_tooling/configure.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/configure.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/get_facts.ipynb b/docs/howto/simple_tooling/get_facts.ipynb new file mode 120000 index 00000000..450584f1 --- /dev/null +++ b/docs/howto/simple_tooling/get_facts.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/get_facts.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/index.rst b/docs/howto/simple_tooling/index.rst new file mode 100644 index 00000000..4a338209 --- /dev/null +++ b/docs/howto/simple_tooling/index.rst @@ -0,0 +1,13 @@ +Simple Tooling +============== + +In this series we are going to build on top of the runbooks we built on :doc:`the previous section <../simple_runbooks/index>` and build more versatile tooling. Most tools will be not that very different from its equivalent runbook so you should be able to look at them side by side and realize how easy it was to build a cli tool from a previous runbook thanks to the fact that brigade is a native python framework and integrates natively with other frameworks like `click `. + + +.. toctree:: + :maxdepth: 1 + :glob: + + * + + diff --git a/docs/howto/simple_tooling/rollback.ipynb b/docs/howto/simple_tooling/rollback.ipynb new file mode 120000 index 00000000..bd29b917 --- /dev/null +++ b/docs/howto/simple_tooling/rollback.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/rollback.ipynb \ No newline at end of file diff --git a/docs/howto/simple_tooling/validate.ipynb b/docs/howto/simple_tooling/validate.ipynb new file mode 120000 index 00000000..e7b38cfd --- /dev/null +++ b/docs/howto/simple_tooling/validate.ipynb @@ -0,0 +1 @@ +../../../examples/2_simple_tooling/validate.ipynb \ No newline at end of file diff --git a/docs/ref/api/brigade.rst b/docs/ref/api/brigade.rst index 94fdc833..0ea4f024 100644 --- a/docs/ref/api/brigade.rst +++ b/docs/ref/api/brigade.rst @@ -1,3 +1,10 @@ +Data +#### + +.. autoclass:: brigade.core.Data + :members: + :undoc-members: + Brigade ####### diff --git a/docs/ref/api/easy.rst b/docs/ref/api/easy.rst new file mode 100644 index 00000000..02de78a5 --- /dev/null +++ b/docs/ref/api/easy.rst @@ -0,0 +1,6 @@ +Easy +==== + +.. automodule:: brigade.easy + :members: + :undoc-members: diff --git a/docs/ref/api/index.rst b/docs/ref/api/index.rst index a53ebe21..7ca6e828 100644 --- a/docs/ref/api/index.rst +++ b/docs/ref/api/index.rst @@ -8,5 +8,6 @@ Brigade API Reference brigade configuration inventory + easy task - exceptions \ No newline at end of file + exceptions diff --git a/docs/ref/api/task.rst b/docs/ref/api/task.rst index 2a86f698..8ccf2b8e 100644 --- a/docs/ref/api/task.rst +++ b/docs/ref/api/task.rst @@ -18,3 +18,10 @@ AggregatedResult .. autoclass:: brigade.core.task.AggregatedResult :members: :undoc-members: + +MultiResult +################ + +.. autoclass:: brigade.core.task.MultiResult + :members: + :undoc-members: diff --git a/docs/ref/functions/index.rst b/docs/ref/functions/index.rst new file mode 100644 index 00000000..cf781f1d --- /dev/null +++ b/docs/ref/functions/index.rst @@ -0,0 +1,8 @@ +Functions +========= + +.. toctree:: + :maxdepth: 2 + :glob: + + * diff --git a/docs/ref/functions/text.rst b/docs/ref/functions/text.rst new file mode 100644 index 00000000..6f62d224 --- /dev/null +++ b/docs/ref/functions/text.rst @@ -0,0 +1,6 @@ +Text +==== + +.. automodule:: brigade.plugins.functions.text + :members: + :undoc-members: diff --git a/docs/ref/index.rst b/docs/ref/index.rst index 7b3d72bb..9e863098 100644 --- a/docs/ref/index.rst +++ b/docs/ref/index.rst @@ -24,4 +24,5 @@ Reference Guides :caption: Plugins tasks/index + functions/index inventory/index diff --git a/docs/ref/inventory/index.rst b/docs/ref/inventory/index.rst index 5af16f3f..cf659aea 100644 --- a/docs/ref/inventory/index.rst +++ b/docs/ref/inventory/index.rst @@ -1,3 +1,5 @@ +.. _ref-inventory: + Inventory ========= diff --git a/docs/requirements.txt b/docs/requirements.txt index 6fab96cb..e28da7c4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,8 @@ sphinx sphinx_rtd_theme sphinxcontrib-napoleon +jupyter +nbsphinx -r ../requirements.txt future # jtextfsm diff --git a/docs/tutorials/intro/brigade.rst b/docs/tutorials/intro/brigade.rst new file mode 100644 index 00000000..2a5fb807 --- /dev/null +++ b/docs/tutorials/intro/brigade.rst @@ -0,0 +1,74 @@ +Brigade +======= + +Now that we know how the inventory works let's create a brigade object we can start working with. There are two ways we can use: + +1. Using the :obj:`brigade.core.Brigade` directly, which is quite simple and the most flexible and versatile option. +2. Using :obj:`brigade.easy.easy_brigade`, which is simpler and good enough for most cases. + +Using the "raw" API +------------------- + +If you want to use the "raw" API you need two things: + +1. A :obj:`brigade.core.configuration.Config` object. +2. An :doc:`inventory ` object. + +Once you have them, you can create the brigade object yourself. For example:: + + >>> from brigade.core import Brigade + >>> from brigade.core.configuration import Config + >>> from brigade.plugins.inventory.simple import SimpleInventory + >>> + >>> brigade = Brigade( + ... inventory=SimpleInventory("hosts.yaml", "groups.yaml"), + ... dry_run=False, + ... config=Config(raise_on_error=False), + ... ) + >>> + +Using ``easy_brigade`` +---------------------- + +With :obj:`brigade.easy.easy_brigade` you only need to do:: + + >>> from brigade.easy import easy_brigade + >>> brigade = easy_brigade( + ... host_file="hosts.yaml", group_file="groups.yaml", + ... dry_run=True, + ... raise_on_error=False, + ... ) + >>> + +As you can see is not that different from above but you save a few imports. + +Brigade's Inventory +------------------- + +Brigade's object will always have a reference to the inventory you can inspect and work with if you have the need. For instance:: + + >>> brigade.inventory + + >>> brigade.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} + >>> brigade.inventory.groups + {'all': Group: all, 'bma': Group: bma, 'cmh': Group: cmh} + +As you will see further on in the tutorial you will rarely need to work with the inventory yourself as brigade will take care of it for you automatically but it's always good to know you have it there if you need to. + +Filtering the hosts +___________________ + +As we could see in the :doc:`Inventory ` section we could filter hosts based on data and attributes. The brigade object can leverage on that feature to "replicate" itself with subsets of devices allowing you to group your devices and perform actions on them as you see fit:: + + >>> switches = brigade.filter(type="network_device") + >>> switches.inventory.hosts + {'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> switches_in_bma = switches.filter(site="bma") + >>> switches_in_bma.inventory.hosts + {'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma} + >>> hosts = brigade.filter(type="host") + >>> hosts.inventory.hosts + {'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma} + +All of the "replicas" of brigade will contain the same data and configuration, only the hosts will differ. diff --git a/docs/tutorials/intro/explore.rst b/docs/tutorials/intro/explore.rst deleted file mode 100644 index 08bda4be..00000000 --- a/docs/tutorials/intro/explore.rst +++ /dev/null @@ -1,2 +0,0 @@ -Exploring the inventory in Brigade -================================== diff --git a/docs/tutorials/intro/index.rst b/docs/tutorials/intro/index.rst index 68737c6c..850a458e 100644 --- a/docs/tutorials/intro/index.rst +++ b/docs/tutorials/intro/index.rst @@ -13,6 +13,9 @@ We're glad you made it here! This is a great place to learn the basics of Brigad Brigade at a glance 100% Python Installation guide - Creating an inventory - Exploring the inventory - Running tasks \ No newline at end of file + inventory + brigade + running_tasks + running_tasks_different_hosts + running_tasks_grouping + running_tasks_errors diff --git a/docs/tutorials/intro/inventory.rst b/docs/tutorials/intro/inventory.rst index 9a019474..a8f089cb 100644 --- a/docs/tutorials/intro/inventory.rst +++ b/docs/tutorials/intro/inventory.rst @@ -1,2 +1,135 @@ -Creating an inventory for Brigade -================================= +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/docs/tutorials/intro/run.rst b/docs/tutorials/intro/run.rst deleted file mode 100644 index a09cc28b..00000000 --- a/docs/tutorials/intro/run.rst +++ /dev/null @@ -1,2 +0,0 @@ -Running tasks with Brigade -========================== diff --git a/docs/tutorials/intro/running_tasks.rst b/docs/tutorials/intro/running_tasks.rst new file mode 100644 index 00000000..63edcbf9 --- /dev/null +++ b/docs/tutorials/intro/running_tasks.rst @@ -0,0 +1,89 @@ +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 new file mode 100644 index 00000000..864d5b00 --- /dev/null +++ b/docs/tutorials/intro/running_tasks_different_hosts.rst @@ -0,0 +1,72 @@ +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 new file mode 100644 index 00000000..7e34e1cc --- /dev/null +++ b/docs/tutorials/intro/running_tasks_errors.rst @@ -0,0 +1,390 @@ +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 new file mode 100644 index 00000000..f3330cf7 --- /dev/null +++ b/docs/tutorials/intro/running_tasks_grouping.rst @@ -0,0 +1,227 @@ +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/examples/1_simple_runbooks/backup.ipynb b/examples/1_simple_runbooks/backup.ipynb new file mode 100644 index 00000000..8f5956ec --- /dev/null +++ b/examples/1_simple_runbooks/backup.ipynb @@ -0,0 +1,557 @@ +{ + "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 new file mode 100755 index 00000000..baec0417 --- /dev/null +++ b/examples/1_simple_runbooks/backup.py @@ -0,0 +1,46 @@ +#!/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 new file mode 100644 index 00000000..2f409b64 --- /dev/null +++ b/examples/1_simple_runbooks/configure.ipynb @@ -0,0 +1,673 @@ +{ + "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 new file mode 100755 index 00000000..d120a723 --- /dev/null +++ b/examples/1_simple_runbooks/configure.py @@ -0,0 +1,81 @@ +#!/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 new file mode 100644 index 00000000..920b6eba --- /dev/null +++ b/examples/1_simple_runbooks/get_facts.ipynb @@ -0,0 +1,662 @@ +{ + "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 new file mode 100755 index 00000000..5b308998 --- /dev/null +++ b/examples/1_simple_runbooks/get_facts.py @@ -0,0 +1,32 @@ +#!/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 new file mode 100644 index 00000000..e99f693f --- /dev/null +++ b/examples/1_simple_runbooks/rollback.ipynb @@ -0,0 +1,438 @@ +{ + "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 new file mode 100755 index 00000000..2c219cc7 --- /dev/null +++ b/examples/1_simple_runbooks/rollback.py @@ -0,0 +1,48 @@ +#!/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 new file mode 100644 index 00000000..8af4499e --- /dev/null +++ b/examples/1_simple_runbooks/validate.ipynb @@ -0,0 +1,689 @@ +{ + "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 new file mode 100755 index 00000000..4453babd --- /dev/null +++ b/examples/1_simple_runbooks/validate.py @@ -0,0 +1,61 @@ +#!/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 new file mode 100644 index 00000000..7df81f0d --- /dev/null +++ b/examples/2_simple_tooling/backup.ipynb @@ -0,0 +1,1062 @@ +{ + "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 new file mode 100755 index 00000000..ba3d79b3 --- /dev/null +++ b/examples/2_simple_tooling/backup.py @@ -0,0 +1,69 @@ +#!/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 new file mode 100644 index 00000000..fd28dd6c --- /dev/null +++ b/examples/2_simple_tooling/configure.ipynb @@ -0,0 +1,747 @@ +{ + "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 new file mode 100755 index 00000000..82b75744 --- /dev/null +++ b/examples/2_simple_tooling/configure.py @@ -0,0 +1,96 @@ +#!/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 new file mode 100644 index 00000000..87049422 --- /dev/null +++ b/examples/2_simple_tooling/get_facts.ipynb @@ -0,0 +1,306 @@ +{ + "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 new file mode 100755 index 00000000..8bbe60e0 --- /dev/null +++ b/examples/2_simple_tooling/get_facts.py @@ -0,0 +1,45 @@ +#!/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 new file mode 100644 index 00000000..e09fc848 --- /dev/null +++ b/examples/2_simple_tooling/rollback.ipynb @@ -0,0 +1,411 @@ +{ + "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 new file mode 100755 index 00000000..175d7dd5 --- /dev/null +++ b/examples/2_simple_tooling/rollback.py @@ -0,0 +1,56 @@ +#!/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 new file mode 100644 index 00000000..75711ffc --- /dev/null +++ b/examples/2_simple_tooling/validate.ipynb @@ -0,0 +1,404 @@ +{ + "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 new file mode 100755 index 00000000..04597074 --- /dev/null +++ b/examples/2_simple_tooling/validate.py @@ -0,0 +1,77 @@ +#!/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 new file mode 100755 index 00000000..e78acd91 --- /dev/null +++ b/examples/3_advanced_tooling/mate.py @@ -0,0 +1,43 @@ +#!/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/__init__.py b/examples/3_advanced_tooling/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/3_advanced_tooling/tasks/backup.py b/examples/3_advanced_tooling/tasks/backup.py new file mode 100755 index 00000000..67262c38 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/backup.py @@ -0,0 +1,37 @@ +#!/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 new file mode 100755 index 00000000..6ef6c512 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/configure.py @@ -0,0 +1,77 @@ +#!/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 new file mode 100755 index 00000000..e31dcdd9 --- /dev/null +++ b/examples/3_advanced_tooling/tasks/get_facts.py @@ -0,0 +1,21 @@ +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 new file mode 100755 index 00000000..a22bf30b --- /dev/null +++ b/examples/3_advanced_tooling/tasks/validate.py @@ -0,0 +1,49 @@ +#!/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/Vagrantfile b/examples/Vagrantfile deleted file mode 100644 index bb2bcabb..00000000 --- a/examples/Vagrantfile +++ /dev/null @@ -1,32 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : -""" -You will need the boxes: - * vEOS-4.17.5M - * JunOS - juniper/ffp-12.1X47-D20.7-packetmode - * To provision and test JunOS first you have to add the ssh vagrant ssh key into the ssh-agent. I.e.: - ssh-add /opt/vagrant/embedded/gems/gems/vagrant-`vagrant --version | awk '{ print $2 }'`/keys/vagrant -""" - -Vagrant.configure(2) do |config| - config.vbguest.auto_update = false - - config.vm.define "eos" do |eos| - eos.vm.box = "vEOS-lab-4.17.5M" - - eos.vm.network :forwarded_port, guest: 443, host: 12443, id: 'https' - - eos.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false - eos.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false - end - - config.vm.define "junos" do |junos| - junos.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" - - junos.vm.network :forwarded_port, guest: 22, host: 12203, id: 'ssh' - - junos.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false - junos.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false - end - -end diff --git a/examples/configure.py b/examples/configure.py deleted file mode 100644 index f88334cf..00000000 --- a/examples/configure.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -In this example we write a CLI tool with brigade and click to deploy configuration. -""" -import logging - -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import data, networking, text - -import click - - -def base_config(task): - """ - 1. logs all the facts, even the ones inherited from groups - 2. Creates a placeholder for device configuration - 3. Initializes some basic configuration - """ - logging.info({task.host.name: task.host.items()}) - - task.host["config"] = "" - - r = text.template_file(task=task, - template="base.j2", - path="templates/base/{nos}") - task.host["config"] += r.result - - -def configure_interfaces(task): - """ - 1. Load interface data from an external yaml file - 2. Creates interface configuration - """ - r = data.load_yaml(task=task, - file="extra_data/{host}/interfaces.yaml") - task.host["interfaces"] = r.result - - r = text.template_file(task=task, - template="interfaces.j2", - path="templates/interfaces/{nos}") - task.host["config"] += r.result - - -def deploy_config(task): - """ - 1. Load configuration into the device - 2. Prints diff - """ - r = networking.napalm_configure(task=task, - replace=False, - configuration=task.host["config"]) - - click.secho("--- {} ({})".format(task.host, r.changed), fg="blue", bold=True) - click.secho(r.diff, fg='yellow') - click.echo() - - -@click.command() -@click.option('--commit/--no-commit', default=False) -@click.option('--debug/--no-debug', default=False) -@click.argument('site') -@click.argument('role') -def deploy(commit, debug, site, role): - logging.basicConfig( - filename="log", - level=logging.DEBUG if debug else logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - ) - - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=not commit, - ) - - filtered = brigade.filter(site=site, role=role) - filtered.run(task=base_config) - filtered.run(task=configure_interfaces) - filtered.run(task=deploy_config) - - -if __name__ == "__main__": - deploy() diff --git a/examples/extra_data/leaf00.cmh/l3.yaml b/examples/extra_data/leaf00.cmh/l3.yaml new file mode 100644 index 00000000..f5fb9854 --- /dev/null +++ b/examples/extra_data/leaf00.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +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 new file mode 100644 index 00000000..f3fb1c5c --- /dev/null +++ b/examples/extra_data/leaf01.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +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 new file mode 100644 index 00000000..19c58537 --- /dev/null +++ b/examples/extra_data/spine00.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +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 new file mode 100644 index 00000000..7bdd33cc --- /dev/null +++ b/examples/extra_data/spine01.cmh/l3.yaml @@ -0,0 +1,16 @@ +--- +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/extra_data/switch00.bma/interfaces.yaml b/examples/extra_data/switch00.bma/interfaces.yaml deleted file mode 100644 index 64d0d9b6..00000000 --- a/examples/extra_data/switch00.bma/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -Ethernet1: - description: "An Interface in bma" - enabled: true -Ethernet2: - description: "Another interface in bma" - enabled: false diff --git a/examples/extra_data/switch00.cmh/interfaces.yaml b/examples/extra_data/switch00.cmh/interfaces.yaml deleted file mode 100644 index 4f0c5e0b..00000000 --- a/examples/extra_data/switch00.cmh/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -Ethernet1: - description: "An Interface in cmh" - enabled: true -Ethernet2: - description: "Another interface in cmh" - enabled: false diff --git a/examples/extra_data/switch01.bma/interfaces.yaml b/examples/extra_data/switch01.bma/interfaces.yaml deleted file mode 100644 index db67fca1..00000000 --- a/examples/extra_data/switch01.bma/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -ge-0/0/1: - description: "An Interface in bma" - enabled: true -ge-0/0/2: - description: "Another interface in bma" - enabled: false diff --git a/examples/extra_data/switch01.cmh/interfaces.yaml b/examples/extra_data/switch01.cmh/interfaces.yaml deleted file mode 100644 index e847b411..00000000 --- a/examples/extra_data/switch01.cmh/interfaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -ge-0/0/1: - description: "An Interface in cmh" - enabled: true -ge-0/0/2: - description: "Another interface in cmh" - enabled: false diff --git a/examples/get_facts_grouping.py b/examples/get_facts_grouping.py deleted file mode 100644 index f016dd96..00000000 --- a/examples/get_facts_grouping.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -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.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import networking - -import click - - -def get_facts(task, facts): - return networking.napalm_get_facts(task, facts) - - -@click.command() -@click.argument('site') -@click.argument('role') -@click.argument('facts') -def main(site, role, facts): - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=True, - ) - - filtered = brigade.filter(site=site, role=role) - result = filtered.run(task=get_facts, - facts=facts) - - for host, r in result.items(): - print(host) - print("============") - print(r.result) - print() - - -if __name__ == "__main__": - main() diff --git a/examples/get_facts_simple.py b/examples/get_facts_simple.py deleted file mode 100644 index 5cbc1fbd..00000000 --- a/examples/get_facts_simple.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -This is a simple example where we use click and brigade to build a simple CLI tool to retrieve -hosts information. -""" -from brigade.core import Brigade -from brigade.plugins.inventory.simple import SimpleInventory -from brigade.plugins.tasks import networking - -import click - - -@click.command() -@click.argument('site') -@click.argument('role') -@click.argument('facts') -def main(site, role, facts): - brigade = Brigade( - inventory=SimpleInventory("hosts.yaml", "groups.yaml"), - dry_run=True, - ) - - filtered = brigade.filter(site=site, role=role) - result = filtered.run(task=networking.napalm_get_facts, - facts=facts) - - for host, r in result.items(): - print(host) - print("============") - print(r.result) - print() - - -if __name__ == "__main__": - main() diff --git a/examples/groups.yaml b/examples/groups.yaml deleted file mode 100644 index 54ce9cd4..00000000 --- a/examples/groups.yaml +++ /dev/null @@ -1,22 +0,0 @@ ---- -all: - group: null - domain: acme.com - -bma-leaf: - group: bma - -bma-host: - group: bma - -bma: - group: all - -cmh-leaf: - group: cmh - -cmh-host: - group: cmh - -cmh: - group: all diff --git a/examples/highlighter.py b/examples/highlighter.py new file mode 100644 index 00000000..ebe31b33 --- /dev/null +++ b/examples/highlighter.py @@ -0,0 +1,34 @@ +from __future__ import print_function + +from IPython.core.magic import register_line_magic +from IPython.display import HTML + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name + + +HTML_TEMPLATE = """ +{} +""" + + +@register_line_magic +def highlight_file(filename): + lexer = get_lexer_by_name("py3") + + linenos = "inline" + + formatter = HtmlFormatter(style='default', + cssclass='pygments', + linenos=linenos) + + with open(filename) as f: + code = f.read() + + html_code = highlight(code, lexer, formatter) + css = formatter.get_style_defs() + + return HTML(HTML_TEMPLATE.format(css, html_code)) diff --git a/examples/hosts.yaml b/examples/hosts.yaml deleted file mode 100644 index 0a1b80db..00000000 --- a/examples/hosts.yaml +++ /dev/null @@ -1,64 +0,0 @@ ---- -host1.cmh: - site: cmh - role: host - group: cmh-host - nos: linux - -host2.cmh: - site: cmh - role: host - group: cmh-host - nos: linux - -switch00.cmh: - brigade_ip: 127.0.0.1 - brigade_username: vagrant - brigade_password: vagrant - napalm_port: 12443 - site: cmh - role: leaf - group: cmh-leaf - nos: eos - -switch01.cmh: - brigade_ip: 127.0.0.1 - brigade_username: vagrant - brigade_password: "" - napalm_port: 12203 - site: cmh - role: leaf - group: cmh-leaf - nos: junos - -host1.bma: - site: bma - role: host - group: bma-host - nos: linux - -host2.bma: - site: bma - role: host - group: bma-host - nos: linux - -switch00.bma: - brigade_ip: 127.0.0.1 - brigade_username: vagrant - brigade_password: vagrant - napalm_port: 12443 - site: bma - role: leaf - group: bma-leaf - nos: eos - -switch01.bma: - brigade_ip: 127.0.0.1 - brigade_username: vagrant - brigade_password: "" - napalm_port: 12203 - site: bma - role: leaf - group: bma-leaf - nos: junos diff --git a/examples/inventory/Vagrantfile b/examples/inventory/Vagrantfile new file mode 100644 index 00000000..d576a0d7 --- /dev/null +++ b/examples/inventory/Vagrantfile @@ -0,0 +1,51 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : +""" +You will need the boxes: + * vEOS-4.17.5M + * JunOS - juniper/ffp-12.1X47-D20.7-packetmode + * To provision and test JunOS first you have to add the ssh vagrant ssh key into the ssh-agent. I.e.: + ssh-add /opt/vagrant/embedded/gems/gems/vagrant-`vagrant --version | awk '{ print $2 }'`/keys/vagrant +""" + +Vagrant.configure(2) do |config| + config.vbguest.auto_update = false + + config.vm.define "spine00" do |spine00| + spine00.vm.box = "vEOS-lab-4.17.5M" + + spine00.vm.network :forwarded_port, guest: 443, host: 12444, id: 'https' + + spine00.vm.network "private_network", virtualbox__intnet: "link_1", ip: "169.254.1.11", auto_config: false + spine00.vm.network "private_network", virtualbox__intnet: "link_2", ip: "169.254.1.11", auto_config: false + end + + config.vm.define "spine01" do |spine01| + spine01.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" + + spine01.vm.network :forwarded_port, guest: 22, host: 12204, id: 'ssh' + + spine01.vm.network "private_network", virtualbox__intnet: "link_3", ip: "169.254.1.11", auto_config: false + spine01.vm.network "private_network", virtualbox__intnet: "link_4", ip: "169.254.1.11", auto_config: false + end + + + config.vm.define "leaf00" do |leaf00| + leaf00.vm.box = "vEOS-lab-4.17.5M" + + leaf00.vm.network :forwarded_port, guest: 443, host: 12443, id: 'https' + + 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 + end + + config.vm.define "leaf01" do |leaf01| + leaf01.vm.box = "juniper/ffp-12.1X47-D20.7-packetmode" + + leaf01.vm.network :forwarded_port, guest: 22, host: 12203, id: 'ssh' + + 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 + end + +end diff --git a/examples/inventory/groups.yaml b/examples/inventory/groups.yaml new file mode 100644 index 00000000..8cbbb15c --- /dev/null +++ b/examples/inventory/groups.yaml @@ -0,0 +1,13 @@ +--- +all: + domain: acme.com + +bma: + group: all + +cmh: + group: all + asn: 65000 + vlans: + 100: frontend + 200: backend diff --git a/examples/inventory/hosts.yaml b/examples/inventory/hosts.yaml new file mode 100644 index 00000000..15ccd489 --- /dev/null +++ b/examples/inventory/hosts.yaml @@ -0,0 +1,118 @@ +--- +host1.cmh: + site: cmh + role: host + group: cmh + brigade_nos: linux + type: host + +host2.cmh: + site: cmh + role: host + group: cmh + brigade_nos: linux + type: host + +spine00.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12444 + site: cmh + role: spine + group: cmh + brigade_nos: eos + type: network_device + +spine01.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: "" + brigade_network_api_port: 12204 + site: cmh + role: spine + group: cmh + brigade_nos: junos + type: network_device + +leaf00.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12443 + site: cmh + role: leaf + group: cmh + brigade_nos: eos + type: network_device + asn: 65100 + +leaf01.cmh: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: "" + brigade_network_api_port: 12203 + site: cmh + role: leaf + group: cmh + brigade_nos: junos + type: network_device + asn: 65101 + +host1.bma: + site: bma + role: host + group: bma + brigade_nos: linux + type: host + +host2.bma: + site: bma + role: host + group: bma + brigade_nos: linux + type: host + +spine00.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12444 + site: bma + role: spine + group: bma + brigade_nos: eos + type: network_device + +spine01.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: "" + brigade_network_api_port: 12204 + site: bma + role: spine + group: bma + brigade_nos: junos + type: network_device + +leaf00.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: vagrant + brigade_network_api_port: 12443 + site: bma + role: leaf + group: bma + brigade_nos: eos + type: network_device + +leaf01.bma: + brigade_host: 127.0.0.1 + brigade_username: vagrant + brigade_password: wrong_password + brigade_network_api_port: 12203 + site: bma + role: leaf + group: bma + brigade_nos: junos + type: network_device diff --git a/examples/inventory/network_diagram.graffle b/examples/inventory/network_diagram.graffle new file mode 100644 index 00000000..871fb479 Binary files /dev/null and b/examples/inventory/network_diagram.graffle differ diff --git a/examples/inventory/network_diagram.png b/examples/inventory/network_diagram.png new file mode 100644 index 00000000..064b4a05 Binary files /dev/null and b/examples/inventory/network_diagram.png differ diff --git a/examples/templates/base/eos/base.j2 b/examples/templates/eos/base.j2 similarity index 100% rename from examples/templates/base/eos/base.j2 rename to examples/templates/eos/base.j2 diff --git a/examples/templates/eos/interfaces.j2 b/examples/templates/eos/interfaces.j2 new file mode 100644 index 00000000..6aea0d99 --- /dev/null +++ b/examples/templates/eos/interfaces.j2 @@ -0,0 +1,8 @@ +{% 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 new file mode 100644 index 00000000..4d6c9506 --- /dev/null +++ b/examples/templates/eos/leaf.j2 @@ -0,0 +1,4 @@ +{% 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 new file mode 100644 index 00000000..dea73518 --- /dev/null +++ b/examples/templates/eos/routing.j2 @@ -0,0 +1,12 @@ +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/eos/spine.j2 b/examples/templates/eos/spine.j2 new file mode 100644 index 00000000..e69de29b diff --git a/examples/templates/interfaces/eos/interfaces.j2 b/examples/templates/interfaces/eos/interfaces.j2 deleted file mode 100644 index fd38a50e..00000000 --- a/examples/templates/interfaces/eos/interfaces.j2 +++ /dev/null @@ -1,6 +0,0 @@ -{% for interface, data in interfaces.items() %} -interface {{ interface }} - description {{ data.description }} - {{ "no" if data.enabled else "" }} shutdown -{% endfor %} - diff --git a/examples/templates/interfaces/junos/interfaces.j2 b/examples/templates/interfaces/junos/interfaces.j2 deleted file mode 100644 index 07b36eca..00000000 --- a/examples/templates/interfaces/junos/interfaces.j2 +++ /dev/null @@ -1,9 +0,0 @@ -interfaces { -{% for interface, data in interfaces.items() %} - {{ interface }} { - description "{{ data.description }}"; - {{ "disable;" if not data.enabled else "" }} - } -{% endfor %} -} - diff --git a/examples/templates/base/junos/base.j2 b/examples/templates/junos/base.j2 similarity index 100% rename from examples/templates/base/junos/base.j2 rename to examples/templates/junos/base.j2 diff --git a/examples/templates/junos/interfaces.j2 b/examples/templates/junos/interfaces.j2 new file mode 100644 index 00000000..1c794330 --- /dev/null +++ b/examples/templates/junos/interfaces.j2 @@ -0,0 +1,15 @@ +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 new file mode 100644 index 00000000..f2e40819 --- /dev/null +++ b/examples/templates/junos/leaf.j2 @@ -0,0 +1,8 @@ +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 new file mode 100644 index 00000000..cc231986 --- /dev/null +++ b/examples/templates/junos/routing.j2 @@ -0,0 +1,30 @@ +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/examples/templates/junos/spine.j2 b/examples/templates/junos/spine.j2 new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index 235d09d0..2efafbfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +colorama pyyaml jinja2 napalm>=2.2.0 diff --git a/tests/core/test_multithreading.py b/tests/core/test_multithreading.py index a03e4452..5c334d34 100644 --- a/tests/core/test_multithreading.py +++ b/tests/core/test_multithreading.py @@ -51,28 +51,28 @@ def test_failing_task_simple_singlethread(self, brigade): brigade.run(failing_task_simple, num_workers=1) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, Exception), v + assert isinstance(v.exception, Exception), v 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(): assert isinstance(k, str), k - assert isinstance(v, Exception), v + assert isinstance(v.exception, Exception), v 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(): assert isinstance(k, str), k - assert isinstance(v, CommandError), v + assert isinstance(v.exception, CommandError), v def test_failing_task_complex_multithread(self, brigade): with pytest.raises(BrigadeExecutionError) as e: brigade.run(failing_task_complex, num_workers=NUM_WORKERS) for k, v in e.value.result.items(): assert isinstance(k, str), k - assert isinstance(v, CommandError), v + assert isinstance(v.exception, CommandError), v def test_change_data_in_thread(self, brigade): brigade.run(change_data, num_workers=NUM_WORKERS) diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index 93a942ea..a3ac2ede 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -1,9 +1,19 @@ 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") + else: + task.run(commands.command, + command="echo {}".format(task.host)) + + def sub_task(task): - return task.run(commands.command, - command="echo {}".format(task.host)) + task.run(commands.command, + command="echo {}".format(task.host)) class Test(object): @@ -19,4 +29,30 @@ def test_sub_task(self, brigade): result = brigade.run(sub_task) assert result for h, r in result.items(): - assert h == r.stdout.strip() + assert r[0].name == "sub_task" + assert r[1].name == "command" + assert h == r[1].stdout.strip() + + def test_skip_failed_host(self, brigade): + result = brigade.run(task_fails_for_some, raise_on_error=False) + assert result.failed + assert not result.skipped + for h, r in result.items(): + if h == "dev3.group_2": + assert r.failed + else: + assert not r.failed + assert h == r[1].stdout.strip() + + 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 + else: + assert not r.skipped + assert h == r[1].stdout.strip() + + # let's reset it + brigade.data.failed_hosts = set() diff --git a/tests/inventory_data/nsot/nsot.sqlite3 b/tests/inventory_data/nsot/nsot.sqlite3 index 5fd8479b..43db53f9 100644 Binary files a/tests/inventory_data/nsot/nsot.sqlite3 and b/tests/inventory_data/nsot/nsot.sqlite3 differ diff --git a/tests/plugins/tasks/commands/test_command.py b/tests/plugins/tasks/commands/test_command.py index 312d559e..7ed7e6cd 100644 --- a/tests/plugins/tasks/commands/test_command.py +++ b/tests/plugins/tasks/commands/test_command.py @@ -18,13 +18,13 @@ def test_command_error(self, brigade): brigade.run(commands.command, command="ech") assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, OSError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, OSError) 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 exc in e.value.failed_hosts.values(): - assert isinstance(exc, CommandError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, CommandError) diff --git a/tests/plugins/tasks/commands/test_remote_command.py b/tests/plugins/tasks/commands/test_remote_command.py index 63d3b85f..9486745f 100644 --- a/tests/plugins/tasks/commands/test_remote_command.py +++ b/tests/plugins/tasks/commands/test_remote_command.py @@ -18,5 +18,5 @@ def test_remote_command_error_generic(self, brigade): brigade.run(commands.remote_command, command="ls /asdadsd") assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, CommandError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, CommandError) diff --git a/tests/plugins/tasks/data/test_data/simple.json b/tests/plugins/tasks/data/test_data/simple.json index 370fa8df..3a17c39d 100644 --- a/tests/plugins/tasks/data/test_data/simple.json +++ b/tests/plugins/tasks/data/test_data/simple.json @@ -4,4 +4,4 @@ "dhcp", "dns" ] -} \ No newline at end of file +} diff --git a/tests/plugins/tasks/data/test_load_json.py b/tests/plugins/tasks/data/test_load_json.py index 03e767ec..e53d778b 100644 --- a/tests/plugins/tasks/data/test_load_json.py +++ b/tests/plugins/tasks/data/test_load_json.py @@ -29,8 +29,8 @@ def test_load_json_error_broken_file(self, brigade): brigade.run(data.load_json, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) def test_load_json_error_missing_file(self, brigade): test_file = '{}/missing.json'.format(data_dir) @@ -43,5 +43,5 @@ def test_load_json_error_missing_file(self, brigade): brigade.run(data.load_json, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, not_found) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, not_found) diff --git a/tests/plugins/tasks/data/test_load_yaml.py b/tests/plugins/tasks/data/test_load_yaml.py index 15fbde6b..4421cb96 100644 --- a/tests/plugins/tasks/data/test_load_yaml.py +++ b/tests/plugins/tasks/data/test_load_yaml.py @@ -33,8 +33,8 @@ def test_load_yaml_error_broken_file(self, brigade): brigade.run(data.load_yaml, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ScannerError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ScannerError) def test_load_yaml_error_missing_file(self, brigade): test_file = '{}/missing.yaml'.format(data_dir) @@ -48,5 +48,5 @@ def test_load_yaml_error_missing_file(self, brigade): brigade.run(data.load_yaml, file=test_file) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, not_found) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, not_found) diff --git a/tests/plugins/tasks/files/test_write.py b/tests/plugins/tasks/files/test_write.py new file mode 100644 index 00000000..79584369 --- /dev/null +++ b/tests/plugins/tasks/files/test_write.py @@ -0,0 +1,158 @@ +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/networking/data/validate_error.yaml b/tests/plugins/tasks/networking/data/validate_error.yaml new file mode 100644 index 00000000..9ecfe461 --- /dev/null +++ b/tests/plugins/tasks/networking/data/validate_error.yaml @@ -0,0 +1,6 @@ +--- +- get_facts: + os_version: 4.15.5M-3054042.4155M +- get_interfaces: + Ethernet1: + description: "unset" diff --git a/tests/plugins/tasks/networking/data/validate_ok.yaml b/tests/plugins/tasks/networking/data/validate_ok.yaml new file mode 100644 index 00000000..68d68381 --- /dev/null +++ b/tests/plugins/tasks/networking/data/validate_ok.yaml @@ -0,0 +1,6 @@ +--- +- get_facts: + os_version: 4.15.5M-3054042.4155M +- get_interfaces: + Ethernet1: + description: "" diff --git a/tests/plugins/tasks/networking/test_napalm_cli.py b/tests/plugins/tasks/networking/test_napalm_cli.py index 4eec82b5..7bc712ff 100644 --- a/tests/plugins/tasks/networking/test_napalm_cli.py +++ b/tests/plugins/tasks/networking/test_napalm_cli.py @@ -34,6 +34,6 @@ def test_napalm_cli(self, brigade): # "show interfacesa"], # optional_args=opt) # assert len(e.value.failed_hosts) - # for exc in e.value.failed_hosts.values(): - # assert isinstance(exc, exceptions.CommandErrorException) + # 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 7d9cf0c7..fcbe0e9d 100644 --- a/tests/plugins/tasks/networking/test_napalm_configure.py +++ b/tests/plugins/tasks/networking/test_napalm_configure.py @@ -55,5 +55,5 @@ def test_napalm_configure_change_error(self, brigade): with pytest.raises(BrigadeExecutionError) as e: d.run(networking.napalm_configure, configuration=configuration) assert len(e.value.failed_hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, exceptions.MergeConfigException) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, exceptions.MergeConfigException) diff --git a/tests/plugins/tasks/networking/test_napalm_get.py b/tests/plugins/tasks/networking/test_napalm_get.py index d3520bb1..459e3933 100644 --- a/tests/plugins/tasks/networking/test_napalm_get.py +++ b/tests/plugins/tasks/networking/test_napalm_get.py @@ -33,5 +33,5 @@ def test_napalm_getters_error(self, brigade): getters=["facts", "interfaces"]) assert len(e.value.failed_hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, KeyError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, KeyError) diff --git a/tests/plugins/tasks/networking/test_napalm_validate.py b/tests/plugins/tasks/networking/test_napalm_validate.py new file mode 100644 index 00000000..5a6c3b0a --- /dev/null +++ b/tests/plugins/tasks/networking/test_napalm_validate.py @@ -0,0 +1,51 @@ +import os + +from brigade.plugins.tasks import connections, networking + + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + + +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") + 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") + assert result + for h, r in result.items(): + assert not r.failed + assert not r.result["complies"] + + 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": ""}}} + ] + + result = d.run(networking.napalm_validate, + validation_source=validation_dict) + + assert result + for h, r in result.items(): + assert not r.failed + assert r.result["complies"] diff --git a/tests/plugins/tasks/networking/test_tcp_ping.py b/tests/plugins/tasks/networking/test_tcp_ping.py index 0699e775..eb42993a 100644 --- a/tests/plugins/tasks/networking/test_tcp_ping.py +++ b/tests/plugins/tasks/networking/test_tcp_ping.py @@ -37,16 +37,16 @@ def test_tcp_ping_invalid_port(self, brigade): brigade.run(networking.tcp_ping, ports='web') assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) 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 exc in e.value.failed_hosts.values(): - assert isinstance(exc, ValueError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, ValueError) def test_tcp_ping_external_hosts(): diff --git a/tests/plugins/tasks/text/test_template_file.py b/tests/plugins/tasks/text/test_template_file.py index ba17d60e..b7aa0070 100644 --- a/tests/plugins/tasks/text/test_template_file.py +++ b/tests/plugins/tasks/text/test_template_file.py @@ -33,5 +33,5 @@ def test_template_file_error_broken_file(self, brigade): template='broken.j2', path=data_dir) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, TemplateSyntaxError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, TemplateSyntaxError) diff --git a/tests/plugins/tasks/text/test_template_string.py b/tests/plugins/tasks/text/test_template_string.py index 52aef9b9..6919517a 100644 --- a/tests/plugins/tasks/text/test_template_string.py +++ b/tests/plugins/tasks/text/test_template_string.py @@ -49,5 +49,5 @@ def test_template_string_error_broken_string(self, brigade): brigade.run(text.template_string, template=broken_j2) assert len(e.value.failed_hosts) == len(brigade.inventory.hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, TemplateSyntaxError) + for result in e.value.failed_hosts.values(): + assert isinstance(result.exception, TemplateSyntaxError)