Skip to content

Commit 6d84d61

Browse files
committed
Merge branch 'develop'
2 parents 31dbfef + cba201a commit 6d84d61

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+6654
-271
lines changed

Diff for: Dockerfile

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM debian:stretch
2+
3+
## Install min deps
4+
RUN apt-get update
5+
6+
COPY ./ /var/cache/napalm/
7+
8+
## Install NAPALM & underlying libraries dependencies
9+
RUN apt-get install -y python-cffi python-dev libxslt1-dev libssl-dev libffi-dev \
10+
&& apt-get install -y python-pip \
11+
&& pip install -U cffi \
12+
&& pip install -U cryptography \
13+
&& pip install /var/cache/napalm/
14+
15+
RUN rm -rf /var/lib/apt/lists/*

Diff for: docs/index.rst

+1-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ Supported Network Operating Systems:
2222
extras
2323
______
2424

25-
In addition to the core drivers napalm also supports community driven drivers. You can find more information about them here:
26-
27-
TBD Link to extras/core drivers' documentation
25+
In addition to the core drivers napalm also supports community driven drivers. You can find more information about them here: :ref:`contributing-drivers`
2826

2927
Selecting the right driver
3028
--------------------------

Diff for: docs/support/index.rst

+11-11
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,24 @@ NAPALM supports passing certain optional arguments to some drivers. To do that y
109109
List of supported optional arguments
110110
____________________________________
111111

112-
* :code:`allow_agent` (ios, iosxr) - Paramiko argument, enable connecting to the SSH agent (default: ``False``).
113-
* :code:`alt_host_keys` (ios, iosxr) - If ``True``, host keys will be loaded from the file specified in ``alt_key_file``.
114-
* :code:`alt_key_file` (ios, iosxr) - SSH host key file to use (if ``alt_host_keys`` is ``True``).
112+
* :code:`allow_agent` (ios, iosxr, nxos_ssh) - Paramiko argument, enable connecting to the SSH agent (default: ``False``).
113+
* :code:`alt_host_keys` (ios, iosxr, nxos_ssh) - If ``True``, host keys will be loaded from the file specified in ``alt_key_file``.
114+
* :code:`alt_key_file` (ios, iosxr, nxos_ssh) - SSH host key file to use (if ``alt_host_keys`` is ``True``).
115115
* :code:`auto_rollback_on_error` (ios) - Disable automatic rollback (certain versions of IOS support configure replace, but not rollback on error) (default: ``True``).
116116
* :code:`config_lock` (iosxr, junos) - Lock the config during open() (default: ``False``).
117117
* :code:`canonical_int` (ios) - Convert operational interface's returned name to canonical name (fully expanded name) (default: ``False``).
118118
* :code:`dest_file_system` (ios) - Destination file system for SCP transfers (default: ``flash:``).
119119
* :code:`enable_password` (eos) - Password required to enter privileged exec (enable) (default: ``''``).
120-
* :code:`global_delay_factor` (ios) - Allow for additional delay in command execution (default: ``1``).
120+
* :code:`global_delay_factor` (ios, nxos_ssh) - Allow for additional delay in command execution (default: ``1``).
121121
* :code:`ignore_warning` (junos) - Allows to set `ignore_warning` when loading configuration to avoid exceptions via junos-pyez. (default: ``False``).
122-
* :code:`keepalive` (junos, iosxr) - SSH keepalive interval, in seconds (default: ``30`` seconds).
123-
* :code:`key_file` (junos, ios, iosxr) - Path to a private key file. (default: ``False``).
124-
* :code:`port` (eos, iosxr, junos, ios, nxos) - Allows you to specify a port other than the default.
125-
* :code:`secret` (ios) - Password required to enter privileged exec (enable) (default: ``''``).
126-
* :code:`ssh_config_file` (junos, ios, iosxr) - File name of OpenSSH configuration file.
127-
* :code:`ssh_strict` (iosxr, ios) - Automatically reject unknown SSH host keys (default: ``False``, which means unknown SSH host keys will be accepted).
122+
* :code:`keepalive` (iosxr, junos) - SSH keepalive interval, in seconds (default: ``30`` seconds).
123+
* :code:`key_file` (ios, iosxr, junos, nxos_ssh) - Path to a private key file. (default: ``False``).
124+
* :code:`port` (eos, ios, iosxr, junos, nxos, nxos_ssh) - Allows you to specify a port other than the default.
125+
* :code:`secret` (ios, nxos_ssh) - Password required to enter privileged exec (enable) (default: ``''``).
126+
* :code:`ssh_config_file` (ios, iosxr, junos, nxos_ssh) - File name of OpenSSH configuration file.
127+
* :code:`ssh_strict` (ios, iosxr, nxos_ssh) - Automatically reject unknown SSH host keys (default: ``False``, which means unknown SSH host keys will be accepted).
128128
* :code:`transport` (eos, ios, nxos) - Protocol to connect with (see `The transport argument`_ for more information).
129-
* :code:`use_keys` (iosxr, ios, panos) - Paramiko argument, enable searching for discoverable private key files in ``~/.ssh/`` (default: ``False``).
129+
* :code:`use_keys` (ios, iosxr, nxos_ssh) - Paramiko argument, enable searching for discoverable private key files in ``~/.ssh/`` (default: ``False``).
130130
* :code:`eos_autoComplete` (eos) - Allows to set `autoComplete` when running commands. (default: ``None`` equivalent to ``False``)
131131

132132
The transport argument

Diff for: docs/tutorials/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Tutorials
1313
extend_driver
1414
wrapup
1515
ansible-napalm
16+
mock_driver

Diff for: docs/tutorials/mock_driver.rst

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
Unit tests: Mock driver
2+
=======================
3+
4+
A mock driver is a software that imitates the response pattern of another
5+
system. It is meant to do nothing but returns the same predictable result,
6+
usually of the cases in a testing environment.
7+
8+
A driver `mock` can mock all actions done by a common napalm driver. It can be
9+
used for unit tests, either to test napalm itself or inside external projects
10+
making use of napalm.
11+
12+
13+
Overview
14+
--------
15+
16+
For any action, the ``mock`` driver will use a file matching a specific pattern
17+
to return its content as a result.
18+
19+
Each of these files will be located inside a directory specified at the driver
20+
initialization. Their names depend on the entire call name made to the
21+
driver, and about their order in the call stack.
22+
23+
24+
Replacing a standard driver by a ``mock``
25+
-----------------------------------------
26+
27+
Get the driver in napalm::
28+
29+
>>> import napalm
30+
>>> driver = napalm.get_network_driver('mock')
31+
32+
And instantiate it with any host and credentials::
33+
34+
device = driver(
35+
hostname='foo', username='user', password='pass',
36+
optional_args={'path': path_to_results}
37+
)
38+
39+
Like other drivers, ``mock`` takes optional arguments:
40+
41+
- ``path`` - Required. Directory where results files are located
42+
43+
Open the driver::
44+
45+
>>> device.open()
46+
47+
A user should now be able to call any function of a standard driver::
48+
49+
>>> device.get_network_instances()
50+
51+
But should get an error because no mocked data is yet written::
52+
53+
NotImplementedError: You can provide mocked data in get_network_instances.1
54+
55+
56+
Mocked data
57+
-----------
58+
59+
We will use ``/tmp/mock`` as an example of a directory that will contain
60+
our mocked data. Define a device using this path::
61+
62+
>>> with driver('foo', 'user', 'pass', optional_args={'path': '/tmp/mock'}) as device:
63+
64+
Mock a single call
65+
~~~~~~~~~~~~~~~~~~
66+
67+
In order to be able to call, for example, ``device.get_interfaces()``, a mocked
68+
data is needed.
69+
70+
To build the file name that the driver will look for, take the function name
71+
(``get_interfaces``) and suffix it with the place of this call in the device
72+
call stack.
73+
74+
.. note::
75+
``device.open()`` counts as a command. Each following order of call will
76+
start at 1.
77+
78+
Here, ``get_interfaces`` is the first call made to ``device`` after ``open()``,
79+
so the mocked data need to be put in ``/tmp/mock/get_interfaces.1``::
80+
81+
82+
{
83+
"Ethernet1/1": {
84+
"is_up": true, "is_enabled": true, "description": "",
85+
"last_flapped": 1478175306.5162635, "speed": 10000,
86+
"mac_address": "FF:FF:FF:FF:FF:FF"
87+
},
88+
"Ethernet1/2": {
89+
"is_up": true, "is_enabled": true, "description": "",
90+
"last_flapped": 1492172106.5163276, "speed": 10000,
91+
"mac_address": "FF:FF:FF:FF:FF:FF"
92+
}
93+
}
94+
95+
The content is the wanted result of ``get_interfaces`` in JSON, exactly as
96+
another driver would return it.
97+
98+
Mock multiple iterative calls
99+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100+
101+
If ``/tmp/mock/get_interfaces.1`` was defined and used, for any other call on
102+
the same device, the number of calls needs to be incremented.
103+
104+
For example, to call ``device.get_interfaces_ip()`` after
105+
``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces_ip.2`` needs
106+
to be defined::
107+
108+
{
109+
"Ethernet1/1": {
110+
"ipv6": {"2001:DB8::": {"prefix_length": 64}}
111+
}
112+
}
113+
114+
Mock a CLI call
115+
~~~~~~~~~~~~~~~
116+
117+
``device.cli(commands)`` calls are a bit different to mock, as a suffix
118+
corresponding to the command applied to the device needs to be added. As
119+
before, the data mocked file will start by ``cli`` and the number of calls done
120+
before (here, ``cli.1``). Then, the same process needs to be applied to each
121+
command.
122+
123+
Each command needs to be sanitized: any special character (`` -,./\``, etc.)
124+
needs to be replaced by ``_``. Add the index of this command as it is sent to
125+
``device.cli()``. Each file then will contain the raw wanted output of its
126+
associated command.
127+
128+
Example
129+
^^^^^^^
130+
131+
Example with 2 commands, ``show interface Ethernet 1/1`` and ``show interface
132+
Ethernet 1/2``.
133+
134+
To define the mocked data, create a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_1.0``::
135+
136+
Ethernet1/1 is up
137+
admin state is up, Dedicated Interface
138+
139+
And a file ``/tmp/mock/cli.1.show_interface_Ethernet_1_2.1``::
140+
141+
Ethernet1/2 is up
142+
admin state is up, Dedicated Interface
143+
144+
And now they can be called::
145+
146+
>>> device.cli(["show interface Ethernet 1/1", "show interface Ethernet 1/2"])
147+
148+
149+
Mock an error
150+
~~~~~~~~~~~~~
151+
152+
The `mock` driver can raise an exception during a call, to simulate an error.
153+
An error definition is actually a json composed of 3 keys:
154+
155+
* `exception`: the exception type that will be raised
156+
* `args` and `kwargs`: parameters sent to the exception constructor
157+
158+
For example, to raise the exception `ConnectionClosedException` when calling
159+
``device.get_interfaces()``, the file ``/tmp/mock/get_interfaces.1`` needs to
160+
be defined::
161+
162+
{
163+
"exception": "napalm.base.exceptions.ConnectionClosedException",
164+
"args": [
165+
"Connection closed."
166+
],
167+
"kwargs": {}
168+
}
169+
170+
Now calling `get_interfaces()` for the 1st time will raise an exception::
171+
172+
>>> device.get_interfaces()
173+
ConnectionClosedException: Connection closed
174+
175+
As before, mock will depend on the number of calls. If a second file
176+
``/tmp/mock/get_interfaces.2`` was defined and filled with some expected data
177+
(not an exception), retrying `get_interfaces()` will run correctly if the first
178+
exception was caught.

Diff for: napalm/base/helpers.py

+36-34
Original file line numberDiff line numberDiff line change
@@ -102,44 +102,46 @@ def textfsm_extractor(cls, template_name, raw_text):
102102
:return: table-like list of entries
103103
"""
104104
textfsm_data = list()
105-
cls.__class__.__name__.replace('Driver', '')
106-
current_dir = os.path.dirname(os.path.abspath(sys.modules[cls.__module__].__file__))
107-
template_dir_path = '{current_dir}/utils/textfsm_templates'.format(
108-
current_dir=current_dir
109-
)
110-
template_path = '{template_dir_path}/{template_name}.tpl'.format(
111-
template_dir_path=template_dir_path,
112-
template_name=template_name
113-
)
114-
115-
try:
116-
fsm_handler = textfsm.TextFSM(open(template_path))
117-
except IOError:
118-
raise napalm.base.exceptions.TemplateNotImplemented(
119-
"TextFSM template {template_name}.tpl is not defined under {path}".format(
120-
template_name=template_name,
121-
path=template_dir_path
122-
)
105+
fsm_handler = None
106+
for c in cls.__class__.mro():
107+
if c is object:
108+
continue
109+
current_dir = os.path.dirname(os.path.abspath(sys.modules[c.__module__].__file__))
110+
template_dir_path = '{current_dir}/utils/textfsm_templates'.format(
111+
current_dir=current_dir
123112
)
124-
except textfsm.TextFSMTemplateError as tfte:
125-
raise napalm.base.exceptions.TemplateRenderException(
126-
"Wrong format of TextFSM template {template_name}: {error}".format(
127-
template_name=template_name,
128-
error=py23_compat.text_type(tfte)
129-
)
113+
template_path = '{template_dir_path}/{template_name}.tpl'.format(
114+
template_dir_path=template_dir_path,
115+
template_name=template_name
130116
)
131117

132-
objects = fsm_handler.ParseText(raw_text)
133-
134-
for obj in objects:
135-
index = 0
136-
entry = {}
137-
for entry_value in obj:
138-
entry[fsm_handler.header[index].lower()] = entry_value
139-
index += 1
140-
textfsm_data.append(entry)
118+
try:
119+
with open(template_path) as f:
120+
fsm_handler = textfsm.TextFSM(f)
121+
122+
for obj in fsm_handler.ParseText(raw_text):
123+
entry = {}
124+
for index, entry_value in enumerate(obj):
125+
entry[fsm_handler.header[index].lower()] = entry_value
126+
textfsm_data.append(entry)
127+
128+
return textfsm_data
129+
except IOError: # Template not present in this class
130+
continue # Continue up the MRO
131+
except textfsm.TextFSMTemplateError as tfte:
132+
raise napalm.base.exceptions.TemplateRenderException(
133+
"Wrong format of TextFSM template {template_name}: {error}".format(
134+
template_name=template_name,
135+
error=py23_compat.text_type(tfte)
136+
)
137+
)
141138

142-
return textfsm_data
139+
raise napalm.base.exceptions.TemplateNotImplemented(
140+
"TextFSM template {template_name}.tpl is not defined under {path}".format(
141+
template_name=template_name,
142+
path=template_dir_path
143+
)
144+
)
143145

144146

145147
def find_txt(xml_tree, path, default=''):

Diff for: napalm/base/mock.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
from __future__ import unicode_literals
1818

1919
from napalm.base.base import NetworkDriver
20+
from napalm.base.utils import py23_compat
2021
import napalm.base.exceptions
2122

22-
import inspect
2323
import json
2424
import os
2525
import re
@@ -28,10 +28,6 @@
2828
from pydoc import locate
2929

3030

31-
# inspect.getargspec deprecated in Python 3.5, use getfullargspec if available
32-
inspect_getargspec = getattr(inspect, "getfullargspec", inspect.getargspec)
33-
34-
3531
def raise_exception(result):
3632
exc = locate(result["exception"])
3733
if exc:
@@ -49,7 +45,7 @@ def is_mocked_method(method):
4945

5046
def mocked_method(path, name, count):
5147
parent_method = getattr(NetworkDriver, name)
52-
parent_method_args = inspect_getargspec(parent_method)
48+
parent_method_args = py23_compat.argspec(parent_method)
5349
modifier = 0 if 'self' not in parent_method_args.args else 1
5450

5551
def _mocked_method(*args, **kwargs):

Diff for: napalm/base/netmiko_helpers.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# License for the specific language governing permissions and limitations under
1111
# the License.
1212
from __future__ import unicode_literals
13-
import inspect
13+
from napalm.base.utils import py23_compat
1414
from netmiko import BaseConnection
1515

1616

@@ -20,13 +20,15 @@ def netmiko_args(optional_args):
2020
Return a dictionary of these optional args that will be passed into the Netmiko
2121
ConnectHandler call.
2222
"""
23-
netmiko_args, _, _, netmiko_defaults = inspect.getargspec(BaseConnection.__init__)
23+
fields = py23_compat.argspec(BaseConnection.__init__)
24+
args = fields[0]
25+
defaults = fields[3]
2426

25-
check_self = netmiko_args.pop(0)
27+
check_self = args.pop(0)
2628
if check_self != 'self':
2729
raise ValueError("Error processing Netmiko arguments")
2830

29-
netmiko_argument_map = dict(zip(netmiko_args, netmiko_defaults))
31+
netmiko_argument_map = dict(zip(args, defaults))
3032

3133
# Netmiko arguments that are integrated into NAPALM already
3234
netmiko_filter = ['ip', 'host', 'username', 'password', 'device_type', 'timeout']

0 commit comments

Comments
 (0)