XOS Scale test
- creating OLTs, ONUs and required ports
- creating whitelist
- sending ONU activation events

Change-Id: If2b2d02ae4a966756edbedd659108481558636ee
diff --git a/.gitignore b/.gitignore
index 6f5a2ab..b209a27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+# IDEs artifacts
+.idea
+
+# Virtual envs
+src/test/cord-api/venv-cord-tester/
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
diff --git a/src/test/cord-api/Tests/XosScaleValidations/README.md b/src/test/cord-api/Tests/XosScaleValidations/README.md
new file mode 100644
index 0000000..a37c11a
--- /dev/null
+++ b/src/test/cord-api/Tests/XosScaleValidations/README.md
@@ -0,0 +1,30 @@
+# XOS Scale test
+
+This test suite is still under active developemnt so some
+manual steps are needed in order to execute it.
+
+## Setup the test
+
+There a `setup-venv.sh` script in `../..`, use that.
+
+In order to connect to kafka, find the correct IP with:
+
+```bash
+kubectl get svc | grep cord-platform-kafka
+```
+
+It will be needed while running the command
+
+## Run the test
+
+Tests can executed via the cli with this command:
+
+```bash
+robot --variable xos_chameleon_url:127.0.0.1 \
+--variable xos_chameleon_port:30006 \
+--variable cord_kafka:10.152.183.118 \
+--variable num_olts:10 \
+--variable num_onus:1 \
+--variable num_pon_ports:10 \
+xos-scale-att-workflow.robot
+```
\ No newline at end of file
diff --git a/src/test/cord-api/Tests/XosScaleValidations/utils/devices.py b/src/test/cord-api/Tests/XosScaleValidations/utils/devices.py
new file mode 100644
index 0000000..fe93488
--- /dev/null
+++ b/src/test/cord-api/Tests/XosScaleValidations/utils/devices.py
@@ -0,0 +1,235 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+
+from tinydb import TinyDB, Query
+from robot.api import logger
+
+class devices(object):
+
+    def __init__(self):
+        self.olts = TinyDB('olts.json')
+        self.pon_ports = TinyDB('pon_ports.json')
+        self.onus = TinyDB('onus.json')
+        self.uni_ports = TinyDB('uni_ports.json')
+
+    def get_mock_data(self):
+        """
+        Get all the mock data,
+        this method is mostly intended for debugging
+        :return: a dictionary containing all the mocked data
+        """
+        olts = self.olts.all()
+        pon_ports = self.pon_ports.all()
+        onus = self.onus.all()
+        uni_ports = self.uni_ports.all()
+        return {
+            'olts': olts,
+            'pon_ports': pon_ports,
+            'onus': onus,
+            'uni_ports': uni_ports,
+        }
+
+    def clean_storage(self):
+        self.olts.purge()
+        self.pon_ports.purge()
+        self.onus.purge()
+        self.uni_ports.purge()
+
+###############################################################
+#                             OLT                             #
+###############################################################
+
+    def create_mock_olts(self, num_olts, voltservice_id):
+        """
+        :param num_olts: Number of OLTs to be created
+        :param voltservice_id: ID if the vOLT service
+        :return:
+        """
+        olts = []
+        for index in range(1, int(num_olts) + 1):
+            olt = {
+                'name': 'Test OLT%s' % index,
+                'volt_service_id': voltservice_id,
+                'device_type':  'fake_olt',
+                'host': '127.0.0.1',
+                'port': index,
+                'uplink': '65536',
+                'switch_datapath_id': 'of:0000000000000001',
+                'switch_port': str(index),
+                'of_id': 'of:000000%s' % index,
+            }
+            # logger.info('Created OLT %s' % olt, also_console=True)
+            olts.append(olt)
+            self.olts.insert(olt)
+
+        return olts
+
+    def get_rest_olts(self):
+        """
+        Get all the OLTs that have been created for the test
+        formatted for the XOS Rest API
+        :return: a list of OLTs
+        """
+        return self.olts.all()
+
+    def update_olt_id(self, olt, id):
+        """
+        Update in the memory storage the XOS ID
+        of a particular OLT as it's needed to create PON Ports
+        :param olt: The OLT object to update
+        :param id:  The ID returned from XOS
+        :return: None
+        """
+        Olt = Query()
+        self.olts.update({'id': id}, Olt.name == olt['name'])
+
+###############################################################
+#                        PON PORT                             #
+###############################################################
+
+    def create_mock_pon_ports(self, num_pon):
+
+        ports = []
+        for olt in self.olts.all():
+            for index in range(1, int(num_pon) +1):
+                port = {
+                    'name': 'Test PonPort %s' % index,
+                    'port_no': index,
+                    'olt_device_id': olt['id']
+                }
+                ports.append(port)
+                self.pon_ports.insert(port)
+        return ports
+
+    def get_rest_pon_ports(self):
+        """
+        Get all the PON Ports that have been created for the test
+        formatted for the XOS Rest API
+        :return: a list of PON Ports
+        """
+        return self.pon_ports.all();
+
+    def update_pon_port_id(self, pon_port, id):
+        """
+        Update in the memory storage the XOS ID
+        of a particular PON Port as it's needed to create ONUs
+        :param pon_port: The PON Port object to update
+        :param id:  The ID returned from XOS
+        :return: None
+        """
+        PonPort = Query()
+        self.pon_ports.update({'id': id}, PonPort.name == pon_port['name'])
+
+###############################################################
+#                             ONU                             #
+###############################################################
+
+    def create_mock_onus(self, num_onus):
+        onus = []
+        j = 0
+        for port in self.pon_ports.all():
+            j = j + 1
+            for index in range(1, int(num_onus) + 1):
+                onu = {
+                    'serial_number': "ROBOT%s%s" % (j, index),
+                    'vendor': 'Robot',
+                    'pon_port_id': port['id']
+                }
+                onus.append(onu)
+                self.onus.insert(onu)
+        return onus
+
+    def get_rest_onus(self):
+        return self.onus.all();
+
+    def update_onu_id(self, onu, id):
+        Onu = Query()
+        self.onus.update({'id': id}, Onu.serial_number == onu['serial_number'])
+
+###############################################################
+#                             UNI                             #
+###############################################################
+
+    def create_mock_unis(self):
+        # NOTE I believe UNI port number must be unique across OLT
+        unis = []
+        i = 0
+        for onu in self.onus.all():
+            uni = {
+                'name': 'Test UniPort %s' % i,
+                'port_no': i,
+                'onu_device_id': onu['id']
+            }
+            unis.append(uni)
+            self.uni_ports.insert(uni)
+            i = i+1
+        return unis
+
+    def get_rest_unis(self):
+        return self.uni_ports.all();
+
+    def update_uni_id(self, uni, id):
+        UniPort = Query()
+        self.uni_ports.update({'id': id}, UniPort.name == uni['name'])
+
+###############################################################
+#                             WHITELIST                       #
+###############################################################
+
+    def create_mock_whitelist(self, attworkflowservice_id):
+        entries = []
+        for onu in self.onus.all():
+            e = {
+                'owner_id': attworkflowservice_id,
+                'serial_number': onu['serial_number'],
+                'pon_port_id': self._find_pon_port_by_onu(onu)['port_no'],
+                'device_id': self._find_olt_by_onu(onu)['of_id']
+            }
+            entries.append(e)
+
+        return entries
+
+###############################################################
+#                             EVENTS                          #
+###############################################################
+
+    def generate_onu_events(self):
+        events = []
+        for onu in self.onus.all():
+            ev = {
+                'status': 'activated',
+                'serial_number': onu['serial_number'],
+                'uni_port_id': self._find_uni_by_onu(onu)['port_no'],
+                'of_dpid': self._find_olt_by_onu(onu)['of_id'],
+            }
+            events.append(ev)
+        return events
+
+###############################################################
+#                             HELPERS                         #
+###############################################################
+
+    def _find_uni_by_onu(self, onu):
+        Uni = Query()
+        # NOTE there's an assumption that 1 ONU has 1 UNI Port
+        return self.uni_ports.search(Uni.onu_device_id == onu['id'])[0]
+
+    def _find_pon_port_by_onu(self, onu):
+        PonPort = Query()
+        return self.pon_ports.search(PonPort.id == onu['pon_port_id'])[0]
+
+    def _find_olt_by_onu(self, onu):
+        pon_port = self._find_pon_port_by_onu(onu)
+        Olt = Query()
+        return self.olts.search(Olt.id == pon_port['olt_device_id'])[0]
\ No newline at end of file
diff --git a/src/test/cord-api/Tests/XosScaleValidations/xos-scale-att-workflow.robot b/src/test/cord-api/Tests/XosScaleValidations/xos-scale-att-workflow.robot
new file mode 100644
index 0000000..d084334
--- /dev/null
+++ b/src/test/cord-api/Tests/XosScaleValidations/xos-scale-att-workflow.robot
@@ -0,0 +1,232 @@
+*** Settings ***
+Documentation    Scale up models in a SEBA deployment with no backends
+Library           KafkaLibrary
+Library           RequestsLibrary
+Library           HttpLibrary.HTTP
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           ./utils/devices.py
+Suite Setup       Setup
+Suite Teardown    Teardown
+
+*** Variables ***
+${xos_chameleon_url}    xos-chameleon
+${xos_chameleon_port}   9101
+${cord_kafka}           cord-kafka
+
+${num_olts}             1
+${num_pon_ports}        1
+${num_onus}             1
+
+${timeout}              300s
+
+*** Test Cases ***
+
+Check OLTs Created
+    ${res} =   CORD Get    /xosapi/v1/volt/oltdevices
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =      Convert To Integer   ${num_olts}
+    Should Be Equal    ${length}    ${total}
+
+Check PON Ports Created
+    ${res} =   CORD Get    /xosapi/v1/volt/ponports
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =      Evaluate  ${num_olts} * ${num_pon_ports}
+    Should Be Equal    ${length}    ${total}
+
+Check ONUs Created
+    ${res} =   CORD Get    /xosapi/v1/volt/onudevices
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =      Evaluate  ${num_olts} * ${num_pon_ports} * ${num_onus}
+    Should Be Equal    ${length}    ${total}
+
+Check UNI Ports Created
+    ${res} =   CORD Get    /xosapi/v1/volt/uniports
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =      Evaluate  ${num_olts} * ${num_pon_ports} * ${num_onus}
+    Should Be Equal    ${length}    ${total}
+
+Check Whitelist Created
+    ${res} =   CORD Get    /xosapi/v1/att-workflow-driver/attworkflowdriverwhitelistentries
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =      Evaluate  ${num_olts} * ${num_pon_ports} * ${num_onus}
+    Should Be Equal    ${length}    ${total}
+
+Activate ONUs
+    [Documentation]    Send activation events for all the ONUs and waits for the model_policies of ATT Workflow Driver Service Instances to have completed
+    ${events} =     Generate Onu Events
+    : FOR   ${e}  IN  @{events}
+    \   Send Kafka Event    onu.events    ${e}
+    Wait Until Keyword Succeeds    ${timeout}    5s    Validate ATT_SI Number    ${events}
+
+*** Keywords ***
+
+Validate ATT_SI Number
+    [Arguments]    ${events}
+    ${res} =   CORD Get    /xosapi/v1/att-workflow-driver/attworkflowdriverserviceinstances
+    ${jsondata} =    To Json    ${res.content}
+    ${length} =    Get Length    ${jsondata['items']}
+    ${total} =    Get Length    ${events}
+    Should Be Equal    ${length}    ${total}
+    ModelPolicy completed   ${jsondata['items']}
+
+ModelPolicy completed
+    [Documentation]     Check that model_policies had run for all the items in the list
+    [Arguments]     ${list}
+    # NOTE we assume that here we get res.content["items"]
+    : FOR   ${i}  IN  @{list}
+    \   Should Be Equal As Integers     ${i["policy_code"]}     1
+
+Setup
+    ${target} =     Evaluate    ${num_olts} * ${num_pon_ports} * ${num_onus}
+    Log     Testing with ${target} ONUs
+    Connect Producer    ${cord_kafka}:9092    onu.events
+    Connect Producer    ${cord_kafka}:9092    authentication.events
+    Connect Producer    ${cord_kafka}:9092    dhcp.events
+    ${auth} =    Create List    admin@opencord.org    letmein
+    ${HEADERS}    Create Dictionary    Content-Type=application/json    allow_modify_feedback=True
+    Create Session    XOS    http://${xos_chameleon_url}:${xos_chameleon_port}    auth=${AUTH}    headers=${HEADERS}
+    Store Data
+    Mock Data
+    ${mock} =   Get Mock Data
+    Log     ${mock}
+
+Teardown
+    [Documentation]    Delete all models created
+    Log     Teardown
+    Delete OLTs
+    Delete Whitelist
+    Delete ServiceInstances
+    Delete All Sessions
+    Clean Storage
+
+Mock Data
+    [Documentation]     Mock all the data needed from the test
+    Create Mock Olts    ${num_olts}     ${voltservice_id}
+    Create Olts
+    Create Mock Pon Ports   ${num_pon_ports}
+    Create Pon Ports
+    Create Mock Onus        ${num_onus}
+    Create Onus
+    Create Mock Unis
+    Create Unis
+    Create Whitelist
+
+Create Olts
+    [Documentation]     Stub OLT for the test
+    ${olts} =   Get Rest Olts
+    : FOR   ${OLT}  IN  @{olts}
+    \   Log     ${OLT}
+    \   ${res} =    CORD Post   /xosapi/v1/volt/oltdevices    ${OLT}
+    \   ${jsondata} =    To Json    ${res.content}
+    \   Update Olt Id       ${OLT}  ${jsondata['id']}
+
+Create Pon Ports
+    ${pon_ports} =   Get Rest Pon Ports
+    : FOR   ${port}  IN  @{pon_ports}
+    \   Log     ${port}
+    \   ${res} =    CORD Post   /xosapi/v1/volt/ponports    ${port}
+    \   ${jsondata} =    To Json    ${res.content}
+    \   Update Pon Port Id       ${port}  ${jsondata['id']}
+
+Create Onus
+    ${onus} =   Get Rest Onus
+    : FOR   ${onu}  IN  @{onus}
+    \   Log     ${onu}
+    \   ${res} =    CORD Post   /xosapi/v1/volt/onudevices    ${onu}
+    \   ${jsondata} =    To Json    ${res.content}
+    \   Update Onu Id       ${onu}  ${jsondata['id']}
+
+Create Unis
+    ${unis} =   Get Rest Unis
+    : FOR   ${uni}  IN  @{unis}
+    \   Log     ${uni}
+    \   ${res} =    CORD Post   /xosapi/v1/volt/uniports    ${uni}
+    \   ${jsondata} =    To Json    ${res.content}
+    \   Update Uni Id       ${uni}  ${jsondata['id']}
+
+Create Whitelist
+    [Documentation]     Create a whitelist for the tests
+    ${whitelist} =      Create Mock Whitelist   ${attworkflowservice_id}
+    : FOR   ${e}  IN  @{whitelist}
+    \   Log     ${e}
+    \   ${res} =    CORD Post   /xosapi/v1/att-workflow-driver/attworkflowdriverwhitelistentries    ${e}
+
+Delete Olts
+    [Documentation]     Remove all the OLTs created for the test
+    ${res} =    CORD Get    /xosapi/v1/volt/oltdevices
+    ${jsondata} =    To Json    ${res.content}
+    :FOR    ${ELEMENT}    IN  @{jsondata['items']}
+    \   ${id}=    Get From Dictionary    ${ELEMENT}    id
+    \   CORD Delete     /xosapi/v1/volt/oltdevices    ${id}
+
+Delete Whitelist
+    [Documentation]     Clean the whitelist
+    ${res} =    CORD Get    /xosapi/v1/att-workflow-driver/attworkflowdriverwhitelistentries
+    ${jsondata} =    To Json    ${res.content}
+    :FOR    ${ELEMENT}    IN  @{jsondata['items']}
+    \   ${id}=    Get From Dictionary    ${ELEMENT}    id
+    \   CORD Delete     /xosapi/v1/att-workflow-driver/attworkflowdriverwhitelistentries    ${id}
+
+Delete ServiceInstances
+    [Documentation]     Clean the whitelist
+    ${res} =    CORD Get    /xosapi/v1/att-workflow-driver/attworkflowdriverserviceinstances
+    ${jsondata} =    To Json    ${res.content}
+    :FOR    ${ELEMENT}    IN  @{jsondata['items']}
+    \   ${id}=    Get From Dictionary    ${ELEMENT}    id
+    \   CORD Delete     /xosapi/v1/att-workflow-driver/attworkflowdriverserviceinstances    ${id}
+
+Store Data
+    [Documentation]     Store all the ids needed for the test to work
+    # Get AttWorkflowDriverService id
+    ${resp}=    CORD Get    /xosapi/v1/att-workflow-driver/attworkflowdriverservices
+    ${jsondata}=    To Json    ${resp.content}
+    ${attworkflowservice}=    Get From List    ${jsondata['items']}    0
+    ${attworkflowservice_id}=    Get From Dictionary    ${attworkflowservice}    id
+    Set Suite Variable    ${attworkflowservice_id}
+
+    # Get VoltService id
+    ${resp}=    CORD Get    /xosapi/v1/volt/voltservices
+    ${jsondata}=    To Json    ${resp.content}
+    ${voltservice}=    Get From List    ${jsondata['items']}    0
+    ${voltservice_id}=    Get From Dictionary    ${voltservice}    id
+    Set Suite Variable    ${voltservice_id}
+
+Send Kafka Event
+    [Documentation]    Send event
+    [Arguments]    ${topic}    ${message}
+    Log    Sending event
+    ${event}=    evaluate    json.dumps(${message})    json
+    Send    ${topic}    ${event}
+    Flush
+
+CORD Get
+    [Documentation]    Make a GET call to XOS
+    [Arguments]    ${service}
+    ${resp}=    Get Request    XOS    ${service}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    [Return]    ${resp}
+
+CORD Post
+    [Documentation]    Make a POST call to XOS
+    [Arguments]    ${service}    ${data}
+    ${data}=    Evaluate    json.dumps(${data})    json
+    ${resp}=    Post Request    XOS    uri=${service}    data=${data}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    [Return]    ${resp}
+
+CORD Delete
+    [Documentation]    Make a DELETE call to the CORD controller
+    [Arguments]    ${service}    ${data_id}
+    ${resp}=    Delete Request    XOS    uri=${service}/${data_id}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    [Return]    ${resp}
\ No newline at end of file
diff --git a/src/test/cord-api/setup_venv.sh b/src/test/cord-api/setup_venv.sh
index 6975307..cf08442 100755
--- a/src/test/cord-api/setup_venv.sh
+++ b/src/test/cord-api/setup_venv.sh
@@ -32,6 +32,6 @@
 pip install --upgrade pip
 pip install robotframework robotframework-requests robotframework-sshlibrary pexpect  \
     robotframework-httplibrary robotframework-kafkalibrary pygments pyyaml
-pip install requests
+pip install requests tinydb
 
 echo "CORD-TESTER virtualenv created. Run 'source ${VENVDIR}/bin/activate'."
\ No newline at end of file