PageRenderTime 1133ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/snmpsim/commands/responder_lite.py

https://github.com/etingof/snmpsim
Python | 529 lines | 453 code | 68 blank | 8 comment | 24 complexity | 7a6a415d4a6f7ca4f87b1fd803a51583 MD5 | raw file
  1. # This file is part of snmpsim software.
  2. #
  3. # Copyright (c) 2010-2019, Ilya Etingof <etingof@gmail.com>
  4. # License: http://snmplabs.com/snmpsim/license.html
  5. #
  6. # SNMP Agent Simulator: lightweight SNMP v1/v2c command responder
  7. #
  8. import argparse
  9. import os
  10. import sys
  11. import traceback
  12. from pyasn1 import debug as pyasn1_debug
  13. from pyasn1.codec.ber import decoder
  14. from pyasn1.codec.ber import encoder
  15. from pyasn1.type import univ
  16. from pysnmp import debug as pysnmp_debug
  17. from pysnmp.carrier.asyncore.dgram import udp
  18. from pysnmp.carrier.asyncore.dgram import udp6
  19. from pysnmp.carrier.asyncore.dispatch import AsyncoreDispatcher
  20. from pysnmp.proto import api
  21. from pysnmp.proto import rfc1902
  22. from pysnmp.proto import rfc1905
  23. from snmpsim import confdir
  24. from snmpsim import controller
  25. from snmpsim import daemon
  26. from snmpsim import datafile
  27. from snmpsim import endpoints
  28. from snmpsim import log
  29. from snmpsim import utils
  30. from snmpsim import variation
  31. from snmpsim.error import NoDataNotification
  32. from snmpsim.error import SnmpsimError
  33. from snmpsim.reporting.manager import ReportingManager
  34. SNMP_2TO1_ERROR_MAP = {
  35. rfc1902.Counter64.tagSet: 5,
  36. rfc1905.NoSuchObject.tagSet: 2,
  37. rfc1905.NoSuchInstance.tagSet: 2,
  38. rfc1905.EndOfMibView.tagSet: 2
  39. }
  40. DESCRIPTION = (
  41. 'Lightweight SNMP agent simulator: responds to SNMP v1/v2c requests, '
  42. 'variate responses based on transport addresses, SNMP community name '
  43. 'or via variation modules.')
  44. def main():
  45. parser = argparse.ArgumentParser(description=DESCRIPTION)
  46. parser.add_argument(
  47. '-v', '--version', action='version',
  48. version=utils.TITLE)
  49. parser.add_argument(
  50. '--quiet', action='store_true',
  51. help='Do not print out informational messages')
  52. parser.add_argument(
  53. '--debug', choices=pysnmp_debug.flagMap,
  54. action='append', type=str, default=[],
  55. help='Enable one or more categories of SNMP debugging.')
  56. parser.add_argument(
  57. '--debug-asn1', choices=pyasn1_debug.FLAG_MAP,
  58. action='append', type=str, default=[],
  59. help='Enable one or more categories of ASN.1 debugging.')
  60. parser.add_argument(
  61. '--logging-method', type=lambda x: x.split(':'),
  62. metavar='=<%s[:args]>]' % '|'.join(log.METHODS_MAP),
  63. default='stderr', help='Logging method.')
  64. parser.add_argument(
  65. '--log-level', choices=log.LEVELS_MAP,
  66. type=str, default='info', help='Logging level.')
  67. parser.add_argument(
  68. '--reporting-method', type=lambda x: x.split(':'),
  69. metavar='=<%s[:args]>]' % '|'.join(ReportingManager.REPORTERS),
  70. default='null', help='Activity metrics reporting method.')
  71. parser.add_argument(
  72. '--daemonize', action='store_true',
  73. help='Disengage from controlling terminal and become a daemon')
  74. parser.add_argument(
  75. '--process-user', type=str,
  76. help='If run as root, switch simulator daemon to this user right '
  77. 'upon binding privileged ports')
  78. parser.add_argument(
  79. '--process-group', type=str,
  80. help='If run as root, switch simulator daemon to this group right '
  81. 'upon binding privileged ports')
  82. parser.add_argument(
  83. '--pid-file', metavar='<FILE>', type=str,
  84. default='/var/run/%s/%s.pid' % (__name__, os.getpid()),
  85. help='SNMP simulation data file to write records to')
  86. parser.add_argument(
  87. '--cache-dir', metavar='<DIR>', type=str,
  88. help='Location for SNMP simulation data file indices to create')
  89. parser.add_argument(
  90. '--force-index-rebuild', action='store_true',
  91. help='Rebuild simulation data files indices even if they seem '
  92. 'up to date')
  93. parser.add_argument(
  94. '--validate-data', action='store_true',
  95. help='Validate simulation data files on daemon start-up')
  96. parser.add_argument(
  97. '--variation-modules-dir', metavar='<DIR>', type=str,
  98. action='append', default=[],
  99. help='Variation modules search path(s)')
  100. parser.add_argument(
  101. '--variation-module-options', metavar='<module[=alias][:args]>',
  102. type=str, action='append', default=[],
  103. help='Options for a specific variation module')
  104. parser.add_argument(
  105. '--v2c-arch', action='store_true',
  106. help='Use lightweight, legacy SNMP architecture capable to support '
  107. 'v1/v2c versions of SNMP')
  108. parser.add_argument(
  109. '--v3-only', action='store_true',
  110. help='Trip legacy SNMP v1/v2c support to gain a little lesser memory '
  111. 'footprint')
  112. parser.add_argument(
  113. '--transport-id-offset', type=int, default=0,
  114. help='Start numbering the last sub-OID of transport endpoint OIDs '
  115. 'starting from this ID')
  116. parser.add_argument(
  117. '--max-var-binds', type=int, default=64,
  118. help='Maximum number of variable bindings to include in a single '
  119. 'response')
  120. parser.add_argument(
  121. '--data-dir', type=str, action='append', metavar='<DIR>',
  122. dest='data_dirs',
  123. help='SNMP simulation data recordings directory.')
  124. endpoint_group = parser.add_mutually_exclusive_group(required=True)
  125. endpoint_group.add_argument(
  126. '--agent-udpv4-endpoint', type=str,
  127. action='append', metavar='<[X.X.X.X]:NNNNN>',
  128. dest='agent_udpv4_endpoints', default=[],
  129. help='SNMP agent UDP/IPv4 address to listen on (name:port)')
  130. endpoint_group.add_argument(
  131. '--agent-udpv6-endpoint', type=str,
  132. action='append', metavar='<[X:X:..X]:NNNNN>',
  133. dest='agent_udpv6_endpoints', default=[],
  134. help='SNMP agent UDP/IPv6 address to listen on ([name]:port)')
  135. args = parser.parse_args()
  136. if args.cache_dir:
  137. confdir.cache = args.cache_dir
  138. if args.variation_modules_dir:
  139. confdir.variation = args.variation_modules_dir
  140. variation_modules_options = variation.parse_modules_options(
  141. args.variation_module_options)
  142. with daemon.PrivilegesOf(args.process_user, args.process_group):
  143. proc_name = os.path.basename(sys.argv[0])
  144. try:
  145. log.set_logger(proc_name, *args.logging_method, force=True)
  146. if args.log_level:
  147. log.set_level(args.log_level)
  148. except SnmpsimError as exc:
  149. sys.stderr.write('%s\r\n' % exc)
  150. parser.print_usage(sys.stderr)
  151. return 1
  152. try:
  153. ReportingManager.configure(*args.reporting_method)
  154. except SnmpsimError as exc:
  155. sys.stderr.write('%s\r\n' % exc)
  156. parser.print_usage(sys.stderr)
  157. return 1
  158. if args.daemonize:
  159. try:
  160. daemon.daemonize(args.pid_file)
  161. except Exception as exc:
  162. sys.stderr.write(
  163. 'ERROR: cant daemonize process: %s\r\n' % exc)
  164. parser.print_usage(sys.stderr)
  165. return 1
  166. if not os.path.exists(confdir.cache):
  167. try:
  168. with daemon.PrivilegesOf(args.process_user, args.process_group):
  169. os.makedirs(confdir.cache)
  170. except OSError as exc:
  171. log.error('failed to create cache directory "%s": '
  172. '%s' % (confdir.cache, exc))
  173. return 1
  174. else:
  175. log.info('Cache directory "%s" created' % confdir.cache)
  176. variation_modules = variation.load_variation_modules(
  177. confdir.variation, variation_modules_options)
  178. with daemon.PrivilegesOf(args.process_user, args.process_group):
  179. variation.initialize_variation_modules(
  180. variation_modules, mode='variating')
  181. def configure_managed_objects(
  182. data_dirs, data_index_instrum_controller, snmp_engine=None,
  183. snmp_context=None):
  184. """Build pysnmp Managed Objects base from data files information"""
  185. _mib_instrums = {}
  186. _data_files = {}
  187. for dataDir in data_dirs:
  188. log.info(
  189. 'Scanning "%s" directory for %s data '
  190. 'files...' % (dataDir, ','.join(
  191. [' *%s%s' % (os.path.extsep, x.ext)
  192. for x in variation.RECORD_TYPES.values()])))
  193. if not os.path.exists(dataDir):
  194. log.info('Directory "%s" does not exist' % dataDir)
  195. continue
  196. log.msg.inc_ident()
  197. for (full_path,
  198. text_parser,
  199. community_name) in datafile.get_data_files(dataDir):
  200. if community_name in _data_files:
  201. log.error(
  202. 'ignoring duplicate Community/ContextName "%s" for data '
  203. 'file %s (%s already loaded)' % (community_name, full_path,
  204. _data_files[community_name]))
  205. continue
  206. elif full_path in _mib_instrums:
  207. mib_instrum = _mib_instrums[full_path]
  208. log.info('Configuring *shared* %s' % (mib_instrum,))
  209. else:
  210. data_file = datafile.DataFile(
  211. full_path, text_parser, variation_modules)
  212. data_file.index_text(args.force_index_rebuild, args.validate_data)
  213. MibController = controller.MIB_CONTROLLERS[data_file.layout]
  214. mib_instrum = MibController(data_file)
  215. _mib_instrums[full_path] = mib_instrum
  216. _data_files[community_name] = full_path
  217. log.info('Configuring %s' % (mib_instrum,))
  218. log.info('SNMPv1/2c community name: %s' % (community_name,))
  219. contexts[univ.OctetString(community_name)] = mib_instrum
  220. data_index_instrum_controller.add_data_file(
  221. full_path, community_name
  222. )
  223. log.msg.dec_ident()
  224. del _mib_instrums
  225. del _data_files
  226. def get_bulk_handler(
  227. req_var_binds, non_repeaters, max_repetitions, read_next_vars):
  228. """Only v2c arch GETBULK handler"""
  229. N = min(int(non_repeaters), len(req_var_binds))
  230. M = int(max_repetitions)
  231. R = max(len(req_var_binds) - N, 0)
  232. if R:
  233. M = min(M, args.max_var_binds / R)
  234. if N:
  235. rsp_var_binds = read_next_vars(req_var_binds[:N])
  236. else:
  237. rsp_var_binds = []
  238. var_binds = req_var_binds[-R:]
  239. while M and R:
  240. rsp_var_binds.extend(read_next_vars(var_binds))
  241. var_binds = rsp_var_binds[-R:]
  242. M -= 1
  243. return rsp_var_binds
  244. def commandResponderCbFun(
  245. transport_dispatcher, transport_domain, transport_address,
  246. whole_msg):
  247. """v2c arch command responder request handling callback"""
  248. while whole_msg:
  249. msg_ver = api.decodeMessageVersion(whole_msg)
  250. if msg_ver in api.protoModules:
  251. p_mod = api.protoModules[msg_ver]
  252. else:
  253. log.error('Unsupported SNMP version %s' % (msg_ver,))
  254. return
  255. req_msg, whole_msg = decoder.decode(whole_msg, asn1Spec=p_mod.Message())
  256. community_name = req_msg.getComponentByPosition(1)
  257. for candidate in datafile.probe_context(
  258. transport_domain, transport_address,
  259. context_engine_id=datafile.SELF_LABEL,
  260. context_name=community_name):
  261. if candidate in contexts:
  262. log.info(
  263. 'Using %s selected by candidate %s; transport ID %s, '
  264. 'source address %s, context engine ID <empty>, '
  265. 'community name '
  266. '"%s"' % (contexts[candidate], candidate,
  267. univ.ObjectIdentifier(transport_domain),
  268. transport_address[0], community_name))
  269. community_name = candidate
  270. break
  271. else:
  272. log.error(
  273. 'No data file selected for transport ID %s, source '
  274. 'address %s, community name '
  275. '"%s"' % (univ.ObjectIdentifier(transport_domain),
  276. transport_address[0], community_name))
  277. return whole_msg
  278. rsp_msg = p_mod.apiMessage.getResponse(req_msg)
  279. rsp_pdu = p_mod.apiMessage.getPDU(rsp_msg)
  280. req_pdu = p_mod.apiMessage.getPDU(req_msg)
  281. if req_pdu.isSameTypeWith(p_mod.GetRequestPDU()):
  282. backend_fun = contexts[community_name].readVars
  283. elif req_pdu.isSameTypeWith(p_mod.SetRequestPDU()):
  284. backend_fun = contexts[community_name].writeVars
  285. elif req_pdu.isSameTypeWith(p_mod.GetNextRequestPDU()):
  286. backend_fun = contexts[community_name].readNextVars
  287. elif (hasattr(p_mod, 'GetBulkRequestPDU') and
  288. req_pdu.isSameTypeWith(p_mod.GetBulkRequestPDU())):
  289. if not msg_ver:
  290. log.info(
  291. 'GETBULK over SNMPv1 from %s:%s' % (
  292. transport_domain, transport_address))
  293. return whole_msg
  294. def backend_fun(var_binds):
  295. return get_bulk_handler(
  296. var_binds, p_mod.apiBulkPDU.getNonRepeaters(req_pdu),
  297. p_mod.apiBulkPDU.getMaxRepetitions(req_pdu),
  298. contexts[community_name].readNextVars
  299. )
  300. else:
  301. log.error(
  302. 'Unsupported PDU type %s from '
  303. '%s:%s' % (req_pdu.__class__.__name__, transport_domain,
  304. transport_address))
  305. return whole_msg
  306. try:
  307. var_binds = backend_fun(p_mod.apiPDU.getVarBinds(req_pdu))
  308. except NoDataNotification:
  309. return whole_msg
  310. except Exception as exc:
  311. log.error('Ignoring SNMP engine failure: %s' % exc)
  312. return whole_msg
  313. if not msg_ver:
  314. for idx in range(len(var_binds)):
  315. oid, val = var_binds[idx]
  316. if val.tagSet in SNMP_2TO1_ERROR_MAP:
  317. var_binds = p_mod.apiPDU.getVarBinds(req_pdu)
  318. p_mod.apiPDU.setErrorStatus(
  319. rsp_pdu, SNMP_2TO1_ERROR_MAP[val.tagSet])
  320. p_mod.apiPDU.setErrorIndex(
  321. rsp_pdu, idx + 1)
  322. break
  323. p_mod.apiPDU.setVarBinds(rsp_pdu, var_binds)
  324. transport_dispatcher.sendMessage(
  325. encoder.encode(rsp_msg), transport_domain, transport_address)
  326. return whole_msg
  327. # Configure access to data index
  328. log.info('Maximum number of variable bindings in SNMP '
  329. 'response: %s' % args.max_var_binds)
  330. data_index_instrum_controller = controller.DataIndexInstrumController()
  331. contexts = {univ.OctetString('index'): data_index_instrum_controller}
  332. with daemon.PrivilegesOf(args.process_user, args.process_group):
  333. configure_managed_objects(
  334. args.data_dirs or confdir.data, data_index_instrum_controller)
  335. contexts['index'] = data_index_instrum_controller
  336. # Configure socket server
  337. transport_dispatcher = AsyncoreDispatcher()
  338. transport_index = args.transport_id_offset
  339. for agent_udpv4_endpoint in args.agent_udpv4_endpoints:
  340. transport_domain = udp.domainName + (transport_index,)
  341. transport_index += 1
  342. agent_udpv4_endpoint = (
  343. endpoints.IPv4TransportEndpoints().add(agent_udpv4_endpoint))
  344. transport_dispatcher.registerTransport(
  345. transport_domain, agent_udpv4_endpoint[0])
  346. log.info('Listening at UDP/IPv4 endpoint %s, transport ID '
  347. '%s' % (agent_udpv4_endpoint[1],
  348. '.'.join([str(handler) for handler in transport_domain])))
  349. transport_index = args.transport_id_offset
  350. for agent_udpv6_endpoint in args.agent_udpv6_endpoints:
  351. transport_domain = udp6.domainName + (transport_index,)
  352. transport_index += 1
  353. agent_udpv6_endpoint = (
  354. endpoints.IPv4TransportEndpoints().add(agent_udpv6_endpoint))
  355. transport_dispatcher.registerTransport(
  356. transport_domain, agent_udpv6_endpoint[0])
  357. log.info('Listening at UDP/IPv6 endpoint %s, transport ID '
  358. '%s' % (agent_udpv6_endpoint[1],
  359. '.'.join([str(handler) for handler in transport_domain])))
  360. transport_dispatcher.registerRecvCbFun(commandResponderCbFun)
  361. transport_dispatcher.jobStarted(1) # server job would never finish
  362. with daemon.PrivilegesOf(args.process_user, args.process_group, final=True):
  363. try:
  364. transport_dispatcher.runDispatcher()
  365. except KeyboardInterrupt:
  366. log.info('Shutting down process...')
  367. finally:
  368. if variation_modules:
  369. log.info('Shutting down variation modules:')
  370. for name, contexts in variation_modules.items():
  371. body = contexts[0]
  372. try:
  373. body['shutdown'](options=body['args'], mode='variation')
  374. except Exception as exc:
  375. log.error(
  376. 'Variation module "%s" shutdown FAILED: '
  377. '%s' % (name, exc))
  378. else:
  379. log.info('Variation module "%s" shutdown OK' % name)
  380. transport_dispatcher.closeDispatcher()
  381. log.info('Process terminated')
  382. return 0
  383. if __name__ == '__main__':
  384. try:
  385. rc = main()
  386. except KeyboardInterrupt:
  387. sys.stderr.write('shutting down process...')
  388. rc = 0
  389. except Exception as exc:
  390. sys.stderr.write('process terminated: %s' % exc)
  391. for line in traceback.format_exception(*sys.exc_info()):
  392. sys.stderr.write(line.replace('\n', ';'))
  393. rc = 1
  394. sys.exit(rc)