PNF Simulator to support Control Loop subscription model

Change-Id: I9919edb32f3f68f86fad28c908f808fcee3fc548
Issue-ID: INT-1312
Signed-off-by: rajendrajaiswal <rajendra.jaiswal@ericsson.com>
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/FileReadyEvent.json b/test/mocks/pmsh-pnf-sim/docker-compose/FileReadyEvent.json
new file mode 100644
index 0000000..0f8df3d
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/FileReadyEvent.json
@@ -0,0 +1 @@
+{"event":{"commonEventHeader":{"version":"4.0.1","vesEventListenerVersion":"7.0.1","domain":"notification","eventName":"Noti_RnNode-Ericsson_FileReady","eventId":"FileReady_1797490e-10ae-4d48-9ea7-3d7d790b25e1","lastEpochMicrosec":8745745764578,"priority":"Normal","reportingEntityName":"otenb5309","sequence":0,"sourceName":"oteNB5309","startEpochMicrosec":8745745764578,"timeZoneOffset":"UTC+05.30"},"notificationFields":{"changeIdentifier":"PM_MEAS_FILES","changeType":"FileReady","notificationFieldsVersion":"2.0","arrayOfNamedHashMap":[{"name":"Apmfilename.xml.gz","hashMap":{"location":"sftp://bulkpm:bulkpm@sftpserver:22/upload/Apmfilename.xml.gz","compression":"gzip","fileFormatType":"org.3GPP.32.435#measCollec","fileFormatVersion":"V10"}}]}}}
\ No newline at end of file
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/docker-compose.yml b/test/mocks/pmsh-pnf-sim/docker-compose/docker-compose.yml
new file mode 100644
index 0000000..419e54b
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/docker-compose.yml
@@ -0,0 +1,20 @@
+version: '3'
+
+services:
+  netopeer2:
+    image: registry.gitlab.com/blue-onap/docker/sysrepo-netopeer2:v0.7-r2-5
+    container_name: netopeer2
+    restart: always
+    ports:
+      - "830:830"
+      - "6513:6513"
+    volumes:
+      - ./:/config/models/pnf-subscriptions
+  sftp:
+    container_name: sftpserver
+    image: atmoz/sftp
+    ports:
+      - "2222:22"
+    volumes:
+      - /host/upload:/home/admin
+    command: admin:admin:1001
\ No newline at end of file
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/pnf-subscriptions.yang b/test/mocks/pmsh-pnf-sim/docker-compose/pnf-subscriptions.yang
new file mode 100644
index 0000000..6adce57
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/pnf-subscriptions.yang
@@ -0,0 +1,47 @@
+module pnf-subscriptions {
+    namespace "http://onap.org/pnf-subscriptions";
+    prefix subscriptions;
+
+    revision "2019-11-22" {
+        description
+          "initial version";
+    }
+    container subscriptions {
+        list configuration{
+            key "subscriptionName";
+            leaf subscriptionName {
+                type string;
+            }
+            leaf administrativeState {
+                type string;
+            }
+            leaf fileBasedGP {
+                type int16;
+            }
+            leaf fileLocation {
+                type string;
+            }
+            list measurementGroups {
+                key "id";
+                leaf id{
+                    type int16;
+                }
+                container measurementGroup {
+                                    list measurementTypes {
+                                        key "measurementType";
+                                        leaf measurementType {
+                                            type string;
+                                        }
+                                    }
+                                    list managedObjectDNsBasic {
+                                        key "DN";
+                                        leaf DN {
+                                            type string;
+                                        }
+                                    }
+                }
+
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/pnf.py b/test/mocks/pmsh-pnf-sim/docker-compose/pnf.py
new file mode 100644
index 0000000..05b09ba
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/pnf.py
@@ -0,0 +1,103 @@
+import gzip
+import json
+import os
+import shutil
+import time
+import xml.etree.ElementTree as ET
+from random import randint
+import requests
+import pnfconfig
+
+
+class PNF:
+    """ Handle update on xml and send file ready event to ves collector """
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def create_job_id(jobid, change_list):
+        """
+        create new measinfo tag and add new sub element in existing xml.
+        :param jobid: create unique job id within xml sub element.
+        :param change_list: list to create sub elements itmes.
+        """
+        try:
+            measurement_type = []
+            meas_object_dn = []
+            for items in range(len(change_list)):
+                if "/measurementType =" in change_list[items]:
+                    measurement_type.append(((change_list[items].rsplit('/', 1))[1].rsplit('=', 1))[1].strip())
+                if "/DN =" in change_list[items]:
+                    meas_object_dn.append(((change_list[items].rsplit('/', 1))[1].rsplit('=', 1))[1].strip())
+            script_dir = os.path.dirname(__file__)
+            pm_rel_file_path = "sftp/"
+            pm_location = os.path.join(script_dir, pm_rel_file_path)
+            ET.register_namespace('', "http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec")
+            tree = ET.parse(pm_location + "pm.xml")
+            root = tree.getroot()
+            attrib = {}
+            measinfo = ET.SubElement(root[1], 'measInfo', attrib)
+            attrib = {'jobId': jobid}
+            ET.SubElement(measinfo, 'job', attrib)
+            ET.SubElement(measinfo, 'granPeriod', {'duration': 'PT900S', 'endTime': '2000-03-01T14:14:30+02:00'})
+            ET.SubElement(measinfo, 'repPeriod', {'duration': 'PT1800S'})
+            for items in range(len(measurement_type)):
+                meastype = ET.SubElement(measinfo, 'measType', {'p': (items + 1).__str__()})
+                meastype.text = measurement_type[items]
+            for items in range(len(meas_object_dn)):
+                measvalue = ET.SubElement(measinfo, 'measValue', {'measObjLdn': meas_object_dn[items]})
+                for item in range(len(measurement_type)):
+                    value = ET.SubElement(measvalue, 'r', {'p': (item + 1).__str__()})
+                    value.text = randint(100, 900).__str__()
+            tree.write(pm_location + "pm.xml", encoding="utf-8", xml_declaration=True)
+        except Exception as error:
+            print(error)
+
+    @staticmethod
+    def delete_job_id(jobid):
+        """
+        delete measinfo tag from existing xml pm file based on jobid.
+        :param jobid: element within measinfo tag.
+        """
+        try:
+            script_dir = os.path.dirname(__file__)
+            pm_rel_file_path = "sftp/"
+            pm_location = os.path.join(script_dir, pm_rel_file_path)
+            ET.register_namespace('', "http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec")
+            tree = ET.parse(pm_location + "pm.xml")
+            root = tree.getroot()
+            for measinfo in root[1].findall(
+                    '{http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec}measInfo'):
+                xml_id = measinfo.find('{http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec}job').attrib
+                if xml_id["jobId"] == jobid:
+                    root[1].remove(measinfo)
+            tree.write(pm_location + "pm.xml", encoding="utf-8", xml_declaration=True)
+        except Exception as error:
+            print(error)
+
+    @staticmethod
+    def pm_job():
+        """
+        create timestemp based gunzip xml file and send file ready event to ves collector.
+        """
+        try:
+            script_dir = os.path.dirname(__file__)
+            timestemp = time.time()
+            pm_rel_file_path = "sftp/"
+            pm_location = os.path.join(script_dir, pm_rel_file_path)
+            shutil.copy(pm_location + "pm.xml", pm_location + "A{}.xml".format(timestemp))
+            with open(pm_location + "A{}.xml".format(timestemp), 'rb') as f_in:
+                with gzip.open(pm_location + "A{}.xml.gz".format(timestemp), 'wb') as f_out:
+                    shutil.copyfileobj(f_in, f_out)
+            os.remove(pm_location + "A{}.xml".format(timestemp))
+            rel_path = "FileReadyEvent.json"
+            file_ready_event_path = os.path.join(script_dir, rel_path)
+            with open(file_ready_event_path) as json_file:
+                data = json_file.read().replace("pmfilename", str(timestemp))
+                eventdata = json.loads(data)
+            url = "http://{}:{}/eventListener/v7".format(pnfconfig.VES_IP, pnfconfig.VES_PORT)
+            print("Sending File Ready Event to VES Collector " + url + " -- data @" + data)
+            headers = {'content-type': 'application/json'}
+            requests.post(url, json=eventdata, headers=headers)
+        except Exception as error:
+            print(error)
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/pnfconfig.py b/test/mocks/pmsh-pnf-sim/docker-compose/pnfconfig.py
new file mode 100644
index 0000000..ca58cea
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/pnfconfig.py
@@ -0,0 +1,3 @@
+VES_IP = "10.209.57.227"
+VES_PORT = "30235"
+ROP = 300   # in seconds
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/schedulepmjob.py b/test/mocks/pmsh-pnf-sim/docker-compose/schedulepmjob.py
new file mode 100644
index 0000000..ecbd744
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/schedulepmjob.py
@@ -0,0 +1,14 @@
+import time
+import schedule
+from pnf import PNF
+import pnfconfig
+
+if __name__ == "__main__":
+    try:
+        pnf = PNF()
+        schedule.every(pnfconfig.rop).seconds.do(lambda: pnf.pm_job(pnfconfig.VES_IP, pnfconfig.VES_PORT))
+        while True:
+            schedule.run_pending()
+            time.sleep(1)
+    except Exception as error:
+        print(error)
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/sftp/pm.xml b/test/mocks/pmsh-pnf-sim/docker-compose/sftp/pm.xml
new file mode 100644
index 0000000..375bbbd
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/sftp/pm.xml
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='utf-8'?>
+<measCollecFile xmlns="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec">
+    <fileHeader fileFormatVersion="32.435 V7.0" vendorName="Company NN" dnPrefix="DC=a1.companyNN.com,SubNetwork=1,IRPAgent=1">
+        <fileSender localDn="SubNetwork=CountryNN,MeContext=MEC-Gbg-1,ManagedElement=RNC-Gbg-1" elementType="RNC" />
+        <measCollec beginTime="2000-03-01T14:00:00+02:00" />
+    </fileHeader>
+    <measData>
+        <managedElement localDn="SubNetwork=CountryNN,MeContext=MEC-Gbg-1,ManagedElement=RNC-Gbg-1" userLabel="RNC Telecomville" />
+        <measInfo>
+            <job jobId="sub0" />
+            <granPeriod duration="PT900S" endTime="2000-03-01T14:14:30+02:00" />
+            <repPeriod duration="PT1800S" />
+            <measType p="1">attTCHSeizures</measType>
+            <measType p="2">succTCHSeizures</measType>
+            <measType p="3">attImmediateAssignProcs</measType>
+            <measType p="4">succImmediateAssignProcs</measType>
+            <measValue measObjLdn="RncFunction=RF-1,UtranCell=Gbg-997">
+                <r p="1">234</r>
+                <r p="2">345</r>
+                <r p="3">567</r>
+                <r p="4">789</r>
+            </measValue>
+            <measValue measObjLdn="RncFunction=RF-1,UtranCell=Gbg-998">
+                <r p="1">890</r>
+                <r p="2">901</r>
+                <r p="3">123</r>
+                <r p="4">234</r>
+            </measValue>
+            <measValue measObjLdn="RncFunction=RF-1,UtranCell=Gbg-999">
+                <r p="1">456</r>
+                <r p="2">567</r>
+                <r p="3">678</r>
+                <r p="4">789</r>
+                <suspect>true</suspect>
+            </measValue>
+        </measInfo>
+    </measData>
+    <fileFooter>
+        <measCollec endTime="2000-03-01T14:15:00+02:00" />
+    </fileFooter>
+</measCollecFile>
\ No newline at end of file
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/startup.xml b/test/mocks/pmsh-pnf-sim/docker-compose/startup.xml
new file mode 100644
index 0000000..7bd8950
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/startup.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<subscriptions xmlns="http://onap.org/pnf-subscriptions">
+  <configuration>
+    <subscriptionName>sub0</subscriptionName>
+    <administrativeState>UNLOCKED</administrativeState>
+    <fileBasedGP>15</fileBasedGP>
+    <fileLocation>c://PM</fileLocation>
+    <measurementGroups>
+      <id>1</id>
+      <measurementGroup>
+        <measurementTypes>
+          <measurementType>EutranCellRelation.pmCounter1</measurementType>
+        </measurementTypes>
+        <measurementTypes>
+          <measurementType>EutranCellRelation.pmCounter2</measurementType>
+        </measurementTypes>
+        <managedObjectDNsBasic>
+          <DN>ManagedElement=1,ENodeBFunction=1,EUtranCell=CityCenter1</DN>
+        </managedObjectDNsBasic>
+        <managedObjectDNsBasic>
+          <DN>ManagedElement=1,ENodeBFunction=1,EUtranCell=CityCenter1, EUtranCellRelation=CityCenter2</DN>
+        </managedObjectDNsBasic>
+      </measurementGroup>
+    </measurementGroups>
+  </configuration>
+</subscriptions>
\ No newline at end of file
diff --git a/test/mocks/pmsh-pnf-sim/docker-compose/subscriber.py b/test/mocks/pmsh-pnf-sim/docker-compose/subscriber.py
new file mode 100644
index 0000000..44109a1
--- /dev/null
+++ b/test/mocks/pmsh-pnf-sim/docker-compose/subscriber.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+import re
+import sysrepo as sr
+from pnf import PNF
+
+
+def module_change_cb(sess, module_name, event, private_ctx):
+    """  Handle event change based on yang operation. """
+    try:
+        change_path = "/" + module_name + ":*"
+        iterate = sess.get_changes_iter(change_path)
+        change = sess.get_change_next(iterate)
+        changelist = []
+        operation = change.oper()
+        pnf = PNF()
+        if event == sr.SR_EV_APPLY:
+            print("------------------> Start Handle Change <------------------")
+            if operation == sr.SR_OP_CREATED:
+                while True:
+                    change = sess.get_change_next(iterate)
+                    if change is None:
+                        break
+                    changelist.append(change.new_val().to_string())
+                result = re.findall(r'\'(.*?)\'', changelist[0])
+                jobid = result[0]
+                print("Subscription Created : " + changelist[0])
+                pnf.create_job_id(jobid, changelist)
+                pnf.pm_job()
+            elif operation == sr.SR_OP_DELETED:
+                changelist.append(change.old_val().to_string())
+                result = re.findall(r'\'(.*?)\'', changelist[0])
+                jobid = result[0]
+                print("Subscription Deleted : " + changelist[0])
+                pnf.delete_job_id(jobid)
+                pnf.pm_job()
+            elif operation == sr.SR_OP_MODIFIED:
+                changelist.append(change.new_val().to_string())
+                element = changelist[0]
+                print("Subscription Modified :" + element)
+                result = re.findall(r'\'(.*?)\'', changelist[0])
+                jobid = result[0]
+                administrative_state = ((element.rsplit('/', 1)[1]).split('=', 1))[1].strip()
+                if administrative_state == "LOCKED":
+                    pnf.delete_job_id(jobid)
+                    pnf.pm_job()
+                elif administrative_state == "UNLOCKED":
+                    select_xpath = "/" + module_name + ":*//*"
+                    values = sess.get_items(select_xpath)
+                    if values is not None:
+                        for i in range(values.val_cnt()):
+                            if jobid in values.val(i).to_string():
+                                changelist.append(values.val(i).to_string())
+                        pnf.create_job_id(jobid, changelist)
+                        pnf.pm_job()
+            else:
+                print("Unknown Operation")
+            print("------------------> End Handle Change <------------------")
+    except Exception as error:
+        print(error)
+    return sr.SR_ERR_OK
+
+
+def start():
+    """ main function to create connection based on moudule name. """
+    try:
+        module_name = "pnf-subscriptions"
+        conn = sr.Connection(module_name)
+        sess = sr.Session(conn)
+        subscribe = sr.Subscribe(sess)
+        subscribe.module_change_subscribe(module_name, module_change_cb)
+        sr.global_loop()
+        print("Application exit requested, exiting.")
+    except Exception as error:
+        print(error)
+
+
+if __name__ == '__main__':
+    start()