Merge pull request #40473 from cloudflare/napalm-yang

New execution module: Napalm YANG
This commit is contained in:
Nicole Thomas 2017-04-03 09:30:50 -06:00 committed by GitHub
commit 595888a439
5 changed files with 682 additions and 0 deletions

View File

@ -258,6 +258,7 @@ execution modules
napalm_route
napalm_snmp
napalm_users
napalm_yang_mod
netaddress
netbsd_sysctl
netbsdservice

View File

@ -0,0 +1,5 @@
salt.modules.napalm_yang_mod module
===================================
.. automodule:: salt.modules.napalm_yang_mod
:members:

View File

@ -253,6 +253,10 @@ New modules:
load ACL (firewall) configuration on network devices.
- :mod:`Network ACL state <salt.states.netacl>` - Manage the firewall
configuration. It only requires writing the pillar structure correctly!
- :mod:`NAPALM YANG execution module <salt.modules.napalm_yang_mod>` - Parse,
generate and load native device configuration in a standard way,
using the OpenConfig/IETF models. This module cotains also helpers for
the states.
- :mod:`NET finder <salt.runners.net>` - Runner to find details easily and
fast. It's smart enough to know what you are looking for. It will search
in the details of the network interfaces, IP addresses, MAC address tables,

View File

@ -166,3 +166,78 @@ def call(method, *args, **kwargs):
*args,
**clean_kwargs
)
@proxy_napalm_wrap
def compliance_report(filepath, **kwargs):
'''
Return the compliance report.
filepath
The absolute path to the validation file.
CLI Example:
.. code-block:: bash
salt '*' napalm.compliance_report ~/validate.yml
Validation File Example:
.. code-block:: yaml
- get_facts:
os_version: 4.17
- get_interfaces_ip:
Management1:
ipv4:
10.0.2.14:
prefix_length: 24
_mode: strict
Output Example:
.. code-block:: yaml
device1:
----------
comment:
out:
----------
complies:
False
get_facts:
----------
complies:
False
extra:
missing:
present:
----------
os_version:
----------
actual_value:
15.1F6-S1.4
complies:
False
nested:
False
get_interfaces_ip:
----------
complies:
False
extra:
missing:
- Management1
present:
----------
skipped:
result:
True
'''
return salt.utils.napalm.call(
napalm_device, # pylint: disable=undefined-variable
'compliance_report',
validation_file=filepath
)

View File

@ -0,0 +1,597 @@
# -*- coding: utf-8 -*-
'''
NAPALM YANG
===========
NAPALM YANG basic operations.
.. versionadded:: Nitrogen
'''
from __future__ import absolute_import
# Import python stdlib
import logging
# Import third party libs
try:
import napalm_yang
HAS_NAPALM_YANG = True
except ImportError:
HAS_NAPALM_YANG = False
# import NAPALM utils
import salt.utils.napalm
from salt.utils.napalm import proxy_napalm_wrap
# -----------------------------------------------------------------------------
# module properties
# -----------------------------------------------------------------------------
__virtualname__ = 'napalm_yang'
__proxyenabled__ = ['napalm']
# uses NAPALM-based proxy to interact with network devices
log = logging.getLogger(__file__)
# -----------------------------------------------------------------------------
# property functions
# -----------------------------------------------------------------------------
def __virtual__():
'''
NAPALM library must be installed for this module to work and run in a (proxy) minion.
This module in particular requires also napalm-yang.
'''
if not HAS_NAPALM_YANG:
return (False, 'Unable to load napalm_yang execution module: please install napalm-yang!')
return salt.utils.napalm.virtual(__opts__, __virtualname__, __file__)
# -----------------------------------------------------------------------------
# helper functions -- will not be exported
# -----------------------------------------------------------------------------
def _get_root_object(*models):
'''
Read list of models and returns a Root object with the proper models added.
'''
root = napalm_yang.base.Root()
for model in models:
current = napalm_yang
for part in model.split('.'):
current = getattr(current, part)
root.add_model(current)
return root
# -----------------------------------------------------------------------------
# callable functions
# -----------------------------------------------------------------------------
def diff(candidate, running, *models):
'''
Returns the difference between two configuration entities structured
according to the YANG model.
.. note::
This function is recommended to be used mostly as a state helper.
candidate
First model to compare.
running
Second model to compare.
models
A list of models to be used when comparing.
CLI Example:
.. code-block:: bash
salt '*' napalm_yang.diff {} {} models.openconfig_interfaces
Output Example:
.. code-block:: python
{
"interfaces": {
"interface": {
"both": {
"Port-Channel1": {
"config": {
"mtu": {
"first": "0",
"second": "9000"
}
}
}
},
"first_only": [
"Loopback0"
],
"second_only": [
"Loopback1"
]
}
}
}
'''
first = _get_root_object(*models)
first.load_dict(candidate)
second = _get_root_object(*models)
second.load_dict(running)
return napalm_yang.utils.diff(first, second)
@proxy_napalm_wrap
def parse(*models, **kwargs):
'''
Parse configuration from the device.
models
A list of models to be used when parsing.
config: ``False``
Parse config.
state: ``False``
Parse state.
profiles: ``None``
Use certain profiles to parse. If not specified, will use the device
default profile(s).
CLI Example:
.. code-block:: bash
salt '*' napalm_yang.parse models.openconfig_interfaces
Output Example:
.. code-block:: json
{
"interfaces": {
"interface": {
".local.": {
"name": ".local.",
"state": {
"admin-status": "UP",
"counters": {
"in-discards": 0,
"in-errors": 0,
"out-errors": 0
},
"enabled": True,
"ifindex": 0,
"last-change": 0,
"oper-status": "UP",
"type": "softwareLoopback"
},
"subinterfaces": {
"subinterface": {
".local..0": {
"index": ".local..0",
"state": {
"ifindex": 0,
"name": ".local..0"
}
}
}
}
},
"ae0": {
"name": "ae0",
"state": {
"admin-status": "UP",
"counters": {
"in-discards": 0,
"in-errors": 0,
"out-errors": 0
},
"enabled": True,
"ifindex": 531,
"last-change": 255203,
"mtu": 1518,
"oper-status": "DOWN"
},
"subinterfaces": {
"subinterface": {
"ae0.0": {
"index": "ae0.0",
"state": {
"description": "ASDASDASD",
"ifindex": 532,
"name": "ae0.0"
}
}
"ae0.32767": {
"index": "ae0.32767",
"state": {
"ifindex": 535,
"name": "ae0.32767"
}
}
}
}
},
"dsc": {
"name": "dsc",
"state": {
"admin-status": "UP",
"counters": {
"in-discards": 0,
"in-errors": 0,
"out-errors": 0
},
"enabled": True,
"ifindex": 5,
"last-change": 0,
"oper-status": "UP"
}
},
"ge-0/0/0": {
"name": "ge-0/0/0",
"state": {
"admin-status": "UP",
"counters": {
"in-broadcast-pkts": 0,
"in-discards": 0,
"in-errors": 0,
"in-multicast-pkts": 0,
"in-unicast-pkts": 16877,
"out-broadcast-pkts": 0,
"out-errors": 0,
"out-multicast-pkts": 0,
"out-unicast-pkts": 15742
},
"description": "management interface",
"enabled": True,
"ifindex": 507,
"last-change": 258467,
"mtu": 1400,
"oper-status": "UP"
},
"subinterfaces": {
"subinterface": {
"ge-0/0/0.0": {
"index": "ge-0/0/0.0",
"state": {
"description": "ge-0/0/0.0",
"ifindex": 521,
"name": "ge-0/0/0.0"
}
}
}
}
}
"irb": {
"name": "irb",
"state": {
"admin-status": "UP",
"counters": {
"in-discards": 0,
"in-errors": 0,
"out-errors": 0
},
"enabled": True,
"ifindex": 502,
"last-change": 0,
"mtu": 1514,
"oper-status": "UP",
"type": "ethernetCsmacd"
}
},
"lo0": {
"name": "lo0",
"state": {
"admin-status": "UP",
"counters": {
"in-discards": 0,
"in-errors": 0,
"out-errors": 0
},
"description": "lo0",
"enabled": True,
"ifindex": 6,
"last-change": 0,
"oper-status": "UP",
"type": "softwareLoopback"
},
"subinterfaces": {
"subinterface": {
"lo0.0": {
"index": "lo0.0",
"state": {
"description": "lo0.0",
"ifindex": 16,
"name": "lo0.0"
}
},
"lo0.16384": {
"index": "lo0.16384",
"state": {
"ifindex": 21,
"name": "lo0.16384"
}
},
"lo0.16385": {
"index": "lo0.16385",
"state": {
"ifindex": 22,
"name": "lo0.16385"
}
},
"lo0.32768": {
"index": "lo0.32768",
"state": {
"ifindex": 248,
"name": "lo0.32768"
}
}
}
}
}
}
}
}
'''
config = kwargs.pop('config', False)
state = kwargs.pop('state', False)
profiles = kwargs.pop('profiles', [])
if not profiles and hasattr(napalm_device, 'profile'): # pylint: disable=undefined-variable
profiles = napalm_device.profile # pylint: disable=undefined-variable
root = _get_root_object(*models)
parser_kwargs = {
'device': napalm_device, # pylint: disable=undefined-variable
'profile': profiles
}
if config:
root.parse_config(**parser_kwargs)
if state:
root.parse_state(**parser_kwargs)
return root
@proxy_napalm_wrap
def get_config(data, *models, **kwargs):
'''
Return the native config.
data
Dictionary structured with respect to the models referenced.
models
A list of models to be used when generating the config.
profiles: ``None``
Use certain profiles to generate the config.
If not specified, will use the platform default profile(s).
CLI Example:
.. code-block:: bash
salt '*' napalm_yang.get_config {} models.openconfig_interfaces
Output Example:
.. code-block:: text
interface et1
ip address 192.168.1.1/24
description Uplink1
mtu 9000
interface et2
ip address 192.168.2.1/24
description Uplink2
mtu 9000
'''
profiles = kwargs.pop('profiles', [])
if not profiles and hasattr(napalm_device, 'profile'): # pylint: disable=undefined-variable
profiles = napalm_device.profile # pylint: disable=undefined-variable
parser_kwargs = {
'profile': profiles
}
root = _get_root_object(*models)
root.load_dict(data)
return root.translate_config(**parser_kwargs)
@proxy_napalm_wrap
def load_config(data, *models, **kwargs):
'''
Generate and load the config on the device using the OpenConfig or IETF
models and device profiles.
data
Dictionary structured with respect to the models referenced.
models
A list of models to be used when generating the config.
profiles: ``None``
Use certain profiles to generate the config.
If not specified, will use the platform default profile(s).
test: ``False``
Dry run? If set as ``True``, will apply the config, discard
and return the changes. Default: ``False`` and will commit
the changes on the device.
commit: ``True``
Commit? Default: ``True``.
debug: ``False``
Debug mode. Will insert a new key under the output dictionary,
as ``loaded_config`` contaning the raw configuration loaded on the device.
replace: ``False``
Should replace the config with the new generate one?
CLI Example:
.. code-block:: bash
salt '*' napalm_yang.load_config {} models.openconfig_interfaces test=True debug=True
Output Example:
.. code-block:: yaml
device1:
----------
already_configured:
False
comment:
diff:
[edit interfaces ge-0/0/0]
- mtu 1400;
[edit interfaces ge-0/0/0 unit 0 family inet]
- dhcp;
[edit interfaces lo0]
- unit 0 {
- description lo0.0;
- }
+ unit 1 {
+ description "new loopback";
+ }
loaded_config:
<configuration>
<interfaces replace="replace">
<interface>
<name>ge-0/0/0</name>
<unit>
<name>0</name>
<family>
<inet/>
</family>
<description>ge-0/0/0.0</description>
</unit>
<description>management interface</description>
</interface>
<interface>
<name>ge-0/0/1</name>
<disable/>
<description>ge-0/0/1</description>
</interface>
<interface>
<name>ae0</name>
<unit>
<name>0</name>
<vlan-id>100</vlan-id>
<family>
<inet>
<address>
<name>192.168.100.1/24</name>
</address>
<address>
<name>172.20.100.1/24</name>
</address>
</inet>
</family>
<description>a description</description>
</unit>
<vlan-tagging/>
<unit>
<name>1</name>
<vlan-id>1</vlan-id>
<family>
<inet>
<address>
<name>192.168.101.1/24</name>
</address>
</inet>
</family>
<disable/>
<description>ae0.1</description>
</unit>
<vlan-tagging/>
<unit>
<name>2</name>
<vlan-id>2</vlan-id>
<family>
<inet>
<address>
<name>192.168.102.1/24</name>
</address>
</inet>
</family>
<description>ae0.2</description>
</unit>
<vlan-tagging/>
</interface>
<interface>
<name>lo0</name>
<unit>
<name>1</name>
<description>new loopback</description>
</unit>
<description>lo0</description>
</interface>
</interfaces>
</configuration>
result:
True
'''
config = get_config(data, *models, **kwargs)
test = kwargs.pop('test', False)
debug = kwargs.pop('debug', False)
commit = kwargs.pop('commit', True)
replace = kwargs.pop('replace', False)
return __salt__['net.load_config'](config=config,
test=test,
debug=debug,
commit=commit,
replace=replace,
inherit_napalm_device=napalm_device) # pylint: disable=undefined-variable
@proxy_napalm_wrap
def compliance_report(data, *models, **kwargs):
'''
Return the compliance report using YANG objects.
data
Dictionary structured with respect to the models referenced.
models
A list of models to be used when generating the config.
filepath
The absolute path to the validation file.
CLI Example:
.. code-block:: bash
salt '*' napalm_yang.compliance_report {} models.openconfig_interfaces filepath=~/validate.yml
Output Example:
.. code-block:: json
{
"skipped": [],
"complies": true,
"get_interfaces_ip": {
"missing": [],
"complies": true,
"present": {
"ge-0/0/0.0": {
"complies": true,
"nested": true
}
},
"extra": []
}
}
'''
filepath = kwargs.pop('filepath', '')
root = _get_root_object(*models)
root.load_dict(data)
return root.compliance_report(validation_file=filepath)