summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Poessinger <christian@poessinger.com>2019-12-22 22:16:48 +0100
committerChristian Poessinger <christian@poessinger.com>2019-12-22 22:16:48 +0100
commit5c5febcb940e2ee2f31822cce405535a8b10a5dd (patch)
tree316b86017e368e3b7bb975e016a59463eaf1dab4 /src
parent03fd2b7a966575172a8eb4a3a14ccd842d21a27b (diff)
parent7158bc3cc82e1a66473ef6c65e95e00f64d42b92 (diff)
downloadvyos-1x-5c5febcb940e2ee2f31822cce405535a8b10a5dd.tar.gz
vyos-1x-5c5febcb940e2ee2f31822cce405535a8b10a5dd.zip
Merge branch 't393-lldp-rewrite' of github.com:c-po/vyos-1x into current
* 't393-lldp-rewrite' of github.com:c-po/vyos-1x: lldp: T393: support both ELIN and ccordinate based location service in MED fixup lldp: T393: support IPv6 management address lldp: T393: add Emergency Location Identifier Number (ELIN) support lldp: T393: support parsing MED values lldp: T393: use flat dictionary when generating configs lldp: T393: support listen interfaces lldp: T393: add config options to /etc/lldpd.d lldp: T393: interface disable node must be valueless lldp: T393: first op mode command version lldp: T393: first running version of lldpd lldp: T393: 'snmp enable' must be <valueless/> lldp: T393: legacy-protocols must be <valueless/> Debian: lldp: T393: add lldpd dependency lldp: T393: rename XML interface definition to xml.in Revert "[LLDP] Remove the lldp.xml interface definition to avoid template ..."
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/lldp.py299
-rwxr-xr-xsrc/op_mode/lldp_op.py164
2 files changed, 361 insertions, 102 deletions
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)