Skip to content

Commit 1f6289a

Browse files
wangxinvikumarks
authored andcommitted
Compatibility fixes for docker-sonic-mgmt based on Ubuntu 24.04 (sonic-net#21045)
What is the motivation for this PR? This PR is to continue the effort made by @yutongzhang-microsoft in sonic-net#18339 The current docker-sonic-mgmt image is based on Ubuntu 20.04 which is end of support now. PR sonic-net/sonic-buildimage#24306 upgraded the base image of docker-sonic-mgmt to Ubuntu 24.04. Together, version of most packages are upgraded too. The upgrade introduced lots of compatibility issues. This PR is to fix all the compatibility issues. All the fixes are backward compatible. The code change woks with both current and new docker-sonic-mgmt. How did you do it? Fix the snmp code caused by pysnmp upgrade. Fix json dump of ansible result caused by pytest-ansible upgrade. How did you verify/test it? Take advantage of the current sonic-mgmt PR testing. Verified that the code change works with new docker-sonic-mgmt in sonic-net#20851 Verified that the code change works with the current docker-sonic-mgmt in this PR. Signed-off-by: vikumarks <vikumar7ks@gmail.com>
1 parent e58996a commit 1f6289a

12 files changed

Lines changed: 1161 additions & 167 deletions

File tree

.azure-pipelines/pytest-collect-only.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ steps:
22

33
- script: |
44
sudo apt-get -o DPkg::Lock::Timeout=180 update
5-
sudo apt-get install \
5+
sudo apt-get -o DPkg::Lock::Timeout=180 install \
66
ca-certificates \
77
curl \
88
gnupg \
@@ -12,8 +12,8 @@ steps:
1212
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
1313
https://download.docker.com/linux/ubuntu \
1414
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
15-
sudo apt-get update
16-
sudo apt-get install docker-ce docker-ce-cli containerd.io -y
15+
sudo apt-get -o DPkg::Lock::Timeout=180 update
16+
sudo apt-get -o DPkg::Lock::Timeout=180 install docker-ce docker-ce-cli containerd.io -y
1717
displayName: 'Install Docker'
1818

1919
- checkout: self

ansible/devutil/devices/ansible_hosts.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@
5454
except Exception as e:
5555
logging.error("Hack for https://github.com/ansible/pytest-ansible/issues/47 failed: {}".format(repr(e)))
5656

57+
try:
58+
# Initialize ansible plugin loader to avoid issues with ansbile-core 2.18
59+
from ansible.plugins.loader import init_plugin_loader
60+
init_plugin_loader()
61+
except ImportError:
62+
# Nothing need to do for ansible-core 2.13
63+
pass
64+
5765

5866
try:
5967
# Initialize ansible plugin loader to avoid issues with ansbile-core 2.18

ansible/library/lldp_facts.py

Lines changed: 207 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@
33
import json
44
from collections import defaultdict
55
from ansible.module_utils.basic import AnsibleModule
6-
try:
6+
7+
import asyncio
8+
import pysnmp
9+
10+
if pysnmp.version[0] < 5:
711
from pysnmp.entity.rfc3413.oneliner import cmdgen
8-
has_pysnmp = True
9-
except Exception:
10-
has_pysnmp = False
12+
else:
13+
from pysnmp.hlapi.v3arch.asyncio import (
14+
cmdgen,
15+
UdpTransportTarget,
16+
walk_cmd,
17+
SnmpEngine,
18+
ContextData,
19+
ObjectType,
20+
ObjectIdentity
21+
)
1122

1223
DOCUMENTATION = '''
1324
---
@@ -90,11 +101,12 @@ def __init__(self, dotprefix=False):
90101
else:
91102
dp = ""
92103

93-
# From IF-MIB
104+
# From IF-MIB, refer to https://mibs.observium.org/mib/IF-MIB/
94105
# ifdescr is common support, replace the lldpportid
95106
self.if_descr = dp + ".3.6.1.2.1.2.2.1.2"
96107

97108
# From LLDP-MIB
109+
self.lldp_rem_entry = dp + ".0.8802.1.1.2.1.4.1.1" # for snmp_walk
98110
self.lldp_rem_port_id = dp + ".0.8802.1.1.2.1.4.1.1.7"
99111
self.lldp_rem_port_desc = dp + ".0.8802.1.1.2.1.4.1.1.8"
100112
self.lldp_rem_sys_desc = dp + ".0.8802.1.1.2.1.4.1.1.10"
@@ -129,27 +141,12 @@ def get_iftable(snmp_data):
129141
return (if_table, inverse_if_table)
130142

131143

132-
def main():
133-
module = AnsibleModule(
134-
argument_spec=dict(
135-
host=dict(required=True),
136-
version=dict(required=True, choices=['v2', 'v2c', 'v3']),
137-
community=dict(required=False, default=False),
138-
username=dict(required=False),
139-
level=dict(required=False, choices=['authNoPriv', 'authPriv']),
140-
integrity=dict(required=False, choices=['md5', 'sha']),
141-
privacy=dict(required=False, choices=['des', 'aes']),
142-
authkey=dict(required=False),
143-
privkey=dict(required=False),
144-
removeplaceholder=dict(required=False)),
145-
required_together=(['username', 'level', 'integrity', 'authkey'], [
146-
'privacy', 'privkey'],),
147-
supports_check_mode=False)
144+
def Tree():
145+
return defaultdict(Tree)
148146

149-
m_args = module.params
150147

151-
if not has_pysnmp:
152-
module.fail_json(msg='Missing required pysnmp module (check docs)')
148+
def main_legacy(module):
149+
m_args = module.params
153150

154151
cmd_gen = cmdgen.CommandGenerator()
155152

@@ -196,8 +193,6 @@ def main():
196193
# Use v without a prefix to use with return values
197194
v = DefineOid(dotprefix=False)
198195

199-
def Tree(): return defaultdict(Tree)
200-
201196
results = Tree()
202197

203198
host = m_args['host']
@@ -278,4 +273,189 @@ def Tree(): return defaultdict(Tree)
278273
module.exit_json(ansible_facts=results)
279274

280275

281-
main()
276+
class LLDPFactsCollector:
277+
278+
def __init__(self, module):
279+
self.module = module
280+
self.m_args = module.params
281+
self.results = Tree()
282+
self.if_table = dict()
283+
self.inverse_if_table = dict()
284+
self.snmp_engine = SnmpEngine()
285+
self.context = ContextData()
286+
self.transport = None
287+
self._init_auth()
288+
self.p = DefineOid(dotprefix=True)
289+
self.v = DefineOid(dotprefix=False)
290+
291+
def _init_auth(self):
292+
# Verify that we receive a community when using snmp v2
293+
if self.m_args['version'] == "v2" or self.m_args['version'] == "v2c":
294+
if self.m_args['community'] is False:
295+
self.module.fail_json(
296+
msg='Community not set when using snmp version 2'
297+
)
298+
299+
if self.m_args['version'] == "v3":
300+
if self.m_args['username'] is None:
301+
self.module.fail_json(
302+
msg='Username not set when using snmp version 3'
303+
)
304+
305+
if self.m_args['level'] == "authPriv" and self.m_args['privacy'] is None:
306+
self.module.fail_json(
307+
msg='Privacy algorithm not set when using authPriv'
308+
)
309+
310+
if self.m_args['integrity'] == "sha":
311+
integrity_proto = cmdgen.usmHMACSHAAuthProtocol
312+
elif self.m_args['integrity'] == "md5":
313+
integrity_proto = cmdgen.usmHMACMD5AuthProtocol
314+
315+
if self.m_args['privacy'] == "aes":
316+
privacy_proto = cmdgen.usmAesCfb128Protocol
317+
elif self.m_args['privacy'] == "des":
318+
privacy_proto = cmdgen.usmDESPrivProtocol
319+
320+
# Use SNMP Version 2
321+
if self.m_args['version'] == "v2" or self.m_args['version'] == "v2c":
322+
self.snmp_auth = cmdgen.CommunityData(self.m_args['community'])
323+
324+
# Use SNMP Version 3 with authNoPriv
325+
elif self.m_args['level'] == "authNoPriv":
326+
self.snmp_auth = cmdgen.UsmUserData(
327+
self.m_args['username'],
328+
authKey=self.m_args['authkey'],
329+
authProtocol=integrity_proto
330+
)
331+
# Use SNMP Version 3 with authPriv
332+
else:
333+
self.snmp_auth = cmdgen.UsmUserData(
334+
self.m_args['username'],
335+
authKey=self.m_args['authkey'],
336+
privKey=self.m_args['privkey'],
337+
authProtocol=integrity_proto,
338+
privProtocol=privacy_proto
339+
)
340+
341+
async def setup(self):
342+
self.transport = await UdpTransportTarget.create(
343+
(self.m_args['host'], 161),
344+
timeout=self.m_args['timeout']
345+
)
346+
347+
async def collect(self):
348+
if self.transport is None:
349+
raise Exception('Transport not initialized. Call setup() first.')
350+
351+
async for errorIndication, errorStatus, errorIndex, varBinds in walk_cmd(
352+
self.snmp_engine,
353+
self.snmp_auth,
354+
self.transport,
355+
ContextData(),
356+
ObjectType(ObjectIdentity(self.p.if_descr)),
357+
lookupMib=False,
358+
lexicographicMode=False
359+
):
360+
if errorIndication:
361+
self.module.fail_json(
362+
msg=f"{str(errorIndication)} querying if_descr."
363+
)
364+
365+
for oid, val in varBinds:
366+
ifIndex = str(oid).split(".")[-1]
367+
ifDescr = str(val)
368+
self.if_table[ifDescr] = ifIndex
369+
self.inverse_if_table[ifIndex] = ifDescr
370+
371+
lldp_rem_sys = dict()
372+
lldp_rem_port_id = dict()
373+
lldp_rem_port_desc = dict()
374+
lldp_rem_chassis_id = dict()
375+
lldp_rem_sys_desc = dict()
376+
377+
async for errorIndication, errorStatus, errorIndex, varBinds in walk_cmd(
378+
self.snmp_engine,
379+
self.snmp_auth,
380+
self.transport,
381+
ContextData(),
382+
ObjectType(ObjectIdentity(self.p.lldp_rem_entry)),
383+
lookupMib=False,
384+
lexicographicMode=False
385+
):
386+
if errorIndication:
387+
self.module.fail_json(
388+
msg=f"{str(errorIndication)} querying lldp_rem_entry."
389+
)
390+
391+
for oid, val in varBinds:
392+
current_oid = oid.prettyPrint()
393+
current_val = val.prettyPrint()
394+
395+
ifIndex = str(current_oid).split(".")[-2]
396+
397+
try:
398+
if_name = self.inverse_if_table[ifIndex]
399+
except Exception:
400+
print(
401+
json.dumps({"unbound_interface_index": ifIndex})
402+
)
403+
module.fail_json(msg="unboundinterface in inverse if table")
404+
405+
if self.v.lldp_rem_sys_name in current_oid:
406+
lldp_rem_sys[if_name] = current_val
407+
elif self.v.lldp_rem_port_id in current_oid:
408+
lldp_rem_port_id[if_name] = current_val
409+
elif self.v.lldp_rem_port_desc in current_oid:
410+
lldp_rem_port_desc[if_name] = current_val
411+
elif self.v.lldp_rem_chassis_id in current_oid:
412+
lldp_rem_chassis_id[if_name] = current_val
413+
elif self.v.lldp_rem_sys_desc in current_oid:
414+
lldp_rem_sys_desc[if_name] = current_val
415+
416+
lldp_data = dict()
417+
418+
for if_name in lldp_rem_sys:
419+
lldp_data[if_name] = {
420+
'neighbor_sys_name': lldp_rem_sys[if_name],
421+
'neighbor_port_desc': lldp_rem_port_desc[if_name],
422+
'neighbor_port_id': lldp_rem_port_id[if_name],
423+
'neighbor_sys_desc': lldp_rem_sys_desc[if_name],
424+
'neighbor_chassis_id': lldp_rem_chassis_id[if_name]
425+
}
426+
427+
self.results['ansible_lldp_facts'] = lldp_data
428+
429+
430+
async def main(module):
431+
collector = LLDPFactsCollector(module)
432+
await collector.setup()
433+
await collector.collect()
434+
module.exit_json(ansible_facts=collector.results)
435+
436+
437+
if __name__ == '__main__':
438+
module = AnsibleModule(
439+
argument_spec=dict(
440+
host=dict(required=True),
441+
timeout=dict(reqired=False, type='int', default=20),
442+
version=dict(required=True, choices=['v2', 'v2c', 'v3']),
443+
community=dict(required=False, default=False),
444+
username=dict(required=False),
445+
level=dict(required=False, choices=['authNoPriv', 'authPriv']),
446+
integrity=dict(required=False, choices=['md5', 'sha']),
447+
privacy=dict(required=False, choices=['des', 'aes']),
448+
authkey=dict(required=False),
449+
privkey=dict(required=False),
450+
removeplaceholder=dict(required=False)),
451+
required_together=(
452+
['username', 'level', 'integrity', 'authkey'],
453+
['privacy', 'privkey'],
454+
),
455+
supports_check_mode=False
456+
)
457+
458+
if pysnmp.version[0] < 5:
459+
main_legacy(module)
460+
else:
461+
asyncio.run(main(module))

0 commit comments

Comments
 (0)