Skip to content

Commit 37dbf74

Browse files
mykolafqiluo-msft
authored andcommitted
[lldp_syncd] add new OIDs - lldpRemTable & lldpLocPortTable (#5)
* [lldp_syncd] add new OIDs - lldpRemTable & lldpLocPortTable * [lldp_syncd] add new OIDs - lldpLocalSystemData * enhance code style, set default port descr to ' ' instead of '' * [lldp_syncd] introduce cache to avoid writing to db every 5s * review comments - merge conflict, exception handling, pep8 * LLDP_ENTRY_TABLE fix indentation * review comments - move scrap_output() out of class, return '' for missing system capabilities * review comments - test for isinstance dict, move _scrap_output()
1 parent 94f2700 commit 37dbf74

File tree

7 files changed

+386
-54
lines changed

7 files changed

+386
-54
lines changed

src/lldp_syncd/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
logger.addHandler(logging.NullHandler())
66

77
from .daemon import LldpSyncDaemon
8+
from .dbsyncd import DBSyncDaemon

src/lldp_syncd/daemon.py

Lines changed: 151 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from sonic_syncd import SonicSyncDaemon
1212
from . import logger
13-
from .conventions import LldpPortIdSubtype, LldpChassisIdSubtype
13+
from .conventions import LldpPortIdSubtype, LldpChassisIdSubtype, LldpSystemCapabilitiesMap
1414

1515
LLDPD_TIME_FORMAT = '%H:%M:%S'
1616

@@ -44,7 +44,8 @@ def parse_time(time_str):
4444
"""
4545
days, hour_min_secs = re.split(LLDPD_UPTIME_RE_SPLIT_PATTERN, time_str)
4646
struct_time = time.strptime(hour_min_secs, LLDPD_TIME_FORMAT)
47-
time_delta = datetime.timedelta(days=int(days), hours=struct_time.tm_hour, minutes=struct_time.tm_min,
47+
time_delta = datetime.timedelta(days=int(days), hours=struct_time.tm_hour,
48+
minutes=struct_time.tm_min,
4849
seconds=struct_time.tm_sec)
4950
return int(time_delta.total_seconds())
5051

@@ -56,6 +57,7 @@ class LldpSyncDaemon(SonicSyncDaemon):
5657
within the same Redis instance on a switch
5758
"""
5859
LLDP_ENTRY_TABLE = 'LLDP_ENTRY_TABLE'
60+
LLDP_LOC_CHASSIS_TABLE = 'LLDP_LOC_CHASSIS'
5961

6062
@unique
6163
class PortIdSubtypeMap(int, Enum):
@@ -109,19 +111,54 @@ class ChassisIdSubtypeMap(int, Enum):
109111
# chassis = int(LldpChassisIdSubtype.chassisComponent) # (unsupported by lldpd)
110112
local = int(LldpPortIdSubtype.local)
111113

114+
def get_sys_capability_list(self, if_attributes):
115+
"""
116+
Get a list of capabilities from interface attributes dictionary.
117+
:param if_attributes: interface attributes
118+
:return: list of capabilities
119+
"""
120+
try:
121+
# [{'enabled': ..., 'type': 'capability1'}, {'enabled': ..., 'type': 'capability2'}]
122+
capability_list = if_attributes['chassis'].values()[0]['capability']
123+
# {'enabled': ..., 'type': 'capability'}
124+
if isinstance(capability_list, dict):
125+
capability_list = [capability_list]
126+
except KeyError:
127+
logger.error("Failed to get system capabilities")
128+
return []
129+
return capability_list
130+
131+
def parse_sys_capabilities(self, capability_list, enabled=False):
132+
"""
133+
Get a bit map of capabilities, accoding to textual convention.
134+
:param capability_list: list of capabilities
135+
:param enabled: if true, consider only the enabled capabilities
136+
:return: string representing a bit map
137+
"""
138+
# chassis is incomplete, missing capabilities
139+
if not capability_list:
140+
return ""
141+
142+
sys_cap = 0x00
143+
for capability in capability_list:
144+
try:
145+
if (not enabled) or capability["enabled"]:
146+
sys_cap |= 128 >> LldpSystemCapabilitiesMap[capability["type"].lower()]
147+
except KeyError:
148+
logger.warning("Unknown capability {}".format(capability["type"]))
149+
return "%0.2X 00" % sys_cap
150+
112151
def __init__(self, update_interval=None):
113152
super(LldpSyncDaemon, self).__init__()
114153
self._update_interval = update_interval or DEFAULT_UPDATE_INTERVAL
115154
self.db_connector = SonicV2Connector()
116155
self.db_connector.connect(self.db_connector.APPL_DB)
117156

118-
def source_update(self):
119-
"""
120-
Invoke lldpctl and format as JSON
121-
"""
122-
cmd = ['/usr/sbin/lldpctl', '-f', 'json']
123-
logger.debug("Invoking lldpctl with: {}".format(cmd))
157+
self.chassis_cache = {}
158+
self.interfaces_cache = {}
124159

160+
@staticmethod
161+
def _scrap_output(cmd):
125162
try:
126163
# execute the subprocess command
127164
lldpctl_output = subprocess.check_output(cmd)
@@ -135,8 +172,22 @@ def source_update(self):
135172
except ValueError:
136173
logger.exception("Failed to parse lldpctl output")
137174
return None
138-
else:
139-
return lldpctl_json
175+
176+
return lldpctl_json
177+
178+
def source_update(self):
179+
"""
180+
Invoke lldpctl and format as JSON
181+
"""
182+
cmd = ['/usr/sbin/lldpctl', '-f', 'json']
183+
logger.debug("Invoking lldpctl with: {}".format(cmd))
184+
cmd_local = ['/usr/sbin/lldpcli', '-f', 'json', 'show', 'chassis']
185+
logger.debug("Invoking lldpcli with: {}".format(cmd_local))
186+
187+
lldp_json = self._scrap_output(cmd)
188+
lldp_json['lldp_loc_chassis'] = self._scrap_output(cmd_local)
189+
190+
return lldp_json
140191

141192
def parse_update(self, lldp_json):
142193
"""
@@ -148,20 +199,19 @@ def parse_update(self, lldp_json):
148199
149200
LldpRemEntry ::= SEQUENCE {
150201
lldpRemTimeMark TimeFilter,
151-
*lldpRemLocalPortNum LldpPortNumber,
152-
*lldpRemIndex Integer32,
202+
lldpRemLocalPortNum LldpPortNumber,
203+
lldpRemIndex Integer32,
153204
lldpRemChassisIdSubtype LldpChassisIdSubtype,
154205
lldpRemChassisId LldpChassisId,
155206
lldpRemPortIdSubtype LldpPortIdSubtype,
156207
lldpRemPortId LldpPortId,
157208
lldpRemPortDesc SnmpAdminString,
158209
lldpRemSysName SnmpAdminString,
159210
lldpRemSysDesc SnmpAdminString,
160-
*lldpRemSysCapSupported LldpSystemCapabilitiesMap,
161-
*lldpRemSysCapEnabled LldpSystemCapabilitiesMap
211+
lldpRemSysCapSupported LldpSystemCapabilitiesMap,
212+
lldpRemSysCapEnabled LldpSystemCapabilitiesMap
162213
}
163214
"""
164-
# TODO: *Implement
165215
try:
166216
interface_list = lldp_json['lldp'].get('interface') or []
167217
parsed_interfaces = defaultdict(dict)
@@ -175,12 +225,45 @@ def parse_update(self, lldp_json):
175225
if_attributes = interface_list[if_name]
176226

177227
if 'port' in if_attributes:
178-
parsed_interfaces[if_name].update(self.parse_port(if_attributes['port']))
228+
rem_port_keys = ('lldp_rem_port_id_subtype',
229+
'lldp_rem_port_id',
230+
'lldp_rem_port_desc')
231+
parsed_port = zip(rem_port_keys, self.parse_port(if_attributes['port']))
232+
parsed_interfaces[if_name].update(parsed_port)
233+
179234
if 'chassis' in if_attributes:
180-
parsed_interfaces[if_name].update(self.parse_chassis(if_attributes['chassis']))
235+
rem_chassis_keys = ('lldp_rem_chassis_id_subtype',
236+
'lldp_rem_chassis_id',
237+
'lldp_rem_sys_name',
238+
'lldp_rem_sys_desc')
239+
parsed_chassis = zip(rem_chassis_keys,
240+
self.parse_chassis(if_attributes['chassis']))
241+
parsed_interfaces[if_name].update(parsed_chassis)
181242

182243
# lldpRemTimeMark TimeFilter,
183-
parsed_interfaces[if_name].update({'lldp_rem_time_mark': str(parse_time(if_attributes.get('age')))})
244+
parsed_interfaces[if_name].update({'lldp_rem_time_mark':
245+
str(parse_time(if_attributes.get('age')))})
246+
247+
# lldpRemIndex
248+
parsed_interfaces[if_name].update({'lldp_rem_index': str(if_attributes.get('rid'))})
249+
250+
capability_list = self.get_sys_capability_list(if_attributes)
251+
# lldpSysCapSupported
252+
parsed_interfaces[if_name].update({'lldp_rem_sys_cap_supported':
253+
self.parse_sys_capabilities(capability_list)})
254+
# lldpSysCapEnabled
255+
parsed_interfaces[if_name].update({'lldp_rem_sys_cap_enabled':
256+
self.parse_sys_capabilities(
257+
capability_list, enabled=True)})
258+
if lldp_json['lldp_loc_chassis']:
259+
loc_chassis_keys = ('lldp_loc_chassis_id_subtype',
260+
'lldp_loc_chassis_id',
261+
'lldp_loc_sys_name',
262+
'lldp_loc_sys_desc')
263+
parsed_chassis = zip(loc_chassis_keys,
264+
self.parse_chassis(lldp_json['lldp_loc_chassis']
265+
['local-chassis']['chassis']))
266+
parsed_interfaces['local-chassis'].update(parsed_chassis)
184267

185268
return parsed_interfaces
186269
except (KeyError, ValueError):
@@ -190,29 +273,25 @@ def parse_chassis(self, chassis_attributes):
190273
try:
191274
if 'id' in chassis_attributes and 'id' not in chassis_attributes['id']:
192275
sys_name = ''
193-
rem_attributes = chassis_attributes
276+
attributes = chassis_attributes
194277
id_attributes = chassis_attributes['id']
195278
else:
196-
(sys_name, rem_attributes) = chassis_attributes.items()[0]
197-
id_attributes = rem_attributes.get('id', '')
279+
(sys_name, attributes) = chassis_attributes.items()[0]
280+
id_attributes = attributes.get('id', '')
198281

199282
chassis_id_subtype = str(self.ChassisIdSubtypeMap[id_attributes['type']].value)
200283
chassis_id = id_attributes.get('value', '')
201-
rem_desc = rem_attributes.get('descr', '')
284+
descr = attributes.get('descr', '')
202285
except (KeyError, ValueError):
203-
logger.exception("Could not infer system information from: {}".format(chassis_attributes))
204-
chassis_id_subtype = chassis_id = sys_name = rem_desc = ''
205-
206-
return {
207-
# lldpRemChassisIdSubtype LldpChassisIdSubtype,
208-
'lldp_rem_chassis_id_subtype': chassis_id_subtype,
209-
# lldpRemChassisId LldpChassisId,
210-
'lldp_rem_chassis_id': chassis_id,
211-
# lldpRemSysName SnmpAdminString,
212-
'lldp_rem_sys_name': sys_name,
213-
# lldpRemSysDesc SnmpAdminString,
214-
'lldp_rem_sys_desc': rem_desc,
215-
}
286+
logger.exception("Could not infer system information from: {}"
287+
.format(chassis_attributes))
288+
chassis_id_subtype = chassis_id = sys_name = descr = ''
289+
290+
return (chassis_id_subtype,
291+
chassis_id,
292+
sys_name,
293+
descr,
294+
)
216295

217296
def parse_port(self, port_attributes):
218297
port_identifiers = port_attributes.get('id')
@@ -224,33 +303,52 @@ def parse_port(self, port_attributes):
224303
logger.exception("Could not infer chassis subtype from: {}".format(port_attributes))
225304
subtype, value = None
226305

227-
return {
228-
# lldpRemPortIdSubtype LldpPortIdSubtype,
229-
'lldp_rem_port_id_subtype': subtype,
230-
# lldpRemPortId LldpPortId,
231-
'lldp_rem_port_id': value,
232-
# lldpRemSysDesc SnmpAdminString,
233-
'lldp_rem_port_desc': port_attributes.get('descr', '')
234-
}
306+
return (subtype,
307+
value,
308+
port_attributes.get('descr', ''),
309+
)
310+
311+
def cache_diff(self, cache, update):
312+
"""
313+
Find difference in keys between update and local cache dicts
314+
:param cache: Local cache dict
315+
:param update: Update dict
316+
:return: new, changed, deleted keys tuple
317+
"""
318+
new_keys = [key for key in update.keys() if key not in cache.keys()]
319+
changed_keys = list(set(key for key in update.keys() + cache.keys()
320+
if update[key] != cache.get(key)))
321+
deleted_keys = [key for key in cache.keys() if key not in update.keys()]
322+
return new_keys, changed_keys, deleted_keys
235323

236324
def sync(self, parsed_update):
237325
"""
238326
Sync LLDP information to redis DB.
239327
"""
240328
logger.debug("Initiating LLDPd sync to Redis...")
241329

242-
# First, delete all entries from the LLDP_ENTRY_TABLE
243-
client = self.db_connector.redis_clients[self.db_connector.APPL_DB]
244-
pattern = '{}:*'.format(LldpSyncDaemon.LLDP_ENTRY_TABLE)
245-
self.db_connector.delete_all_by_pattern(self.db_connector.APPL_DB, pattern)
330+
# push local chassis data to APP DB
331+
chassis_update = parsed_update.pop('local-chassis')
332+
if chassis_update != self.chassis_cache:
333+
self.db_connector.delete(self.db_connector.APPL_DB,
334+
LldpSyncDaemon.LLDP_LOC_CHASSIS_TABLE)
335+
for k, v in chassis_update.items():
336+
self.db_connector.set(self.db_connector.APPL_DB,
337+
LldpSyncDaemon.LLDP_LOC_CHASSIS_TABLE, k, v, blocking=True)
338+
logger.debug("sync'd: {}".format(json.dumps(chassis_update, indent=3)))
246339

247-
# Repopulate LLDP_ENTRY_TABLE by adding all elements from parsed_update
248-
for interface, if_attributes in parsed_update.items():
340+
new, changed, deleted = self.cache_diff(self.interfaces_cache, parsed_update)
341+
# Delete LLDP_ENTRIES which were modified or are missing
342+
for interface in changed + deleted:
343+
table_key = ':'.join([LldpSyncDaemon.LLDP_ENTRY_TABLE, interface])
344+
self.db_connector.delete(self.db_connector.APPL_DB, table_key)
345+
# Repopulate LLDP_ENTRY_TABLE by adding all changed elements
346+
for interface in changed + new:
249347
if re.match(SONIC_ETHERNET_RE_PATTERN, interface) is None:
250348
logger.warning("Ignoring interface '{}'".format(interface))
251349
continue
252-
for k, v in if_attributes.items():
253-
# port_table_key = LLDP_ENTRY_TABLE:INTERFACE_NAME;
254-
table_key = ':'.join([LldpSyncDaemon.LLDP_ENTRY_TABLE, interface])
350+
# port_table_key = LLDP_ENTRY_TABLE:INTERFACE_NAME;
351+
table_key = ':'.join([LldpSyncDaemon.LLDP_ENTRY_TABLE, interface])
352+
for k, v in parsed_update[interface].items():
255353
self.db_connector.set(self.db_connector.APPL_DB, table_key, k, v, blocking=True)
256-
logger.debug("sync'd: \n{}".format(json.dumps(if_attributes, indent=3)))
354+
logger.debug("sync'd: \n{}".format(json.dumps(parsed_update[interface], indent=3)))

src/lldp_syncd/dbsyncd.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import subprocess
2+
from swsssdk import ConfigDBConnector
3+
4+
from sonic_syncd import SonicSyncDaemon
5+
from . import logger
6+
7+
8+
class DBSyncDaemon(SonicSyncDaemon):
9+
"""
10+
A Thread that listens to changes in CONFIG DB,
11+
and contains handlers to configure lldpd accordingly.
12+
"""
13+
14+
def __init__(self):
15+
super(DBSyncDaemon, self).__init__()
16+
self.config_db = ConfigDBConnector()
17+
self.config_db.connect()
18+
logger.info("[lldp dbsyncd] Connected to configdb")
19+
self.port_table = {}
20+
21+
def run_command(self, command):
22+
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
23+
stdout = p.communicate()[0]
24+
p.wait()
25+
if p.returncode != 0:
26+
logger.error("[lldp dbsyncd] command execution returned {}. "
27+
"Command: '{}', stdout: '{}'".format(p.returncode, command, stdout))
28+
29+
def port_handler(self, key, data):
30+
"""
31+
Handle updates in 'PORT' table.
32+
"""
33+
# we're interested only in description for now
34+
if self.port_table[key].get("description") != data.get("description"):
35+
new_descr = data.get("description", " ")
36+
logger.info("[lldp dbsyncd] Port {} description changed to {}."
37+
.format(key, new_descr))
38+
self.run_command("lldpcli configure lldp portidsubtype local {} description '{}'"
39+
.format(key, new_descr))
40+
# update local cache
41+
self.port_table[key] = data
42+
43+
def run(self):
44+
self.port_table = self.config_db.get_table('PORT')
45+
# supply LLDP_LOC_ENTRY_TABLE and lldpd with correct values on start
46+
for port_name, attributes in self.port_table.items():
47+
self.run_command("lldpcli configure lldp portidsubtype local {} description '{}'"
48+
.format(port_name, attributes.get("description", " ")))
49+
50+
# subscribe for further changes
51+
self.config_db.subscribe('PORT', lambda table, key, data:
52+
self.port_handler(key, data))
53+
54+
logger.info("[lldp dbsyncd] Subscribed to configdb PORT table")
55+
self.config_db.listen()

src/lldp_syncd/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import logger
22
from .daemon import LldpSyncDaemon
3+
from .dbsyncd import DBSyncDaemon
34

45
DEFAULT_UPDATE_FREQUENCY = 10
56

@@ -8,8 +9,11 @@ def main(update_frequency=None):
89
try:
910
lldp_syncd = LldpSyncDaemon(update_frequency or DEFAULT_UPDATE_FREQUENCY)
1011
logger.info('Starting SONiC LLDP sync daemon...')
12+
dbsyncd = DBSyncDaemon()
1113
lldp_syncd.start()
14+
dbsyncd.start()
1215
lldp_syncd.join()
16+
dbsyncd.join()
1317
except KeyboardInterrupt:
1418
logger.info("ctrl-C captured, shutting down.")
1519
except Exception:

0 commit comments

Comments
 (0)