diff options
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/lldp.xml.in | 317 | ||||
-rw-r--r-- | op-mode-definitions/lldp.xml | 37 | ||||
-rwxr-xr-x | src/conf_mode/lldp.py | 299 | ||||
-rwxr-xr-x | src/op_mode/lldp_op.py | 164 |
5 files changed, 716 insertions, 102 deletions
diff --git a/debian/control b/debian/control index f62037af2..931b34bc1 100644 --- a/debian/control +++ b/debian/control @@ -69,6 +69,7 @@ Depends: python3, vyos-qat-utilities, ssl-cert, nginx-light, + lldpd, hostapd (>= 0.6.8), wpasupplicant (>= 0.6.7), iw, diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in new file mode 100644 index 000000000..b44f4baf6 --- /dev/null +++ b/interface-definitions/lldp.xml.in @@ -0,0 +1,317 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="lldp" owner="${vyos_conf_scripts_dir}/lldp.py"> + <properties> + <help>LLDP settings</help> + <priority>985</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Location data for interface</help> + <valueHelp> + <format>all</format> + <description>Location data all interfaces</description> + </valueHelp> + <valueHelp> + <format><intf></format> + <description>Location data for a specific interface</description> + </valueHelp> + <completionHelp> + <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + </completionHelp> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable lldp on this interface</help> + <valueless/> + </properties> + </leafNode> + <node name="location"> + <properties> + <help>LLDP-MED location data [REQUIRED]</help> + </properties> + <children> + <node name="civic-based"> + <properties> + <help>Civic-based location data</help> + </properties> + <children> + <tagNode name="ca-type"> + <properties> + <help>LLDP-MED Civic Address type [REQUIRED]</help> + <valueHelp> + <format>0</format> + <description>Language</description> + </valueHelp> + <valueHelp> + <format>1</format> + <description>National subdivisions</description> + </valueHelp> + <valueHelp> + <format>2</format> + <description>County, parish, district</description> + </valueHelp> + <valueHelp> + <format>3</format> + <description>City, township</description> + </valueHelp> + <valueHelp> + <format>4</format> + <description>City division, borough, ward</description> + </valueHelp> + <valueHelp> + <format>5</format> + <description>Neighborhood, block</description> + </valueHelp> + <valueHelp> + <format>6</format> + <description>Street</description> + </valueHelp> + <valueHelp> + <format>16</format> + <description>Leading street direction</description> + </valueHelp> + <valueHelp> + <format>17</format> + <description>Trailing street suffix</description> + </valueHelp> + <valueHelp> + <format>18</format> + <description>Street suffix</description> + </valueHelp> + <valueHelp> + <format>19</format> + <description>House number</description> + </valueHelp> + <valueHelp> + <format>20</format> + <description>House number suffix</description> + </valueHelp> + <valueHelp> + <format>21</format> + <description>Landmark or vanity address</description> + </valueHelp> + <valueHelp> + <format>22</format> + <description>Additional location info</description> + </valueHelp> + <valueHelp> + <format>23</format> + <description>Name</description> + </valueHelp> + <valueHelp> + <format>24</format> + <description>Postal/ZIP code</description> + </valueHelp> + <valueHelp> + <format>25</format> + <description>Building</description> + </valueHelp> + <valueHelp> + <format>26</format> + <description>Unit</description> + </valueHelp> + <valueHelp> + <format>27</format> + <description>Floor</description> + </valueHelp> + <valueHelp> + <format>28</format> + <description>Room number</description> + </valueHelp> + <valueHelp> + <format>29</format> + <description>Place type</description> + </valueHelp> + <valueHelp> + <format>128</format> + <description>Script</description> + </valueHelp> + <constraintErrorMessage>ca-type must between 0-128</constraintErrorMessage> + <constraint> + <validator name="numeric" argument="--range 0-128"/> + </constraint> + </properties> + <children> + <leafNode name="ca-value"> + <properties> + <help>Civic address value for ca-type [REQUIRED]</help> + <constraintErrorMessage>ca-value must be less than 256 characters</constraintErrorMessage> + <constraint> + <regex>^[A-Za-z0-9\ \-\_\.\,]{0,255}$</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="country-code"> + <properties> + <help>Country code [REQUIRED]</help> + <valueHelp> + <format><AA></format> + <description>Two letter ISO 3166 country code</description> + </valueHelp> + <constraintErrorMessage>country-code must be 2 characters</constraintErrorMessage> + <constraint> + <regex>^[a-zA-Z]{2}$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="coordinate-based"> + <properties> + <help>Coordinate based location</help> + </properties> + <children> + <leafNode name="altitude"> + <properties> + <help>Altitude in meters</help> + <valueHelp> + <format>[+-]<meters></format> + <description>Altitude in meters</description> + </valueHelp> + <constraintErrorMessage>Altitude should be a positive or negative number</constraintErrorMessage> + <constraint> + <validator name="numeric"/> + </constraint> + </properties> + </leafNode> + <leafNode name="datum"> + <properties> + <help>Coordinate datum type</help> + <valueHelp> + <format>WGS84</format> + <description>WGS84 (default)</description> + </valueHelp> + <valueHelp> + <format>NAD83</format> + <description>NAD83</description> + </valueHelp> + <valueHelp> + <format>MLLW</format> + <description>NAD83/MLLW</description> + </valueHelp> + <completionHelp> + <list>WGS84 NAD83 MLLW</list> + </completionHelp> + <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage> + <constraint> + <regex>^(WGS84|NAD83|MLLW)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="latitude"> + <properties> + <help>Latitude [REQUIRED]</help> + <valueHelp> + <format><latitude></format> + <description>Latitude (example "37.524449N")</description> + </valueHelp> + <constraintErrorMessage>Latitude should be a number followed by S or N</constraintErrorMessage> + <constraint> + <regex>^(\d+)(\.\d+)?[nNsS]$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="longitude"> + <properties> + <help>Longitude [REQUIRED]</help> + <valueHelp> + <format><longitude></format> + <description>Longitude (example "122.267255W")</description> + </valueHelp> + <constraintErrorMessage>Longiture should be a number followed by E or W</constraintErrorMessage> + <constraint> + <regex>^(\d+)(\.\d+)?[eEwW]$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="elin"> + <properties> + <help>ECS ELIN (Emergency location identifier number)</help> + <valueHelp> + <format>0-9999999999</format> + <description>Emergency Call Service ELIN number (between 10-25 numbers)</description> + </valueHelp> + <constraint> + <regex>[0-9]{10,25}$</regex> + </constraint> + <constraintErrorMessage>ELIN number must be between 10-25 numbers</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <node name="legacy-protocols"> + <properties> + <help>Legacy (vendor specific) protocols</help> + </properties> + <children> + <leafNode name="cdp"> + <properties> + <help>Listen for CDP for Cisco routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="edp"> + <properties> + <help>Listen for EDP for Extreme routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="fdp"> + <properties> + <help>Listen for FDP for Foundry routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="sonmp"> + <properties> + <help>Listen for SONMP for Nortel routers/switches</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="management-address"> + <properties> + <help>Management IP Address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 Management Address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 Management Address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <node name="snmp"> + <properties> + <help>SNMP parameters for LLDP</help> + </properties> + <children> + <leafNode name="enable"> + <properties> + <help>Enable SNMP queries of the LLDP database</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/lldp.xml b/op-mode-definitions/lldp.xml new file mode 100644 index 000000000..105bfe237 --- /dev/null +++ b/op-mode-definitions/lldp.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="lldp"> + <properties> + <help>Show LLDP (Link Layer Discovery Protocol)</help> + </properties> + <children> + <node name="neighbors"> + <properties> + <help>Show LLDP neighbors</help> + </properties> + <command>${vyos_op_scripts_dir}/lldp_op.py --all</command> + <children> + <node name="detail"> + <properties> + <help>Show LLDP neighbor details</help> + </properties> + <command>/usr/sbin/lldpctl -f plain</command> + </node> + <tagNode name="interface"> + <properties> + <help>Show LLDP for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/lldp_op.py --interface $4</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index 27749c81c..da01de56f 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright (C) 2017-2019 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,23 +13,60 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -# import re import sys +import os +import jinja2 +from copy import deepcopy from vyos.config import Config from vyos import ConfigError +# Please be careful if you edit the template. +config_file = "/etc/default/lldpd" +lldp_tmpl = """ +### Autogenerated by lldp.py ### +DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}" + +""" + +vyos_config_file = "/etc/lldpd.d/01-vyos.conf" +vyos_tmpl = """ +### Autogenerated by lldp.py ### + +configure system platform VyOS +configure system description "VyOS {{ options.description }}" +{%- if listen_on -%} +configure system interface pattern "{{ options.listen_on | join(",") }}" +{%- endif %} +{% if options.addr -%} +configure system ip management pattern "{{ options.addr }}" +{%- endif %} +{%- for loc in location -%} +{%- if loc.elin %} +configure ports {{ loc.name }} med location elin "{{ loc.elin }}" +{%- endif %} +{%- if loc.coordinate_based %} +configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %} +{%- endif %} + + +{% endfor %} +""" + +default_config_data = { + "options": '', + "interface_list": '', + "location": '' +} def get_options(config): options = {} config.set_level('service lldp') - options['listen_vlan'] = config.exists('listen-vlan') - options["addr"] = config.return_value('management-address') + options['listen_vlan'] = config.exists('listen-vlan') + options['addr'] = config.return_value('management-address') snmp = config.exists('snmp enable') options["snmp"] = snmp @@ -39,75 +76,79 @@ def get_options(config): config.set_level('service lldp') config.set_level('service lldp legacy-protocols') - options["cdp"] = config.exists("cdp") - options["edp"] = config.exists("edp") - options["fdp"] = config.exists("fdp") - options["sonmp"] = config.exists("sonmp") - return options + options['cdp'] = config.exists('cdp') + options['edp'] = config.exists('edp') + options['fdp'] = config.exists('fdp') + options['sonmp'] = config.exists('sonmp') + + # start with an unknown version information + options['description'] = 'unknown' + options['listen_on'] = [] + return options def get_interface_list(config): config.set_level('service lldp') intfs_names = config.list_nodes('interface') if len(intfs_names) < 0: return 0 + interface_list = [] for name in intfs_names: - config.set_level("service lldp interface {0}".format(name)) + config.set_level('service lldp interface {0}'.format(name)) disable = config.exists('disable') intf = { - "name": name, - "disable": disable + 'name': name, + 'disable': disable } interface_list.append(intf) return interface_list def get_location_intf(config, name): - path = "service lldp interface {0}".format(name) + path = 'service lldp interface {0}'.format(name) config.set_level(path) - if config.exists("location"): - return 0 - config.set_level("{} location".format(path)) + + config.set_level('{} location'.format(path)) civic_based = {} - elin = None + elin = '' coordinate_based = {} if config.exists('civic-based'): - config.set_level("{} location civic-based".format(path)) - cc = config.return_value("country-code") - civic_based["country_code"] = cc - civic_based["ca_type"] = [] - ca_types_names = config.list_nodes('ca-type') - if ca_types_names: - for ca_types_name in ca_types_names: - config.set_level("{0} location civic-based ca-type {1}".format(path, ca_types_name)) - ca_val = config.return_value('ca-value') - ca_type = { - "name": ca_types_name, - "ca_val": ca_val - } - civic_based["ca_type"].append(ca_type) - - elif config.exists("elin"): - elin = config.return_value("elin") - - elif config.exists("coordinate-based"): - config.set_level("{} location coordinate-based".format(path)) - alt = config.return_value("altitude") - lat = config.return_value("latitude") - long = config.return_value("longitude") - datum = config.return_value("datum") - coordinate_based["altitude"] = alt - coordinate_based["latitude"] = lat - coordinate_based["longitude"] = long - coordinate_based["datum"] = datum + config.set_level('{} location civic-based'.format(path)) + civic_based['country_code'] = config.return_value('country-code') + civic_based['ca_type'] = [] + for ca_types_name in config.list_nodes('ca-type'): + config.set_level('{} location civic-based ca-type {}'.format(path, ca_types_name)) + ca_val = config.return_value('ca-value') + ca_type = { + 'name': ca_types_name, + 'ca_val': ca_val + } + civic_based['ca_type'].append(ca_type) + + if config.exists('elin'): + elin = config.return_value('elin') + + if config.exists('coordinate-based'): + config.set_level('{} location coordinate-based'.format(path)) + + coordinate_based['latitude'] = config.return_value('latitude') + coordinate_based['longitude'] = config.return_value('longitude') + + coordinate_based['altitude'] = '0' + if config.exists('altitude'): + coordinate_based['altitude'] = config.return_value('altitude') + + coordinate_based['datum'] = 'WGS84' + if config.exists('datum'): + coordinate_based['datum'] = config.return_value('datum') intf = { - "name": name, - "civic_based": civic_based, - "elin": elin, - "coordinate_based": coordinate_based + 'name': name, + 'civic_based': civic_based, + 'elin': elin, + 'coordinate_based': coordinate_based } return intf @@ -118,92 +159,146 @@ def get_location(config): intfs_names = config.list_nodes('interface') if len(intfs_names) < 0: return 0 - if config.exists("disable"): + + if config.exists('disable'): return 0 + intfs_location = [] for name in intfs_names: intf = get_location_intf(config, name) intfs_location.append(intf) + return intfs_location def get_config(): + lldp = deepcopy(default_config_data) conf = Config() - options = get_options(conf) - interface_list = get_interface_list(conf) - location = get_location(conf) - lldp = {"options": options, "interface_list": interface_list, "location": location} - return lldp + if not conf.exists('service lldp'): + return None + else: + lldp['options'] = get_options(conf) + lldp['interface_list'] = get_interface_list(conf) + lldp['location'] = get_location(conf) + + return lldp def verify(lldp): + # bail out early - looks like removal from running config + if lldp is None: + return # check location - for location in lldp["location"]: - + for location in lldp['location']: # check civic-based - if len(location["civic_based"]) > 0: - if len(location["coordinate_based"]) > 0 or location["elin"]: - raise ConfigError("Can only configure 1 location type for interface {0}".format(location["name"])) + if len(location['civic_based']) > 0: + if len(location['coordinate_based']) > 0 or location['elin']: + raise ConfigError('Can only configure 1 location type for interface {0}'.format(location['name'])) # check country-code - if not location["civic_based"]["country_code"]: - raise ConfigError("Invalid location for interface {0}: must configure the country code".format(location["name"])) + if not location['civic_based']['country_code']: + raise ConfigError('Invalid location for interface {0}:\n' \ + 'must configure the country code'.format(location['name'])) - if not re.match(r"^[a-zA-Z]{2}$", location["civic_based"]["country_code"]): - raise ConfigError("Invalid location for interface {0}: country-code must be 2 characters".format(location["name"])) + if not re.match(r'^[a-zA-Z]{2}$', location['civic_based']['country_code']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'country-code must be 2 characters'.format(location['name'])) # check ca-type - if len(location["civic_based"]["ca_type"]) < 0: - raise ConfigError("Invalid location for interface {0}: must define at least 1 ca-type".format(location["name"])) + if len(location['civic_based']['ca_type']) < 0: + raise ConfigError('Invalid location for interface {0}:\n' \ + 'must define at least 1 ca-type'.format(location['name'])) - for ca_type in location["civic_based"]["ca_type"]: - if not int(ca_type["name"]) in range(0, 129): - raise ConfigError("Invalid location for interface {0}: ca-type must between 0-128".format(location["name"])) + for ca_type in location['civic_based']['ca_type']: + if not int(ca_type['name']) in range(0, 129): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'ca-type must between 0-128'.format(location['name'])) - if not ca_type["ca_val"]: - raise ConfigError("Invalid location for interface {0}: must configure the ca-value for ca-type {1}".format(location["name"],ca_type["name"])) + if not ca_type['ca_val']: + raise ConfigError('Invalid location for interface {0}:\n' \ + 'must configure the ca-value for ca-type {1}'.format(location["name"],ca_type['name'])) # check coordinate-based - elif len(location["coordinate_based"]) > 0: + elif len(location['coordinate_based']) > 0: # check longitude and latitude - if not location["coordinate_based"]["longitude"]: - raise ConfigError("Must define longitude for interface {0}".format(location["name"])) + if not location['coordinate_based']['longitude']: + raise ConfigError('Must define longitude for interface {0}'.format(location['name'])) - if not location["coordinate_based"]["latitude"]: - raise ConfigError("Must define latitude for interface {0}".format(location["name"])) + if not location['coordinate_based']['latitude']: + raise ConfigError('Must define latitude for interface {0}'.format(location['name'])) - if not re.match(r"^(\d+)(\.\d+)?[nNsS]$", location["coordinate_based"]["latitude"]): - raise ConfigError("Invalid location for interface {0}: latitude should be a number followed by S or N".format(location["name"])) + if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'latitude should be a number followed by S or N'.format(location['name'])) - if not re.match(r"^(\d+)(\.\d+)?[eEwW]$", location["coordinate_based"]["longitude"]): - raise ConfigError("Invalid location for interface {0}: longitude should be a number followed by E or W".format(location["name"])) + if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'longitude should be a number followed by E or W'.format(location['name'])) # check altitude and datum if exist - if location["coordinate_based"]["altitude"]: - if not re.match(r"^[-+0-9\.]+$", location["coordinate_based"]["altitude"]): - raise ConfigError("Invalid location for interface {0}: altitude should be a positive or negative number".format(location["name"])) + if location['coordinate_based']['altitude']: + if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'altitude should be a positive or negative number'.format(location['name'])) - if location["coordinate_based"]["datum"]: - if not re.match(r"^(WGS84|NAD83|MLLW)$", location["coordinate_based"]["datum"]): - raise ConfigError("Invalid location for interface {0}: datum should be WGS84, NAD83, or MLLW".format(location["name"])) + if location['coordinate_based']['datum']: + if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']): + raise ConfigError("Invalid location for interface {0}:\n' \ + 'datum should be WGS84, NAD83, or MLLW".format(location['name'])) # check elin - elif len(location["elin"]) > 0: - if not re.match(r"^[0-9]{10,25}$", location["elin"]): - raise ConfigError("Invalid location for interface {0}: ELIN number must be between 10-25 numbers".format(location["name"])) + elif location['elin']: + if not re.match(r'^[0-9]{10,25}$', location['elin']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'ELIN number must be between 10-25 numbers'.format(location['name'])) # check options - if lldp["options"]["snmp"]: - if not lldp["options"]["sys_snmp"]: - raise ConfigError("SNMP must be configured to enable LLDP SNMP") - - -def generate(config): - pass - - -def apply(config): - pass + if lldp['options']['snmp']: + if not lldp['options']['sys_snmp']: + raise ConfigError('SNMP must be configured to enable LLDP SNMP') + + +def generate(lldp): + # bail out early - looks like removal from running config + if lldp is None: + return + + with open('/opt/vyatta/etc/version', 'r') as f: + tmp = f.read() + lldp['options']['description'] = tmp.split()[1] + + + # generate listen on interfaces + for intf in lldp['interface_list']: + tmp = '' + # add exclamation mark if interface is disabled + if intf['disable']: + tmp = '!' + + tmp += intf['name'] + lldp['options']['listen_on'].append(tmp) + + # generate /etc/default/lldpd + tmpl = jinja2.Template(lldp_tmpl) + config_text = tmpl.render(lldp) + with open(config_file, 'w') as f: + f.write(config_text) + + # generate /etc/lldpd.d/01-vyos.conf + tmpl = jinja2.Template(vyos_tmpl) + config_text = tmpl.render(lldp) + with open(vyos_config_file, 'w') as f: + f.write(config_text) + + +def apply(lldp): + if lldp: + # start/restart lldp service + os.system('sudo systemctl restart lldpd.service') + else: + # LLDP service has been terminated + os.system('sudo systemctl stop lldpd.service') + os.unlink(config_file) if __name__ == '__main__': try: diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py new file mode 100755 index 000000000..4d8fdbc99 --- /dev/null +++ b/src/op_mode/lldp_op.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import argparse +import jinja2 + +from xml.dom import minidom +from sys import exit +from subprocess import Popen, PIPE, STDOUT +from tabulate import tabulate + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") +parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") + +# Please be careful if you edit the template. +lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station + D - Docsis, T - Telephone, O - Other + +Device ID Local Proto Cap Platform Port ID +--------- ----- ----- --- -------- ------- +{% for n in neighbors -%} +{{ "%-25s" | format(n.chassis) }} {{ "%-9s" | format(n.interface) }} {{ "%-6s" | format(n.proto) }} {{ "%-5s" | format(n.cap) }} {{ "%-20s" | format(n.platform) }} {{ n.port }} +{% endfor -%} +""" + +def _get_neighbors(): + command = '/usr/sbin/lldpcli -f xml show neighbors' + p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + tmp = p.communicate()[0].strip() + return tmp.decode() + +def extract_neighbor(neighbor): + """ + Extract LLDP neighbor information from XML document passed as param neighbor + + <lldp> + <interface label="Interface" name="eth0" via="LLDP" rid="3" age="0 day, 00:17:42"> + <chassis label="Chassis"> + <id label="ChassisID" type="mac">00:50:56:9d:a6:11</id> + <name label="SysName">VyOS</name> + <descr label="SysDescr">VyOS unknown</descr> + <mgmt-ip label="MgmtIP">172.18.254.203</mgmt-ip> + <mgmt-ip label="MgmtIP">fe80::250:56ff:fe9d:a611</mgmt-ip> + <capability label="Capability" type="Bridge" enabled="off"/> + <capability label="Capability" type="Router" enabled="on"/> + <capability label="Capability" type="Wlan" enabled="off"/> + <capability label="Capability" type="Station" enabled="off"/> + </chassis> + <port label="Port"> + <id label="PortID" type="mac">00:50:56:9d:a6:11</id> + <descr label="PortDescr">eth0</descr> + <ttl label="TTL">120</ttl> + <auto-negotiation label="PMD autoneg" supported="no" enabled="no"> + <current label="MAU oper type">10GigBaseCX4 - X copper over 8 pair 100-Ohm balanced cable</current> + </auto-negotiation> + </port> + <vlan label="VLAN" vlan-id="203">eth0.203</vlan> + <lldp-med label="LLDP-MED"> + <device-type label="Device Type">Network Connectivity Device</device-type> + <capability label="Capability" type="Capabilities" available="yes"/> + <capability label="Capability" type="Policy" available="yes"/> + <capability label="Capability" type="Location" available="yes"/> + <capability label="Capability" type="MDI/PSE" available="yes"/> + <capability label="Capability" type="MDI/PD" available="yes"/> + <capability label="Capability" type="Inventory" available="yes"/> + <inventory label="Inventory"> + <hardware label="Hardware Revision">None</hardware> + <software label="Software Revision">4.19.54-amd64-vyos</software> + <firmware label="Firmware Revision">6.00</firmware> + <serial label="Serial Number">VMware-42 1d cf 87 ab 7f da 7e-3</serial> + <manufacturer label="Manufacturer">VMware, Inc.</manufacturer> + <model label="Model">VMware Virtual Platform</model> + <asset label="Asset ID">No Asset Tag</asset> + </inventory> + </lldp-med> + </interface> + </lldp> + """ + + device = { + 'interface' : neighbor.getAttribute('name'), + 'chassis' : '', + 'proto' : neighbor.getAttribute('via'), + 'descr' : '', + 'cap' : '', + 'platform' : '', + 'port' : '' + } + + # first change to <chassis> node and then retrieve <name> and <descr> + chassis = neighbor.getElementsByTagName('chassis') + device['chassis'] = chassis[0].getElementsByTagName('name')[0].firstChild.data + # Cisco IOS comes with a ',' remove character .... + device['platform'] = chassis[0].getElementsByTagName('descr')[0].firstChild.data[:20].replace(',',' ') + + # extract capabilities + for capability in chassis[0].getElementsByTagName('capability'): + # we are only interested in enabled capabilities ... + if capability.getAttribute('enabled') == "on": + if capability.getAttribute('type') == "Router": + device['cap'] += 'R' + elif capability.getAttribute('type') == "Bridge": + device['cap'] += 'B' + elif capability.getAttribute('type') == "Wlan": + device['cap'] += 'W' + elif capability.getAttribute('type') == "Station": + device['cap'] += 'S' + elif capability.getAttribute('type') == "Repeater": + device['cap'] += 'r' + elif capability.getAttribute('type') == "Telephone": + device['cap'] += 'T' + elif capability.getAttribute('type') == "Docsis": + device['cap'] += 'D' + elif capability.getAttribute('type') == "Other": + device['cap'] += 'O' + + # first change to <port> node and then retrieve <descr> + port = neighbor.getElementsByTagName('port') + port = port[0].getElementsByTagName('descr')[0].firstChild.data + device['port'] = port + + + return device + +if __name__ == '__main__': + args = parser.parse_args() + tmp = { 'neighbors' : [] } + + if args.all: + neighbors = minidom.parseString(_get_neighbors()) + for neighbor in neighbors.getElementsByTagName('interface'): + tmp['neighbors'].append( extract_neighbor(neighbor) ) + + elif args.interface: + neighbors = minidom.parseString(_get_neighbors()) + for neighbor in neighbors.getElementsByTagName('interface'): + # check if neighbor appeared on proper interface + if neighbor.getAttribute('name') == args.interface: + tmp['neighbors'].append( extract_neighbor(neighbor) ) + + else: + parser.print_help() + exit(1) + + tmpl = jinja2.Template(lldp_out) + config_text = tmpl.render(tmp) + print(config_text) + + exit(0) |