-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathdaemon.py
More file actions
380 lines (334 loc) · 16.3 KB
/
Copy pathdaemon.py
File metadata and controls
380 lines (334 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import datetime
import json
import re
import subprocess
import time
from collections import defaultdict
from enum import unique, Enum
from swsscommon.swsscommon import SonicV2Connector
from sonic_syncd import SonicSyncDaemon
from . import logger
from .conventions import LldpPortIdSubtype, LldpChassisIdSubtype, LldpSystemCapabilitiesMap
LLDPD_TIME_FORMAT = '%H:%M:%S'
DEFAULT_UPDATE_INTERVAL = 10
# Match Front | Backplace | Management interface
# TODO: Need to chamge to util function which can provide
# backplane interface name.
SONIC_ETHERNET_RE_PATTERN = r'^(Ethernet(\d+)|Ethernet-BP(\d+)|eth0)$'
LLDPD_UPTIME_RE_SPLIT_PATTERN = r' days?, '
def parse_time(time_str):
"""
From LLDPd/src/client/display.c:
static const char*
display_age(time_t lastchange)
{
static char sage[30];
int age = (int)(time(NULL) - lastchange);
if (snprintf(sage, sizeof(sage),
"%d day%s, %02d:%02d:%02d",
age / (60*60*24),
(age / (60*60*24) > 1)?"s":"",
(age / (60*60)) % 24,
(age / 60) % 60,
age % 60) >= sizeof(sage))
return "too much";
else
return sage;
}
:return: parsed age in time ticks (or seconds)
"""
days, hour_min_secs = re.split(LLDPD_UPTIME_RE_SPLIT_PATTERN, time_str)
struct_time = time.strptime(hour_min_secs, LLDPD_TIME_FORMAT)
time_delta = datetime.timedelta(days=int(days), hours=struct_time.tm_hour,
minutes=struct_time.tm_min,
seconds=struct_time.tm_sec)
return int(time_delta.total_seconds())
class LldpSyncDaemon(SonicSyncDaemon):
"""
This script uploads lldp information to Redis DB.
Required lldp counters are kept in a separate database (number 1)
within the same Redis instance on a switch
"""
LLDP_ENTRY_TABLE = 'LLDP_ENTRY_TABLE'
LLDP_LOC_CHASSIS_TABLE = 'LLDP_LOC_CHASSIS'
@unique
class PortIdSubtypeMap(int, Enum):
"""
This class follows the 802.1AB TEXTUAL-CONVENTION for mapping LLDP subtypes to integers (enum).
`lldpd` does this as well. This avoids using regex to parse `lldpd` data.
From lldpd / src / lib / atoms / port.c:
static lldpctl_map_t port_id_subtype_map[] = {
{ LLDP_PORTID_SUBTYPE_IFNAME, "ifname"},
{ LLDP_PORTID_SUBTYPE_IFALIAS, "ifalias" },
{ LLDP_PORTID_SUBTYPE_LOCAL, "local" },
{ LLDP_PORTID_SUBTYPE_LLADDR, "mac" },
{ LLDP_PORTID_SUBTYPE_ADDR, "ip" },
{ LLDP_PORTID_SUBTYPE_PORT, "unhandled" },
{ LLDP_PORTID_SUBTYPE_AGENTCID, "unhandled" },
{ 0, NULL},
};
"""
ifalias = int(LldpPortIdSubtype.interfaceAlias)
# port = LldpPortIdSubtype.portComponent # (unsupported by lldpd)
mac = int(LldpPortIdSubtype.macAddress)
ip = int(LldpPortIdSubtype.networkAddress)
ifname = int(LldpPortIdSubtype.interfaceName)
# agentcircuitid = int(LldpPortIdSubtype.agentCircuitId) # (unsupported by lldpd)
local = int(LldpPortIdSubtype.local)
@unique
class ChassisIdSubtypeMap(int, Enum):
"""
This class follows the 802.1AB TEXTUAL-CONVENTION for mapping LLDP subtypes to integers (enum).
`lldpd` does this as well. This avoids using regex to parse `lldpd` data.
From lldpd / src / lib / atoms / chassis.c:
static lldpctl_map_t chassis_id_subtype_map[] = {
{ LLDP_CHASSISID_SUBTYPE_IFNAME, "ifname"},
{ LLDP_CHASSISID_SUBTYPE_IFALIAS, "ifalias" },
{ LLDP_CHASSISID_SUBTYPE_LOCAL, "local" },
{ LLDP_CHASSISID_SUBTYPE_LLADDR, "mac" },
{ LLDP_CHASSISID_SUBTYPE_ADDR, "ip" },
{ LLDP_CHASSISID_SUBTYPE_PORT, "unhandled" },
{ LLDP_CHASSISID_SUBTYPE_CHASSIS, "unhandled" },
{ 0, NULL},
};
"""
ifname = int(LldpChassisIdSubtype.interfaceName)
ifalias = int(LldpChassisIdSubtype.interfaceAlias)
# port = int(LldpChassisIdSubtype.portComponent) # (unsupported by lldpd)
mac = int(LldpChassisIdSubtype.macAddress)
ip = int(LldpChassisIdSubtype.networkAddress)
# chassis = int(LldpChassisIdSubtype.chassisComponent) # (unsupported by lldpd)
local = int(LldpPortIdSubtype.local)
def get_sys_capability_list(self, if_attributes, if_name, chassis_id):
"""
Get a list of capabilities from interface attributes dictionary.
:param if_attributes: interface attributes
:return: list of capabilities
"""
try:
# [{'enabled': ..., 'type': 'capability1'}, {'enabled': ..., 'type': 'capability2'}]
if 'capability' in if_attributes['chassis']:
capability_list = if_attributes['chassis']['capability']
else:
capability_list = list(if_attributes['chassis'].values())[0]['capability']
# {'enabled': ..., 'type': 'capability'}
if isinstance(capability_list, dict):
capability_list = [capability_list]
except KeyError:
logger.info("Failed to get system capabilities on {} ({})".format(if_name, chassis_id))
return []
return capability_list
def parse_sys_capabilities(self, capability_list, enabled=False):
"""
Get a bit map of capabilities, accoding to textual convention.
:param capability_list: list of capabilities
:param enabled: if true, consider only the enabled capabilities
:return: string representing a bit map
"""
# chassis is incomplete, missing capabilities
if not capability_list:
return ""
sys_cap = 0x00
for capability in capability_list:
try:
if (not enabled) or capability["enabled"]:
sys_cap |= 1 << LldpSystemCapabilitiesMap[capability["type"].lower()]
except KeyError:
logger.debug("Unknown capability {}".format(capability["type"]))
return "00 %0.2X" % sys_cap
def __init__(self, update_interval=None):
super(LldpSyncDaemon, self).__init__()
self._update_interval = update_interval or DEFAULT_UPDATE_INTERVAL
self.db_connector = SonicV2Connector()
self.db_connector.connect(self.db_connector.APPL_DB)
self.chassis_cache = {}
self.interfaces_cache = {}
@staticmethod
def _scrap_output(cmd):
try:
# execute the subprocess command
lldpctl_output = subprocess.check_output(cmd)
except subprocess.CalledProcessError:
logger.exception("lldpctl exited with non-zero status")
return None
try:
# parse the scrapped output
lldpctl_json = json.loads(lldpctl_output)
except ValueError:
logger.exception("Failed to parse lldpctl output")
return None
return lldpctl_json
def source_update(self):
"""
Invoke lldpctl and format as JSON
"""
cmd = ['/usr/sbin/lldpctl', '-f', 'json']
logger.debug("Invoking lldpctl with: {}".format(cmd))
cmd_local = ['/usr/sbin/lldpcli', '-f', 'json', 'show', 'chassis']
logger.debug("Invoking lldpcli with: {}".format(cmd_local))
lldp_json = self._scrap_output(cmd)
lldp_json['lldp_loc_chassis'] = self._scrap_output(cmd_local)
return lldp_json
def parse_update(self, lldp_json):
"""
Parse lldpd output to extract
(1) LldpRemPortDesc;
(2) LldpRemPortID;
(3) LldpRemPortIdSubtype;
(4) LldpRemSysName.
LldpRemEntry ::= SEQUENCE {
lldpRemTimeMark TimeFilter,
lldpRemLocalPortNum LldpPortNumber,
lldpRemIndex Integer32,
lldpRemChassisIdSubtype LldpChassisIdSubtype,
lldpRemChassisId LldpChassisId,
lldpRemPortIdSubtype LldpPortIdSubtype,
lldpRemPortId LldpPortId,
lldpRemPortDesc SnmpAdminString,
lldpRemSysName SnmpAdminString,
lldpRemSysDesc SnmpAdminString,
lldpRemSysCapSupported LldpSystemCapabilitiesMap,
lldpRemSysCapEnabled LldpSystemCapabilitiesMap
}
"""
try:
interface_list = lldp_json['lldp'].get('interface') or []
parsed_interfaces = defaultdict(dict)
for interface in interface_list:
try:
# [{'if_name' : { attributes...}}, {'if_other': {...}}, ...]
(if_name, if_attributes), = interface.items()
except AttributeError:
# {'if_name' : { attributes...}}, {'if_other': {...}}
if_name = interface
if_attributes = interface_list[if_name]
if 'port' in if_attributes:
rem_port_keys = ('lldp_rem_port_id_subtype',
'lldp_rem_port_id',
'lldp_rem_port_desc')
parsed_port = list(zip(rem_port_keys, self.parse_port(if_attributes['port'])))
parsed_interfaces[if_name].update(parsed_port)
chassis_id = ''
if 'chassis' in if_attributes:
rem_chassis_keys = ('lldp_rem_chassis_id_subtype',
'lldp_rem_chassis_id',
'lldp_rem_sys_name',
'lldp_rem_sys_desc',
'lldp_rem_man_addr')
parsed_chassis = list(zip(rem_chassis_keys,
self.parse_chassis(if_attributes['chassis'])))
parsed_interfaces[if_name].update(parsed_chassis)
chassis_id = parsed_chassis[1][1]
# lldpRemTimeMark TimeFilter,
parsed_interfaces[if_name].update({'lldp_rem_time_mark':
str(parse_time(if_attributes.get('age')))})
# lldpRemIndex
parsed_interfaces[if_name].update({'lldp_rem_index': str(if_attributes.get('rid'))})
capability_list = self.get_sys_capability_list(if_attributes, if_name, chassis_id)
# lldpSysCapSupported
parsed_interfaces[if_name].update({'lldp_rem_sys_cap_supported':
self.parse_sys_capabilities(capability_list)})
# lldpSysCapEnabled
parsed_interfaces[if_name].update({'lldp_rem_sys_cap_enabled':
self.parse_sys_capabilities(
capability_list, enabled=True)})
if lldp_json['lldp_loc_chassis']:
loc_chassis_keys = ('lldp_loc_chassis_id_subtype',
'lldp_loc_chassis_id',
'lldp_loc_sys_name',
'lldp_loc_sys_desc',
'lldp_loc_man_addr')
parsed_chassis = dict(zip(loc_chassis_keys,
self.parse_chassis(lldp_json['lldp_loc_chassis']
['local-chassis']['chassis'])))
loc_capabilities = self.get_sys_capability_list(lldp_json['lldp_loc_chassis']
['local-chassis'], 'local', 'chassis')
# lldpLocSysCapSupported
parsed_chassis.update({'lldp_loc_sys_cap_supported':
self.parse_sys_capabilities(loc_capabilities)})
# lldpLocSysCapEnabled
parsed_chassis.update({'lldp_loc_sys_cap_enabled':
self.parse_sys_capabilities(loc_capabilities, enabled=True)})
parsed_interfaces['local-chassis'].update(parsed_chassis)
return parsed_interfaces
except (KeyError, ValueError):
logger.exception("Failed to parse LLDPd JSON. \n{}\n -- ".format(lldp_json))
def parse_chassis(self, chassis_attributes):
try:
if 'id' in chassis_attributes and 'id' not in chassis_attributes['id']:
sys_name = ''
attributes = chassis_attributes
id_attributes = chassis_attributes['id']
else:
(sys_name, attributes) = list(chassis_attributes.items())[0]
id_attributes = attributes.get('id', '')
chassis_id_subtype = str(self.ChassisIdSubtypeMap[id_attributes['type']].value)
chassis_id = id_attributes.get('value', '')
descr = attributes.get('descr', '')
mgmt_ip = attributes.get('mgmt-ip', '')
if isinstance(mgmt_ip, list):
mgmt_ip = ','.join(mgmt_ip)
except (KeyError, ValueError):
logger.exception("Could not infer system information from: {}"
.format(chassis_attributes))
chassis_id_subtype = chassis_id = sys_name = descr = mgmt_ip = ''
return (chassis_id_subtype,
chassis_id,
sys_name,
descr,
mgmt_ip,
)
def parse_port(self, port_attributes):
port_identifiers = port_attributes.get('id')
try:
subtype = str(self.PortIdSubtypeMap[port_identifiers['type']].value)
value = port_identifiers['value']
except ValueError:
logger.exception("Could not infer chassis subtype from: {}".format(port_attributes))
subtype, value = None
return (subtype,
value,
port_attributes.get('descr', ''),
)
def cache_diff(self, cache, update):
"""
Find difference in keys between update and local cache dicts
:param cache: Local cache dict
:param update: Update dict
:return: new, changed, deleted keys tuple
"""
new_keys = list(set(update.keys()) - set(cache.keys()))
changed_keys = list(set(key for key in set(update.keys()) & set(cache.keys()) if update[key] != cache[key]))
deleted_keys = list(set(cache.keys()) - set(update.keys()))
return new_keys, changed_keys, deleted_keys
def sync(self, parsed_update):
"""
Sync LLDP information to redis DB.
"""
logger.debug("Initiating LLDPd sync to Redis...")
# push local chassis data to APP DB
if 'local-chassis' in parsed_update:
chassis_update = parsed_update.pop('local-chassis')
if chassis_update != self.chassis_cache:
self.db_connector.delete(self.db_connector.APPL_DB,
LldpSyncDaemon.LLDP_LOC_CHASSIS_TABLE)
for k, v in chassis_update.items():
self.db_connector.set(self.db_connector.APPL_DB,
LldpSyncDaemon.LLDP_LOC_CHASSIS_TABLE, k, v, blocking=True)
logger.debug("sync'd: {}".format(json.dumps(chassis_update, indent=3)))
new, changed, deleted = self.cache_diff(self.interfaces_cache, parsed_update)
self.interfaces_cache = parsed_update
# Delete LLDP_ENTRIES which were modified or are missing
for interface in changed + deleted:
table_key = ':'.join([LldpSyncDaemon.LLDP_ENTRY_TABLE, interface])
self.db_connector.delete(self.db_connector.APPL_DB, table_key)
# Repopulate LLDP_ENTRY_TABLE by adding all changed elements
for interface in changed + new:
if re.match(SONIC_ETHERNET_RE_PATTERN, interface) is None:
logger.warning("Ignoring interface '{}'".format(interface))
continue
# port_table_key = LLDP_ENTRY_TABLE:INTERFACE_NAME;
table_key = ':'.join([LldpSyncDaemon.LLDP_ENTRY_TABLE, interface])
for k, v in parsed_update[interface].items():
self.db_connector.set(self.db_connector.APPL_DB, table_key, k, v, blocking=True)
logger.debug("sync'd: \n{}".format(json.dumps(parsed_update[interface], indent=3)))