Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First stable release almost ready #68

Merged
merged 33 commits into from
Jan 20, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
92fed6f
allow passing dry_run to individual tasks
dbarrosop Dec 21, 2017
37e1fa5
added write task
dbarrosop Dec 21, 2017
29f4c56
redundant
dbarrosop Dec 24, 2017
2a926df
log by default to ./brigade.log
dbarrosop Dec 24, 2017
b0ca3e0
raise an error properly if we fail to connect to the device
dbarrosop Dec 24, 2017
8063186
added more devices to the lab
dbarrosop Dec 24, 2017
eadbf11
examples WIP
dbarrosop Dec 27, 2017
a230151
improvements to napalm_configure
dbarrosop Dec 27, 2017
3184632
unnecessary
dbarrosop Dec 27, 2017
027a440
improvements to logging
dbarrosop Dec 27, 2017
5e59282
better error handling
dbarrosop Dec 27, 2017
52f63f9
document new Result attributes
dbarrosop Dec 27, 2017
4972edd
allow overriding raise_on_error per task
dbarrosop Dec 29, 2017
cecc260
added napalm_validate task
dbarrosop Dec 29, 2017
30e8570
render automatically template path
dbarrosop Dec 29, 2017
0b265ce
Added helper functions to make output pretty
dbarrosop Dec 29, 2017
3e89f8c
fix napalm_validate tests
dbarrosop Dec 29, 2017
0e4027d
bugfix
dbarrosop Dec 31, 2017
ca49110
return properly dict_keys and dict_values
dbarrosop Dec 31, 2017
1e06a18
subtasks now return MultiResults which aggregate mutiple results for …
dbarrosop Jan 6, 2018
4041eec
Merge branch 'develop' of github.com:napalm-automation/brigade into r…
dbarrosop Jan 6, 2018
f830286
skip hosts if they fail and raise_on_error is False
dbarrosop Jan 6, 2018
8b8a4d6
added easy_brigade
dbarrosop Jan 6, 2018
a7096c6
added tutorials
dbarrosop Jan 6, 2018
499d11d
Merge branch 'develop' of github.com:napalm-automation/brigade into r…
dbarrosop Jan 6, 2018
308fd67
bugfix
dbarrosop Jan 15, 2018
57853e2
allow telling a task to be run on hosts that should be skipped otherwise
dbarrosop Jan 15, 2018
7824871
for consistency with tasks
dbarrosop Jan 15, 2018
b9a0d09
allow printing multiresult objects
dbarrosop Jan 15, 2018
df26d81
update docstring
dbarrosop Jan 15, 2018
707c9cc
updated examples
dbarrosop Jan 15, 2018
7e1bb53
minor fixes proposed in the comments
dbarrosop Jan 20, 2018
7b9eef3
adding missing deps
dbarrosop Jan 20, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/howto/basic-napalm-getters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ Let's start by seeing how to work with the inventory. Let's assume the following

* Hosts file:

.. literalinclude:: ../../examples/hosts.yaml
.. literalinclude:: ../../examples/inventory/hosts.yaml
:name: hosts.yaml
:language: yaml

* Groups file:

.. literalinclude:: ../../examples/groups.yaml
.. literalinclude:: ../../examples/inventory/groups.yaml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to add general comments about the tutorial here:

  1. On line 57 it needs to be:
host["brigade_nos"]
  1. The inconsistency between attribute names and keys is a bit strange? Should these be identical?
# dir(host) with underscore attributes filtered out
 'brigade',
 'connections',
 'data',
 'get',
 'get_connection',
 'group',
 'host',
 'items',
 'keys',
 'name',
 'network_api_port',
 'nos',
 'os',
 'password',
 'ssh_port',
 'username',
 'values'

In [9]: host.keys()
Out[9]: dict_keys(['name', 'group', 'domain', 'brigade_host', 'brigade_username', 'brigade_password', 'brigade_nos', 'brigade_port', 'site', 'role'])

The part that is a bit strange is that we have names that are similar, but not identical. Should we just make them identical. Also should we have a one-to-one correspondence between attribute access and dictionary keys (i.e. all dictionary keys could just be accessed as host.foo)?

  1. Use of brigade as a variable name here:
     >>> brigade = Brigade(
     ...         inventory=SimpleInventory("hosts.yaml", "groups.yaml"),
     ...         dry_run=True)

This doesn't work:

>>> result = cmh_leaf.run(task=tasks.napalm_get_facts,
...                       facts="facts")

Instead needed to use:

from brigade.plugins.tasks.networking import napalm_get
result = cmh_leaf.run(task=napalm_get, getters='get_facts')
  1. print(result) on line 88 doesn't work as shown:
# Here is what I saw at that point:
>>> print(result)
AggregatedResult: napalm_get

Here is what I ended up doing to print the result:

>>> for k, v in result.items():
...   print("-" * 80)
...   print(k)
...   for e in v:
...     print(e.result)
...   print("-" * 80)
  1. I thought it was a bit strange in the output printing above that the result.items() that the v was a MultiResult and I needed to loop over it. The part that was strange to me was when we do the get_task(task) later in this tutorial...in that situation we just get a result that we can print and not a MultiResult (or vice versa). In other words, it isn't clear to me why we return a MultiResult in one context and not in the other.

  2. There were some changes that I needed to make to get the get_info(task) to run properly. Once again for the import of napalm_get and for the result printing:

def get_info(task):
    # Grouping multiple tasks that go together
    r = napalm_get(task, "get_facts")
    print(task.host.name)
    print("============")
    print(r.result)
    r = napalm_get(task, "get_interfaces")
    print(task.host.name)
    print("============")
    print(r.result)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay...maybe this is all old as it is in docs/howto and not docs/tutorials. Probably should only have one place and not three (examples, docs/howto, docs/tutorials and I am 0 for 2 on reviewing the right one).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is due to the structure of the documentation in general. Saw that you were talking about this on Twitter too. Basically howto's aim to solve a specific task and tutorials is more handholding. Currently they might basically cover the same topics. Doesn't look like the howtos are included in this PR, but they probably need to be updated.

Also think that the examples are good for people who only want to view the code and not have it spreadout on a page with different sections as can happen in a tutorial.

:name: groups.yaml
:language: yaml

Expand Down
7 changes: 7 additions & 0 deletions docs/ref/api/brigade.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Data
####

.. autoclass:: brigade.core.Data
:members:
:undoc-members:

Brigade
#######

Expand Down
6 changes: 6 additions & 0 deletions docs/ref/api/easy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Easy
====

.. automodule:: brigade.easy
:members:
:undoc-members:
3 changes: 2 additions & 1 deletion docs/ref/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Brigade API Reference
brigade
configuration
inventory
easy
task
exceptions
exceptions
7 changes: 7 additions & 0 deletions docs/ref/api/task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ AggregatedResult
.. autoclass:: brigade.core.task.AggregatedResult
:members:
:undoc-members:

MultiResult
################

.. autoclass:: brigade.core.task.MultiResult
:members:
:undoc-members:
8 changes: 8 additions & 0 deletions docs/ref/functions/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Functions
=========

.. toctree::
:maxdepth: 2
:glob:

*
6 changes: 6 additions & 0 deletions docs/ref/functions/text.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Text
====

.. automodule:: brigade.plugins.functions.text
:members:
:undoc-members:
1 change: 1 addition & 0 deletions docs/ref/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Reference Guides
:caption: Plugins

tasks/index
functions/index
inventory/index
2 changes: 2 additions & 0 deletions docs/ref/inventory/index.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _ref-inventory:

Inventory
=========

Expand Down
74 changes: 74 additions & 0 deletions docs/tutorials/intro/brigade.rst
Original file line number Diff line number Diff line change
@@ -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 configuration object.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place a link to the Config object?

2. An 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.plugins.inventory.simple.SimpleInventory object at 0x10606bf28>
>>> 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 <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.
2 changes: 0 additions & 2 deletions docs/tutorials/intro/explore.rst

This file was deleted.

9 changes: 6 additions & 3 deletions docs/tutorials/intro/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ We're glad you made it here! This is a great place to learn the basics of Brigad
Brigade at a glance <overview>
100% Python <python>
Installation guide <install>
Creating an inventory <inventory>
Exploring the inventory <explore>
Running tasks <run>
inventory
brigade
running_tasks
running_tasks_different_hosts
running_tasks_grouping
running_tasks_errors
133 changes: 131 additions & 2 deletions docs/tutorials/intro/inventory.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,131 @@
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.

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 <brigade.plugins.inventory.simple.SimpleInventory>` with the data located in ``/examples/inventory/``. We will also use the ``Vagrantfile`` located there so you should be able to reproduce everything. You can head to `Hosts/Groups contents`_ to see the contents of the file just for reference.

First, let's create the inventory::
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this say "create", not load?


>>> 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}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I don't like about this is that the output requires side scrolling to see it all. However I don't know if it would be better to print it vertically either since that will just make the page longer..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I pretty strongly disliked the horizontal scrolling (it was close to unreadable). It would be much better if it displayed vertically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will try to fix. I am going to move the tutorial to a jupyter notebook anyway so we can run it and make sure it's still valid. I already moved most of the examples.

>>> 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 "<stdin>", line 1, in <module>
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.


Hosts/Groups contents
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful if this was displayed before the inventory was loaded. I think it makes it easier to understand what data we are looking at.

---------------------


* ``hosts.yaml``

.. literalinclude:: ../../../examples/inventory/hosts.yaml

* ``groups.yaml``

.. literalinclude:: ../../../examples/inventory/groups.yaml
2 changes: 0 additions & 2 deletions docs/tutorials/intro/run.rst

This file was deleted.

Loading