1616from datadog_checks .dev .tooling .commands .console import (
1717 CONTEXT_SETTINGS ,
1818 abort ,
19+ echo_debug ,
1920 echo_failure ,
2021 echo_info ,
2122 echo_success ,
3031ALLOWED_EXTENSIONS_BY_FORMAT = {"json" : [".json" ], "yaml" : [".yml" , ".yaml" ]}
3132
3233
34+ def _name_for_output (name : str ) -> str :
35+ """Normalize trap/variable name for output: hyphens to underscores (pysmi 0.3 compatibility)."""
36+ return name .replace ("-" , "_" )
37+
38+
3339class MappingType (Enum ):
3440 INTEGER = 0
3541 BITS = 1
@@ -105,16 +111,92 @@ def generate_traps_db(mib_sources, output_dir, output_file, output_format, no_de
105111 """
106112 from pysmi .codegen import JsonCodeGen
107113 from pysmi .compiler import MibCompiler
114+ from pysmi .mibinfo import MibInfo
108115 from pysmi .parser import SmiV1CompatParser
109- from pysmi .reader import getReadersFromUrls
116+ from pysmi .reader import get_readers_from_urls
117+ from pysmi .reader .httpclient import HttpReader
110118 from pysmi .searcher import AnyFileSearcher
111119 from pysmi .writer import FileWriter
112120
121+ # pysmi's HttpReader decodes HTTP response as UTF-8 only; MIBs with non-UTF-8 bytes
122+ # (e.g. https://github.com/DataDog/mibs.snmplabs.com/blob/master/asn1/A3COM-HUAWEI-DEVICE-MIB#L593)
123+ # raise UnicodeDecodeError and are reported as "missing".
124+ # Use a tolerant reader for HTTP(S) URLs that decodes with errors='replace'.
125+
126+ def _make_readers (sources , fuzzy_matching = True ):
127+ readers = []
128+ for src in sources :
129+ if src .startswith ('http://' ) or src .startswith ('https://' ):
130+ readers .append (_TolerantHttpReader (src , fuzzy_matching = fuzzy_matching ))
131+ else :
132+ readers .extend (get_readers_from_urls (src , fuzzy_matching = fuzzy_matching ))
133+ return readers
134+
135+ # This code is copied from https://github.com/lextudio/pysmi/blob/main/pysmi/reader/httpclient.py.
136+ # Except that it decodes the response content with errors='replace' for non-UTF-8 bytes.
137+ class _TolerantHttpReader (HttpReader ):
138+ """HttpReader that decodes response content with errors='replace' for non-UTF-8 bytes."""
139+
140+ def __init__ (self , url , fuzzy_matching = True ):
141+ super ().__init__ (url )
142+ self .set_options (fuzzy_matching = fuzzy_matching )
143+
144+ def get_data (self , mibname , ** options ):
145+ import sys
146+ import time
147+
148+ from pysmi import error
149+ from pysmi .compat import decode
150+
151+ headers = {"Accept" : "text/plain" , "User-Agent" : self ._user_agent }
152+
153+ mibname = decode (mibname )
154+
155+ echo_debug (f"looking for MIB { mibname } " )
156+
157+ for mibalias , mibfile in self .get_mib_variants (mibname , ** options ):
158+ if self .MIB_MAGIC in self ._url :
159+ url = self ._url .replace (self .MIB_MAGIC , mibfile )
160+ else :
161+ url = self ._url + mibfile
162+
163+ echo_debug (f"trying to fetch MIB from { url } " )
164+
165+ try :
166+ response = self .session .get (url , headers = headers )
167+
168+ except Exception :
169+ echo_debug (f"failed to fetch MIB from { url } : { sys .exc_info ()[1 ]} " )
170+ continue
171+
172+ echo_debug (f"HTTP response { response .status_code } " )
173+
174+ if response .status_code == 200 :
175+ try :
176+ mtime = time .mktime (
177+ time .strptime (
178+ response .headers ["Last-Modified" ],
179+ "%a, %d %b %Y %H:%M:%S %Z" ,
180+ )
181+ )
182+
183+ except Exception :
184+ echo_debug (f"malformed HTTP headers: { sys .exc_info ()[1 ]} " )
185+ mtime = time .time ()
186+
187+ echo_debug (f"fetching source MIB { url } , mtime { response .headers .get ('Last-Modified' , '' )} " )
188+
189+ return MibInfo (path = url , file = mibfile , name = mibalias , mtime = mtime ), response .content .decode (
190+ "utf-8" , errors = 'replace'
191+ )
192+
193+ raise error .PySmiReaderFileNotFoundError (f"source MIB { mibname } not found" , reader = self )
194+
113195 if debug :
114196 set_debug ()
115- from pysmi import debug
197+ from pysmi import debug as pysmi_debug
116198
117- debug .setLogger (debug .Debug ('all' ))
199+ pysmi_debug .setLogger (pysmi_debug .Debug ('all' ))
118200
119201 # Defaulting to github.com/DataDog/mibs.snmplabs.com/
120202 mib_sources = [mib_sources ] if mib_sources else [MIB_SOURCE_URL ]
@@ -144,12 +226,12 @@ def generate_traps_db(mib_sources, output_dir, output_file, output_format, no_de
144226 mib_sources = sorted ({pathlib .Path (x ).parent .as_uri () for x in mib_files if os .path .sep in x }) + mib_sources
145227
146228 mib_files = [os .path .basename (x ) for x in mib_files ]
147- searchers = [AnyFileSearcher (compiled_mibs_sources ).setOptions (exts = ['.json' ])]
229+ searchers = [AnyFileSearcher (compiled_mibs_sources ).set_options (exts = ['.json' ])]
148230 code_generator = JsonCodeGen ()
149- file_writer = FileWriter (compiled_mibs_sources ).setOptions (suffix = '.json' )
231+ file_writer = FileWriter (compiled_mibs_sources ).set_options (suffix = '.json' )
150232 mib_compiler = MibCompiler (SmiV1CompatParser (tempdir = '' ), code_generator , file_writer )
151- mib_compiler .addSources ( * getReadersFromUrls ( * mib_sources , ** { 'fuzzyMatching' : True } ))
152- mib_compiler .addSearchers (* searchers )
233+ mib_compiler .add_sources ( * _make_readers ( mib_sources , fuzzy_matching = True ))
234+ mib_compiler .add_searchers (* searchers )
153235
154236 compiled_mibs , compiled_dependencies_mibs = compile_and_report_status (mib_files , mib_compiler )
155237
@@ -307,12 +389,13 @@ def generate_trap_db(compiled_mibs, compiled_mibs_sources, no_descr):
307389 trap_name = trap ['name' ]
308390 trap_oid = trap ['oid' ]
309391 trap_descr = trap .get ('description' , '' )
310- trap_db ["traps" ][trap_oid ] = {"name" : trap_name , "mib" : file_mib_name }
392+ trap_db ["traps" ][trap_oid ] = {"name" : _name_for_output ( trap_name ) , "mib" : file_mib_name }
311393 if not no_descr :
312394 trap_db ["traps" ][trap_oid ]["descr" ] = trap_descr
313395 for trap_var in trap .get ('objects' , []):
314396 try :
315- var_name , mib_name = trap_var ['object' ], trap_var ['module' ]
397+ var_name = trap_var ['object' ]
398+ mib_name = trap_var ['module' ]
316399 var_metadata = get_var_metadata (
317400 var_name ,
318401 mib_name ,
@@ -331,8 +414,7 @@ def generate_trap_db(compiled_mibs, compiled_mibs_sources, no_descr):
331414 "Ignoring this variable." .format (trap_name , var_name , mib_name )
332415 )
333416 continue
334- var_name = trap_var ['object' ]
335- trap_db ["vars" ][var_metadata .oid ] = {"name" : var_name }
417+ trap_db ["vars" ][var_metadata .oid ] = {"name" : _name_for_output (var_name )}
336418 if not no_descr :
337419 trap_db ["vars" ][var_metadata .oid ]["descr" ] = var_metadata .description
338420 if var_metadata .enum :
@@ -430,8 +512,14 @@ def get_mapping(var_name, mib_name, mapping_type: MappingType, search_locations=
430512 :param search_locations: Tuple of path to directories containing json-compiled MIB files
431513 :return: The oid and the description of the variable.
432514 """
515+ visited = set () # Guard against circular type references (would cause infinite loop)
433516 mapping = {}
434517 while mib_name and var_name and not mapping :
518+ key = (var_name , mib_name )
519+ if key in visited :
520+ break
521+ visited .add (key )
522+
435523 for location in search_locations :
436524 file_name = os .path .join (location , mib_name + '.json' )
437525 if os .path .isfile (file_name ):
@@ -481,7 +569,7 @@ def get_import_mib(var_name, mib_name, search_locations=None):
481569 if os .path .isfile (file_name ):
482570 break
483571 else :
484- return MissingMIBException
572+ raise MissingMIBException ()
485573
486574 with open (file_name , 'r' ) as f :
487575 file_content = json .load (f )
0 commit comments