PONSIM: Generate alarms as FrameIO egress packets
- Option to enable generation of alarms
- Alarms are received by PONSIM OLT and submitted to kafka
- Option to configure alarm frequency

Change-Id: I93a05eaaae7eb2a6f25937ec76470c1b24c2842b
diff --git a/ponsim/main.py b/ponsim/main.py
index b6e305e..2100879 100755
--- a/ponsim/main.py
+++ b/ponsim/main.py
@@ -113,6 +113,23 @@
                         action='count',
                         help=_help)
 
+    _help = 'enable generation of simulated alarms'
+    parser.add_argument('-a', '--alarm-simulation',
+                        dest='alarm_simulation',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    _help = 'frequency of simulated alarms (in seconds)'
+    parser.add_argument('-f', '--alarm-frequency',
+                        dest='alarm_frequency',
+                        action='store',
+                        type=int,
+                        metavar="[30-300]",
+                        choices=range(30,301),
+                        default=60,
+                        help=_help)
+
     _help = 'omit startup banner log lines'
     parser.add_argument('-n', '--no-banner',
                         dest='no_banner',
@@ -142,6 +159,10 @@
         self.ponsim = None
         self.grpc_server = None
 
+        self.alarm_config = dict()
+        self.alarm_config['simulation'] = self.args.alarm_simulation
+        self.alarm_config['frequency'] = self.args.alarm_frequency
+
         if not args.no_banner:
             print_banner(self.log)
 
@@ -158,7 +179,7 @@
             iface_map = self.setup_networking_assets(self.args.name,
                                                      self.args.onus)
             self.io = yield RealIo(iface_map).start()
-            self.ponsim = PonSim(self.args.onus, self.io.egress)
+            self.ponsim = PonSim(self.args.onus, self.io.egress, self.alarm_config)
             self.io.register_ponsim(self.ponsim)
 
             self.grpc_server = GrpcServer(self.args.grpc_port, self.ponsim)
diff --git a/ponsim/ponsim.py b/ponsim/ponsim.py
index 437290e..b060a10 100644
--- a/ponsim/ponsim.py
+++ b/ponsim/ponsim.py
@@ -20,15 +20,21 @@
 handle 0-tagged packets (no comment).
 """
 import structlog
-from scapy.layers.inet import IP, UDP
+import random
+import arrow
+import json
+from scapy.layers.inet import IP, UDP, TCP, Raw
 from scapy.layers.l2 import Ether, Dot1Q
 from scapy.packet import Packet
 
 from voltha.protos import third_party
 from voltha.protos.ponsim_pb2 import PonSimMetrics, PonSimPortMetrics, \
     PonSimPacketCounter
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, \
+    AlarmEventState, AlarmEventCategory
 from voltha.core.flow_decomposer import *
 from twisted.internet.task import LoopingCall
+from twisted.internet import reactor
 
 _ = third_party
 
@@ -147,6 +153,79 @@
         return sim_metrics
 
 
+class SimAlarms:
+    def __init__(self):
+        self.lc = None
+
+    @staticmethod
+    def _prepare_alarm():
+        alarm_event = dict()
+
+        try:
+            # Randomly choose values for each enum types
+            alm_severity = random.choice(list(
+                v for k, v in
+                AlarmEventSeverity.DESCRIPTOR.enum_values_by_name.items()))
+
+            alm_type = random.choice(list(
+                v for k, v in
+                AlarmEventType.DESCRIPTOR.enum_values_by_name.items()))
+
+            alm_category = random.choice(list(
+                v for k, v in
+                AlarmEventCategory.DESCRIPTOR.enum_values_by_name.items()))
+
+            alarm_event['severity'] = alm_severity.number
+            alarm_event['type'] = alm_type.number
+            alarm_event['category'] = alm_category.number
+            alarm_event['state'] = AlarmEventState.RAISED
+            alarm_event['ts'] = arrow.utcnow().timestamp
+            alarm_event['description'] = "{}.{} alarm".format(alm_type.name, alm_category.name)
+
+            return alarm_event
+
+        except Exception as e:
+            log.exception('failed-to-prepare-alarm', e=e)
+
+    @staticmethod
+    def _raise_alarm(alarm_event, olt, egress):
+        try:
+            frame = Ether() / Dot1Q(vlan=4000) / IP() / TCP() / Raw(load=json.dumps(alarm_event))
+            egress(0, frame)
+
+        except Exception as e:
+            log.exception('failed-to-raise-alarm', e=e)
+
+    @staticmethod
+    def _clear_alarm(alarm_event, olt, egress):
+        try:
+            alarm_event['state'] = AlarmEventState.CLEARED
+            frame = Ether() / Dot1Q(vlan=4000) / IP() / TCP() / Raw(load=json.dumps(alarm_event))
+            egress(0, frame)
+
+        except Exception as e:
+            log.exception('failed-to-clear-alarm', e=e)
+
+    def _generate_alarm(self, olt, egress):
+        try:
+            alarm = self._prepare_alarm()
+            self._raise_alarm(alarm, olt, egress)
+            reactor.callLater(random.randint(20, 60), self._clear_alarm, alarm, olt, egress)
+        except Exception as e:
+            log.exception(e=e)
+
+    def start_simulation(self, olt, egress, config):
+        log.info("starting-alarm-simulation")
+
+        """Simulate periodic device alarms"""
+        self.lc = LoopingCall(self._generate_alarm, olt, egress)
+        self.lc.start(config['frequency'])
+
+    def stop_simulation(self):
+        log.info("stopping-alarm-simulation")
+        self.lc.stop()
+
+
 class SimDevice(object):
     def __init__(self, name, logical_port_no):
         self.name = name
@@ -338,7 +417,7 @@
 
 
 class PonSim(object):
-    def __init__(self, onus, egress_fun):
+    def __init__(self, onus, egress_fun, alarm_config):
         self.egress_fun = egress_fun
 
         self.log = structlog.get_logger()
@@ -372,6 +451,10 @@
             self.log.info("pon-sim-init", port=d, name=self.devices[d].name,
                           links=self.devices[d].links)
 
+        if alarm_config['simulation']:
+            self.alarms = SimAlarms()
+            self.alarms.start_simulation(self.olt, self.egress_fun, alarm_config)
+
     def get_ports(self):
         return sorted(self.devices.keys())
 
diff --git a/voltha/adapters/ponsim_olt/ponsim_olt.py b/voltha/adapters/ponsim_olt/ponsim_olt.py
index f2b3b76..26283af 100644
--- a/voltha/adapters/ponsim_olt/ponsim_olt.py
+++ b/voltha/adapters/ponsim_olt/ponsim_olt.py
@@ -21,8 +21,10 @@
 
 import arrow
 import grpc
+import json
 import structlog
 from scapy.layers.l2 import Ether, Dot1Q
+from scapy.layers.inet import Raw
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
 from zope.interface import implementer
@@ -41,8 +43,6 @@
 from voltha.protos.device_pb2 import DeviceType, DeviceTypes, Port, Device, \
     PmConfig, PmConfigs
 from voltha.protos.events_pb2 import KpiEvent, KpiEventType, MetricValuePairs
-from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, \
-    AlarmEventState, AlarmEventCategory
 from voltha.protos.health_pb2 import HealthStatus
 from google.protobuf.empty_pb2 import Empty
 
@@ -147,6 +147,36 @@
         self.lc.start(interval=self.default_freq / 10)
 
 
+class AdapterAlarms:
+    def __init__(self, adapter, device):
+        self.adapter = adapter
+        self.device = device
+        self.lc = None
+
+    def send_alarm(self, context_data, alarm_data):
+        try:
+            current_context = {}
+            for key, value in context_data.__dict__.items():
+                current_context[key] = str(value)
+
+            alarm_event = self.adapter.adapter_agent.create_alarm(
+                resource_id=self.device.id,
+                description="{}.{} - {}".format(self.adapter.name, self.device.id,
+                                                alarm_data['description']) if 'description' in alarm_data else None,
+                type=alarm_data['type'] if 'type' in alarm_data else None,
+                category=alarm_data['category'] if 'category' in alarm_data else None,
+                severity=alarm_data['severity'] if 'severity' in alarm_data else None,
+                state=alarm_data['state'] if 'state' in alarm_data else None,
+                raised_ts=alarm_data['ts'] if 'ts' in alarm_data else 0,
+                context=current_context
+            )
+
+            self.adapter.adapter_agent.submit_alarm(alarm_event)
+
+        except Exception as e:
+            log.exception('failed-to-send-alarm', e=e)
+
+
 @implementer(IAdapterInterface)
 class PonSimOltAdapter(object):
     name = 'ponsim_olt'
@@ -278,7 +308,7 @@
         self.ofp_port_no = None
         self.interface = registry('main').get_args().interface
         self.pm_metrics = None
-        self.alarm_lc = None
+        self.alarms = None
 
     def __del__(self):
         if self.io_port is not None:
@@ -316,6 +346,9 @@
         log.info("initial-pm-config", pm_config=pm_config)
         self.adapter_agent.update_device_pm_config(pm_config, init=True)
 
+        # Setup alarm handler
+        self.alarms = AdapterAlarms(self.adapter, device)
+
         nni_port = Port(
             port_no=2,
             label='NNI facing Ethernet port',
@@ -406,9 +439,6 @@
         # Start collecting stats from the device after a brief pause
         self.start_kpi_collection(device.id)
 
-        # Start generating simulated alarms
-        self.start_alarm_simulation(device.id)
-
     def rcv_io(self, port, frame):
         self.log.info('reveived', iface_name=port.iface_name,
                       frame_len=len(frame))
@@ -430,6 +460,9 @@
                 self.log.info('sending-packet-in', **kw)
                 self.adapter_agent.send_packet_in(
                     packet=str(popped_frame), **kw)
+            elif pkt.haslayer(Raw):
+                raw_data = json.loads(pkt.getlayer(Raw).load)
+                self.alarms.send_alarm(self, raw_data)
 
     def update_flow_table(self, flows):
         stub = ponsim_pb2.PonSimStub(self.get_channel())
@@ -480,7 +513,7 @@
 
         # Update the child devices connect state to UNREACHABLE
         self.adapter_agent.update_child_devices_state(self.device_id,
-                                    connect_status=ConnectStatus.UNREACHABLE)
+                                                      connect_status=ConnectStatus.UNREACHABLE)
 
         # Sleep 10 secs, simulating a reboot
         # TODO: send alert and clear alert after the reboot
@@ -497,7 +530,7 @@
 
         # Update the child devices connect state to REACHABLE
         self.adapter_agent.update_child_devices_state(self.device_id,
-                                    connect_status=ConnectStatus.REACHABLE)
+                                                      connect_status=ConnectStatus.REACHABLE)
 
         self.log.info('rebooted', device_id=self.device_id)
 
@@ -519,7 +552,7 @@
 
         # Disable all child devices first
         self.adapter_agent.update_child_devices_state(self.device_id,
-                                            admin_state=AdminState.DISABLED)
+                                                      admin_state=AdminState.DISABLED)
 
         # Remove the peer references from this device
         self.adapter_agent.delete_all_peer_references(self.device_id)
@@ -530,9 +563,6 @@
         # close the frameio port
         registry('frameio').close_port(self.io_port)
 
-        # Stop the generation of simulated alarms
-        self.stop_alarm_simulation(self.device_id)
-
         # TODO:
         # 1) Remove all flows from the device
         # 2) Remove the device from ponsim
@@ -603,7 +633,7 @@
 
         # Reenable all child devices
         self.adapter_agent.update_child_devices_state(device.id,
-                                            admin_state=AdminState.ENABLED)
+                                                      admin_state=AdminState.ENABLED)
 
         # finally, open the frameio port to receive in-band packet_in messages
         self.log.info('registering-frameio')
@@ -657,99 +687,3 @@
                 log.exception('failed-to-submit-kpis', e=e)
 
         self.pm_metrics.start_collector(_collect)
-
-    def start_alarm_simulation(self, device_id):
-        log.info("starting-alarm-simulation", device_id=device_id)
-
-        """Simulate periodic device alarms"""
-        import random
-
-        @inlineCallbacks
-        def _generate_alarm(device_id):
-            log.info("generate-alarm", device_id=device_id)
-
-            alarm = _prepare_alarm(device_id)
-            _raise_alarm(alarm)
-            yield asleep(random.randint(30, 50))
-            _clear_alarm(alarm)
-
-        def _prepare_alarm(device_id):
-            log.info("prepare-alarm", device_id=device_id)
-
-            try:
-                # Randomly choose values for each enum types
-                severity = random.choice(list(
-                    v for k, v in
-                    AlarmEventSeverity.DESCRIPTOR.enum_values_by_name.items()))
-
-                type = random.choice(list(
-                    v for k, v in
-                    AlarmEventType.DESCRIPTOR.enum_values_by_name.items()))
-
-                category = random.choice(list(
-                    v for k, v in
-                    AlarmEventCategory.DESCRIPTOR.enum_values_by_name.items()))
-
-                current_context = {}
-                for key, value in self.__dict__.items():
-                    current_context[key] = str(value)
-
-                return self.adapter_agent.create_alarm(
-                    resource_id=device_id,
-                    type=type.number,
-                    category=category.number,
-                    severity=severity.number,
-                    context=current_context)
-
-            except Exception as e:
-                log.exception('failed-to-prepare-alarm', e=e)
-
-        def _raise_alarm(alarm_event):
-            log.info("raise-alarm", alarm_event=alarm_event)
-
-            try:
-                alarm_event.description = \
-                    "RAISE simulated alarm - " \
-                    "resource:{} " \
-                    "type:{} " \
-                    "severity:{} " \
-                    "category:{}".format(
-                        alarm_event.resource_id,
-                        alarm_event.type,
-                        alarm_event.severity,
-                        alarm_event.category
-                    )
-
-                self.adapter_agent.submit_alarm(alarm_event)
-
-            except Exception as e:
-                log.exception('failed-to-raise-alarm', e=e)
-
-        def _clear_alarm(alarm_event):
-            log.info("clear-alarm", alarm_event=alarm_event)
-
-            try:
-                alarm_event.description = \
-                    "CLEAR simulated alarm - " \
-                    "resource:{} " \
-                    "type:{} " \
-                    "severity:{} " \
-                    "category:{}".format(
-                        alarm_event.resource_id,
-                        alarm_event.type,
-                        alarm_event.severity,
-                        alarm_event.category
-                    )
-
-                alarm_event.state = AlarmEventState.CLEARED
-                self.adapter_agent.submit_alarm(alarm_event)
-
-            except Exception as e:
-                log.exception('failed-to-clear-alarm', e=e)
-
-        self.alarm_lc = LoopingCall(_generate_alarm, device_id)
-        self.alarm_lc.start(60)
-
-    def stop_alarm_simulation(self, device_id):
-        log.info("stopping-alarm-simulation", device_id=device_id)
-        self.alarm_lc.stop()