aboutsummaryrefslogtreecommitdiffstats
path: root/snmptrap/snmptrapd.py
blob: 9cf828c99186a07f2f47a2839d3a9bb749f0b1aa (plain)
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# ============LICENSE_START=======================================================)
# org.onap.dcae
# ================================================================================
# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============LICENSE_END=========================================================
#
# ECOMP is a trademark and service mark of AT&T Intellectual Property.
#
"""
dcae_snmptrapd is responsible for SNMP trap receipt and publishing activities.
It's behavior is controlled by CBS (config binding service) using a
JSON construct obtained via a "get_config" call or (for testing/standalone
purposes) a file specified using the env variable "CBS_SIM_JSON".

As traps arrive they are decomposed and transformed into a JSON message which
is published to a dmaap instance that has been defined by controller.

:Parameters:
    usage:  snmptrapd.py [-v]
:Keywords:
    onap dcae snmp trap publish dmaap
"""

__docformat__ = 'restructuredtext'

# basics
import argparse
import array
import asyncio
from collections import Counter
import datetime
import errno
import inspect
import json
import logging
import logging.handlers
import os
import pprint
import requests
import re
import sys
import signal
import string
import socket
import time
import traceback
import unicodedata
import uuid as uuid_mod

# pysnmp
from pysnmp.entity import engine, config
from pysnmp.carrier.asyncio.dgram import udp, udp6
# from pysnmp.carrier.asyncore.dgram import udp
from pysnmp.entity.rfc3413 import ntfrcv
from pysnmp.proto.api import v2c

# dcae_snmptrap
import trapd_settings as tds
from trapd_runtime_pid import save_pid, rm_pid
from trapd_get_cbs_config import get_cbs_config
from trapd_exit import cleanup_and_exit
from trapd_http_session import init_session_obj

from trapd_io import roll_all_logs, open_eelf_logs, roll_file, open_file, close_file, ecomp_logger, stdout_logger

prog_name = os.path.basename(__file__)
verbose = False

# # # # # # # # # # #
# fx: usage_err
# # # # # # # # # # #


def usage_err():
    """
    Notify of incorrect (argument) usage
    :Parameters:
       none
    :Exceptions:
       none
    :Keywords:
       usage args
    """

    print('Incorrect usage invoked.  Correct usage:')
    print('  %s [-v]' % prog_name)
    cleanup_and_exit(1, "undefined")


# # # # # # # # # # # # # # # # # # #
# fx: load_all_configs
# # # # # # # # # # ## # # # # # # #


def load_all_configs(_signum, _frame):
    """
    Calls individual functions to read various config files required.  This
    function is called directly (e.g. at startup) and is also registered
    with signal handling (e.g. kill -sigusr1 <pid>)

    :Parameters:
      signum and frame (only present when called via signal to running process)
    :Exceptions:
      none
    :Keywords:
      config files
    :Variables:
      yaml_conf_file
      rs
    """

    if int(_signum) != 0:
        msg = ("received signal %s at frame %s; re-reading configs"
               % (_signum, _frame))
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)

    # Initialize dmaap requests session object. Close existing session
    # if applicable.
    if tds.http_requ_session is not None:
        tds.http_requ_session.close()

    tds.http_requ_session = init_session_obj()
    if tds.http_requ_session is None:
        msg = "Unable to create new http session - FATAL ERROR, exiting"
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_FATAL, tds.CODE_GENERAL, msg)
        stdout_logger(msg)
        cleanup_and_exit(1, tds.pid_file_name)

    # re-request config from config binding service
    # (either broker, or json file override)
    if not get_cbs_config():
        msg = "error (re)loading CBS config - FATAL ERROR, exiting"
        stdout_logger(msg)
        cleanup_and_exit(1, tds.pid_file_name)
    else:
        current_runtime_config_file_name = tds.c_config['files.runtime_base_dir'] + \
            "/tmp/current_config.json"

        msg = "current config logged to : %s" % current_runtime_config_file_name
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)

        with open(current_runtime_config_file_name, 'w') as outfile:
            json.dump(tds.c_config, outfile)

        # if here, config re-read successfully
        return True

# # # # # # # # # # # # #
# fx: log_all_arriving_traps
# # # # # # # # # # # # #


def log_all_arriving_traps():

    # roll logs as needed/defined in files.roll_frequency
    if tds.c_config['files.roll_frequency'] == "minute":
        curr_minute = datetime.datetime.now().minute
        if curr_minute != tds.last_minute:
            roll_all_logs()
            tds.last_minute = curr_minute
    elif tds.c_config['files.roll_frequency'] == "hour":
        curr_hour = datetime.datetime.now().hour
        if curr_hour != tds.last_hour:
            roll_all_logs()
            tds.last_hour = curr_hour
    else:
        # otherwise, assume daily roll
        curr_day = datetime.datetime.now().day
        if curr_day != tds.last_day:
            roll_all_logs()
            tds.last_day = curr_day

    # now log latest arriving trap
    try:
        # going for:
        #    1520971776 Tue Mar 13 16:09:36 2018; 1520971776 2018-03-13 16:09:36 DCAE-COLLECTOR-UCSNMP 15209717760049 .1.3.6.1.4.1.2636.4.1.6 gfpmt5pcs10.oss.att.com 135.91.10.139 12.123.1.240 12.123.1.240 2 varbinds: [0] .1.3.6.1.2.1.1.3.0 {10} 1212058366 140 days, 6:49:43.66 [1] .1.3.6.1.6.3.1.1.4.1.0 {6} .1.3.6.1.4.1.2636.4.1.6 [2] .1.3.6.1.4.1.2636.3.1.15.1.1.2.4.0.0 {2} 2 [3] .1.3.6.1.4.1.2636.3.1.15.1.2.2.4.0.0 {2} 4 [4] .1.3.6.1.4.1.2636.3.1.15.1.3.2.4.0.0 {2} 0 [5] .1.3.6.1.4.1.2636.3.1.15.1.4.2.4.0.0 {2} 0 [6] .1.3.6.1.4.1.2636.3.1.15.1.5.2.4.0.0 {4} PEM 3 [7] .1.3.6.1.4.1.2636.3.1.15.1.6.2.4.0.0 {2} 7 [8] .1.3.6.1.4.1.2636.3.1.15.1.7.2.4.0.0 {2} 4 [9] .1.3.6.1.6.3.18.1.3.0 {7} 12.123.1.240

        tds.arriving_traps_fd.write('%s %s; %s %s %s %s %s %s %s %s %s %s %s\n' %
                                    (tds.trap_dict["time received"],
                                     time.strftime(
                                         "%a %b %d %H:%M:%S %Y", time.localtime(time.time())),
                                        time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(
                                            tds.trap_dict["time received"])),
                                        tds.trap_dict["trap category"],
                                        tds.trap_dict["epoch_serno"],
                                        tds.trap_dict["notify OID"],
                                        tds.trap_dict["agent name"],
                                        tds.trap_dict["agent address"],
                                        tds.trap_dict["cambria.partition"],
                                        tds.trap_dict["protocol version"],
                                        tds.trap_dict["sysUptime"],
                                        tds.trap_dict["uuid"],
                                        tds.all_vb_json_str))

    except Exception as e:
        msg = "Error writing to %s : %s - arriving trap %s NOT LOGGED" % (
            tds.arriving_traps_filename, str(e), tds.trap_dict["uuid"])
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_CRIT, tds.CODE_GENERAL, msg)


# # # # # # # # # # # # #
# fx: log_published_messages
# # # # # # # # # # # # #


def log_published_messages(_post_data_enclosed):

    # FIXME: should keep data dictionary of Fd's open, and reference those vs.
    #        repeatedly opening append-mode

    msg = "adding trap UUID %s to json log" % tds.trap_dict["uuid"]
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)

    try:
        tds.json_traps_fd.write('%s\n' % _post_data_enclosed)
        msg = "successfully logged json for %s to %s" % (
            tds.trap_dict["uuid"], tds.json_traps_filename)
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)
    except Exception as e:
        msg = "Error writing to %s : %s - trap %s NOT LOGGED" % (
            tds.json_traps_filename, str(e), tds.trap_dict["uuid"])
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_CRIT, tds.CODE_GENERAL, msg)


# # # # # # # # # # # # #
# fx: post_dmaap
# # # # # # # # # # # # #


def post_dmaap():
    """
    Publish trap daata in json format to dmaap
    :Parameters:
    :Exceptions:
      none
    :Keywords:
      http post dmaap json message
    :Variables:
    """

    http_headers = {"Content-type": "application/json"}

    if tds.http_requ_session is None:
        msg = "tds.http_requ_session is None - getting new (%s)" % tds.http_requ_session
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)
        tds.http_requ_session = init_session_obj()

    # if only 1 trap, ship as-is
    if tds.traps_since_last_publish == 1:
        post_data_enclosed = tds.all_traps_str
    else:
        # otherwise, add brackets around package
        post_data_enclosed = '[' + tds.all_traps_str + ']'

    msg = "post_data_enclosed: %s" % (post_data_enclosed)
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED, tds.CODE_GENERAL, msg)

    k = 0
    dmaap_pub_success = False

    while not dmaap_pub_success and k < (int(tds.c_config['publisher.http_retries'])):
        try:
            if tds.c_config['streams_publishes']['sec_fault_unsecure']['aaf_username'] == "" or tds.c_config['streams_publishes']['sec_fault_unsecure']['aaf_username'] is None:
                msg = "%d trap(s) : %s - attempt %d (unsecure)" % (
                    tds.traps_since_last_publish, tds.trap_uuids_in_buffer, k)
                ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                             tds.CODE_GENERAL, msg)
                http_resp = tds.http_requ_session.post(tds.c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url'], post_data_enclosed,
                                                       headers=http_headers,
                                                       timeout=tds.timeout_seconds)
            else:
                msg = "%d trap(s) : %s - attempt %d (secure)" % (
                    tds.traps_since_last_publish, tds.trap_uuids_in_buffer, k)
                ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                             tds.CODE_GENERAL, msg)
                http_resp = tds.http_requ_session.post(tds.c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url'], post_data_enclosed,
                                                       auth=(tds.c_config['streams_publishes']['sec_fault_unsecure']['aaf_username'],
                                                             tds.c_config['streams_publishes']['sec_fault_unsecure']['aaf_password']),
                                                       headers=http_headers,
                                                       timeout=tds.timeout_seconds)

            if http_resp.status_code == requests.codes.ok:
                # msg = "%d trap(s) : %s successfully published - response from %s: %d %s" % (traps_since_last_publish, trap_uuids_in_buffer, ((c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url']).split('/')[2][:-5]) ,http_resp.status_code, http_resp.text)
                msg = "%d trap(s) successfully published: %s" % (
                    tds.traps_since_last_publish, tds.trap_uuids_in_buffer)
                ecomp_logger(tds.LOG_TYPE_METRICS, tds.SEV_INFO,
                             tds.CODE_GENERAL, msg)
                log_published_messages(post_data_enclosed)
                tds.last_pub_time = time.time()
                dmaap_pub_success = True
                break
            else:
                msg = "Trap(s) %s publish attempt %d returned non-normal: %d %s" % (
                    tds.trap_uuids_in_buffer, k, http_resp.status_code, http_resp.text)
                ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_WARN,
                             tds.CODE_GENERAL, msg)

        except OSError as e:
            msg = "OS exception while attempting to post %s attempt %s: (%s) %s %s" % (
                tds.trap_uuids_in_buffer, k,  e.errno, e.strerror, str(e))
            ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_WARN,
                         tds.CODE_GENERAL, msg)

        except requests.exceptions.RequestException as e:
            msg = "Requests exception while attempting to post %s attempt %d: (%d) %s" % (
                tds.trap_uuids_in_buffer, int(k),  int(e.errno), str(e.strerror))
            ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_WARN,
                         tds.CODE_GENERAL, msg)

        k += 1

        if k < tds.c_config['publisher.http_retries']:
            msg = "sleeping %.4f seconds and retrying" % (
                tds.seconds_between_retries)
            ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                         tds.CODE_GENERAL, msg)
            time.sleep(tds.seconds_between_retries)
        else:
            break

    if not dmaap_pub_success:
        msg = "ALL publish attempts failed for traps %s to URL %s "\
            % (tds.trap_uuids_in_buffer, tds.c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url'])
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_CRIT, tds.CODE_GENERAL, msg)

    # FIXME: This currently tries, then logs error and trashes buffer if all dmaap attempts fail. Better way?
    tds.traps_since_last_publish = 0
    tds.trap_uuids_in_buffer = ""
    tds.all_traps_str = ""
    tds.first_trap = True

# # # # # # # # # # # # # # # # # # #
# fx: request_observer for community string rewrite
# # # # # # # # # # # # # # # # # # #


def comm_string_rewrite_observer(snmpEngine, execpoint, variables, cbCtx):

    # match ALL community strings
    if re.match('.*', str(variables['communityName'])):
        # msg = "Rewriting communityName '%s' from %s into 'public'"  % (variables['communityName'], ':'.join([str(x) for x in
        #                                                                variables['transportInformation'][1]]))
        # ecomp_logger(eelf_debug_fd, eelf_debug_fd, tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)
        variables['communityName'] = variables['communityName'].clone('public')

# # # # # # # # # # # # # # # # # # #
# fx: snmp_engine_observer_cb
#     callback for when trap is received
# # # # # # # # # # # # # # # # # # #


def snmp_engine_observer_cb(snmp_engine, execpoint, variables, cbCtx):
    """
    Decompose trap attributes and load in dictionary which is later used to
    create json string for publishing to dmaap.
    :Parameters:
      snmp_engine
         snmp engine created to listen for arriving traps
      execpoint
         point in code that snmp_engine_observer_cb was invoked
      variables
         trap attributes
      cbCtx
         callback context
    :Exceptions:
      none
    :Keywords:
      UEB non-AAF legacy http post
    :Variables:
    """

    # init dictionary on new trap
    tds.trap_dict = {}

    # assign uuid to trap
    tds.trap_dict["uuid"] = str(uuid_mod.uuid1())

    # ip and hostname
    ip_addr_str = str(variables['transportAddress'][0])
    tds.trap_dict["agent address"] = ip_addr_str

    msg = 'snmp trap arrived from %s, assigned uuid: %s' % \
        (ip_addr_str, tds.trap_dict["uuid"])
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED, tds.CODE_GENERAL, msg)

    try:
        if int(tds.dns_cache_ip_expires[ip_addr_str] < int(time.time())):
            raise Exception('cache expired for %s at %d - updating value' %
                            (ip_addr_str, (tds.dns_cache_ip_expires[ip_addr_str])))
        else:
            tds.trap_dict["agent name"] = tds.dns_cache_ip_to_name[ip_addr_str]
    except Exception as e:
        msg = "dns cache expired or missing for %s - refreshing" % ip_addr_str
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)
        try:
            agent_fqdn, alias, addresslist = socket.gethostbyaddr(ip_addr_str)
        except Exception as e:
            agent_fqdn = ip_addr_str

        tds.trap_dict["agent name"] = agent_fqdn

        tds.dns_cache_ip_to_name[ip_addr_str] = agent_fqdn
        tds.dns_cache_ip_expires[ip_addr_str] = (
            time.time() + tds.c_config['cache.dns_cache_ttl_seconds'])
        msg = "cache for %s (%s) updated - set to expire at %d" % \
            (agent_fqdn, ip_addr_str, tds.dns_cache_ip_expires[ip_addr_str])
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)

    tds.trap_dict["cambria.partition"] = str(tds.trap_dict["agent name"])
    # do not include cleartext community in pub
    tds.trap_dict["community"] = ""
    tds.trap_dict["community len"] = 0

    # FIXME.CHECK_WITH_DOWNSTREAM_CONSUMERS: get rid of round for millisecond val
    # epoch_second = int(round(time.time()))
    epoch_msecond = time.time()
    epoch_second = int(round(epoch_msecond))
    if epoch_second == tds.last_epoch_second:
        tds.traps_in_epoch += 1
    else:
        tds.traps_in_epoch = 0
    tds.last_epoch_second = epoch_second
    traps_in_epoch_04d = format(tds.traps_in_epoch, '04d')
    tds.trap_dict['epoch_serno'] = int(
        (str(epoch_second) + str(traps_in_epoch_04d)))

    snmp_version = variables['securityModel']
    if snmp_version == 1:
        tds.trap_dict["protocol version"] = "v1"
    else:
        if snmp_version == 2:
            tds.trap_dict["protocol version"] = "v2c"
        else:
            if snmp_version == 3:
                tds.trap_dict["protocol version"] = "v3"
            else:
                tds.trap_dict["protocol version"] = "unknown"

    if snmp_version == 3:
        tds.trap_dict["protocol version"] = "v3"
        tds.trap_dict["security level"] = str(variables['securityLevel'])
        tds.trap_dict["context name"] = str(
            variables['contextName'].prettyPrint())
        tds.trap_dict["security name"] = str(variables['securityName'])
        tds.trap_dict["security engine"] = str(
            variables['contextEngineId'].prettyPrint())
    tds.trap_dict['time received'] = epoch_msecond
    tds.trap_dict['trap category'] = (
        tds.c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url']).split('/')[-1]


# # # # # # # # # # # # # # # # # # #
# fx: request_observer for community string rewrite
# # # # # # # # # # # # # # # # # # #
def add_varbind_to_json(vb_idx, vb_oid, vb_type, vb_val):
    """
    Called for each varbind, adds individual attributes of varbind instance to
    vb_json_str.  vb_json_str will be added to curr_trap_json_str prior to publish.
    :Parameters:
      vb_idx
        index to specific varbind being processed
      vb_oid
        the varbind oid
      vb_val
        the value of the varbind
    :Exceptions:
      none
    :Keywords:
      varbind extract json
    :Variables:
    """

    _individual_vb_dict = {}

    if tds.trap_dict["protocol version"] == "v2c":
        # if v2c and first 2 varbinds, special handling required - e.g. put
        # in trap_dict, not vb_json_str
        if vb_idx == 0:
            tds.trap_dict["sysUptime"] = str(vb_val.prettyPrint())
            return True
        else:
            if vb_idx == 1:
                tds.trap_dict["notify OID"] = str(vb_val.prettyPrint())
                tds.trap_dict["notify OID len"] = (
                    tds.trap_dict["notify OID"].count('.') + 1)
                return True
    if tds.first_varbind:
        tds.all_vb_json_str = ', \"varbinds\": ['
        tds.first_varbind = False
    else:
        # all_vb_json_str = ''.join([all_vb_json_str, ' ,'])
        # all_vb_json_str = "%s ," % all_vb_json_str
        tds.all_vb_json_str = tds.all_vb_json_str + " ,"

    _individual_vb_dict.clear()
    _individual_vb_dict['varbind_oid'] = vb_oid.prettyPrint()
    _individual_vb_dict['varbind_type'] = vb_type
    _individual_vb_dict['varbind_value'] = vb_val.prettyPrint()

    _individual_vb_json_str = json.dumps(_individual_vb_dict)

    # all_vb_json_str = "%s%s" % (all_vb_json_str, individual_vb_json_str)
    # all_vb_json_str = ''.join([all_vb_json_str, individual_vb_json_str])
    tds.all_vb_json_str = tds.all_vb_json_str + _individual_vb_json_str
    return True


# Callback function for receiving notifications
# noinspection PyUnusedLocal,PyUnusedLocal,PyUnusedLocal
def notif_receiver_cb(snmp_engine, stateReference, contextEngineId, contextName,
                      varBinds, cbCtx):
    """
    Callback executed when trap arrives
    :Parameters:
      snmp_engine
        snmp engine created to listen for arriving traps
      stateReference
      contextEngineId
      contextName
      varBinds
        trap varbinds - why we are here
      cbCtx
        callback context
    :Exceptions:
      none
    :Keywords:
      callback trap arrival
    :Variables:
    """

    msg = "processing varbinds for %s" % (tds.trap_dict["uuid"])
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED, tds.CODE_GENERAL, msg)

    # FIXME update reset location when batching publishes
    vb_idx = 0

    # For reference:
    #
    # print('\nvarBinds ==> %s' % (varBinds))
    #
    # varBinds ==> [(ObjectName('1.3.6.1.2.1.1.3.0'), TimeTicks(1243175676)),
    #               (ObjectName('1.3.6.1.6.3.1.1.4.1.0'), ObjectIdentifier('1.3.6.1.4.1.74.2.46.12.1.1')),
    #               (ObjectName('1.3.6.1.4.1.74.2.46.12.1.1.1'), OctetString(b'ucsnmp heartbeat - ignore')),
    #               (ObjectName('1.3.6.1.4.1.74.2.46.12.1.1.2'), OctetString(b'Fri Aug 11 17:46:01 EDT 2017'))]
    #

    tds.all_vb_json_str = ""
    vb_idx = 0
    tds.first_varbind = True

    # iterate over varbinds, add to json struct
    for vb_oid, vb_val in varBinds:
        add_varbind_to_json(vb_idx, vb_oid, vb_val.__class__.__name__, vb_val)
        vb_idx += 1

    # FIXME: DL back out first 2 varbinds for v2c notifs prior to publishing varbind count
    # trap_dict["varbind count"] = vb_idx
    curr_trap_json_str = json.dumps(tds.trap_dict)
    # now have everything except varbinds in "curr_trap_json_str"

    # if varbinds present - which will almost always be the case - add all_vb_json_str to trap_json_message
    if vb_idx != 0:
        # close out vb array
        # all_vb_json_str += "]"
        # all_vb_json_str = ''.join([all_vb_json_str, ']'])
        tds.all_vb_json_str = tds.all_vb_json_str + ']'

        # remove last close bracket from curr_trap_json_str
        curr_trap_json_str = curr_trap_json_str[:-1]

        # add vb_json_str to payload
        # curr_trap_json_str += all_vb_json_str
        # curr_trap_json_str = ''.join([curr_trap_json_str, all_vb_json_str])
        curr_trap_json_str = curr_trap_json_str + tds.all_vb_json_str

        # add last close brace back in
        # curr_trap_json_str += "}"
        # curr_trap_json_str = ''.join([curr_trap_json_str, '}'])
        curr_trap_json_str = curr_trap_json_str + '}'

    msg = "trap %s : %s" % (tds.trap_dict["uuid"], curr_trap_json_str)
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED, tds.CODE_GENERAL, msg)

    # now have a complete json message for this trap in "curr_trap_json_str"
    tds.traps_since_last_publish += 1
    milliseconds_since_last_publish = (time.time() - tds.last_pub_time) * 1000

    msg = "adding %s to buffer" % (tds.trap_dict["uuid"])
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED, tds.CODE_GENERAL, msg)
    if tds.first_trap:
        tds.all_traps_str = curr_trap_json_str
        tds.trap_uuids_in_buffer = tds.trap_dict["uuid"]
        tds.first_trap = False
    else:
        tds.trap_uuids_in_buffer = tds.trap_uuids_in_buffer + \
            ', ' + tds.trap_dict["uuid"]
        tds.all_traps_str = tds.all_traps_str + ', ' + curr_trap_json_str

    # always log arriving traps
    log_all_arriving_traps()

    # publish to dmaap after last varbind is processed
    if tds.traps_since_last_publish >= tds.c_config['publisher.max_traps_between_publishes']:
        msg = "num traps since last publish (%d) exceeds threshold (%d) - publish traps" % (
            tds.traps_since_last_publish, tds.c_config['publisher.max_traps_between_publishes'])
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)
        post_dmaap()
    elif milliseconds_since_last_publish >= tds.c_config['publisher.max_milliseconds_between_publishes']:
        msg = "num milliseconds since last publish (%.0f) exceeds threshold - publish traps" % milliseconds_since_last_publish
        ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_DETAILED,
                     tds.CODE_GENERAL, msg)
        post_dmaap()


# # # # # # # # # # # # #
# Main  MAIN  Main  MAIN
# # # # # # # # # # # # #
# parse command line args

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description='Post SNMP traps '
                                                 'to message bus')
    parser.add_argument('-v', action="store_true", dest="verbose",
                        help="verbose logging")
    parser.add_argument('-?', action="store_true", dest="usage_requested",
                        help="show command line use")
    
    # parse args
    args = parser.parse_args()
    
    # set vars from args
    verbose = args.verbose
    usage_requested = args.usage_requested
    
    # if usage, just display and exit
    if usage_requested:
        usage_err()
    
    # init vars
    tds.init()
    
    # Set initial startup hour for rolling logfile
    tds.last_hour = datetime.datetime.now().hour
    
    # get config binding service (CBS) values (either broker, or json file override)
    load_all_configs(0, 0)
    msg = "%s : %s version %s starting" % (
        prog_name, tds.c_config['snmptrap.title'], tds.c_config['snmptrap.version'])
    stdout_logger(msg)
    
    # Avoid this unless needed for testing; it prints sensitive data to log
    #
    # msg = "Running config: "
    # stdout_logger(msg)
    # msg = json.dumps(c_config, sort_keys=False, indent=4)
    # stdout_logger(msg)
    
    # open various ecomp logs
    open_eelf_logs()
    
    # bump up logging level if overridden at command line
    if verbose:
        msg = "WARNING:  '-v' argument present.  All diagnostic messages will be logged.  This can slow things down, use only when needed."
        tds.minimum_severity_to_log = 0
        stdout_logger(msg)
    
    # name and open arriving trap log
    tds.arriving_traps_filename = tds.c_config['files.runtime_base_dir'] + "/" + \
        tds.c_config['files.log_dir'] + "/" + \
        (tds.c_config['files.arriving_traps_log'])
    tds.arriving_traps_fd = open_file(tds.arriving_traps_filename)
    msg = ("arriving traps logged to: %s" % tds.arriving_traps_filename)
    stdout_logger(msg)
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)
    
    # name and open json trap log
    tds.json_traps_filename = tds.c_config['files.runtime_base_dir'] + "/" + tds.c_config['files.log_dir'] + "/" + "DMAAP_" + (
        tds.c_config['streams_publishes']['sec_fault_unsecure']['dmaap_info']['topic_url'].split('/')[-1]) + ".json"
    tds.json_traps_fd = open_file(tds.json_traps_filename)
    msg = ("published traps logged to: %s" % tds.json_traps_filename)
    stdout_logger(msg)
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)
    
    # setup signal handling for config reload
    signal.signal(signal.SIGUSR1, load_all_configs)
    
    # save current PID for future/external reference
    tds.pid_file_name = tds.c_config['files.runtime_base_dir'] + \
        '/' + tds.c_config['files.pid_dir'] + '/' + prog_name + ".pid"
    msg = "Runtime PID file: %s" % tds.pid_file_name
    ecomp_logger(tds.LOG_TYPE_DEBUG, tds.SEV_INFO, tds.CODE_GENERAL, msg)
    rc = save_pid(tds.pid_file_name)
    
    # Get the event loop for this thread
    loop = asyncio.get_event_loop()
    
    # Create SNMP engine with autogenernated engineID pre-bound
    # to socket transport dispatcher
    snmp_engine = engine.SnmpEngine()
    
    # # # # # # # # # # # #
    # Transport setup
    # # # # # # # # # # # #
    
    # UDP over IPv4
    # FIXME:  add check for presense of ipv4_interface prior to attempting add OR just put entire thing in try/except clause
    try:
        ipv4_interface = tds.c_config['protocols.ipv4_interface']
        ipv4_port = tds.c_config['protocols.ipv4_port']
    
        try:
            config.addTransport(
                snmp_engine,
                udp.domainName + (1,),
                udp.UdpTransport().openServerMode(
                    (ipv4_interface, ipv4_port))
            )
        except Exception as e:
            msg = "Unable to bind to %s:%s - %s" % (
                ipv4_interface, ipv4_port, str(e))
            ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_FATAL, tds.CODE_GENERAL, msg)
            stdout_logger(msg)
            cleanup_and_exit(1, tds.pid_file_name)
    
    except Exception as e:
        msg = "IPv4 interface and/or port not specified in config - not listening for IPv4 traps"
        stdout_logger(msg)
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_WARN, tds.CODE_GENERAL, msg)
    
    
    # UDP over IPv4, second listening interface/port example if you don't want to listen on all
    # config.addTransport(
    #     snmp_engine,
    #     udp.domainName + (2,),
    #     udp.UdpTransport().openServerMode(('127.0.0.1', 2162))
    # )
    
    
    # UDP over IPv6
    # FIXME:  add check for presense of ipv6_interface prior to attempting add OR just put entire thing in try/except clause
    try:
        ipv6_interface = tds.c_config['protocols.ipv6_interface']
        ipv6_port = tds.c_config['protocols.ipv6_port']
    
        try:
            config.addTransport(
                snmp_engine,
                udp6.domainName,
                udp6.Udp6Transport().openServerMode(
                    (ipv6_interface, ipv6_port))
            )
        except Exception as e:
            msg = "Unable to bind to %s:%s - %s" % (
                ipv6_interface, ipv6_port, str(e))
            stdout_logger(msg)
            ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_FATAL, tds.CODE_GENERAL, msg)
            cleanup_and_exit(1, tds.pid_file_name)
    
    except Exception as e:
        msg = "IPv6 interface and/or port not specified in config - not listening for IPv6 traps"
        stdout_logger(msg)
        ecomp_logger(tds.LOG_TYPE_ERROR, tds.SEV_WARN, tds.CODE_GENERAL, msg)
    
    
    # # # # # # # # # # # #
    # SNMPv1/2c setup
    # # # # # # # # # # # #
    
    # SecurityName <-> CommunityName mapping
    #     to restrict trap reception to only those with specific community
    #     strings
    config.addV1System(snmp_engine, 'my-area', 'public')
    
    # register comm_string_rewrite_observer for message arrival
    snmp_engine.observer.registerObserver(
        comm_string_rewrite_observer,
        'rfc2576.processIncomingMsg:writable'
    )
    
    # register snmp_engine_observer_cb for message arrival
    snmp_engine.observer.registerObserver(
        snmp_engine_observer_cb,
        'rfc3412.receiveMessage:request',
        'rfc3412.returnResponsePdu',
    )
    
    # Register SNMP Application at the SNMP engine
    ntfrcv.NotificationReceiver(snmp_engine, notif_receiver_cb)
    
    snmp_engine.transportDispatcher.jobStarted(1)  # loop forever
    
    # Run I/O dispatcher which will receive traps
    try:
        snmp_engine.transportDispatcher.runDispatcher()
    except Exception as e:
        snmp_engine.observer.unregisterObserver()
        snmp_engine.transportDispatcher.closeDispatcher()
        cleanup_and_exit(1, tds.pid_file_name)