diff --git a/.gitignore b/.gitignore index 73d0e20b..8e0b0df3 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/ref/configuration/generated/*.rst # PyBuilder target/ @@ -95,8 +96,4 @@ tags .vars output/ -demo/log - -tests.log - .DS_Store diff --git a/.travis.yml b/.travis.yml index 7bbf31f5..4c2dfcd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,8 @@ services: - docker addons: - apt: - packages: - - sshpass + apt_packages: + - pandoc language: python python: diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..53536625 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: start_nsot +start_nsot: + docker run -v $(PWD)/tests/inventory_data/nsot/nsot.sqlite3:/nsot.sqlite3 -p 8990:8990 -d --name=nsot nsot/nsot start --noinput + +.PHONY: stop_nsot +stop_nsot: + docker rm -f nsot diff --git a/README.md b/README.md index f1114d2e..72344946 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ Brigade ======= -See [docs](https://brigade.readthedocs.io) and [demo/get_facts_simple.py](demo/get_facts_simple.py), [demo/get_facts_role.py](demo/get_facts_role.py), and [demo/configure.py](demo/configure.py). +See [docs](https://brigade.readthedocs.io). diff --git a/brigade/__init__.py b/brigade/__init__.py index e69de29b..4e5de560 100644 --- a/brigade/__init__.py +++ b/brigade/__init__.py @@ -0,0 +1,6 @@ +import pkg_resources + +try: + __version__ = pkg_resources.get_distribution('brigade').version +except pkg_resources.DistributionNotFound: + __version__ = "Not installed" diff --git a/brigade/core/__init__.py b/brigade/core/__init__.py index 7cf5af0a..a02ea1ce 100644 --- a/brigade/core/__init__.py +++ b/brigade/core/__init__.py @@ -1,9 +1,12 @@ import logging +import logging.config import sys import traceback from multiprocessing.dummy import Pool -from brigade.core.task import AggregatedResult, Task +from brigade.core.configuration import Config +from brigade.core.task import AggregatedResult, Result, Task +from brigade.plugins.tasks import connections if sys.version_info.major == 2: @@ -31,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): @@ -41,32 +54,74 @@ 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 - num_workers(``int``): How many hosts run in parallel - raise_on_error (``bool``): If set to ``True``, :meth:`run` method of will - raise an exception if at least a host failed. + config (:obj:`brigade.core.configuration.Config`): Configuration object + config_file (``str``): Path to Yaml configuration file + available_connections (``dict``): dict of connection types that will be made available. + Defaults to :obj:`brigade.plugins.tasks.connections.available_connections` 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 - num_workers(``int``): How many hosts run in parallel - raise_on_error (``bool``): If set to ``True``, :meth:`run` method of will - raise an exception if at least a host failed. + config (:obj:`brigade.core.configuration.Config`): Configuration parameters + available_connections (``dict``): dict of connection types are available """ - def __init__(self, inventory, dry_run, num_workers=20, raise_on_error=True): + def __init__(self, inventory, dry_run, config=None, config_file=None, + available_connections=None, logger=None, data=None): + self.logger = logger or logging.getLogger("brigade") + + self.data = data or Data() self.inventory = inventory + self.inventory.brigade = self self.dry_run = dry_run - self.num_workers = num_workers - self.raise_on_error = raise_on_error + if config_file: + self.config = Config(config_file=config_file) + 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): """ @@ -79,39 +134,28 @@ def filter(self, **kwargs): b.inventory = self.inventory.filter(**kwargs) return b - def _run_serial(self, task, **kwargs): - t = Task(task, **kwargs) - result = AggregatedResult() + def _run_serial(self, task, dry_run, **kwargs): + 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=self.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, **kwargs): - result = AggregatedResult() + def _run_parallel(self, task, num_workers, dry_run, **kwargs): + result = AggregatedResult(kwargs.get("name") or task.__name__) pool = Pool(processes=num_workers) - result_pool = [pool.apply_async(run_task, args=(h, self, 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] = exc - 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, **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. @@ -119,32 +163,46 @@ def run(self, task, num_workers=None, **kwargs): task (``callable``): function or callable that will be run against each device in the inventory num_workers(``int``): Override for how many hosts to run in paralell for this task + dry_run(``bool``): Whether if we are testing the changes or not + raise_on_error (``bool``): Override raise_on_error behavior **kwargs: additional argument to pass to ``task`` when calling it Raises: - :obj:`brigade.core.exceptions.BrigadeExceptionError`: if at least a task fails - and self.raise_on_error is set to ``True`` + :obj:`brigade.core.exceptions.BrigadeExecutionError`: if at least a task fails + and self.config.raise_on_error is set to ``True`` Returns: :obj:`brigade.core.task.AggregatedResult`: results of each execution """ - num_workers = num_workers or self.num_workers + 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, **kwargs) + result = self._run_serial(task, dry_run, **kwargs) else: - result = self._run_parallel(task, num_workers, **kwargs) + result = self._run_parallel(task, num_workers, dry_run, **kwargs) - if self.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, task): +def run_task(host, brigade, dry_run, task): + logger = logging.getLogger("brigade") try: - logger.debug("{}: running task {}".format(host.name, task)) - r = task._start(host=host, brigade=brigade, dry_run=brigade.dry_run) - return host.name, r, None, None + logger.info("{}: {}: running task".format(host.name, task.name)) + r = task._start(host=host, brigade=brigade, dry_run=dry_run) 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/configuration.py b/brigade/core/configuration.py new file mode 100644 index 00000000..61d8c042 --- /dev/null +++ b/brigade/core/configuration.py @@ -0,0 +1,67 @@ +import ast +import os + + +import yaml + + +CONF = { + 'num_workers': { + 'description': 'Number of Brigade worker processes that are run at the same time, ' + 'configuration can be overridden on individual tasks by using the ' + '`num_workers` argument to (:obj:`brigade.core.Brigade.run`)', + 'type': 'int', + 'default': 20, + }, + 'raise_on_error': { + 'description': "If set to ``True``, (:obj:`brigade.core.Brigade.run`) method of will raise " + "an exception if at least a host failed.", + 'type': 'bool', + 'default': True, + }, + 'ssh_config_file': { + 'description': 'User ssh_config_file', + 'type': 'str', + 'default': os.path.join(os.path.expanduser("~"), ".ssh", "config"), + 'default_doc': '~/.ssh/config' + }, +} + +types = { + 'int': int, + 'str': str, +} + + +class Config: + """ + This object handles the configuration of Brigade. + + Arguments: + config_file(``str``): Yaml configuration file. + """ + + def __init__(self, config_file=None, **kwargs): + + if config_file: + with open(config_file, 'r') as f: + c = yaml.load(f.read()) + else: + c = {} + + self._assign_properties(c) + + for k, v in kwargs.items(): + setattr(self, k, v) + + def _assign_properties(self, c): + + for p in CONF: + env = CONF[p].get('env') or 'BRIGADE_' + p.upper() + v = os.environ.get(env) or c.get(p) + v = v if v is not None else CONF[p]['default'] + if CONF[p]['type'] == 'bool': + v = ast.literal_eval(str(v).title()) + else: + v = types[CONF[p]['type']](v) + setattr(self, p, v) 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 44738ef4..67514857 100644 --- a/brigade/core/helpers/__init__.py +++ b/brigade/core/helpers/__init__.py @@ -2,12 +2,11 @@ def merge_two_dicts(x, y): try: z = x.copy() except AttributeError: - z = x.items() + z = dict(x) z.update(y) return z def format_string(text, task, **kwargs): - merged = merge_two_dicts(task.host.items(), task.brigade.inventory.data) return text.format(host=task.host, - **merge_two_dicts(merged, kwargs)) + **merge_two_dicts(task.host.items(), kwargs)) diff --git a/brigade/core/inventory.py b/brigade/core/inventory.py index 41603d86..da7af8fe 100644 --- a/brigade/core/inventory.py +++ b/brigade/core/inventory.py @@ -1,10 +1,7 @@ import getpass -import os from brigade.core import helpers -import paramiko - class Host(object): """ @@ -13,12 +10,14 @@ class Host(object): Arguments: name (str): Name of the host group (:obj:`Group`, optional): Group the host belongs to + brigade (:obj:`brigade.core.Brigade`): Reference to the parent brigade object **kwargs: Host data Attributes: name (str): Name of the host group (:obj:`Group`): Group the host belongs to data (dict): data about the device + connections (``dict``): Already established connections Note: @@ -58,11 +57,13 @@ class Host(object): * ``my_host.group.group.data["domain"]`` will return ``acme.com`` """ - def __init__(self, name, group=None, **kwargs): + def __init__(self, name, group=None, brigade=None, **kwargs): + self.brigade = brigade self.name = name self.group = group self.data = {} self.data["name"] = name + self.connections = {} if isinstance(group, str): self.data["group"] = group @@ -72,19 +73,17 @@ def __init__(self, name, group=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: @@ -127,11 +126,19 @@ 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): + """Reference to the parent :obj:`brigade.core.Brigade` object""" + return self._brigade + + @brigade.setter + def brigade(self, value): + # If it's already set we don't want to set it again + # because we may lose valuable information + if not getattr(self, "_brigade", None): + self._brigade = value @property def host(self): @@ -171,48 +178,39 @@ def nos(self): """Network OS the device is running. Defaults to ``brigade_nos``.""" return self.get("brigade_nos") - @property - def ssh_connection(self): - """Reusable :obj:`paramiko.client.SSHClient`.""" - if hasattr(self, "_ssh_connection"): - return self._ssh_connection - - # TODO configurable - ssh_config_file = os.path.join(os.path.expanduser("~"), ".ssh", "config") - - client = paramiko.SSHClient() - client._policy = paramiko.WarningPolicy() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - ssh_config = paramiko.SSHConfig() - if os.path.exists(ssh_config_file): - with open(ssh_config_file) as f: - ssh_config.parse(f) - - parameters = { - "hostname": self.host, - "username": self.username, - "password": self.password, - "port": self.ssh_port, - } + def get_connection(self, connection): + """ + The function of this method is twofold: - user_config = ssh_config.lookup(self.host) - for k in ('hostname', 'username', 'port'): - if k in user_config: - parameters[k] = user_config[k] + 1. If an existing connection is already established for the given type return it + 2. If non exists, establish a new connection of that type with default parameters + and return it - if 'proxycommand' in user_config: - parameters['sock'] = paramiko.ProxyCommand(user_config['proxycommand']) + Raises: + AttributeError: if it's unknown how to establish a connection for the given + type - # TODO configurable - # if ssh_key_file: - # parameters['key_filename'] = ssh_key_file - if 'identityfile' in user_config: - parameters['key_filename'] = user_config['identityfile'] + Arguments: + connection_name (str): Name of the connection, for instance, netmiko, paramiko, + napalm... - client.connect(**parameters) - self._ssh_connection = client - return client + Returns: + An already established connection of type ``connection`` + """ + if connection not in self.connections: + try: + conn_task = self.brigade.available_connections[connection] + except KeyError: + raise AttributeError("not sure how to establish a connection for {}".format( + connection)) + # We use `filter(name=self.name)` to call the connection task for only + # the given host. We also have to set `num_workers=1` because chances are + # we are already inside a thread + # Task should establish a connection and populate self.connection[connection] + r = self.brigade.filter(name=self.name).run(conn_task, num_workers=1) + if r[self.name].exception: + raise r[self.name].exception + return self.connections[connection] class Group(Host): @@ -229,20 +227,23 @@ class Inventory(object): representing the host data. groups (dict): keys are group names and values are either :obj:`Group` or a dict representing the group data. + transform_function (callable): we will call this function for each host. This is useful + to manipulate host data and make it more consumable. For instance, if your inventory + has a "user" attribute you could use this function to map it to "brigade_user" Attributes: hosts (dict): keys are hostnames and values are :obj:`Host`. groups (dict): keys are group names and the values are :obj:`Group`. """ - def __init__(self, hosts, groups=None, data=None, host_data=None): - self.data = data or {} + def __init__(self, hosts, groups=None, transform_function=None, brigade=None): + self._brigade = brigade groups = groups or {} self.groups = {} for n, g in groups.items(): if isinstance(g, dict): - g = Group(name=n, **g) + g = Group(name=n, brigade=brigade, **g) self.groups[n] = g for g in self.groups.values(): @@ -252,7 +253,11 @@ def __init__(self, hosts, groups=None, data=None, host_data=None): self.hosts = {} for n, h in hosts.items(): if isinstance(h, dict): - h = Host(name=n, **h) + h = Host(name=n, brigade=brigade, **h) + + if transform_function: + transform_function(h) + if h.group is not None and not isinstance(h.group, Group): h.group = self.groups[h.group] self.hosts[n] = h @@ -286,7 +291,23 @@ def filter(self, filter_func=None, **kwargs): else: filtered = {n: h for n, h in self.hosts.items() if all(h.get(k) == v for k, v in kwargs.items())} - return Inventory(hosts=filtered, groups=self.groups, data=self.data) + return Inventory(hosts=filtered, groups=self.groups, brigade=self.brigade) def __len__(self): return self.hosts.__len__() + + @property + def brigade(self): + """Reference to the parent :obj:`brigade.core.Brigade` object""" + return self._brigade + + @brigade.setter + def brigade(self, value): + if not getattr(self, "_brigade", None): + self._brigade = value + + for h in self.hosts.values(): + h.brigade = value + + for g in self.groups.values(): + g.brigade = value diff --git a/brigade/core/task.py b/brigade/core/task.py index d564b78b..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,42 +28,89 @@ 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__ + 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: - def _start(self, host, brigade, dry_run): - self.host = host - self.brigade = brigade - self.dry_run = dry_run - return self.task(self, **self.params) + def grouped_tasks(task): + task.run(my_first_task) + task.run(my_second_task) + + brigade.run(grouped_tasks) + + This method will ensure the subtask is run only for the host in the current thread. + """ + if not self.host or not self.brigade: + msg = ("You have to call this after setting host and brigade attributes. ", + "You probably called this from outside a nested task") + raise Exception(msg) + r = Task(task, **kwargs)._start(self.host, self.brigade, dry_run, sub_task=True) + + if isinstance(r, MultiResult): + self.results.extend(r) + else: + self.results.append(r) + 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) @@ -70,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/tests/tasks/__init__.py b/brigade/plugins/functions/__init__.py similarity index 100% rename from tests/tasks/__init__.py rename to brigade/plugins/functions/__init__.py 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/inventory/nsot.py b/brigade/plugins/inventory/nsot.py new file mode 100644 index 00000000..b0d2ce3c --- /dev/null +++ b/brigade/plugins/inventory/nsot.py @@ -0,0 +1,52 @@ +import os +from builtins import super + +from brigade.core.inventory import Inventory + +import requests + + +class NSOTInventory(Inventory): + """ + Inventory plugin that uses `nsot `_ as backend. + + Note: + An extra attribute ``site`` will be assigned to the host. The value will be + the name of the site the host belongs to. + + Environment Variables: + * ``NSOT_URL``: URL to nsot's API (defaults to ``http://localhost:8990/api``) + * ``NSOT_EMAIL``: email for authentication (defaults to admin@acme.com) + + Arguments: + flatten_attributes (bool): Assign host attributes to the root object. Useful + for filtering hosts. + """ + + def __init__(self, flatten_attributes=True, **kwargs): + NSOT_URL = os.environ.get('NSOT_URL', 'http://localhost:8990/api') + NSOT_EMAIL = os.environ.get('NSOT_EMAIL', 'admin@acme.com') + + headers = {'X-NSoT-Email': NSOT_EMAIL} + devices = requests.get('{}/devices'.format(NSOT_URL), headers=headers).json() + sites = requests.get('{}/sites'.format(NSOT_URL), headers=headers).json() + interfaces = requests.get('{}/interfaces'.format(NSOT_URL), headers=headers).json() + + # We resolve site_id and assign "site" variable with the name of the site + for d in devices: + d['site'] = sites[d['site_id'] - 1]['name'] + d['interfaces'] = {} + + if flatten_attributes: + # We assign attributes to the root + for k, v in d.pop('attributes').items(): + d[k] = v + + # We assign the interfaces to the hosts + for i in interfaces: + devices[i['device'] - 1]['interfaces'][i['name']] = i + + # Finally the inventory expects a dict of hosts where the key is the hostname + devices = {d['hostname']: d for d in devices} + + super().__init__(devices, None, **kwargs) diff --git a/brigade/plugins/inventory/simple.py b/brigade/plugins/inventory/simple.py index a6ab05cb..e04e733e 100644 --- a/brigade/plugins/inventory/simple.py +++ b/brigade/plugins/inventory/simple.py @@ -103,7 +103,7 @@ class SimpleInventory(Inventory): group: all """ - def __init__(self, host_file, group_file=None): + def __init__(self, host_file, group_file=None, **kwargs): with open(host_file, "r") as f: hosts = yaml.load(f.read()) @@ -113,4 +113,4 @@ def __init__(self, host_file, group_file=None): else: groups = {} - super().__init__(hosts, groups) + super().__init__(hosts, groups, **kwargs) 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 7b241e70..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 @@ -22,7 +17,7 @@ def remote_command(task, command): Raises: :obj:`brigade.core.exceptions.CommandError`: when there is a command error """ - client = task.host.ssh_connection + client = task.host.get_connection("paramiko") chan = client.get_transport().open_session() chan.exec_command(command) diff --git a/brigade/plugins/tasks/connections/__init__.py b/brigade/plugins/tasks/connections/__init__.py new file mode 100644 index 00000000..d504f6e7 --- /dev/null +++ b/brigade/plugins/tasks/connections/__init__.py @@ -0,0 +1,16 @@ +from .napalm_connection import napalm_connection +from .netmiko_connection import netmiko_connection +from .paramiko_connection import paramiko_connection + + +available_connections = { + "napalm": napalm_connection, + "netmiko": netmiko_connection, + "paramiko": paramiko_connection, +} + +__all__ = ( + "napalm_connection", + "netmiko_connection", + "paramiko_connection", +) diff --git a/brigade/plugins/tasks/connections/napalm_connection.py b/brigade/plugins/tasks/connections/napalm_connection.py new file mode 100644 index 00000000..607f5533 --- /dev/null +++ b/brigade/plugins/tasks/connections/napalm_connection.py @@ -0,0 +1,31 @@ +from napalm import get_network_driver + + +def napalm_connection(task=None, timeout=60, optional_args=None): + """ + This tasks connects to the device using the NAPALM driver and sets the + relevant connection. + + Arguments: + timeout (int, optional): defaults to 60 + optional_args (dict, optional): defaults to ``{"port": task.host["network_api_port"]}`` + + Inventory: + napalm_options: maps directly to ``optional_args`` when establishing the connection + network_api_port: maps to ``optional_args["port"]`` + """ + host = task.host + + parameters = { + "hostname": host.host, + "username": host.username, + "password": host.password, + "timeout": timeout, + "optional_args": optional_args or host.get("napalm_options", {}), + } + if "port" not in parameters["optional_args"] and host.network_api_port: + parameters["optional_args"]["port"] = host.network_api_port + network_driver = get_network_driver(host.nos) + + host.connections["napalm"] = network_driver(**parameters) + host.connections["napalm"].open() diff --git a/brigade/plugins/tasks/connections/netmiko_connection.py b/brigade/plugins/tasks/connections/netmiko_connection.py new file mode 100644 index 00000000..e4ba0b46 --- /dev/null +++ b/brigade/plugins/tasks/connections/netmiko_connection.py @@ -0,0 +1,31 @@ +from netmiko import ConnectHandler + +napalm_to_netmiko_map = { + 'ios': 'cisco_ios', + 'nxos': 'cisco_nxos', + 'eos': 'arista_eos', + 'junos': 'juniper_junos', + 'iosxr': 'cisco_iosxr' +} + + +def netmiko_connection(task=None, **netmiko_args): + """Connect to the host using Netmiko and set the relevant connection in the connection map. + + Arguments: + **netmiko_args: All supported Netmiko ConnectHandler arguments + """ + host = task.host + parameters = { + "host": host.host, + "username": host.username, + "password": host.password, + "port": host.ssh_port + } + if host.nos is not None: + # Look device_type up in corresponding map, if no entry return the host.nos unmodified + device_type = napalm_to_netmiko_map.get(host.nos, host.nos) + parameters['device_type'] = device_type + + parameters.update(**netmiko_args) + host.connections["netmiko"] = ConnectHandler(**parameters) diff --git a/brigade/plugins/tasks/connections/paramiko_connection.py b/brigade/plugins/tasks/connections/paramiko_connection.py new file mode 100644 index 00000000..6486a1ca --- /dev/null +++ b/brigade/plugins/tasks/connections/paramiko_connection.py @@ -0,0 +1,45 @@ +import os + +import paramiko + + +def paramiko_connection(task=None): + """ + This tasks connects to the device with paramiko to the device and sets the + relevant connection. + """ + host = task.host + + client = paramiko.SSHClient() + client._policy = paramiko.WarningPolicy() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + ssh_config = paramiko.SSHConfig() + ssh_config_file = task.brigade.config.ssh_config_file + if os.path.exists(ssh_config_file): + with open(ssh_config_file) as f: + ssh_config.parse(f) + + parameters = { + "hostname": host.host, + "username": host.username, + "password": host.password, + "port": host.ssh_port, + } + + user_config = ssh_config.lookup(host.host) + for k in ('hostname', 'username', 'port'): + if k in user_config: + parameters[k] = user_config[k] + + if 'proxycommand' in user_config: + parameters['sock'] = paramiko.ProxyCommand(user_config['proxycommand']) + + # TODO configurable + # if ssh_key_file: + # parameters['key_filename'] = ssh_key_file + if 'identityfile' in user_config: + parameters['key_filename'] = user_config['identityfile'] + + client.connect(**parameters) + host.connections["paramiko"] = client 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 d32608a7..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() @@ -130,7 +126,7 @@ def sftp(task, src, dst, action): "put": put, "get": get, } - client = task.host.ssh_connection + client = task.host.get_connection("paramiko") scp_client = SCPClient(client.get_transport()) sftp_client = paramiko.SFTPClient.from_transport(client.get_transport()) files_changed = actions[action](task, scp_client, sftp_client, src, dst) 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 0dd80984..59e33ed9 100644 --- a/brigade/plugins/tasks/networking/__init__.py +++ b/brigade/plugins/tasks/networking/__init__.py @@ -1,13 +1,15 @@ from .napalm_cli import napalm_cli from .napalm_configure import napalm_configure from .napalm_get import napalm_get -from .netmiko_run import netmiko_run +from .napalm_validate import napalm_validate +from .netmiko_send_command import netmiko_send_command from .tcp_ping import tcp_ping __all__ = ( "napalm_cli", "napalm_configure", "napalm_get", - "netmiko_run", + "napalm_validate", + "netmiko_send_command", "tcp_ping", ) diff --git a/brigade/plugins/tasks/networking/napalm_cli.py b/brigade/plugins/tasks/networking/napalm_cli.py index 5e805627..59879ac5 100644 --- a/brigade/plugins/tasks/networking/napalm_cli.py +++ b/brigade/plugins/tasks/networking/napalm_cli.py @@ -1,33 +1,14 @@ from brigade.core.task import Result -from napalm import get_network_driver - -def napalm_cli(task, commands, timeout=60, optional_args=None): +def napalm_cli(task, commands): """ Run commands on remote devices using napalm - Arguments: - commands (list): list of commands to execute on the device - timeout (int, optional): defaults to 60 - optional_args (dict, optional): defaults to ``{"port": task.host["napalm_port"]}`` - - Returns: :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the result of the commands """ - parameters = { - "hostname": task.host.host, - "username": task.host.username, - "password": task.host.password, - "timeout": timeout, - "optional_args": optional_args or {}, - } - if "port" not in parameters["optional_args"] and task.host.network_api_port: - parameters["optional_args"]["port"] = task.host.network_api_port - network_driver = get_network_driver(task.host.nos) - - with network_driver(**parameters) as device: - result = device.cli(commands) + device = task.host.get_connection("napalm") + result = device.cli(commands) return Result(host=task.host, result=result) diff --git a/brigade/plugins/tasks/networking/napalm_configure.py b/brigade/plugins/tasks/networking/napalm_configure.py index a2f98cd1..663c8404 100644 --- a/brigade/plugins/tasks/networking/napalm_configure.py +++ b/brigade/plugins/tasks/networking/napalm_configure.py @@ -1,44 +1,31 @@ +from brigade.core.helpers import format_string from brigade.core.task import Result -from napalm import get_network_driver - -def napalm_configure(task, configuration, replace=False, timeout=60, optional_args=None): +def napalm_configure(task, filename=None, configuration=None, replace=False): """ Loads configuration into a network devices using napalm Arguments: configuration (str): configuration to load into the device replace (bool): whether to replace or merge the configuration - timeout (int, optional): defaults to 60 - optional_args (dict, optional): defaults to ``{"port": task.host["napalm_port"]}`` - Returns: :obj:`brigade.core.task.Result`: * changed (``bool``): whether if the task is changing the system or not * diff (``string``): change in the system """ - parameters = { - "hostname": task.host.host, - "username": task.host.username, - "password": task.host.password, - "timeout": timeout, - "optional_args": optional_args or {}, - } - if "port" not in parameters["optional_args"] and task.host.network_api_port: - parameters["optional_args"]["port"] = task.host.network_api_port - network_driver = get_network_driver(task.host.nos) - - with network_driver(**parameters) as device: - if replace: - device.load_replace_candidate(config=configuration) - else: - device.load_merge_candidate(config=configuration) - diff = device.compare_config() - - if task.dry_run: - device.discard_config() - else: - device.commit_config() + device = task.host.get_connection("napalm") + filename = format_string(filename, task, **task.host) if filename is not None else None + + if replace: + device.load_replace_candidate(filename=filename, config=configuration) + else: + device.load_merge_candidate(filename=filename, config=configuration) + diff = device.compare_config() + + 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_get.py b/brigade/plugins/tasks/networking/napalm_get.py index e63c5752..f90564d9 100644 --- a/brigade/plugins/tasks/networking/napalm_get.py +++ b/brigade/plugins/tasks/networking/napalm_get.py @@ -1,45 +1,26 @@ from brigade.core.task import Result -from napalm import get_network_driver - -def napalm_get(task, getters, timeout=60, optional_args=None): +def napalm_get(task, getters): """ Gather information from network devices using napalm Arguments: getters (list of str): getters to use - hostname (string, optional): defaults to ``brigade_ip`` - username (string, optional): defaults to ``brigade_username`` - password (string, optional): defaults to ``brigade_password`` - driver (string, optional): defaults to ``nos`` - timeout (int, optional): defaults to 60 - optional_args (dict, optional): defaults to ``{"port": task.host["napalm_port"]}`` Returns: :obj:`brigade.core.task.Result`: * result (``dict``): dictionary with the result of the getter """ - parameters = { - "hostname": task.host.host, - "username": task.host.username, - "password": task.host.password, - "timeout": timeout, - "optional_args": optional_args or {}, - } - if "port" not in parameters["optional_args"] and task.host.network_api_port: - parameters["optional_args"]["port"] = task.host.network_api_port - network_driver = get_network_driver(task.host.nos) + device = task.host.get_connection("napalm") - if not isinstance(getters, list): + if isinstance(getters, str): getters = [getters] - with network_driver(**parameters) as device: - result = {} - for g in getters: - if not g.startswith("get_"): - getter = "get_{}".format(g) - method = getattr(device, getter) - result[g] = method() + result = {} + for g in getters: + getter = g if g.startswith("get_") else "get_{}".format(g) + method = getattr(device, getter) + result[g] = method() return Result(host=task.host, result=result) 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/networking/netmiko_run.py b/brigade/plugins/tasks/networking/netmiko_run.py deleted file mode 100644 index 9a6315a4..00000000 --- a/brigade/plugins/tasks/networking/netmiko_run.py +++ /dev/null @@ -1,41 +0,0 @@ -from brigade.core.task import Result - -from netmiko import ConnectHandler - -napalm_to_netmiko_map = { - 'ios': 'cisco_ios', - 'nxos': 'cisco_nxos', - 'eos': 'arista_eos', - 'junos': 'juniper_junos', - 'iosxr': 'cisco_iosxr' -} - - -def netmiko_run(task, method, netmiko_dict=None, **kwargs): - """ - Execute any Netmiko method from connection class (BaseConnection class and children). - - Arguments: - method(str): Netmiko method to use - netmiko_dict (dict, optional): Additional arguments to pass to Netmiko ConnectHandler, \ - defaults to None - - Returns: - :obj:`brigade.core.task.Result`: - * result (``dict``): dictionary with the result of the getter - """ - parameters = { - "ip": task.host.host, - "username": task.host.username, - "password": task.host.password, - "port": task.host.ssh_port, - } - parameters.update(netmiko_dict or {}) - device_type = task.host.nos - # Convert to netmiko device_type format (if napalm format is used) - parameters['device_type'] = napalm_to_netmiko_map.get(device_type, device_type) - - with ConnectHandler(**parameters) as net_connect: - netmiko_method = getattr(net_connect, method) - result = netmiko_method(**kwargs) - return Result(host=task.host, result=result) diff --git a/brigade/plugins/tasks/networking/netmiko_send_command.py b/brigade/plugins/tasks/networking/netmiko_send_command.py new file mode 100644 index 00000000..7bcb7328 --- /dev/null +++ b/brigade/plugins/tasks/networking/netmiko_send_command.py @@ -0,0 +1,22 @@ +from brigade.core.task import Result + + +def netmiko_send_command(task, command_string, use_timing=False, **kwargs): + """ + Execute Netmiko send_command method (or send_command_timing) + + Arguments: + command_string(str): Command to execute on the remote network device. + use_timing(bool, optional): Set to True to switch to send_command_timing method. + kwargs (dict, optional): Additional arguments to pass to send_command method. + + Returns: + :obj:`brigade.core.task.Result`: + * result (``dict``): dictionary with the result of the getter + """ + net_connect = task.host.get_connection("netmiko") + if use_timing: + result = net_connect.send_command_timing(command_string, **kwargs) + else: + result = net_connect.send_command(command_string, **kwargs) + return Result(host=task.host, result=result) 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/demo/Vagrantfile b/demo/Vagrantfile deleted file mode 100644 index bb2bcabb..00000000 --- a/demo/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/demo/configure.py b/demo/configure.py deleted file mode 100644 index f88334cf..00000000 --- a/demo/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/demo/extra_data/switch00.bma/interfaces.yaml b/demo/extra_data/switch00.bma/interfaces.yaml deleted file mode 100644 index 64d0d9b6..00000000 --- a/demo/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/demo/extra_data/switch00.cmh/interfaces.yaml b/demo/extra_data/switch00.cmh/interfaces.yaml deleted file mode 100644 index 4f0c5e0b..00000000 --- a/demo/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/demo/extra_data/switch01.bma/interfaces.yaml b/demo/extra_data/switch01.bma/interfaces.yaml deleted file mode 100644 index db67fca1..00000000 --- a/demo/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/demo/extra_data/switch01.cmh/interfaces.yaml b/demo/extra_data/switch01.cmh/interfaces.yaml deleted file mode 100644 index e847b411..00000000 --- a/demo/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/demo/get_facts_grouping.py b/demo/get_facts_grouping.py deleted file mode 100644 index f016dd96..00000000 --- a/demo/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/demo/get_facts_simple.py b/demo/get_facts_simple.py deleted file mode 100644 index 5cbc1fbd..00000000 --- a/demo/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/demo/groups.yaml b/demo/groups.yaml deleted file mode 100644 index 54ce9cd4..00000000 --- a/demo/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/demo/hosts.yaml b/demo/hosts.yaml deleted file mode 100644 index 0a1b80db..00000000 --- a/demo/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/demo/templates/interfaces/eos/interfaces.j2 b/demo/templates/interfaces/eos/interfaces.j2 deleted file mode 100644 index fd38a50e..00000000 --- a/demo/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/demo/templates/interfaces/junos/interfaces.j2 b/demo/templates/interfaces/junos/interfaces.j2 deleted file mode 100644 index 07b36eca..00000000 --- a/demo/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/docs/_data_templates/configuration-parameters.j2 b/docs/_data_templates/configuration-parameters.j2 new file mode 100644 index 00000000..0f06931a --- /dev/null +++ b/docs/_data_templates/configuration-parameters.j2 @@ -0,0 +1,25 @@ +The configuration parameters will be set by the :doc:`Brigade.core.configuration.Config ` class. + +{% for k, v in params|dictsort %} +---------- + +{{ k }} +---------------------------------- + + +.. raw:: html + + + + + + + + + + +
Environment variableTypeDefault
{{ v['env'] or 'BRIGADE_' + k|upper }}{{ v['type'] }}{{ v['default_doc'] or v['default'] }}
+ +{{ v['description'] }} + +{% endfor %} \ No newline at end of file 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 11fc5d3a..77bde96b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,10 +20,16 @@ import os import sys +from jinja2 import Environment +from jinja2 import FileSystemLoader + sys.path.insert(0, os.path.abspath('../')) +from brigade.core.configuration import CONF # noqa + # -- General configuration ------------------------------------------------ +BASEPATH = os.path.dirname(__file__) # If your documentation needs a minimal Sphinx version, state it here. # @@ -32,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'] @@ -95,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. @@ -166,3 +172,25 @@ author, 'brigade', 'One line description of project.', 'Miscellaneous'), ] + + +def build_configuration_parameters(app): + """Create documentation for configuration parameters.""" + + env = Environment(loader=FileSystemLoader("{0}/_data_templates".format(BASEPATH))) + template_file = env.get_template("configuration-parameters.j2") + data = {} + data['params'] = CONF + rendered_template = template_file.render(**data) + output_dir = '{0}/ref/configuration/generated'.format(BASEPATH) + with open('{}/parameters.rst'.format(output_dir), 'w') as f: + f.write(rendered_template) + + +def setup(app): + """Map methods to states of the documentation build.""" + app.connect('builder-inited', build_configuration_parameters) + app.add_stylesheet('css/custom.css') + + +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 dac30018..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:: ../../demo/hosts.yaml - :name: hosts.yaml - :language: yaml - -* Groups file: - -.. literalinclude:: ../../demo/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/howto/transforming_inventory_data.rst b/docs/howto/transforming_inventory_data.rst new file mode 100644 index 00000000..b6195ffd --- /dev/null +++ b/docs/howto/transforming_inventory_data.rst @@ -0,0 +1,25 @@ +Transforming Inventory Data +=========================== + +Imagine your data looks like:: + + host1: + username: my_user + password: my_password + host2: + username: my_user + password: my_password + +It turns out brigade is going to look for ``brigade_username`` and ``brigade_password`` to use as credentials. You may not want to change the data in your backend and you may not want to write a custom inventory plugin just to accommodate this difference. Fortunately, ``brigade`` has you covered. You can write a function to do all the data manipulations you want and pass it to any inventory plugin. For instance:: + + def adapt_host_data(host): + host.data["brigade_username"] = host.data["username"] + host.data["brigade_password"] = host.data["password"] + + + inv = NSOTInventory(transform_function=adapt_host_data) + brigade = Brigade(inventory=inv) + +What's going to happen is that the inventory is going to create the :obj:`brigade.core.inventory.Host` and :obj:`brigade.core.inventory.Group` objects as usual and then finally the ``transform_function`` is going to be called for each individual host one by one. + +.. note:: This was a very simple example but the ``transform_function`` can basically do anything you want/need. diff --git a/docs/howto/writing_a_connection_task.rst b/docs/howto/writing_a_connection_task.rst new file mode 100644 index 00000000..dad98a48 --- /dev/null +++ b/docs/howto/writing_a_connection_task.rst @@ -0,0 +1,24 @@ +Writing a connection task +######################### + +Connection tasks are tasks that establish a connection with a device to provide some sort of reusable mechanism to interact with it. You can find some examples of connections tasks in the :doc:`../ref/tasks/connections` section. + +Writing a connection task is no different from writing a regular task. The only difference is that the task will have to establish the connection and assign it to the device. + +A continuation you can see a simplified version of the ``paramiko_connection`` connection task as an example:: + + def paramiko_connection(task=None): + host = task.host + + client = paramiko.SSHClient() + + parameters = { + "hostname": host.host, + "username": host.username, + "password": host.password, + "port": host.ssh_port, + } + client.connect(**parameters) + host.connections["paramiko"] = client + +Note the last line where the connection is assigned to the host. Subsequent tasks will be able to retrieve this connection by host calling ``host.get_connection("paramiko")`` diff --git a/docs/howto/writing_a_custom_inventory.rst b/docs/howto/writing_a_custom_inventory.rst new file mode 100644 index 00000000..95739348 --- /dev/null +++ b/docs/howto/writing_a_custom_inventory.rst @@ -0,0 +1,48 @@ +Writing a custom inventory +========================== + +If you have your own backend with host information or you don't like the provided ones you can write your own custom inventory. Doing so is quite easy. A continuation you can find a very simple one with static data:: + + from builtins import super + + from brigade.core.inventory import Inventory + + + class MyInventory(Inventory): + + def __init__(self, **kwargs): + # code to get the data + hosts = { + "host1": { + "data1": "value1", + "data2": "value2". + "group": "my_group1", + }, + "host2": { + "data1": "value1", + "data2": "value2". + "group": "my_group1", + } + } + groups = { + "my_group1": { + "more_data1": "more_value1", + "more_data2": "more_value2", + } + } + + # passing the data to the parent class so the data is + # transformed into actual Host/Group objects + super().__init__(hosts, groups, **kwargs) + + +So if you want to make it dynamic everything you have to do is get the data yourself and organize it in a similar format to the one described in the example above. + +.. note:: it is not mandatory to use groups. Feel free to skip the attribute ``group`` and just pass and empty dict or ``None`` to ``super()``. + +Finally, to have brigade use it, you can do:: + + inv = MyInventory() + brigade = Brigade(inventory=inv) + +And that's it, you now have your own inventory plugin :) 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/configuration.rst b/docs/ref/api/configuration.rst new file mode 100644 index 00000000..6bb14964 --- /dev/null +++ b/docs/ref/api/configuration.rst @@ -0,0 +1,9 @@ +Configuration +############# + + +.. autoclass:: brigade.core.configuration.Config + :members: + :undoc-members: + +The attributes for the Config object will be the Brigade configuration parameters. For a list of available parameters see :doc:`Brigade configuration parameters ` \ No newline at end of file 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 26d544bb..7ca6e828 100644 --- a/docs/ref/api/index.rst +++ b/docs/ref/api/index.rst @@ -6,6 +6,8 @@ Brigade API Reference :caption: Brigade API 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/tests/tasks/commands/__init__.py b/docs/ref/configuration/generated/.placeholder similarity index 100% rename from tests/tasks/commands/__init__.py rename to docs/ref/configuration/generated/.placeholder diff --git a/docs/ref/configuration/index.rst b/docs/ref/configuration/index.rst new file mode 100644 index 00000000..4b724985 --- /dev/null +++ b/docs/ref/configuration/index.rst @@ -0,0 +1,10 @@ +Brigade Configuration +===================== + +Each configuration parameter are applied in the following order: + +1. Environment variable +2. Parameter in configuration file / object +3. Default value + +.. include:: generated/parameters.rst \ No newline at end of file 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 c4d1551c..9e863098 100644 --- a/docs/ref/index.rst +++ b/docs/ref/index.rst @@ -1,15 +1,28 @@ Reference Guides ================ +.. toctree:: + :maxdepth: 2 + :caption: Brigade Internals + + internals/index + .. toctree:: :maxdepth: 2 :caption: Brigade API API +.. toctree:: + :maxdepth: 2 + :caption: Configuration + + configuration/index + .. toctree:: :maxdepth: 2 :caption: Plugins tasks/index + functions/index inventory/index diff --git a/docs/ref/internals/_static/execution_model.graffle b/docs/ref/internals/_static/execution_model.graffle new file mode 100644 index 00000000..5f0ed3bd Binary files /dev/null and b/docs/ref/internals/_static/execution_model.graffle differ diff --git a/docs/ref/internals/_static/execution_model_1.png b/docs/ref/internals/_static/execution_model_1.png new file mode 100644 index 00000000..1901f03c Binary files /dev/null and b/docs/ref/internals/_static/execution_model_1.png differ diff --git a/docs/ref/internals/_static/execution_model_2.png b/docs/ref/internals/_static/execution_model_2.png new file mode 100644 index 00000000..aec1298a Binary files /dev/null and b/docs/ref/internals/_static/execution_model_2.png differ diff --git a/docs/ref/internals/execution_model.rst b/docs/ref/internals/execution_model.rst new file mode 100644 index 00000000..15574e3a --- /dev/null +++ b/docs/ref/internals/execution_model.rst @@ -0,0 +1,22 @@ +Execution Model +=============== + +One of the many advantages of using brigade is that it will be parallelize the execution of tasks for you. The way it works is as follows: + +1. You trigger the parallelization by running a task via :obj:`brigade.core.Brigade.run` with ``num_workers > 1`` (defaults to ``20``). +2. If ``num_workers == 1`` we run the task over all hosts one after the other in a simple loop. This is useful for troubleshooting/debugging, for writing to disk/database or just for printing on screen. +3. When parallelizing tasks brigade will use a different thread for each host. + +Below you can see a simple diagram illustrating how this works: + +.. image:: _static/execution_model_1.png + +Note that you can create tasks with other tasks inside. When tasks are nested the inner tasks will run serially for that host in parallel to other hosts. This is useful as it let's you control the flow of the execution at your own will. For instance, you could compose a different workflow to the previous one as follows: + +.. image:: _static/execution_model_2.png + +Why would you do this? Most of the time you will want to group as many tasks as possible. That will ensure your script runs as fast as possible. However, some tasks might require to be run after ensuring the some others are done. For instance, you could do something like: + +1. Configure everything in parallel +2. Run some verification tests +3. Enable services diff --git a/docs/ref/internals/index.rst b/docs/ref/internals/index.rst new file mode 100644 index 00000000..4b87b948 --- /dev/null +++ b/docs/ref/internals/index.rst @@ -0,0 +1,7 @@ +Brigade's Internals +=================== + +.. toctree:: + :maxdepth: 2 + + execution_model diff --git a/docs/ref/inventory/index.rst b/docs/ref/inventory/index.rst index 0005b8ea..cf659aea 100644 --- a/docs/ref/inventory/index.rst +++ b/docs/ref/inventory/index.rst @@ -1,6 +1,11 @@ +.. _ref-inventory: + Inventory ========= -.. automodule:: brigade.plugins.inventory.simple - :members: - :undoc-members: +.. toctree:: + :maxdepth: 2 + :caption: Categories: + + simple + nsot diff --git a/docs/ref/inventory/nsot.rst b/docs/ref/inventory/nsot.rst new file mode 100644 index 00000000..8663d573 --- /dev/null +++ b/docs/ref/inventory/nsot.rst @@ -0,0 +1,6 @@ +NSOT +==== + +.. automodule:: brigade.plugins.inventory.nsot + :members: + :undoc-members: diff --git a/docs/ref/inventory/simple.rst b/docs/ref/inventory/simple.rst new file mode 100644 index 00000000..3a87e8f3 --- /dev/null +++ b/docs/ref/inventory/simple.rst @@ -0,0 +1,6 @@ +Simple +====== + +.. automodule:: brigade.plugins.inventory.simple + :members: + :undoc-members: diff --git a/docs/ref/tasks/connections.rst b/docs/ref/tasks/connections.rst new file mode 100644 index 00000000..efc9de64 --- /dev/null +++ b/docs/ref/tasks/connections.rst @@ -0,0 +1,6 @@ +Connections +=========== + +.. automodule:: brigade.plugins.tasks.connections + :members: + :undoc-members: diff --git a/docs/ref/tasks/index.rst b/docs/ref/tasks/index.rst index 55111c31..96558306 100644 --- a/docs/ref/tasks/index.rst +++ b/docs/ref/tasks/index.rst @@ -5,6 +5,7 @@ Tasks :maxdepth: 2 :caption: Categories: + connections commands data files 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 2c70105f..850a458e 100644 --- a/docs/tutorials/intro/index.rst +++ b/docs/tutorials/intro/index.rst @@ -1,12 +1,21 @@ Learning Brigade ================ +We're glad you made it here! This is a great place to learn the basics of Brigade. Good luck on your journey. + +.. note:: + + Just like Brigade, this tutorial and the rest of the documentation is a work in progress. If you are missing something, please open `an issue `_. .. toctree:: :maxdepth: 1 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/install.rst b/docs/tutorials/intro/install.rst index 0b6cbfe7..7d1b042d 100644 --- a/docs/tutorials/intro/install.rst +++ b/docs/tutorials/intro/install.rst @@ -1,2 +1,61 @@ Installing Brigade ================== + +Before you go ahead and install Brigade it's recommended to create your own Python virtualenv. That way you have complete control of your environment and you don't risk overwriting your systems Python environment. + +.. note:: + + This tutorial doesn't cover the creation of a Python virtual environment. The Python documentation offers a guide where you can learn more about `virtualenvs `_. We also won't cover the `installation of pip `_, but changes are that you already have pip on your system. + +Brigade is published to `PyPI `_ and can be installed like most other Python packages using the pip tool. You can verify that you have pip installed by typing: + +.. code-block:: bash + + pip --version + + pip 1.5.4 from /home/vagrant/brigade/local/lib/python2.7/site-packages (python 2.7) + +It could be that you need to use the pip3 binary instead of pip as pip3 is for Python 3 on some systems. + +So above we can see that we have pip installed. However the version 1.5.4 is pretty old. To save ourselves from trouble when installing packages we start by upgrading pip. + +.. code-block:: bash + + pip install "pip>=9.0.1" + + pip --version + + pip 9.0.1 from /home/vagrant/brigade/local/lib/python2.7/site-packages (python 2.7) + +That's more like it! The next step is to install Brigade. + +.. code-block:: bash + + pip install brigade + + Collecting brigade + [...] + Successfully installed MarkupSafe-1.0 + asn1crypto-0.23.0 bcrypt-3.1.4 brigade-0.0.5 + certifi-2017.11.5 cffi-1.11.2 chardet-3.0.4 + cryptography-2.1.4 enum34-1.1.6 future-0.16.0 + idna-2.6 ipaddress-1.0.18 jinja2-2.10 + jtextfsm-0.3.1 junos-eznc-2.1.7 lxml-4.1.1 + napalm-2.2.0 ncclient-0.5.3 netaddr-0.7.19 + netmiko-1.4.3 paramiko-2.4.0 pyIOSXR-0.52 + pyasn1-0.4.2 pycparser-2.18 pyeapi-0.8.1 + pynacl-1.2.1 pynxos-0.0.3 pyserial-3.4 + pyyaml-3.12 requests-2.18.4 scp-0.10.2 + six-1.11.0 urllib3-1.22 + +Please note that the above output has been abbreviated for readability. Your output will probably be longer. You should see that `brigade` is successfully installed. + +Now we can verify that Brigade is installed and that you are able to import the package from Python. + +.. code-block:: python + + python + >>>import brigade.core + >>> + +Great, now you're ready to create an inventory. 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/overview.rst b/docs/tutorials/intro/overview.rst index 84eb542a..a32a478e 100644 --- a/docs/tutorials/intro/overview.rst +++ b/docs/tutorials/intro/overview.rst @@ -1,3 +1,20 @@ What is Brigade? ================ +Brigade is an automation framework written in Python. These days there exists several automation frameworks. What makes Brigade different is that you write Python code in order to use Brigade. This is to be compared to other frameworks which typically use their own configuration language. + +Why does this matter? +--------------------- +Typically, a specific configuration language is easy to use in a basic way. Though after a while you need to use more advanced features and might have to extend that configuration language with another programming language. While this works it can be very hard to troubleshoot once it's started to grow. + +As Brigade allows you to use pure Python code you can troubleshoot and debug it in the same way as you would do with any other Python code. + +What does it compare to? +------------------------ +In some ways, you could compare Brigade to `Flask `_, which is a web framework that allows you to create web applications. Flask provides an easy to use interface which lets you build powerful websites without forcing you to work in a particular way. + +Brigade lets you automate your environment by providing you an interface which does a lot of the heavy lifting. + +How much Python do you need do know? +------------------------------------ +As you write Python code to control Brigade it's assumed that you are somewhat familiar with Python. But how good do you have to be with Python in order to make use of Brigade? That's actually the topic for the next section *spoiler alert* Not a lot! \ No newline at end of file diff --git a/docs/tutorials/intro/python.rst b/docs/tutorials/intro/python.rst new file mode 100644 index 00000000..3b7be274 --- /dev/null +++ b/docs/tutorials/intro/python.rst @@ -0,0 +1,27 @@ +The need to know Python +======================= + +In order to use Brigade you have to know some Python. This might come as wonderful news to you, or you might find it a bit scary. If you're already a comfortable Python user, just go a head and hit :doc:`next `. + +If you haven't written any code before you might be heading somewhere else now. Before you go however, answer me this: + +Do you know Excel? + +Chances are that you know how to use Excel. It's simple right. You just open a sheet and enter some data. It's used by a lot of finance people and unfortunately it's one of the most used IPAM solutions. Though aside from a simple tool to enter data in a sheet Excel has an insane amount of features. Most people will only use 5% of all the features. How good are you at Excel? Does it matter? + +It's the same way with Python, it can take very long time to fully master it. The good part is that you don't have to become a master. As long as you know the very basics you will be able to use Brigade. + +Python skills required +---------------------- + +If you've never seen Python before, and don't have any experience in other programming languages it will probably be a good idea to `pick up the basics `_ and come back here later. + +In order to follow this tutorial you should be able to: + +* Setup Python on your system (Linux or Mac) +* Install Virtualenv and Python packages +* Understand basic Python concepts such as: + + - Variables + - Functions + - Imports 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/tests/tasks/data/__init__.py b/examples/3_advanced_tooling/tasks/__init__.py similarity index 100% rename from tests/tasks/data/__init__.py rename to examples/3_advanced_tooling/tasks/__init__.py 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/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/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/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/demo/requirements.txt b/examples/requirements.txt similarity index 100% rename from demo/requirements.txt rename to examples/requirements.txt diff --git a/demo/templates/base/eos/base.j2 b/examples/templates/eos/base.j2 similarity index 100% rename from demo/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/tests/tasks/files/__init__.py b/examples/templates/eos/spine.j2 similarity index 100% rename from tests/tasks/files/__init__.py rename to examples/templates/eos/spine.j2 diff --git a/demo/templates/base/junos/base.j2 b/examples/templates/junos/base.j2 similarity index 100% rename from demo/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/tests/tasks/networking/__init__.py b/examples/templates/junos/spine.j2 similarity index 100% rename from tests/tasks/networking/__init__.py rename to examples/templates/junos/spine.j2 diff --git a/requirements.txt b/requirements.txt index 8e74d76b..2efafbfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ +colorama pyyaml jinja2 -napalm +napalm>=2.2.0 +netmiko>=2.0.0 paramiko future +requests diff --git a/setup.py b/setup.py index a5b1dfec..2f383cf0 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ __author__ = 'dbarrosop@dravetech.com' __license__ = 'Apache License, version 2' -__version__ = '0.0.5' +__version__ = '0.0.6' setup(name='brigade', version=__version__, 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 new file mode 100644 index 00000000..a3ac2ede --- /dev/null +++ b/tests/core/test_tasks.py @@ -0,0 +1,58 @@ +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): + task.run(commands.command, + command="echo {}".format(task.host)) + + +class Test(object): + + def test_task(self, brigade): + result = brigade.run(commands.command, + command="echo hi") + assert result + for h, r in result.items(): + assert r.stdout.strip() == "hi" + + def test_sub_task(self, brigade): + result = brigade.run(sub_task) + assert result + for h, r in result.items(): + 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.sh b/tests/inventory_data/nsot/nsot.sh new file mode 100644 index 00000000..5a8490c6 --- /dev/null +++ b/tests/inventory_data/nsot/nsot.sh @@ -0,0 +1,79 @@ +nsot sites add --name site1 + +nsot attributes add --site-id 1 --resource-name device --name os +nsot attributes add --site-id 1 --resource-name device --name host +nsot attributes add --site-id 1 --resource-name device --name user +nsot attributes add --site-id 1 --resource-name device --name password --allow-empty +nsot attributes add --site-id 1 --resource-name device --name port + +nsot devices add --site-id 1 --hostname rtr00-site1 +nsot devices update --site-id 1 --id 1 -a os=eos -a host=127.0.0.1 -a user=vagrant -a password=vagrant -a port=12443 +nsot devices add --site-id 1 --hostname rtr01-site1 +nsot devices update --site-id 1 --id 2 -a os=junos -a host=127.0.0.1 -a user=vagrant -a password="" -a port=12203 + + +nsot attributes add --site-id 1 --resource-name device --name asn +nsot attributes add --site-id 1 --resource-name device --name router_id + +nsot devices update --site-id 1 --id 1 -a asn=65001 -a router_id=10.1.1.1 +nsot devices update --site-id 1 --id 2 -a asn=65002 -a router_id=10.1.1.2 + +nsot attributes add --site-id 1 --resource-name network --name type +nsot networks add --site-id 1 --cidr 2001:db8:b33f::/64 -a type=loopbacks + +nsot attributes add --site-id 1 --resource-name interface --name link_type +nsot attributes add --site-id 1 --resource-name interface --name connects_to_device +nsot attributes add --site-id 1 --resource-name interface --name connects_to_iface + +nsot interfaces add --site-id 1 --device 1 --name lo0 --addresses 2001:db8:b33f::100/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 +nsot interfaces add --site-id 1 --device 2 --name lo0 --addresses 2001:db8:b33f::101/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 + +nsot networks add --site-id 1 --cidr 2001:db8:caf3::/64 -a type=ptp +nsot networks add --site-id 1 --cidr 2001:db8:caf3::/127 -a type=ptp +nsot networks add --site-id 1 --cidr 2001:db8:caf3::2/127 -a type=ptp + +nsot interfaces add --site-id 1 --device 1 --name et1 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/1 -c 2001:db8:caf3:: +nsot interfaces add --site-id 1 --device 1 --name et2 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/2 -c 2001:db8:caf3::2 + +nsot interfaces add --site-id 1 --device 2 --name ge-0/0/1 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et1 -c 2001:db8:caf3::1 +nsot interfaces add --site-id 1 --device 2 --name ge-0/0/2 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et2 -c 2001:db8:caf3::3 + +nsot sites add --name site2 + +nsot attributes add --site-id 2 --resource-name device --name os +nsot attributes add --site-id 2 --resource-name device --name host +nsot attributes add --site-id 2 --resource-name device --name user +nsot attributes add --site-id 2 --resource-name device --name password --allow-empty +nsot attributes add --site-id 2 --resource-name device --name port + +nsot devices add --site-id 2 --hostname rtr00-site2 +nsot devices update --site-id 2 --id 3 -a os=eos -a host=127.0.0.1 -a user=vagrant -a password=vagrant -a port=12443 +nsot devices add --site-id 2 --hostname rtr01-site2 +nsot devices update --site-id 2 --id 4 -a os=junos -a host=127.0.0.1 -a user=vagrant -a password="" -a port=12203 + + +nsot attributes add --site-id 2 --resource-name device --name asn +nsot attributes add --site-id 2 --resource-name device --name router_id + +nsot devices update --site-id 2 --id 3 -a asn=65001 -a router_id=10.1.1.1 +nsot devices update --site-id 2 --id 4 -a asn=65002 -a router_id=10.1.1.2 + +nsot attributes add --site-id 2 --resource-name network --name type +nsot networks add --site-id 2 --cidr 2001:db8:f4c3::/64 -a type=loopbacks + +nsot attributes add --site-id 2 --resource-name interface --name link_type +nsot attributes add --site-id 2 --resource-name interface --name connects_to_device +nsot attributes add --site-id 2 --resource-name interface --name connects_to_iface + +nsot interfaces add --site-id 2 --device 3 --name lo0 --addresses 2001:db8:f4c3::100/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 +nsot interfaces add --site-id 2 --device 4 --name lo0 --addresses 2001:db8:f4c3::101/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 + +nsot networks add --site-id 2 --cidr 2001:db8:dead::/64 -a type=ptp +nsot networks add --site-id 2 --cidr 2001:db8:dead::/127 -a type=ptp +nsot networks add --site-id 2 --cidr 2001:db8:dead::2/127 -a type=ptp + +nsot interfaces add --site-id 2 --device 3 --name et1 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/1 -c 2001:db8:dead:: +nsot interfaces add --site-id 2 --device 3 --name et2 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/2 -c 2001:db8:dead::2 + +nsot interfaces add --site-id 2 --device 4 --name ge-0/0/1 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et1 -c 2001:db8:dead::1 +nsot interfaces add --site-id 2 --device 4 --name ge-0/0/2 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et2 -c 2001:db8:dead::3 diff --git a/tests/inventory_data/nsot/nsot.sqlite3 b/tests/inventory_data/nsot/nsot.sqlite3 new file mode 100644 index 00000000..43db53f9 Binary files /dev/null and b/tests/inventory_data/nsot/nsot.sqlite3 differ diff --git a/tests/tasks/text/__init__.py b/tests/plugins/__init__.py similarity index 100% rename from tests/tasks/text/__init__.py rename to tests/plugins/__init__.py diff --git a/tests/plugins/inventory/__init__.py b/tests/plugins/inventory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/inventory/test_nsot.py b/tests/plugins/inventory/test_nsot.py new file mode 100644 index 00000000..0c1cbc11 --- /dev/null +++ b/tests/plugins/inventory/test_nsot.py @@ -0,0 +1,49 @@ +import os +import subprocess +import time + +from brigade.plugins.inventory import nsot + +import pytest + + +def transform_function(host): + attrs = ["user", "password"] + for a in attrs: + if a in host.data: + host["brigade_{}".format(a)] = host.data[a] + + +@pytest.fixture(scope="module") +def inv(request): + """Start/Stop containers needed for the tests.""" + def fin(): + subprocess.check_call(["make", "stop_nsot"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + request.addfinalizer(fin) + + subprocess.check_call(["make", "start_nsot"], + stdout=subprocess.PIPE) + + if os.getenv("TRAVIS"): + time.sleep(10) + else: + time.sleep(3) + + return nsot.NSOTInventory(transform_function=transform_function) + + +@pytest.mark.usefixtures("inv") +class Test(object): + + def test_inventory(self, inv): + assert len(inv.hosts) == 4 + assert len(inv.filter(site="site1").hosts) == 2 + assert len(inv.filter(os="junos").hosts) == 2 + assert len(inv.filter(site="site1", os="junos").hosts) == 1 + + def test_transform_function(self, inv): + for host in inv.hosts.values(): + assert host["user"] == host["brigade_user"] + assert host["password"] == host["brigade_password"] diff --git a/tests/plugins/tasks/__init__.py b/tests/plugins/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/tasks/commands/__init__.py b/tests/plugins/tasks/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tasks/commands/test_command.py b/tests/plugins/tasks/commands/test_command.py similarity index 80% rename from tests/tasks/commands/test_command.py rename to tests/plugins/tasks/commands/test_command.py index 312d559e..7ed7e6cd 100644 --- a/tests/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/tasks/commands/test_remote_command.py b/tests/plugins/tasks/commands/test_remote_command.py similarity index 85% rename from tests/tasks/commands/test_remote_command.py rename to tests/plugins/tasks/commands/test_remote_command.py index 63d3b85f..9486745f 100644 --- a/tests/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/__init__.py b/tests/plugins/tasks/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tasks/data/test_data/broken.json b/tests/plugins/tasks/data/test_data/broken.json similarity index 100% rename from tests/tasks/data/test_data/broken.json rename to tests/plugins/tasks/data/test_data/broken.json diff --git a/tests/tasks/data/test_data/broken.yaml b/tests/plugins/tasks/data/test_data/broken.yaml similarity index 100% rename from tests/tasks/data/test_data/broken.yaml rename to tests/plugins/tasks/data/test_data/broken.yaml diff --git a/tests/tasks/data/test_data/simple.json b/tests/plugins/tasks/data/test_data/simple.json similarity index 97% rename from tests/tasks/data/test_data/simple.json rename to tests/plugins/tasks/data/test_data/simple.json index 370fa8df..3a17c39d 100644 --- a/tests/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/tasks/data/test_data/simple.yaml b/tests/plugins/tasks/data/test_data/simple.yaml similarity index 100% rename from tests/tasks/data/test_data/simple.yaml rename to tests/plugins/tasks/data/test_data/simple.yaml diff --git a/tests/tasks/data/test_load_json.py b/tests/plugins/tasks/data/test_load_json.py similarity index 85% rename from tests/tasks/data/test_load_json.py rename to tests/plugins/tasks/data/test_load_json.py index 03e767ec..e53d778b 100644 --- a/tests/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/tasks/data/test_load_yaml.py b/tests/plugins/tasks/data/test_load_yaml.py similarity index 86% rename from tests/tasks/data/test_load_yaml.py rename to tests/plugins/tasks/data/test_load_yaml.py index 15fbde6b..4421cb96 100644 --- a/tests/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/__init__.py b/tests/plugins/tasks/files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tasks/files/test_sftp.py b/tests/plugins/tasks/files/test_sftp.py similarity index 87% rename from tests/tasks/files/test_sftp.py rename to tests/plugins/tasks/files/test_sftp.py index b6813364..0e7e4450 100644 --- a/tests/tasks/files/test_sftp.py +++ b/tests/plugins/tasks/files/test_sftp.py @@ -9,8 +9,8 @@ class Test(object): def test_sftp_put(self, brigade): - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="put", src="README.md", dst="/tmp/README.md") @@ -19,8 +19,8 @@ def test_sftp_put(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = False result = brigade.run(files.sftp, + dry_run=False, action="put", src="README.md", dst="/tmp/README.md") @@ -29,8 +29,8 @@ def test_sftp_put(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="put", src="README.md", dst="/tmp/README.md") @@ -41,8 +41,8 @@ def test_sftp_put(self, brigade): def test_sftp_get(self, brigade): filename = "/tmp/" + str(uuid.uuid4()) + "-{host}" - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="get", src="/etc/hostname", dst=filename) @@ -51,8 +51,8 @@ def test_sftp_get(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = False result = brigade.run(files.sftp, + dry_run=False, action="get", src="/etc/hostname", dst=filename) @@ -61,8 +61,8 @@ def test_sftp_get(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=False, action="get", src="/etc/hostname", dst=filename) @@ -72,8 +72,8 @@ def test_sftp_get(self, brigade): assert not r.changed def test_sftp_put_directory(self, brigade): - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="put", src="./brigade", dst="/tmp/asd") @@ -82,8 +82,8 @@ def test_sftp_put_directory(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = False result = brigade.run(files.sftp, + dry_run=False, action="put", src="./brigade", dst="/tmp/asd") @@ -92,8 +92,8 @@ def test_sftp_put_directory(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="put", src="./brigade", dst="/tmp/asd") @@ -104,8 +104,8 @@ def test_sftp_put_directory(self, brigade): def test_sftp_get_directory(self, brigade): filename = "/tmp/" + str(uuid.uuid4()) + "-{host}" - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="get", src="/etc/terminfo/", dst=filename) @@ -114,8 +114,8 @@ def test_sftp_get_directory(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = False result = brigade.run(files.sftp, + dry_run=False, action="get", src="/etc/terminfo/", dst=filename) @@ -124,8 +124,8 @@ def test_sftp_get_directory(self, brigade): for h, r in result.items(): assert r.changed, r.files_changed - brigade.dry_run = True result = brigade.run(files.sftp, + dry_run=True, action="get", src="/etc/terminfo/", dst=filename) 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/__init__.py b/tests/plugins/tasks/networking/__init__.py new file mode 100644 index 00000000..e69de29b 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/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_interfaces.1 b/tests/plugins/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_interfaces.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_interfaces.1 rename to tests/plugins/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_interfaces.1 diff --git a/tests/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_version.0 b/tests/plugins/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_version.0 similarity index 100% rename from tests/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_version.0 rename to tests/plugins/tasks/networking/mocked/napalm_cli/test_napalm_cli/cli.1.show_version.0 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/commit_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/commit_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/commit_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/commit_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/compare_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/compare_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/compare_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/compare_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/load_merge_candidate.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/load_merge_candidate.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/load_merge_candidate.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/load_merge_candidate.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/compare_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/compare_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/compare_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/compare_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/discard_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/discard_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step1/discard_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/discard_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/load_merge_candidate.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/load_merge_candidate.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/load_merge_candidate.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/load_merge_candidate.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/compare_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/compare_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/compare_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/compare_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/discard_config.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/discard_config.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_commit/step2/discard_config.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/discard_config.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/load_merge_candidate.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/load_merge_candidate.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/load_merge_candidate.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/load_merge_candidate.1 diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_error/load_merge_candidate.1 b/tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_error/load_merge_candidate.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_error/load_merge_candidate.1 rename to tests/plugins/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_error/load_merge_candidate.1 diff --git a/tests/tasks/networking/mocked/napalm_get/test_napalm_getters/get_facts.1 b/tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters/get_facts.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_get/test_napalm_getters/get_facts.1 rename to tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters/get_facts.1 diff --git a/tests/tasks/networking/mocked/napalm_get/test_napalm_getters/get_interfaces.1 b/tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters/get_interfaces.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_get/test_napalm_getters/get_interfaces.1 rename to tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters/get_interfaces.1 diff --git a/tests/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_facts.1 b/tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_facts.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_facts.1 rename to tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_facts.1 diff --git a/tests/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_interfaces.1 b/tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_interfaces.1 similarity index 100% rename from tests/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_interfaces.1 rename to tests/plugins/tasks/networking/mocked/napalm_get/test_napalm_getters_error/get_interfaces.1 diff --git a/tests/tasks/networking/test_napalm_cli.py b/tests/plugins/tasks/networking/test_napalm_cli.py similarity index 69% rename from tests/tasks/networking/test_napalm_cli.py rename to tests/plugins/tasks/networking/test_napalm_cli.py index b3bc0a90..7bc712ff 100644 --- a/tests/tasks/networking/test_napalm_cli.py +++ b/tests/plugins/tasks/networking/test_napalm_cli.py @@ -1,7 +1,7 @@ import os # from brigade.core.exceptions import BrigadeExecutionError -from brigade.plugins.tasks import networking +from brigade.plugins.tasks import connections, networking # from napalm.base import exceptions @@ -15,10 +15,11 @@ class Test(object): def test_napalm_cli(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_cli"} - result = brigade.filter(name="dev3.group_2").run(networking.napalm_cli, - commands=["show version", - "show interfaces"], - optional_args=opt) + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_cli, + commands=["show version", + "show interfaces"]) assert result for h, r in result.items(): assert r.result["show version"] @@ -33,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/tasks/networking/test_napalm_configure.py b/tests/plugins/tasks/networking/test_napalm_configure.py similarity index 52% rename from tests/tasks/networking/test_napalm_configure.py rename to tests/plugins/tasks/networking/test_napalm_configure.py index 2335fc1e..fcbe0e9d 100644 --- a/tests/tasks/networking/test_napalm_configure.py +++ b/tests/plugins/tasks/networking/test_napalm_configure.py @@ -1,7 +1,7 @@ import os from brigade.core.exceptions import BrigadeExecutionError -from brigade.plugins.tasks import networking +from brigade.plugins.tasks import connections, networking from napalm.base import exceptions @@ -16,9 +16,9 @@ class Test(object): def test_napalm_configure_change_dry_run(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_configure_change_dry_run"} configuration = "hostname changed-hostname" - result = brigade.filter(name="dev3.group_2").run(networking.napalm_configure, - configuration=configuration, - optional_args=opt) + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_configure, configuration=configuration) assert result for h, r in result.items(): assert "+hostname changed-hostname" in r.diff @@ -27,20 +27,20 @@ def test_napalm_configure_change_dry_run(self, brigade): def test_napalm_configure_change_commit(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_configure_change_commit/step1"} configuration = "hostname changed-hostname" - brigade.dry_run = False - result = brigade.filter(name="dev3.group_2").run(networking.napalm_configure, - num_workers=1, - configuration=configuration, - optional_args=opt) - brigade.dry_run = True + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_configure, + dry_run=False, + configuration=configuration) assert result for h, r in result.items(): assert "+hostname changed-hostname" in r.diff assert r.changed opt = {"path": THIS_DIR + "/test_napalm_configure_change_commit/step2"} - result = brigade.filter(name="dev3.group_2").run(networking.napalm_configure, - configuration=configuration, - optional_args=opt) + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_configure, + dry_run=True, + configuration=configuration) assert result for h, r in result.items(): assert "+hostname changed-hostname" not in r.diff @@ -50,10 +50,10 @@ def test_napalm_configure_change_error(self, brigade): opt = {"path": THIS_DIR + "/test_napalm_configure_change_error"} configuration = "hostname changed_hostname" + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) with pytest.raises(BrigadeExecutionError) as e: - brigade.filter(name="dev3.group_2").run(networking.napalm_configure, - configuration=configuration, - optional_args=opt) + 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 new file mode 100644 index 00000000..459e3933 --- /dev/null +++ b/tests/plugins/tasks/networking/test_napalm_get.py @@ -0,0 +1,37 @@ +import os + +from brigade.core.exceptions import BrigadeExecutionError +from brigade.plugins.tasks import connections, networking + +import pytest + + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + "/mocked/napalm_get" + + +class Test(object): + + def test_napalm_getters(self, brigade): + opt = {"path": THIS_DIR + "/test_napalm_getters"} + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + result = d.run(networking.napalm_get, + getters=["facts", + "interfaces"]) + assert result + for h, r in result.items(): + assert r.result["facts"] + assert r.result["interfaces"] + + def test_napalm_getters_error(self, brigade): + opt = {"path": THIS_DIR + "/test_napalm_getters_error"} + d = brigade.filter(name="dev3.group_2") + d.run(connections.napalm_connection, optional_args=opt) + + with pytest.raises(BrigadeExecutionError) as e: + d.run(networking.napalm_get, + getters=["facts", + "interfaces"]) + assert len(e.value.failed_hosts) + for result in e.value.failed_hosts.values(): + 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_netmiko_send_command.py b/tests/plugins/tasks/networking/test_netmiko_send_command.py new file mode 100644 index 00000000..e3fd69ca --- /dev/null +++ b/tests/plugins/tasks/networking/test_netmiko_send_command.py @@ -0,0 +1,19 @@ +from brigade.plugins.tasks import connections, networking + + +class Test(object): + + def test_netmiko_send_command(self, brigade): + brigade.filter(name="dev4.group_2").run(task=connections.netmiko_connection) + result = brigade.filter(name="dev4.group_2").run(networking.netmiko_send_command, + command_string="hostname") + assert result + for h, r in result.items(): + assert h == r.result.strip() + + result = brigade.filter(name="dev4.group_2").run(networking.netmiko_send_command, + command_string="hostname", + use_timing=True) + assert result + for h, r in result.items(): + assert h == r.result.strip() diff --git a/tests/tasks/networking/test_tcp_ping.py b/tests/plugins/tasks/networking/test_tcp_ping.py similarity index 85% rename from tests/tasks/networking/test_tcp_ping.py rename to tests/plugins/tasks/networking/test_tcp_ping.py index 1a5bc70c..eb42993a 100644 --- a/tests/tasks/networking/test_tcp_ping.py +++ b/tests/plugins/tasks/networking/test_tcp_ping.py @@ -10,7 +10,7 @@ import pytest cur_dir = os.path.dirname(os.path.realpath(__file__)) -ext_inv_file = '{}/../../inventory_data/external_hosts.yaml'.format(cur_dir) +ext_inv_file = '{}/../../../inventory_data/external_hosts.yaml'.format(cur_dir) class Test(object): @@ -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/__init__.py b/tests/plugins/tasks/text/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tasks/text/test_data/broken.j2 b/tests/plugins/tasks/text/test_data/broken.j2 similarity index 100% rename from tests/tasks/text/test_data/broken.j2 rename to tests/plugins/tasks/text/test_data/broken.j2 diff --git a/tests/tasks/text/test_data/simple.j2 b/tests/plugins/tasks/text/test_data/simple.j2 similarity index 100% rename from tests/tasks/text/test_data/simple.j2 rename to tests/plugins/tasks/text/test_data/simple.j2 diff --git a/tests/tasks/text/test_template_file.py b/tests/plugins/tasks/text/test_template_file.py similarity index 89% rename from tests/tasks/text/test_template_file.py rename to tests/plugins/tasks/text/test_template_file.py index ba17d60e..b7aa0070 100644 --- a/tests/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/tasks/text/test_template_string.py b/tests/plugins/tasks/text/test_template_string.py similarity index 90% rename from tests/tasks/text/test_template_string.py rename to tests/plugins/tasks/text/test_template_string.py index 52aef9b9..6919517a 100644 --- a/tests/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) diff --git a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/discard_config.1 b/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/discard_config.1 deleted file mode 100644 index 0967ef42..00000000 --- a/tests/tasks/networking/mocked/napalm_configure/test_napalm_configure_change_dry_run/discard_config.1 +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/tasks/networking/test_napalm_get.py b/tests/tasks/networking/test_napalm_get.py deleted file mode 100644 index 000b6f37..00000000 --- a/tests/tasks/networking/test_napalm_get.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -from brigade.core.exceptions import BrigadeExecutionError -from brigade.plugins.tasks import networking - -import pytest - - -THIS_DIR = os.path.dirname(os.path.realpath(__file__)) + "/mocked/napalm_get" - - -class Test(object): - - def test_napalm_getters(self, brigade): - opt = {"path": THIS_DIR + "/test_napalm_getters"} - result = brigade.filter(name="dev3.group_2").run(networking.napalm_get, - getters=["facts", - "interfaces"], - optional_args=opt) - assert result - for h, r in result.items(): - assert r.result["facts"] - assert r.result["interfaces"] - - def test_napalm_getters_error(self, brigade): - opt = {"path": THIS_DIR + "/test_napalm_getters_error"} - - with pytest.raises(BrigadeExecutionError) as e: - brigade.filter(name="dev3.group_2").run(networking.napalm_get, - getters=["facts", - "interfaces"], - optional_args=opt) - assert len(e.value.failed_hosts) - for exc in e.value.failed_hosts.values(): - assert isinstance(exc, KeyError) diff --git a/tests/tasks/networking/test_netmiko_run.py b/tests/tasks/networking/test_netmiko_run.py deleted file mode 100644 index 256f867c..00000000 --- a/tests/tasks/networking/test_netmiko_run.py +++ /dev/null @@ -1,12 +0,0 @@ -from brigade.plugins.tasks import networking - - -class Test(object): - - def test_netmiko_run(self, brigade): - result = brigade.filter(name="dev4.group_2").run(networking.netmiko_run, - method="send_command", - command_string="hostname") - assert result - for h, r in result.items(): - assert h == r.result.strip() diff --git a/tox.ini b/tox.ini index f96e0ec3..6a4e96fc 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,9 @@ envlist = py27,py34,py35,py36 deps = -rrequirements.txt -rrequirements-dev.txt - git+https://github.com/ktbyers/netmiko.git@develop + -rdocs/requirements.txt +passenv = * commands = py.test + sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html