VOL-2642 Add a Makefile, tests, and virtualenv

Convert common python and robot into a CORDRobot python module that can
be installed via standard python tools (pip) and from PyPI

Uses a fork of https://github.com/rasjani/robotframework-importresource,
which has been backported to Python 3.5 (used in Ubuntu 16.04
executors).

Reformatted and moved keywords so resource files are scoped to a
specific topic.

Added tox tests for library consistency

- flake8
- pylint
- robotframework-lint
- Ran robot against installed library to verify it can be loaded and
  used

Added basic lint and tests to whole repo

Removed old tests:

- CORD <6.x era: SanityPhyPOD.robot, and onosUtils.py

Change-Id: I61265a9fb04034a086e20be1f7236a8793a218aa
diff --git a/cord-robot/CORDRobot/CORDDictUtils.py b/cord-robot/CORDRobot/CORDDictUtils.py
new file mode 100644
index 0000000..ea13212
--- /dev/null
+++ b/cord-robot/CORDRobot/CORDDictUtils.py
@@ -0,0 +1,330 @@
+# 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 __future__ import absolute_import, print_function
+
+import json
+import uuid
+import random
+import yaml
+import glob
+import string
+
+
+class CORDDictUtils(object):
+    @staticmethod
+    def listToDict(alist, intListIndex):
+        dictInfo = alist[int(intListIndex)]
+        return dictInfo
+
+    @staticmethod
+    def jsonToList(strFile, strListName):
+        data = json.loads(open(strFile).read())
+        # print "data...",data
+        dataList = data[strListName]
+        return dataList
+
+    def readFile(self, path, single=True):
+        dataDict = {}
+        for fileName in glob.glob(path):
+            print("Reading ", fileName)
+            data = open(fileName).read()
+            dataDict[fileName] = data
+            if bool(single):
+                return data
+        if not dataDict:
+            print("Failed to find the file!")
+            return None
+        return dataDict
+
+    def readFiles(self, path):
+        return self.readFile(path, single=False)
+
+    """
+    @method compare_dict
+    @Description: validates if contents of dict1 exists in dict2
+    @params: dict1 = input_data entered through api
+             dict2 = retrieved data from GET method
+    returns True if contents of dict1 exists in dict2
+    """
+
+    def compare_dict(self, dict1, dict2):
+        print("input data", dict1)
+        print("get data", dict2)
+        if dict1 is None or dict2 is None:
+            return False
+        if not isinstance(dict1, dict) or not isinstance(dict2, dict):
+            return False
+        if dict1 == {}:
+            return True
+        return self.compare_dict_recursive(dict1, dict2)
+
+    """
+    @method compare_dict_recursive
+    @Description: recursive function to validate if dict1 is a subset of dict2
+    returns True if contents of dict1 exists in dict2
+    """
+
+    def compare_dict_recursive(self, dict1, dict2):
+        for key1, value1 in dict1.items():
+            if key1 not in dict2.keys():
+                print("Missing key", key1, "in dict2")
+                return False
+            value2 = dict2[key1]
+            if isinstance(value1, dict) and isinstance(value2, dict):
+                if not self.compare_dict_recursive(value1, value2):
+                    return False
+            else:
+                if value2 != value1:
+                    print("Values of key", key1, "in two dicts are not equal")
+                    return False
+        return True
+
+    """
+    @method compare_list_of_dicts
+    @Description: validates if contents of dicts in list1 exists in dicts of list2
+    returns True if for each dict in list1, there's a dict in list2 that contains its content
+    """
+
+    def compare_list_of_dicts(self, list1, list2):
+        for dict1 in list1:
+            if dict1 == {}:
+                continue
+            key = dict1.keys()[0]
+            value = dict1[key]
+            dict2 = self.getDictFromListOfDict(list2, key, value)
+            if dict2 == {}:
+                print(
+                    "Comparison failed: no dictionaries found in list2 with key",
+                    key,
+                    "and value",
+                    value,
+                )
+                return False
+            if not self.compare_dict(dict1, dict2):
+                print(
+                    "Comparison failed: dictionary",
+                    dict1,
+                    "is not a subset of dictionary",
+                    dict2,
+                )
+                return False
+        return True
+
+    """
+    @method search_dictionary
+    @Description: Searches for a key in the provided nested dictionary
+    @params: input_dict = dictionary to be searched
+             search_key = name of the key to be searched for
+    returns two values: search_key value and status of the search.
+             True if found (False when not found)
+
+    """
+
+    def search_dictionary(self, input_dict, search_key):
+        input_keys = input_dict.keys()
+        key_value = ""
+        found = False
+        for key in input_keys:
+            if key == search_key:
+                key_value = input_dict[key]
+                found = True
+                break
+            elif isinstance(input_dict[key], dict):
+                key_value, found = self.search_dictionary(
+                    input_dict[key], search_key)
+                if found:
+                    break
+            elif isinstance(input_dict[key], list):
+                if not input_dict[key]:
+                    found = False
+                    break
+                for item in input_dict[key]:
+                    if isinstance(item, dict):
+                        key_value, found = self.search_dictionary(
+                            item, search_key)
+                        if found:
+                            break
+        return key_value, found
+
+    """
+    @method getDictFromListOfDict
+        return key_value,found
+    @Description: Searches for the dictionary in the provided list of dictionaries
+                  that matches the value of the key provided
+    @params : List of dictionaries(getResponse Data from the URL),
+             SearchKey - Key that needs to be searched for (ex: account_num)
+             searchKeyValue - Value of the searchKey (ex: 21)
+    @Returns: Dictionary returned when match found for searchKey with the corresponding
+             searchKeyValue provided
+    """
+
+    def getDictFromListOfDict(self, getJsonDataList,
+                              searchKey, searchKeyValue):
+        return_dict = {}
+        result = ""
+        for data in getJsonDataList:
+            print("data", data)
+            return_dict = {}
+            found = False
+            input_keys = data.keys()
+            for key in input_keys:
+                if key == searchKey and str(data[key]) == str(searchKeyValue):
+                    found = True
+                    return_dict = data
+                    print("return_dict", return_dict)
+                    break
+                elif isinstance(data[key], dict):
+                    result, found = self.search_dictionary(
+                        data[key], searchKey)
+                    if found and str(result) == str(searchKeyValue):
+                        return_dict = data
+                        break
+                elif isinstance(data[key], list):
+                    for item in data[key]:
+                        if isinstance(item, dict):
+                            result, found = self.search_dictionary(
+                                data[key], searchKey)
+                            if found and str(
+                                    result) == str(searchKeyValue):
+                                return_dict = data
+                                break
+            if return_dict:
+                break
+        return return_dict
+
+    """
+    @method getFieldValueFromDict
+    @params : search_dict - Dictionary to be searched
+             field - Key to be searched for (ex: account_num)
+    @Returns: Returns the value of the Key that was provided
+    """
+
+    def getFieldValueFromDict(self, search_dict, field):
+        results = ""
+        found = False
+        input_keys = search_dict.keys()
+        for key in input_keys:
+            print("key...", key)
+            if key == field:
+                results = search_dict[key]
+                if not results:
+                    found = True
+                    break
+            elif isinstance(search_dict[key], dict):
+                results, found = self.search_dictionary(
+                    search_dict[key], field)
+                if found:
+                    break
+            elif isinstance(search_dict[key], list):
+                if not search_dict[key]:
+                    found = False
+                    continue
+                for item in search_dict[key]:
+                    if isinstance(item, dict):
+                        results, found = self.search_dictionary(item, field)
+                        if found:
+                            break
+            if results:
+                break
+
+        return results
+
+    def setFieldValueInDict(self, input_dict, field, field_value):
+        input_dict[field] = field_value
+        return input_dict
+
+    """
+    @method getAllFieldValues
+    @params : getJsonDataDictList - List of dictionaries to be searched
+             fieldName - Key to be searched for (ex: instance_id)
+    @Returns: Returns the unique value of the Key that was provided
+    """
+
+    def getAllFieldValues(self, getJsonDataDictList, fieldName):
+        value_list = []
+        # uniqValue = ""  - this is unused, commented out
+        uniq_list = []
+        for data in getJsonDataDictList:
+            fieldValue = ""
+            fieldValue = self.getFieldValueFromDict(data, fieldName)
+            value_list.append(fieldValue)
+        uniq_list = sorted(set(value_list))
+        if len(uniq_list) == 1:
+            pass  # see above, unused?
+            # uniqValue = uniq_list[0]
+        else:
+            print("list of values found for ", fieldName, ";", uniq_list)
+        return fieldValue
+
+    def generate_uuid(self):
+        return uuid.uuid4()
+
+    def generate_random_number_from_blacklist(
+        self, blacklist, min=100, max=500, typeTag=False
+    ):
+        num = None
+        while num in blacklist or num is None:
+            num = random.randrange(int(min), int(max))
+        if typeTag:
+            return num
+        else:
+            return str(num)
+
+    def get_dynamic_resources(self, inputfile, resource):
+        resourceNames = []
+        names = {}
+        dnames = []
+        with open(inputfile, "r") as f:
+            contents = yaml.load(f)
+        resources = contents[resource]
+        for i in resources:
+            resourceNames.append(i["name"])
+        for i in resourceNames:
+            names["name"] = i
+            dnames.append(names.copy())
+        return dnames
+
+    def generate_random_value(
+            self, value, max_length=10, min_int=1, max_int=10000):
+        if value == "string":
+            return "".join(
+                random.choice(string.ascii_lowercase + string.digits)
+                for _ in range(max_length)
+            )
+        if value == "bool":
+            return random.choice([True, False])
+        if value == "int32" or value == "uint32":
+            return random.randint(min_int, max_int)
+        if value == "float":
+            return random.uniform(1, 10)
+        if value == "role":
+            return "admin"
+        if value == "direction":
+            return random.choice(["in", "out"])
+        if value == "flavor":
+            return random.choice(["m1.large", "m1.medium", "m1.small"])
+        if value == "vlan_tag":
+            return random.choice(["555", "1-4096", "ANY"])
+        if value == "ip_address":
+            return ".".join(str(random.randint(0, 255)) for _ in range(4))
+        else:
+            return None
+
+    def generate_random_slice_name(self):
+        random_name = "".join(
+            random.choice(
+                string.ascii_lowercase +
+                string.digits) for _ in range(10))
+        return "testloginbase" + random_name
diff --git a/cord-robot/CORDRobot/__init__.py b/cord-robot/CORDRobot/__init__.py
new file mode 100644
index 0000000..8c357f6
--- /dev/null
+++ b/cord-robot/CORDRobot/__init__.py
@@ -0,0 +1,36 @@
+# 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 __future__ import absolute_import
+
+import os
+
+from .CORDDictUtils import CORDDictUtils
+from .restApi import restApi
+from .testCaseUtils import TestCaseUtils
+
+
+# return the library version
+def _version_():
+    with open(os.path.join(os.path.dirname(__file__), "VERSION")) as f:
+        return f.read().strip()
+
+
+# Inherit all the other sub-classes
+class CORDRobot(CORDDictUtils, restApi, TestCaseUtils):
+
+    ROBOT_LIBRARY_SCOPE = "GLOBAL"
+
+    def cr_version(self):
+        return _version_()
diff --git a/cord-robot/CORDRobot/restApi.py b/cord-robot/CORDRobot/restApi.py
new file mode 100644
index 0000000..afec6a6
--- /dev/null
+++ b/cord-robot/CORDRobot/restApi.py
@@ -0,0 +1,210 @@
+# 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 __future__ import absolute_import, print_function
+
+import requests
+import json
+import os
+
+# These are the default values used with XOS
+restApiDefaults = {
+    'ATT_SERVICE': '/xosapi/v1/att-workflow-driver/attworkflowdriverservices',
+    'ATT_SERVICEINSTANCES': '/xosapi/v1/att-workflow-driver/attworkflowdriverserviceinstances',
+    'ATT_WHITELIST': '/xosapi/v1/att-workflow-driver/attworkflowdriverwhitelistentries',
+    'BNG_MAP': '/xosapi/v1/fabric-crossconnect/bngportmappings',
+    'CH_CORE_DEPLOYMENTS': '/xosapi/v1/core/deployments',
+    'CH_CORE_FLAVORS': '/xosapi/v1/core/flavors',
+    'CH_CORE_IMAGES': '/xosapi/v1/core/images',
+    'CH_CORE_INSTANCES': '/xosapi/v1/core/instances',
+    'CH_CORE_NETWORKS': '/xosapi/v1/core/networks',
+    'CH_CORE_NETWORK_SLICES': '/xosapi/v1/core/networkslices',
+    'CH_CORE_NETWORK_TEMPLATES': '/xosapi/v1/core/networktemplates',
+    'CH_CORE_NODES': '/xosapi/v1/core/nodes',
+    'CH_CORE_PORTS': '/xosapi/v1/core/ports',
+    'CH_CORE_SERVICELINK': '/xosapi/v1/core/serviceinstancelinks',
+    'CH_CORE_SERVICES': '/xosapi/v1/core/services',
+    'CH_CORE_SERVICES': '/xosapi/v1/core/services',
+    'CH_CORE_SITEDEPLOYMENTS': '/xosapi/v1/core/sitedeployments',
+    'CH_CORE_SITES': '/xosapi/v1/core/sites',
+    'CH_CORE_SLICES': '/xosapi/v1/core/slices',
+    'CH_CORE_SLICES': '/xosapi/v1/core/slices',
+    'CH_CORE_USERS': '/xosapi/v1/core/users',
+    'CORE_DEPLOYMENTS': '/api/core/deployments/',
+    'CORE_FLAVORS': '/api/core/flavors/',
+    'CORE_IMAGES': '/api/core/images/',
+    'CORE_INSTANCES': '/api/core/instances/',
+    'CORE_NODES': '/api/core/nodes/',
+    'CORE_SANITY_INSTANCES': '/api/core/instances/?no_hyperlinks=1',
+    'CORE_SANITY_NODES': '/api/core/nodes/?no_hyperlinks=1',
+    'CORE_SANITY_SLICES': '/api/core/slices/?no_hyperlinks=1',
+    'CORE_SERVICES': '/api/core/services/',
+    'CORE_SITEDEPLOYMENTS': '/api/core/sitedeployments',
+    'CORE_SITES': '/api/core/sites/',
+    'CORE_SLICES': '/api/core/slices/',
+    'CORE_USERS': '/api/core/users/',
+    'FABRIC_CROSSCONNECT_SERVICEINSTANCES': '/xosapi/v1/fabric-crossconnect/fabriccrossconnectserviceinstances',
+    'FABRIC_SWITCH': '/xosapi/v1/fabric/switches',
+    'HWVSG_TENANT': '/xosapi/v1/vsg-hw/vsghwserviceinstances',
+    'ONU_DEVICE': '/xosapi/v1/volt/onudevices',
+    'OSS_SERVICE': '/xosapi/v1/hippie-oss/hippieossservices',
+    'OSS_SERVICEINSTANCE': '/xosapi/v1/hippie-oss/hippieossserviceinstances',
+    'OSS_VOLT': '/xosapi/v1/core/servicedependencys',
+    'PON_PORT': '/xosapi/v1/volt/ponports',
+    'PORT_INTERFACE': '/xosapi/v1/fabric/portinterfaces',
+    'SERVER_IP': '127.0.0.1',
+    'SERVER_PORT': '30006',
+    'SWITCH_PORT': '/xosapi/v1/fabric/switchports',
+    'TENANT_SUBSCRIBER': '/api/tenant/cord/subscriber/',
+    'TENANT_VOLT': '/api/tenant/cord/volt/',
+    'UTILS_LOGIN': '/api/utility/login/',
+    'UTILS_SYNCHRONIZER': '/api/utility/synchronizer/',
+    'VOLT_DEVICE': '/xosapi/v1/volt/oltdevices',
+    'VOLT_SERVICE': '/xosapi/v1/volt/voltservices',
+    'VOLT_SUBSCRIBER': '/xosapi/v1/rcord/rcordsubscribers',
+    'VOLT_TENANT': '/xosapi/v1/volt/voltserviceinstances',
+    'VSG_TENANT': '/xosapi/v1/vsg/vsgserviceinstances',
+    'XOS_PASSWD': 'letmein',
+    'XOS_USER': 'admin@opencord.org',
+}
+
+jsonHeader = {"Content-Type": "application/json"}
+
+
+class restApi():
+    """
+    Functions for testing CORD API with POST, GET, PUT, DELETE method
+    """
+
+    def getEnvOrDefault(self, key):
+        """
+        Find a variable in environment, or use Default value
+        """
+        if key in os.environ:
+            value = os.environ[key]
+        elif key in restApiDefaults:
+            value = restApiDefaults[key]
+        else:
+            print("Unable to find '%s' in environment or defaults!" % key)
+            value = None
+
+        return value
+
+    def getURL(self, key):
+        """
+        Get REST API suffix from key and return the full URL
+        """
+        urlSuffix = self.getEnvOrDefault(key)
+        url = "http://" + self.getEnvOrDefault("SERVER_IP") + ":" + self.getEnvOrDefault("SERVER_PORT") + urlSuffix
+        return url
+
+    def checkResult(self, resp, expectedStatus):
+        """
+        Check if the status code in resp equals to the expected number.
+        Return True or False based on the check result.
+        """
+        if resp.status_code == expectedStatus:
+            print("Test passed: " + str(resp.status_code) + ": " + resp.text)
+            return True
+        else:
+            print("Test failed: " + str(resp.status_code) + ": " + resp.text)
+            return False
+
+    def ApiPost(self, key, jsonData):
+        url = self.getURL(key)
+        data = json.dumps(jsonData)
+        print("url, data..", url, data)
+        resp = requests.post(
+            url, data=data, headers=jsonHeader,
+            auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD"))
+        )
+        print("requests.codes.....", requests.codes.created)
+        passed = self.checkResult(resp, requests.codes.created) or self.checkResult(
+            resp, requests.codes.ok
+        )
+        return passed
+
+    def ApiPostReturnJson(self, key, jsonData):
+        url = self.getURL(key)
+        data = json.dumps(jsonData)
+        print("url, data..", url, data)
+        resp = requests.post(
+            url, data=data, headers=jsonHeader,
+            auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD"))
+        )
+        print("requests.codes.....", requests.codes.created)
+        print("posted data...", resp.json())
+        passed = self.checkResult(resp, requests.codes.created) or self.checkResult(
+            resp, requests.codes.ok
+        )
+        return passed, resp.json()
+
+    def ApiGet(self, key, urlSuffix=""):
+        url = self.getURL(key) + str(urlSuffix)
+        print("get url...", url)
+        resp = requests.get(url, auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD")))
+        passed = self.checkResult(resp, requests.codes.ok)
+        if not passed:
+            return None
+        else:
+            return resp.json()
+
+    def ApiChameleonGet(self, key, urlSuffix=""):
+        url = self.getURL(key) + "/" + str(urlSuffix)
+        print("get url...", url)
+        resp = requests.get(url, auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD")))
+        passed = self.checkResult(resp, requests.codes.ok)
+        if not passed:
+            return None
+        else:
+            return resp.json()
+
+    def ApiPut(self, key, jsonData, urlSuffix=""):
+        print("urlSuffix....", type(urlSuffix))
+        url = self.getURL(key) + str(urlSuffix) + "/"
+        data = json.dumps(jsonData)
+        resp = requests.put(
+            url, data=data, headers=jsonHeader,
+            auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD"))
+        )
+        passed = self.checkResult(resp, requests.codes.ok)
+        return passed
+
+    def ApiChameleonPut(self, key, jsonData, urlSuffix=""):
+        print("urlSuffix....", type(urlSuffix))
+        url = self.getURL(key) + "/" + str(urlSuffix)
+        print("url", url)
+        data = json.dumps(jsonData)
+        resp = requests.put(
+            url, data=data, headers=jsonHeader,
+            auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD"))
+        )
+        passed = self.checkResult(resp, requests.codes.ok)
+        return passed
+
+    def ApiDelete(self, key, urlSuffix=""):
+        url = self.getURL(key) + str(urlSuffix)
+        print("url", url)
+        resp = requests.delete(url, auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD")))
+        passed = self.checkResult(resp, requests.codes.no_content)
+        return passed
+
+    def ApiChameleonDelete(self, key, urlSuffix=""):
+        url = self.getURL(key) + "/" + str(urlSuffix)
+        print("url", url)
+        resp = requests.delete(url, auth=(self.getEnvOrDefault("XOS_USER"), self.getEnvOrDefault("XOS_PASSWD")))
+        passed = self.checkResult(resp, requests.codes.created) or self.checkResult(
+            resp, requests.codes.ok
+        )
+        return passed
diff --git a/cord-robot/CORDRobot/rf-resources/ATTWorkFlowDriver.resource b/cord-robot/CORDRobot/rf-resources/ATTWorkFlowDriver.resource
new file mode 100644
index 0000000..8f932f8
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/ATTWorkFlowDriver.resource
@@ -0,0 +1,107 @@
+# 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.
+
+*** Settings ***
+Documentation     Library to retrieve status fields from ATT WorkFlow Driver Service Instance List
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           XML
+Library           RequestsLibrary
+Library           CORDRobot
+
+*** Variable ***
+${ONU_STATE_VAR}    admin_onu_state
+
+*** Keywords ***
+Service Instance Status Check
+    [Arguments]    ${onu_device}
+    [Documentation]    Returns onu_state and authentication_state fields for an ONU device
+    ...    from att workflow driver
+    ${json_result}=    CORDRobot.ApiGet    ATT_SERVICEINSTANCES
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict
+    ...    ${json_result_list}    serial_number    ${onu_device}
+    ${onu_state}=    Get From Dictionary    ${getJsonDict}    ${ONU_STATE_VAR}
+    ${authentication_state}=    Get From Dictionary    ${getJsonDict}    authentication_state
+    ${status_message}=    Get From Dictionary    ${getJsonDict}    status_message
+    [Return]    ${onu_state}    ${authentication_state}    ${status_message}
+
+Service Instance DHCP State Check
+    [Arguments]    ${onu_device}
+    [Documentation]    Returns DHCP state from att workflow driver for a particular ONU device
+    ${json_result}=    CORDRobot.ApiGet    ATT_SERVICEINSTANCES
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict
+    ...    ${json_result_list}    serial_number    ${onu_device}
+    ${state}=    Get From Dictionary    ${getJsonDict}    dhcp_state
+    [Return]    ${state}
+
+Create Whitelist Entry
+    [Arguments]    ${entry_list}    ${list_index}
+    [Documentation]    Sends a POST to create an att whitelist in XOS
+    ${elist} =    Get Variable Value    ${entry_list}
+    ${entry_dictionary}=    CORDRobot.listToDict    ${elist}    ${list_index}
+    ${api_result}=    CORDRobot.ApiPost    ATT_WHITELIST    ${entry_dictionary}
+    Should Be True    ${api_result}
+    ${AttWhiteList_Id}=    Get From Dictionary    ${api_result}    id
+    Set Global Variable    ${AttWhiteList_Id}
+    [Return]    ${AttWhiteList_Id}
+
+Retrieve Whitelist Entry
+    [Arguments]    ${serial_number}
+    [Documentation]    Returns the whitelist entry per the ONU serial number
+    ${json_result}=    CORDRobot.ApiGet    ATT_WHITELIST
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict
+    ...    ${json_result_list}    serial_number    ${serial_number}
+    ${id}=    Get From Dictionary    ${getJsonDict}    id
+    [Return]    ${id}
+
+Retrieve ATT Service Instance ID
+    [Arguments]    ${serial_number}
+    [Documentation]    Returns the whitelist entry per the ONU serial number
+    ${json_result}=    CORDRobot.ApiGet    ATT_SERVICEINSTANCES
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict
+    ...    ${json_result_list}    serial_number    ${serial_number}
+    ${id}=    Get From Dictionary    ${getJsonDict}    id
+    [Return]    ${id}
+
+Delete Whitelist Entry
+    [Arguments]    ${id}
+    [Documentation]    Sends a DELETE to delete an att whitelist in XOS
+    ${api_result}=    CORDRobot.ApiChameleonDelete    ATT_WHITELIST    ${id}
+    Should Be True    ${api_result}
+
+Validate ATT Workflow Driver SI
+    [Documentation]    FIXME
+    [Arguments]    ${expected_status}    ${expected_auth_status}
+    ...    ${onu_device}    ${expected_status_message}=${EMPTY}
+    ${onu_state}    ${authentication_status}    ${status_message}
+    ...    Service Instance Status Check    ${onu_device}
+    Should Be Equal    ${onu_state}    ${expected_status}
+    Should Be Equal    ${authentication_status}    ${expected_auth_status}
+    Run Keyword If    '${expected_status_message}' != '${EMPTY}'
+    ...    Should Be Equal    ${status_message}    ${expected_status_message}
+
+Validate ATT Workflow Driver SI DHCP State
+    [Documentation]    FIXME
+    [Arguments]    ${expected_status}    ${onu_device}
+    ${dhcp_state}=    Service Instance DHCP State Check    ${onu_device}
+    Should Be Equal    ${dhcp_state}    ${expected_status}
diff --git a/cord-robot/CORDRobot/rf-resources/CIAB.resource b/cord-robot/CORDRobot/rf-resources/CIAB.resource
new file mode 100644
index 0000000..e765d2d
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/CIAB.resource
@@ -0,0 +1,76 @@
+# 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.
+
+*** Settings ***
+Documentation     Library for CORD-in-a-Box
+Library           SSHLibrary
+Resource          utils.resource
+
+*** Keywords ***
+Execute Command on CIAB Server in Specific VM
+    [Documentation]    SSHs into ${HOST} where CIAB is running and executes a
+    ...    command in the Prod Vagrant VM where all the containers are running
+    [Arguments]    ${system}    ${vm}    ${cmd}
+    ...    ${user}=${VM_USER}    ${password}=${VM_PASS}
+    ...    ${prompt}=$    ${use_key}=True    ${strip_line}=True
+    ${conn_id}=    SSHLibrary.Open Connection    ${system}    prompt=${prompt}    timeout=300s
+    Run Keyword If    '${use_key}' == 'False'
+    ...    SSHLibrary.Login    ${user}    ${pass}
+    ...    ELSE
+    ...    SSHLibrary.Login With Public Key    ${user}    %{HOME}/.ssh/${SSH_KEY}    any
+    SSHLibrary.Write    ssh ${vm}
+    SSHLibrary.Read Until Prompt
+    SSHLibrary.Write    ${cmd}
+    ${output}=    SSHLibrary.Read Until Prompt
+    SSHLibrary.Close Connection
+    ${output_1}=    Run Keyword If    '${strip_line}' == 'True'    Get Line    ${output}    0
+    ${output}=    Set Variable If    '${strip_line}' == 'True'    ${output_1}    ${output}
+    [Return]    ${output}
+
+Execute Command on Compute Node in CIAB
+    [Documentation]    SSHs into ${system} where CIAB is running and executes a
+    ...    command in the Prod Vagrant VM where all the containers are running
+    [Arguments]    ${system}    ${node}    ${hostname}    ${cmd}
+    ...    ${user}=${VM_USER}    ${password}=${VM_PASS}    ${prompt}=$    ${use_key}=True
+    ${conn_id}=    SSHLibrary.Open Connection    ${system}    prompt=${prompt}    timeout=300s
+    # FIXME: Truthy comparison of strings
+    Run Keyword If    '${use_key}' == 'False'
+    ...    SSHLibrary.Login    ${user}    ${pass}
+    ...    ELSE
+    ...    SSHLibrary.Login With Public Key    ${user}    %{HOME}/.ssh/${SSH_KEY}    any
+    SSHLibrary.Write    ssh ${node}
+    SSHLibrary.Read Until Prompt
+    SSHLibrary.Write    ssh root@${hostname}
+    SSHLibrary.Read Until    \#
+    SSHLibrary.Write    ${cmd}
+    ${output}=    SSHLibrary.Read Until    \#
+    SSHLibrary.Close Connection
+    [Return]    ${output}
+
+Get Docker Logs
+    ##In Ciab, all containers are run in the prod vm so we must log into that
+    [Documentation]    Retrieves the logs of a docker container running inside
+    ...    given ${system}
+    [Arguments]    ${system}    ${container_id}
+    ...    ${user}=${USER}    ${password}=${PASSWD}    ${prompt}=prod:~$
+    ${conn_id}=    SSHLibrary.Open Connection    ${system}    prompt=$    timeout=300s
+    SSHLibrary.Login With Public Key    ${USER}    %{HOME}/.ssh/${SSH_KEY}    any
+    #SSHLibrary.Login    ${HOST_USER}    ${HOST_PASSWORD}
+    SSHLibrary.Write    ssh head1
+    SSHLibrary.Read Until    ${prompt}
+    SSHLibrary.Write    docker logs -t ${container_id}
+    ${container_logs}=    SSHLibrary.Read Until    ${prompt}
+    SSHLibrary.Close Connection
+    Log    ${container_logs}
+    [Return]    ${container_logs}
diff --git a/cord-robot/CORDRobot/rf-resources/DHCP.resource b/cord-robot/CORDRobot/rf-resources/DHCP.resource
new file mode 100644
index 0000000..feb1cbb
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/DHCP.resource
@@ -0,0 +1,72 @@
+# 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.
+
+*** Settings ***
+Documentation     Library for DHCP server and client functions
+Library           OperatingSystem
+Resource          utils.resource
+
+*** Keywords ***
+Send Dhclient Request
+    [Documentation]    Executes a dhclient against a particular interface on the RG (src)
+    [Arguments]    ${iface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ${result}=    Login And Run Command On Remote System
+    ...    dhclient -nw ${iface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    [Return]    ${result}
+
+Send Dhclient Request K8S
+    [Documentation]    Run dhclient inside rg container in K8s
+    ${RG_CONTAINER}=    Wait Until Keyword Succeeds    60s    1s
+    ...    Run    kubectl -n voltha get pod|grep "^rg[0-]"|cut -d' ' -f1
+    Run    kubectl -n voltha exec ${RG_CONTAINER} -- sed -i 's/timeout 300;/timeout 30;/' /etc/dhcp/dhclient.conf
+    Run    kubectl -n voltha exec ${RG_CONTAINER} -- ifconfig eth0 0.0.0.0
+    Run    kubectl -n voltha exec ${RG_CONTAINER} -- dhclient
+
+Add Default Route to Dst Gateway
+    [Documentation]    Adds an entry to the routing table on the RG (src)
+    # FIXME - Argument order of iface/ip/user/pass should match other functions
+    [Arguments]    ${src_gateway}    ${dst_subnet}    ${iface}    ${ip}
+    ...    ${user}    ${pass}=${None}    ${container_type}=${None}    ${container_name}=${None}
+    ${result}=    Login And Run Command On Remote System
+    ...    ip route add ${dst_subnet} via ${src_gateway} dev ${iface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    [Return]    ${result}
+
+Check IPv4 Address on DHCP Client
+    [Documentation]    Check if the sepcified interface has an IPv4 address assigned
+    # FIXME - should ip_should_exist have a default value?
+    [Arguments]    ${ip_should_exist}    ${iface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ${output}=    Login And Run Command On Remote System
+    ...    ip address show ${iface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    # FIXME - ipv4_regex not set if container_type != K8S?
+    ${ipv4_regex}=    Set Variable If    '${container_type}' != 'K8S'
+    ...    \\b([0-9]{1,3}\\.){3}[0-9]{1,3}\\b    \\b(172)\\.(18)(\\.([0-9]{1,3})){2}\\b
+    # FIXME - use a boolean rather than string comparison against truthy value
+    Run Keyword If    '${ip_should_exist}' == 'True'    Should Match Regexp
+    ...    ${output}    ${ipv4_regex}
+    Run Keyword If    '${ip_should_exist}' == 'False'    Should Not Match Regexp
+    ...    ${output}    ${ipv4_regex}
+
+Start DHCP Server on Remote Host
+    [Documentation]    Start the 'dhcpd' process on specified network interface
+    ...    on a remote host
+    [Arguments]    ${interface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ${result}=    Login And Run Command On Remote System
+    ...    dhcpd -cf /etc/dhcp/dhcpd.conf ${interface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
diff --git a/cord-robot/CORDRobot/rf-resources/Kubernetes.resource b/cord-robot/CORDRobot/rf-resources/Kubernetes.resource
new file mode 100644
index 0000000..14857dd
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/Kubernetes.resource
@@ -0,0 +1,101 @@
+# 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.
+
+*** Settings ***
+Documentation     Library of functions related to kubectl and helm
+Library           SSHLibrary
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           RequestsLibrary
+Library           CORDRobot
+Resource          utils.resource
+
+*** Keywords ***
+Helm Chart is Removed
+    [Documentation]    Verify the specified helm chart has been removed
+    [Arguments]    ${helm_chart}
+    ${rc}=    Run And Return Rc
+    ...    helm ls -q | grep ${helm_chart}
+    Should Be Equal As Integers    ${rc}    1
+
+Kubernetes PODs in Namespace are Removed
+    [Documentation]    Verify all Kubernetes pods in specified namespace have been removed
+    [Arguments]    ${namespace}
+    ${rc}    ${output}=    Run And Return Rc And Output
+    ...    kubectl get pods --no-headers -n ${namespace}
+    Should Contain    ${output}    No resources found
+
+Kubernetes PODs in Namespace are Running
+    [Documentation]    Verify the number of Kubernetes pods that are running
+    ...    in specified namespace is as expected
+    [Arguments]    ${namespace}    ${pod_num}
+    ${rc}    ${output}=    Run And Return Rc And Output
+    ...    kubectl get pods -n ${namespace} | grep -i running | grep 1/1 | wc -l
+    Should Be Equal As Integers    ${output}    ${pod_num}
+
+Reinstall Voltha
+    [Documentation]    Remove voltha helm chart and wait
+    Run    helm delete --purge voltha
+    Wait Until Keyword Succeeds    60s    10s
+    ...    Helm Chart is Removed    voltha
+    Wait Until Keyword Succeeds    120s    10s
+    ...    Kubernetes PODs in Namespace are Removed    voltha
+    Sleep    10s
+    Run    helm repo add incubator https://kubernetes-charts-incubator.storage.googleapis.com/
+    # FIXME - HELM_CHARTS_DIR should a parameter
+    Run    cd ${HELM_CHARTS_DIR}; helm dep up voltha
+    # FIXME - KUBERNETES_YAML should a parameter
+    Run    helm install -n voltha -f ${KUBERNETES_YAML} ${HELM_CHARTS_DIR}/voltha
+    Wait Until Keyword Succeeds    60s    10s
+    ...    Kubernetes PODs in Namespace are Running    voltha    ${VOLTHA_POD_NUM}
+    Sleep    10s
+
+Get Current Datetime On Kubernetes Node
+    [Documentation]    Get UTC datetime in RFC3339ish format
+    [Arguments]    ${ip}    ${user}    ${pass}
+    ${result}=    Login And Run Command On Remote System
+    ...    date -u +"%Y-%m-%dT%H:%M:%S.%NZ"    ${ip}    ${user}    ${pass}
+    # FIXME - is this needed? Does date return multiple lines?
+    ${result}=    Get Line    ${result}    0
+    [Return]    ${result}
+
+Log Kubernetes Container Log Since Time
+    [Documentation]    Returns the output of kubectl logs of a pod since timestamp
+    [Arguments]    ${datetime}    ${pod_prefix}
+    # FIXME - rc var isn't checked and then overwritten in this set of commands
+    ${rc}    ${namespace}=    Run And Return Rc And Output
+    ...    kubectl get pods --all-namespaces | grep ' ${pod_prefix}' | head -1 | awk '{print $1}'
+    ${rc}    ${pod_name}=    Run And Return Rc And Output
+    ...    kubectl get pods --all-namespaces | grep ' ${pod_prefix}' | head -1 | awk '{print $2}'
+    ${rc}    ${output}=    Run Keyword If    '${pod_prefix}' == 'onos'
+    ...    Run And Return Rc And Output
+    ...    kubectl logs --timestamps -n ${namespace} --since-time=${datetime} ${pod_name} -c onos
+    ...    ELSE    Run And Return Rc And Output
+    ...    kubectl logs --timestamps -n ${namespace} --since-time=${datetime} ${pod_name}
+    Log    ${output}
+
+Log Kubernetes Containers Logs Since Time
+    [Documentation]    Given a datetime and list of containers, print logs for those containers
+    [Arguments]    ${datetime}    ${pod_list}
+    FOR    ${pod_prefix}    IN    @{pod_list}
+        Log Kubernetes Container Log Since Time    ${datetime}    ${pod_prefix}
+    END
+
+Get Kubernetes POD Name By Prefix
+    [Documentation]    Return the first POD name that starts with the specified prefix
+    [Arguments]    ${prefix}
+    ${rc}    ${output}=    Run And Return Rc And Outputi
+    ...    kubectl get pods --all-namespaces | grep '${prefix}' | head -1 | awk '{print $2}'
+    [Return]    ${output}
diff --git a/cord-robot/CORDRobot/rf-resources/Network.resource b/cord-robot/CORDRobot/rf-resources/Network.resource
new file mode 100644
index 0000000..227f084
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/Network.resource
@@ -0,0 +1,71 @@
+# 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.
+
+*** Settings ***
+Documentation     Library for interacting with network config, such as
+...               interfaces, VLANs, routes on a host
+Resource          utils.resource
+
+*** Keywords ***
+Add Ip Address on Interface on Host
+    [Documentation]    Add IP addresss on interface on remote host
+    [Arguments]    ${ip_address}    ${interface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    ip addr add ${ip_address} dev ${interface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Delete IP Addresses from Interface on Remote Host
+    [Documentation]    Remove specified IP address on an interface on remote host
+    [Arguments]    ${interface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    ip addr flush dev ${interface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Add Double Vlan Interface on Host
+    [Documentation]    Add double S/C-VLAN tagging on an interface on remote host
+    [Arguments]    ${interface}    ${stag}    ${ctag}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    ip link add link ${interface} name ${interface}.${stag} type vlan id ${stag}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Login And Run Command On Remote System
+    ...    ip link set ${interface}.${stag} up
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Login And Run Command On Remote System
+    ...    ip link add link ${interface}.${stag} name ${interface}.${stag}.${ctag} type vlan id ${ctag}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Login And Run Command On Remote System
+    ...    ip link set ${interface}.${stag}.${ctag} up
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Login And Run Command On Remote System
+    ...    ifconfig ${interface}.${stag}.${ctag}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Delete Interface on Remote Host
+    [Documentation]    Deleted interface (link) on remote host
+    [Arguments]    ${interface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    ip link del ${interface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Add Route to Remote Host
+    [Documentation]    Add route on remote host given subnet and gateway
+    [Arguments]    ${subnet}    ${gateway}    ${interface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    ip route add ${subnet} via ${gateway} dev ${interface}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
diff --git a/cord-robot/CORDRobot/rf-resources/OLT.resource b/cord-robot/CORDRobot/rf-resources/OLT.resource
new file mode 100644
index 0000000..bdfc1a1
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/OLT.resource
@@ -0,0 +1,87 @@
+# 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.
+
+*** Settings ***
+Documentation     Library of functions related to OLT
+Library           SSHLibrary
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           RequestsLibrary
+Library           CORDRobot
+
+*** Keywords ***
+Openolt is Up
+    [Documentation]    Verify that openolt process is started and ready to connect to voltha
+    [Arguments]    ${ip}    ${user}    ${pass}    ${prompt}=~#
+    Check Remote File Contents    True
+    ...    /var/log/openolt.log    oper_state: up
+    ...    ${ip}    ${user}    ${pass}    prompt=${prompt}
+
+OLT Status Check
+    [Documentation]    Returns "operational_status" and "admin_status" of a particular
+    ...    OLT device from "olt device list"
+    [Arguments]    ${olt_device}
+    ${json_result}=    CORDRobot.ApiGet    VOLT_DEVICE
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict
+    ...    ${json_result_list}    host    ${olt_device}
+    ${operational_status}=    Get From Dictionary    ${getJsonDict}    oper_status
+    ${admin_status}=    Get From Dictionary    ${getJsonDict}    admin_state
+    [Return]    ${operational_status}    ${admin_status}
+
+Validate OLT States
+    [Documentation]    Check that OLT has the expected 'operational_status' and 'admin_status'
+    [Arguments]    ${expected_op_status}    ${expected_admin_status}    ${olt_device}
+    ${operational_status}    ${admin_status}    OLT Status Check    ${olt_device}
+    Should Be Equal    ${operational_status}    ${expected_op_status}
+    Should Be Equal    ${admin_status}    ${expected_admin_status}
+
+Get VOLTHA Status
+    [Documentation]    Obtain and log output of VOLTHA diagnostic commands
+    ${resp}=    CORD Get    ${VOLT_DEVICE}
+    ${jsondata}=    To Json    ${resp.content}
+    Log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        ${value}=    Get From List    ${jsondata['items']}    ${INDEX}
+        ${olt_device_id}=    Get From Dictionary    ${value}    device_id
+        ${logical_device_id}=    Get From Dictionary    ${value}    of_id
+    END
+    Set Suite Variable    ${olt_device_id}
+    Set Suite Variable    ${logical_device_id}
+    CORDRobot.write_log_of_voltha_cli_comand    /tmp
+    ...    voltha_devices.log    devices
+    ...    host=${server_ip}
+    CORDRobot.write_log_of_voltha_cli_comand    /tmp
+    ...    logical_devices.log    logical_device ${logical_device_id}
+    ...    voltha_logical_ports.log    ports
+    ...    voltha_logical_flows.log    flow
+    ...    host=${server_ip}
+    CORDRobot.write_log_of_voltha_cli_comand    /tmp
+    ...    devices.log    device ${olt_device_id}
+    ...    voltha_olt_ports.log    ports
+    ...    voltha_olt_flows.log    flows
+    ...    host=${server_ip}
+    ${voltha_devices_log}=    Get Binary File    /tmp/voltha_devices.log
+    ${devices_flows}=    Get Binary File    /tmp/voltha_olt_flows.log
+    ${device_ports}=    Get Binary File    /tmp/voltha_olt_ports.log
+    ${logical_devices}=    Get Binary File    /tmp/voltha_logical_flows.log
+    ${l_device_ports}=    Get Binary File    /tmp/voltha_logical_ports.log
+    Log    ${voltha_devices_log}
+    Log    ${devices_flows}
+    Log    ${device_ports}
+    Log    ${logical_devices}
+    Log    ${l_device_ports}
diff --git a/cord-robot/CORDRobot/rf-resources/ONOS.resource b/cord-robot/CORDRobot/rf-resources/ONOS.resource
new file mode 100644
index 0000000..dfe519b
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/ONOS.resource
@@ -0,0 +1,118 @@
+# 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.
+# onos common functions
+
+*** Settings ***
+Documentation     Library for ONOS Functions
+Library           SSHLibrary
+
+*** Keywords ***
+Execute ONOS CLI Command
+    [Documentation]    Establishes an ssh connection to ONOS contoller and executes a command
+    [Arguments]    ${host}    ${port}    ${cmd}    ${user}=karaf    ${pass}=karaf
+    ${conn_id}=    SSHLibrary.Open Connection    ${host}    port=${port}    timeout=300s
+    SSHLibrary.Login    ${user}    ${pass}
+    @{result_values}    SSHLibrary.Execute Command    ${cmd}    return_rc=True
+    ...    return_stderr=True    return_stdout=True
+    ${output}    Set Variable    @{result_values}[0]
+    Log    ${output}
+    Should Be Empty    @{result_values}[1]
+    Should Be Equal As Integers    @{result_values}[2]    0
+    SSHLibrary.Close Connection
+    [Return]    ${output}
+
+Validate XConnect in ONOS
+    [Documentation]    Check if Fabric Crossconnnect matches exists value
+    [Arguments]    ${server_ip}    ${stag}    ${exists}=True    ${port}=30120
+    # FIXME: use Robot-based JSON manipulation, shorten line
+    ${rc}=    Run And Return RC
+    ...    http -a karaf:karaf GET http://${server_ip}:${port}/onos/segmentrouting/xconnect|jq -r '.xconnects[].vlanId'|grep ${stag}
+    Run Keyword If    '${exists}' == 'True'
+    ...    Should Be Equal As Integers    ${rc}    0
+    ...    ELSE
+    ...    Should Be Equal As Integers    ${rc}    1
+
+Get ONOS Status
+    [Documentation]    Obtain and log output of ONOS diagnostic commands
+    [Arguments]    ${server_ip}=${None}    ${server_port}=30115
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_apps.log    apps -a -s
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_apps}    Get Binary File    /tmp/onos_apps.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_devices.log    devices
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_devices}    Get Binary File    /tmp/onos_devices.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_ports.log    ports
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_ports}    Get Binary File    /tmp/onos_ports.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_flows.log    flows -s
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_flows}    Get Binary File    /tmp/onos_flows.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_meters.log    meters
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_meters}    Get Binary File    /tmp/onos_meters.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_volt_prog_subscribers.log    volt-programmed-subscribers
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_volt_prog_subscribers}    Get Binary File    /tmp/onos_volt_prog_subscribers.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_volt_prog_meters.log    volt-programmed-meters
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_volt_prog_meters}    Get Binary File    /tmp/onos_volt_prog_meters.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_volt_bp_meters.log    volt-bpmeter-mappings
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_volt_bp_meters}    Get Binary File    /tmp/onos_volt_bp_meters.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_dhcpl2.log    dhcpl2relay-allocations
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_dhcpl2}    Get Binary File    /tmp/onos_dhcpl2.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_aaa_users.log    aaa-users
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_aaa_users}    Get Binary File    /tmp/onos_aaa_users.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_netcfg.log    netcfg
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_netcfg}    Get Binary File    /tmp/onos_netcfg.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_groups.log    groups
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_groups}    Get Binary File    /tmp/onos_groups.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_hosts.log    hosts
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_hosts}    Get Binary File    /tmp/onos_hosts.log
+    CORDRobot.write_log_of_onos_cli_command    /tmp
+    ...    onos_links.log    links
+    ...    host=${server_ip}    port=${server_port}
+    ${onos_links}    Get Binary File    /tmp/onos_links.log
+    Log    ${onos_apps}
+    Log    ${onos_devices}
+    Log    ${onos_ports}
+    Log    ${onos_flows}
+    Log    ${onos_meters}
+    Log    ${onos_aaa_users}
+    Log    ${onos_volt_prog_subscribers}
+    Log    ${onos_volt_prog_meters}
+    Log    ${onos_volt_bp_meters}
+    Log    ${onos_hosts}
+    Log    ${onos_dhcpl2}
+    Log    ${onos_netcfg}
+    Log    ${onos_groups}
+    Log    ${onos_links}
diff --git a/cord-robot/CORDRobot/rf-resources/ONU.resource b/cord-robot/CORDRobot/rf-resources/ONU.resource
new file mode 100644
index 0000000..7ec8d98
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/ONU.resource
@@ -0,0 +1,69 @@
+# 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.
+
+*** Settings ***
+Documentation     Library to check the status in ONU List
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           XML
+Library           RequestsLibrary
+Library           CORDRobot
+
+*** Keywords ***
+ONU Status Check
+    [Documentation]    Returns "operational_status" and "admin_status" of
+    ...    a particular ONU device from "onu device list"
+    [Arguments]    ${onu_device}
+    ${json_result}=    CORDRobot.ApiGet    ONU_DEVICE
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict    ${json_result_list}
+    ...    serial_number    ${onu_device}
+    ${operational_status}=    Get From Dictionary    ${getJsonDict}    oper_status
+    ${admin_status}=    Get From Dictionary    ${getJsonDict}    admin_state
+    [Return]    ${operational_status}    ${admin_status}
+
+Create ONU Device
+    [Documentation]    Sends a POST to create an att whitelist in XOS
+    [Arguments]    ${device_list}    ${list_index}
+    ${dlist} =    Get Variable Value    ${device_list}
+    ${onu_dictionary}=    CORDRobot.listToDict    ${dlist}    ${list_index}
+    ${api_result}=    CORDRobot.ApiPost    ONU_DEVICE    ${onu_dictionary}
+    Should Be True    ${api_result}
+
+Retrieve ONU Device
+    [Documentation]    Returns the onu device id based on the onu's serial number
+    [Arguments]    ${serial_number}
+    ${json_result}=    CORDRobot.ApiGet    ONU_DEVICE
+    Log    ${json_result}
+    Log To Console    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict    ${json_result_list}
+    ...    serial_number    ${serial_number}
+    ${id}=    Get From Dictionary    ${getJsonDict}    id
+    [Return]    ${id}
+
+Delete ONU Device
+    [Arguments]    ${id}
+    [Documentation]    Sends a DELETE to delete an onu device in XOS
+    ${api_result}=    CORDRobot.ApiChameleonDelete    ONU_DEVICE    ${id}
+    Should Be True    ${api_result}
+
+Validate ONU States
+    [Documentation]    Check that ONU has the expected 'operational_status' and 'admin_status'
+    [Arguments]    ${expected_op_status}    ${expected_admin_status}    ${onu_device}
+    ${operational_status}    ${admin_status}    ONU Status Check    ${onu_device}
+    Should Be Equal    ${operational_status}    ${expected_op_status}
+    Should Be Equal    ${admin_status}    ${expected_admin_status}
diff --git a/cord-robot/CORDRobot/rf-resources/Subscriber.resource b/cord-robot/CORDRobot/rf-resources/Subscriber.resource
new file mode 100644
index 0000000..232d87a
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/Subscriber.resource
@@ -0,0 +1,245 @@
+# 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.
+
+*** Settings ***
+Documentation     Library of functions related a subscriber (RG)
+Resource          ATTWorkFlowDriver.resource
+Resource          DHCP.resource
+Resource          Network.resource
+Resource          ONOS.resource
+Resource          utils.resource
+Resource          XOS.resource
+
+*** Keywords ***
+Subscriber Status Check
+    [Documentation]    Returns Status from Subscribers List for a particular ONU device
+    [Arguments]    ${onu_device}
+    ${json_result}=    CORDRobot.ApiGet    VOLT_SUBSCRIBER
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict    ${json_result_list}
+    ...    onu_device    ${onu_device}
+    ${status}=    Get From Dictionary    ${getJsonDict}    status
+    [Return]    ${status}
+
+Validate Subscriber Status
+    [Documentation]    Check that a subscriber has the expected status
+    [Arguments]    ${expected_status}    ${onu_device}    ${accepted_status}=${EMPTY}
+    ${status}    Subscriber Status Check    ${onu_device}
+    Run Keyword If    '${accepted_status}' == '${EMPTY}'
+    ...    Should Be Equal    ${status}    ${expected_status}
+    ...    ELSE
+    ...    Should Contain Any    ${status}    ${expected_status}    ${accepted_status}
+
+Create Subscriber
+    [Documentation]    Sends a POST to create a subscriber in XOS
+    [Arguments]    ${subscriber_list}    ${list_index}
+    ${slist} =    Get Variable Value    ${subscriber_list}
+    ${subscriber_dictionary}=    CORDRobot.listToDict    ${slist}    ${list_index}
+    ${api_result}=    CORDRobot.ApiPost    VOLT_SUBSCRIBER    ${subscriber_dictionary}
+    Should Be True    ${api_result}
+    ${Subscriber_id}=    Get From Dictionary    ${api_result}    id
+    Set Global Variable    ${Subscriber_id}
+    [Return]    ${Subscriber_id}
+
+Retrieve Subscriber
+    [Documentation]    Returns the subscriber id based on the subscriber's C-Tag
+    [Arguments]    ${ctag}
+    ${json_result}=    CORDRobot.ApiGet    VOLT_SUBSCRIBER
+    Log    ${json_result}
+    ${json_result_list}=    Get From dictionary    ${json_result}    items
+    ${getJsonDict}=    CORDRobot.getDictFromListOfDict    ${json_result_list}
+    ...    c_tag    ${ctag}
+    ${id}=    Get From Dictionary    ${getJsonDict}    id
+    [Return]    ${id}
+
+Delete Subscriber
+    [Documentation]    Deletes a given subscriber based on its c_tag
+    [Arguments]    ${ctag}
+    ${id}=    Retrieve Subscriber    ${ctag}
+    ${api_result}=    CORDRobot.ApiChameleonDelete    VOLT_SUBSCRIBER    ${id}
+    Should Be True    ${api_result}
+
+Send EAPOL Message
+    [Documentation]    Executes a particular auth request on the RG via wpa_supplicant
+    ...    Requested packet should exist on src.
+    [Arguments]    ${iface}    ${conf_file}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    rm -f /tmp/wpa.log; wpa_supplicant -B -i ${iface} -Dwired -c /etc/wpa_supplicant/${conf_file} -f /tmp/wpa.log
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Validate Authentication
+    [Documentation]    Executes a particular auth request on the RG and verifies if it succeeds
+    ...    auth_pass determines if authentication should pass
+    [Arguments]    ${auth_pass}    ${iface}    ${conf_file}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Send EAPOL Message    ${iface}    ${conf_file}    ${ip}    ${user}    ${pass}
+    ...    ${container_type}    ${container_name}
+    # FIXME: Use an If/Else block, not Three separate checks, bools instead of truthy strings
+    Run Keyword If    '${auth_pass}' == 'True'
+    ...    Wait Until Keyword Succeeds    120s    2s
+    ...    Check Remote File Contents    True
+    ...    /tmp/wpa.log    authentication completed successfully
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Run Keyword If    '${auth_pass}' == 'False'
+    ...    Sleep    20s
+    Run Keyword If    '${auth_pass}' == 'False'
+    ...    Check Remote File Contents    False
+    ...    /tmp/wpa.log    authentication completed successfully
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Run Multicast Client
+    [Documentation]    Executes mcjoin (a simple multicast client) on the RG.
+    [Arguments]    ${iface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Login And Run Command On Remote System
+    ...    rm -f /tmp/mcjoin.log; timeout 10 mcjoin -c 5 -i eth0 > /tmp/mcjoin.log || true
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Validate Multicast
+    [Documentation]    Executes a particular auth request on the RG and verifies
+    ...    if it succeeds. auth_pass determines if authentication should pass
+    [Arguments]    ${auth_pass}    ${iface}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    Run Multicast Client    ${iface}    ${ip}    ${user}    ${pass}
+    ...    ${container_type}    ${container_name}
+    Run Keyword If    '${auth_pass}' == 'True'
+    ...    Check Remote File Contents    True
+    ...    /tmp/mcjoin.log    Received total: 5 packets
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Run Keyword If    '${auth_pass}' == 'False'
+    ...    Check Remote File Contents    True
+    ...    /tmp/mcjoin.log    Received total: 0 packets
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+
+Validate DHCP and Ping
+    [Documentation]    Check that DHCP address has been acquired and Ping works
+    [Arguments]    ${dhcp_should_pass}    ${ping_should_pass}
+    ...    ${src_iface}    ${s_tag}    ${c_tag}
+    ...    ${dst_dp_ip}    ${src_ip}    ${src_user}    ${src_pass}=${None}
+    ...    ${src_container_type}=${None}    ${src_container_name}=${None}
+    ...    ${dst_dp_iface}=${None}    ${dst_ip}=${None}
+    ...    ${dst_user}=${None}    ${dst_pass}=${None}
+    ...    ${dst_container_type}=${None}    ${dst_container_name}=${None}
+    Run Keyword If    '${dst_ip}' != '${None}'    Run Keywords
+    ...    Add Double Vlan Interface on Host    ${dst_dp_iface}    ${s_tag}    ${c_tag}
+    ...    ${dst_ip}    ${dst_user}    ${dst_pass}    ${dst_container_type}    ${dst_container_name}
+    ...    AND
+    ...    Add IP Address on Interface on Host
+    ...    ${dst_dp_ip}/24    ${dst_dp_iface}.${s_tag}.${c_tag}
+    ...    ${dst_ip}    ${dst_user}    ${dst_pass}    ${dst_container_type}    ${dst_container_name}
+    ...    AND
+    ...    Start DHCP Server on Remote Host    ${dst_dp_iface}.${s_tag}.${c_tag}    ${dst_ip}
+    ...    ${dst_user}    ${dst_pass}    ${dst_container_type}    ${dst_container_name}
+    Run Keyword If    '${src_container_type}' != 'K8S'
+    ...    Send Dhclient Request    ${src_iface}    ${src_ip}
+    ...    ${src_user}    ${src_pass}    ${src_container_type}    ${src_container_name}
+    ...    ELSE
+    ...    Send Dhclient Request K8S
+    Run Keyword If    '${dhcp_should_pass}' == 'True'
+    ...    Wait Until Keyword Succeeds    90s    5s
+    ...    Check IPv4 Address on DHCP Client    True    ${src_iface}    ${src_ip}
+    ...    ${src_user}    ${src_pass}    ${src_container_type}    ${src_container_name}
+    Run Keyword If    '${dhcp_should_pass}' == 'False'
+    ...    Sleep    15s
+    Run Keyword If    '${dhcp_should_pass}' == 'False'
+    ...    Check IPv4 Address on DHCP Client    False    ${src_iface}    ${src_ip}
+    ...    ${src_user}    ${src_pass}    ${src_container_type}    ${src_container_name}
+    Run Keyword If    '${ping_should_pass}' == 'True'
+    ...    Wait Until Keyword Succeeds    60s    2s
+    ...    Check Ping    True    ${dst_dp_ip}    ${src_iface}    ${src_ip}
+    ...    ${src_user}    ${src_pass}    ${src_container_type}    ${src_container_name}
+    ...    ELSE
+    ...    Wait Until Keyword Succeeds    60s    2s
+    ...    Check Ping    False    ${dst_dp_ip}    ${src_iface}    ${src_ip}
+    ...    ${src_user}    ${src_pass}    ${src_container_type}    ${src_container_name}
+
+Validate Subscriber Service Chain
+    [Documentation]    Check if serial number is list of subcribed_links_ids
+    [Arguments]    ${serial_no}    ${expected}=True
+    ${resp}=    CORD Get    ${VOLT_SUBSCRIBER}
+    ${jsondata}=    To Json    ${resp.content}
+    Log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        ${value}=    Get From List    ${jsondata['items']}    ${INDEX}
+        ${sl}=    Get From Dictionary    ${value}    subscribed_links_ids
+        ${result}    ${slinks}=    Run Keyword And Ignore Error
+        ...    Get From List    ${sl}    0
+        ${sn}=    Get From Dictionary    ${value}    onu_device
+        Run Keyword If    '${sn}' == '${serial_no}'    Exit For Loop
+    END
+
+Validate Fabric CrossConnect SI
+    [Documentation]    Build list of s_tags in fabric crossconnect
+    [Arguments]    ${stag}    ${expected}=True
+    ${resp}=    CORD Get    ${FABRIC_CROSSCONNECT_SERVICEINSTANCES}
+    ${jsondata}=    To Json    ${resp.content}
+    Log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    @{tags}=    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        ${value}=    Get From List    ${jsondata['items']}    ${INDEX}
+        ${tag}=    Get From Dictionary    ${value}    s_tag
+        Append To List    ${tags}    ${tag}
+    END
+
+Validate Subscriber Count
+    [Documentation]    Check if subscriber count matches passed value
+    [Arguments]    ${expected_no}
+    ${resp}=    CORD Get    ${VOLT_SUBSCRIBER}
+    ${jsondata}=    To Json    ${resp.content}
+    Log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    Should Be Equal As Integers    ${length}    ${expected_no}
+
+Subscriber Ready to Authenticate
+    [Documentation]    Check if subscriber is in awaiting-auth state
+    [Arguments]    ${onu_device}
+    Wait Until Keyword Succeeds    60s    15s
+    ...    Validate ONU States    ACTIVE    ENABLED    ${onu_device}
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate ATT Workflow Driver SI    ENABLED    AWAITING    ${onu_device}
+    ...    ONU has been validated - Awaiting Authentication
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Subscriber Status    awaiting-auth    ${onu_device}
+
+Subscriber Provisioned
+    [Documentation]    Check if subscriber has successfully authenticated
+    [Arguments]    ${server_ip}    ${onu_device}    ${stag}
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate ATT Workflow Driver SI    ENABLED    APPROVED    ${onu_device}
+    ...    ONU has been validated - Authentication succeeded
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Subscriber Status    enabled    ${onu_device}
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Subscriber Service Chain    ${onu_device}    True
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate XConnect in ONOS    ${server_ip}    ${stag}    True
+
+Subscriber Service Chain Created
+    [Documentation]    Check if subscriber service chain has been created
+    [Arguments]    ${onu_device}    ${stag}
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate ATT Workflow Driver SI    ENABLED    APPROVED    ${onu_device}
+    ...    ONU has been validated - Authentication succeeded
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Subscriber Status    enabled    ${onu_device}
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Subscriber Service Chain    ${onu_device}    True
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate Fabric CrossConnect SI    ${stag}    True
+    Wait Until Keyword Succeeds    60s    2s
+    ...    Validate XConnect in ONOS    ${server_ip}    ${stag}    True
diff --git a/cord-robot/CORDRobot/rf-resources/XOS.resource b/cord-robot/CORDRobot/rf-resources/XOS.resource
new file mode 100644
index 0000000..fd03ea9
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/XOS.resource
@@ -0,0 +1,86 @@
+# 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.
+
+*** Settings ***
+Documentation     Library for interacting with XOS
+Library           String
+Library           RequestsLibrary
+
+*** Keywords ***
+CORD Get
+    [Documentation]    Make a GET call to XOS
+    [Arguments]    ${service}
+    ${resp}=    Get Request    ${server_ip}    ${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    ${SERVER_IP}    uri=${service}    data=${data}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    [Return]    ${resp}
+
+CORD Put
+    [Documentation]    Make a PUT call to XOS
+    [Arguments]    ${service}    ${data}    ${data_id}
+    ${data}=    Evaluate    json.dumps(${data})    json
+    ${resp}=    Put Request    ${SERVER_IP}    uri=${service}/${data_id}    data=${data}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    ${id}=    Get From Dictionary    ${resp.json()}    id
+    Set Suite Variable    ${id}
+    [Return]    ${resp}
+
+CORD Delete
+    [Documentation]    Make a DELETE call to XOS
+    [Arguments]    ${service}    ${data_id}
+    ${resp}=    Delete Request    ${SERVER_IP}    uri=${service}/${data_id}
+    Log    ${resp.content}
+    Should Be Equal As Strings    ${resp.status_code}    200
+    [Return]    ${resp}
+
+Get Service Owner Id
+    [Documentation]    Find the id of owner of an XOS service
+    [Arguments]    ${service}
+    ${resp}=    CORD Get    ${service}
+    ${jsondata}=    To Json    ${resp.content}
+    log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    # FIXME: should this break after finding the first item?
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        ${value}=    Get From List    ${jsondata['items']}    ${INDEX}
+        ${id}=    Get From Dictionary    ${value}    id
+    END
+    [Return]    ${id}
+
+Clean Up Objects
+    [Documentation]    Delete all objects in XOS data model
+    [Arguments]    ${model_api}
+    @{ids}=    Create List
+    ${resp}=    CORD Get    ${model_api}
+    ${jsondata}=    To Json    ${resp.content}
+    Log    ${jsondata}
+    ${length}=    Get Length    ${jsondata['items']}
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        ${value}=    Get From List    ${jsondata['items']}    ${INDEX}
+        ${id}=    Get From Dictionary    ${value}    id
+        Append To List    ${ids}    ${id}
+    END
+    FOR    ${i}    IN    @{ids}
+        CORD Delete    ${model_api}    ${i}
+    END
diff --git a/cord-robot/CORDRobot/rf-resources/utils.resource b/cord-robot/CORDRobot/rf-resources/utils.resource
new file mode 100644
index 0000000..d85fa20
--- /dev/null
+++ b/cord-robot/CORDRobot/rf-resources/utils.resource
@@ -0,0 +1,222 @@
+# 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.
+
+*** Settings ***
+Documentation     Library for various utilities
+Library           SSHLibrary
+Library           String
+Library           DateTime
+Library           Process
+Library           Collections
+Library           RequestsLibrary
+
+*** Keywords ***
+Login To Remote System
+    [Documentation]    SSH into a remote host (and into a container on that host if container_type
+    ...    and container_name are specified) and returns connection ID
+    [Arguments]    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ...    ${prompt}=~$    ${prompt_timeout}=15s    ${container_prompt}=#
+    ${conn_id}=    SSHLibrary.Open Connection    ${ip}    prompt=${prompt}    timeout=${prompt_timeout}
+    Run Keyword If    '${pass}' != '${None}'
+    ...    SSHLibrary.Login    ${user}    ${pass}
+    ...    ELSE
+    ...    SSHLibrary.Login With Public Key    ${user}    %{HOME}/.ssh/id_rsa
+    # Login to the lxc container
+    Run Keyword If    '${container_type}' == 'LXC'    Run Keywords
+    ...    SSHLibrary.Write    lxc exec ${container_name} /bin/bash
+    ...    AND
+    ...    SSHLibrary.Read Until    ${container_prompt}
+    ...    AND
+    ...    SSHLibrary.Set Client Configuration    prompt=${container_prompt}
+    # Login to the k8s container
+    # FIXME: This fails if /bin/bash isn't installed in the container, run on command
+    Run Keyword If    '${container_type}' == 'K8S'    Run Keywords
+    ...    SSHLibrary.Write    kubectl -n $(kubectl get pods --all-namespaces | grep ${container_name} | awk '{print $1}') exec ${container_name} -it /bin/bash
+    ...    AND
+    ...    SSHLibrary.Read Until    ${container_prompt}
+    ...    AND
+    ...    SSHLibrary.Set Client Configuration    prompt=${container_prompt}
+    # Try to switch to root user
+    # FIXME: Is is useful in the LXC/K8S cases?
+    ${conn}=    SSHLibrary.Get Connection    ${conn_id}
+    Run Keyword And Ignore Error
+    ...    SSHLibrary.Write    sudo -s
+    ${output}=    SSHLibrary.Read Until Regexp    \#|${conn.prompt}|password for ${user}:
+    Run Keyword If    'password for ${user}:' not in '''${output}'''
+    ...    Return From Keyword    ${conn_id}
+    SSHLibrary.Set Client Configuration    prompt=\#
+    SSHLibrary.Write    ${pass}
+    SSHLibrary.Read Until Prompt
+    [Return]    ${conn_id}
+
+Logout From Remote System
+    [Documentation]    Exit from the SSH session to a remote host
+    [Arguments]    ${conn_id}
+    SSHLibrary.Switch Connection    ${conn_id}
+    SSHLibrary.Close Connection
+
+Run Command On Remote System
+    [Documentation]    Executes a command on remote host and returns output
+    [Arguments]    ${cmd}    ${conn_id}    ${user}    ${pass}=${None}
+    ${conn}=    SSHLibrary.Get Connection    ${conn_id}
+    SSHLibrary.Switch Connection    ${conn_id}
+    SSHLibrary.Write    ${cmd}
+    ${output}=    SSHLibrary.Read Until Regexp    ${conn.prompt}|password for ${user}:
+    Run Keyword If    'password for ${user}:' not in '''${output}'''
+    ...    Return From Keyword    ${output}
+    SSHLibrary.Write    ${pass}
+    ${output}=    SSHlibrary.Read Until Prompt
+    [Return]    ${output}
+
+Login And Run Command On Remote System
+    [Documentation]    SSH into a remote host (and into a container on that host if container_type
+    ...    and container_name are specified), switch to root user, executes command, return output
+    [Arguments]    ${cmd}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ...    ${prompt}=~$    ${prompt_timeout}=50s    ${container_prompt}=#
+    ${conn_id}    Login To Remote System    ${ip}    ${user}    ${pass}
+    ...    ${container_type}    ${container_name}
+    ...    ${prompt}    ${prompt_timeout}    ${container_prompt}
+    ${output}=    Run Command On Remote System    ${cmd}    ${conn_id}    ${user}    ${pass}
+    Log    ${output}
+    # FIXME: Look into persisting SSH connection rather than tearing it up/down repeatedly
+    Logout From Remote System    ${conn_id}
+    [Return]    ${output}
+
+Execute Command Locally
+    [Documentation]    Superfluous, use the 'Run' keyword instead which this wraps
+    [Arguments]    ${cmd}
+    ${output}=    Run    ${cmd}
+    [Return]    ${output}
+
+Get Docker Container ID
+    [Documentation]    Retrieves the id of the requested docker container running locally
+    [Arguments]    ${container_name}
+    ${container_id}=    Run    docker ps | grep ${container_name} | awk '{print $1}'
+    Log    ${container_id}
+    [Return]    ${container_id}
+
+Remove Value From List
+    [Documentation]    Removes a value from a dictionary
+    [Arguments]    ${list}    ${val}
+    ${length}=    Get Length    ${list}
+    FOR    ${INDEX}    IN RANGE    0    ${length}
+        Log    ${list[${INDEX}]}
+        ${value}=    Get Dictionary Values    ${list[${INDEX}]}
+        Log    ${value[0]}
+        Run Keyword If    '${value[0]}' == '${val}'    Remove From List    ${list}    ${INDEX}
+        Run Keyword If    '${value[0]}' == '${val}'    Exit For Loop
+    END
+
+Test Ping
+    [Documentation]    SSH's into src and attempts to ping dest.
+    ...    Status determines if ping should pass | fail
+    [Arguments]    ${status}    ${src}    ${user}    ${pass}
+    ...    ${dest}    ${interface}    ${prompt}=$    ${prompt_timeout}=60s
+    ${conn_id}=    SSHLibrary.Open Connection
+    ...    ${src}    prompt=${prompt}    timeout=${prompt_timeout}
+    SSHLibrary.Login    ${user}    ${pass}
+    ${result}=    SSHLibrary.Execute Command
+    ...    ping -I ${interface} -c 5 ${dest}
+    SSHLibrary.Close Connection
+    Log    ${result}
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Contain    ${result}    64 bytes
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Contain    ${result}    0% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    100% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    80% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    60% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    40% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    20% packet loss
+    Run Keyword If    '${status}' == 'PASS'
+    ...    Should Not Contain    ${result}    Destination Host Unreachable
+    Run Keyword If    '${status}' == 'FAIL'
+    ...    Should Not Contain    ${result}    64 bytes
+    Run Keyword If    '${status}' == 'FAIL'
+    ...    Should Contain    ${result}    100% packet loss
+    Log To Console    \n ${result}
+
+Check Ping Result
+    [Documentation]    Check the output of the 'ping' command
+    [Arguments]    ${reachable}    ${result}
+    Run Keyword If    '${reachable}' == 'True'
+    ...    Should Contain    ${result}    64 bytes
+    Run Keyword If    '${reachable}' == 'True'
+    ...    Should Contain Any    ${result}    0% packet loss    0.0% packet loss
+    Run Keyword If    '${reachable}' == 'True'
+    ...    Should Not Contain Any    ${result}    100% packet loss    100.0% packet loss
+    Run Keyword If    '${reachable}' == 'False'
+    ...    Should Not Contain    ${result}    64 bytes
+    Run Keyword If    '${reachable}' == 'False'
+    ...    Should Contain Any    ${result}    100% packet loss    100.0% packet loss
+
+Check Ping
+    [Documentation]    Run 'ping' on remote system and check output
+    [Arguments]    ${ping_should_pass}    ${dst_ip}    ${iface}    ${ip}
+    ...    ${user}    ${pass}=${None}    ${container_type}=${None}    ${container_name}=${None}
+    ${result}=    Login And Run Command On Remote System
+    ...    ping -I ${iface} -c 3 ${dst_ip}
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Check Ping Result    ${ping_should_pass}    ${result}
+
+Check Remote System Reachability
+    [Documentation]    Check if the specified IP address is reachable or not
+    [Arguments]    ${reachable}    ${ip}
+    ${result}=    Run    ping -c 3 ${ip}
+    Check Ping Result    ${reachable}    ${result}
+
+Kill Linux Process
+    [Documentation]    Kill a process on a remote system
+    [Arguments]    ${process}    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}
+    ${rc}=    Login And Run Command On Remote System
+    ...    kill $(ps aux | grep '${process}' | awk '{print $2}'); echo $?
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}
+    Should Be Equal As Integers    ${rc}    0
+
+Check Remote File Contents
+    [Documentation]    Check if file on remote system matches a `grep` regex
+    [Arguments]    ${file_should_exist}    ${file}    ${pattern}
+    ...    ${ip}    ${user}    ${pass}=${None}
+    ...    ${container_type}=${None}    ${container_name}=${None}    ${prompt}=~$
+    ${output}=    Login And Run Command On Remote System
+    ...    cat ${file} | grep '${pattern}'
+    ...    ${ip}    ${user}    ${pass}    ${container_type}    ${container_name}    ${prompt}
+    # FIXME: Comparison against truthy value
+    Run Keyword If    '${file_should_exist}' == 'True'
+    ...    Should Contain    ${output}    ${pattern}
+    ...    ELSE
+    ...    Should Not Contain    ${output}    ${pattern}
+
+Set Deployment Config Variables
+    [Documentation]    Parses through the given deployment config and sets all the "src" and "dst" variables
+    ${source}=    Evaluate    ${hosts}.get("src")
+    ${length_src}=    Get Length    ${source}
+    ${src}=    Set Variable    src
+    FOR    ${INDEX}    IN RANGE    0    ${length_src}
+        Set Suite Variable    ${${src}${INDEX}}    ${source[${INDEX}]}
+    END
+    ${destination}=    Evaluate    ${hosts}.get("dst")
+    ${length_dst}=    Get Length    ${destination}
+    ${dst}=    Set Variable    dst
+    FOR    ${INDEX}    IN RANGE    0    ${length_dst}
+        Set Suite Variable    ${${dst}${INDEX}}    ${destination[${INDEX}]}
+    END
diff --git a/cord-robot/CORDRobot/testCaseUtils.py b/cord-robot/CORDRobot/testCaseUtils.py
new file mode 100755
index 0000000..88f1913
--- /dev/null
+++ b/cord-robot/CORDRobot/testCaseUtils.py
@@ -0,0 +1,244 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# 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.
+#
+
+"""
+Test Case Utils module
+"""
+
+from __future__ import absolute_import
+
+import time
+import subprocess
+import pexpect
+import sys
+
+
+class TestCaseUtils():
+
+    @staticmethod
+    def config_dirs(self, log_dir, root_dir=None, voltha_dir=None):
+        self.dirs["log"] = log_dir
+        self.dirs["root"] = root_dir
+        self.dirs["voltha"] = voltha_dir
+
+    def get_dir(self, directory):
+        return self.dirs.get(directory)
+
+    @staticmethod
+    def remove_leading_line(log_dir, log_file):
+        with open(log_dir + "/" + log_file, "r+") as FILE:
+            lines = FILE.readlines()
+            FILE.seek(0)
+            lines = lines[1:]
+            for line in lines:
+                FILE.write(line)
+            FILE.truncate()
+            FILE.close()
+
+    @staticmethod
+    def write_log_of_voltha_cli_comand(
+        log_dir,
+        log_file1,
+        cmd1,
+        log_file2=None,
+        cmd2=None,
+        log_file3=None,
+        cmd3=None,
+        host="localhost",
+    ):
+        output = open(log_dir + "/" + log_file1, "wb")
+        child = pexpect.spawn(
+            "ssh -p 30110 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no voltha@%s"
+            % host
+        )
+        child.expect(r"[pP]assword:")
+        child.sendline("admin")
+        child.expect(r"\((\x1b\[\d*;?\d+m){1,2}voltha(\x1b\[\d*;?\d+m){1,2}\)")
+        time.sleep(10)
+        child.sendline(cmd1)
+        i = child.expect(
+            [
+                r"\((\x1b\[\d*;?\d+m){1,2}voltha(\x1b\[\d*;?\d+m){1,2}\)",
+                r"\((\x1b\[\d*;?\d+m){1,2}.*device [0-9a-f]{16}(\x1b\[\d*;?\d+m){1,2}\)",
+            ]
+        )
+        if i == 0:
+            output.write(child.before)
+            output.close()
+            TestCaseUtils.remove_leading_line(log_dir, log_file1)
+        elif i == 1:
+            if log_file2 is not None and cmd2 is not None:
+                output = open(log_dir + "/" + log_file2, "wb")
+                child.sendline(cmd2)
+                child.expect(
+                    r"\((\x1b\[\d*;?\d+m){1,2}.*device [0-9a-f]{16}(\x1b\[\d*;?\d+m){1,2}\)"
+                )
+                output.write(child.before)
+                output.close()
+                TestCaseUtils.remove_leading_line(log_dir, log_file2)
+            if log_file3 is not None and cmd3 is not None:
+                output = open(log_dir + "/" + log_file3, "wb")
+                child.sendline(cmd3)
+                child.expect(
+                    r"\((\x1b\[\d*;?\d+m){1,2}.*device [0-9a-f]{16}(\x1b\[\d*;?\d+m){1,2}\)"
+                )
+                output.write(child.before)
+                output.close()
+                TestCaseUtils.remove_leading_line(log_dir, log_file3)
+        child.close()
+
+    @staticmethod
+    def write_log_of_onos_cli_command(log_dir, log_file, cmd, host="localhost", port=30115):
+        output = open(log_dir + "/" + log_file, "wb")
+        child = pexpect.spawn(
+            "ssh -p %s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no karaf@%s"
+            % (port, host)
+        )
+        child.expect(r"[pP]assword:")
+        child.sendline("karaf")
+        # Expected prompt:
+        #  onos>          (ONOS 1.x)
+        #  karaf@root >   (ONOS 2.x)
+        child.expect([r'(\x1b\[\d*;?\d+m){1,2}onos> (\x1b\[\d*;?\d+m){1,2}', r'karaf@root >'])
+        child.sendline(cmd)
+        child.expect([r'(\x1b\[\d*;?\d+m){1,2}onos> (\x1b\[\d*;?\d+m){1,2}', r'karaf@root >'])
+
+        output.write(child.before)
+
+        output.close()
+        child.close()
+
+    def get_fields_from_grep_command(self, search_word, log_file):
+        grepCommand = "grep %s %s/%s" % (search_word, self.get_dir("log"), log_file)
+        statusLines = subprocess.getstatusoutput(grepCommand)[1]
+        return statusLines
+
+    @staticmethod
+    def parse_fields(status_line, delimiter):
+        statusList = status_line.split(delimiter)
+        return statusList
+
+    def print_log_file(self, log_file):
+        with open(self.get_dir("log") + "/" + log_file, "r+") as FILE:
+            lines = FILE.readlines()
+            print
+            for line in lines:
+                sys.stdout.write(line)
+
+    @staticmethod
+    def extract_pod_ip_addr(pod_name):
+        proc1 = subprocess.Popen(
+            ["/usr/bin/kubectl", "get", "svc", "--all-namespaces"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc2 = subprocess.Popen(
+            ["grep", "-e", pod_name],
+            stdin=proc1.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc3 = subprocess.Popen(
+            ["awk", "{print $4}"],
+            stdin=proc2.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+
+        proc1.stdout.close()
+        proc2.stdout.close()
+        out, err = proc3.communicate()
+        return out
+
+    @staticmethod
+    def extract_radius_ip_addr(pod_name):
+        proc1 = subprocess.Popen(
+            ["/usr/bin/kubectl", "describe", "pod", "-n", "voltha", pod_name],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc2 = subprocess.Popen(
+            ["grep", "^IP:"],
+            stdin=proc1.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc3 = subprocess.Popen(
+            ["awk", "{print $2}"],
+            stdin=proc2.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+
+        proc1.stdout.close()
+        proc2.stdout.close()
+        out, err = proc3.communicate()
+        return out
+
+    @staticmethod
+    def extract_pod_name(short_pod_name):
+        proc1 = subprocess.Popen(
+            ["/usr/bin/kubectl", "get", "pods", "--all-namespaces"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc2 = subprocess.Popen(
+            ["grep", "-e", short_pod_name],
+            stdin=proc1.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        proc3 = subprocess.Popen(
+            ["awk", "{print $2}"],
+            stdin=proc2.stdout,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+
+        proc1.stdout.close()
+        proc2.stdout.close()
+        out, err = proc3.communicate()
+        return out
+
+    def modify_radius_ip_in_json_using_sed(self, new_ip_addr):
+        sedCommand = (
+            "sed -i '/radiusIp/c\\      \"radiusIp\":\"'%s'\",' %s/tests/atests/build/aaa_json"
+            % (new_ip_addr, self.get_dir("voltha"))
+        )
+        status = subprocess.getstatusoutput(sedCommand)[0]
+        return status
+
+    @staticmethod
+    def discover_rg_pod_name():
+        return TestCaseUtils.extract_pod_name("rg0").strip()
+
+    @staticmethod
+    def retrieve_authorized_users_device_id_and_port_number(status_line):
+        fields = TestCaseUtils.parse_fields(status_line, ",")
+        deviceField = fields[2].strip()
+        deviceStr, equal, deviceId = deviceField.partition("=")
+        device_Id = deviceId
+        portField = fields[4].strip()
+        portNumStr, equal, portNum = portField.partition("=")
+        portNumber = portNum
+        return device_Id, portNumber
+
+    def add_subscriber_access(self, device_id, port_number):
+        TestCaseUtils.write_log_of_onos_cli_command(
+            self.get_dir("log"),
+            "voltha_add_subscriber_access.log",
+            "volt-add-subscriber-access %s %s" % (device_id, port_number),
+        )
diff --git a/cord-robot/MANIFEST.in b/cord-robot/MANIFEST.in
new file mode 100644
index 0000000..955e985
--- /dev/null
+++ b/cord-robot/MANIFEST.in
@@ -0,0 +1,3 @@
+include requirements.txt
+include CORDRobot/VERSION
+include CORDRobot/rf-resources/*.resource
diff --git a/cord-robot/README.rst b/cord-robot/README.rst
new file mode 100644
index 0000000..4ea4882
--- /dev/null
+++ b/cord-robot/README.rst
@@ -0,0 +1,26 @@
+cord-robot
+----------
+
+This contains both python libraries and resource (Keyword) files for the CORD
+project.
+
+The resource files are imported using:
+https://github.com/rasjani/robotframework-importresource .
+
+To use, import the library and resource files with:
+
+.. code:: robotframework
+
+    Library   CORDRobot
+    Library   ImportResource  resources=CORDRobot
+
+Development notes
+-----------------
+
+Add python libraries to ``src/CORDRobot``,  and include them in the
+``__init__.py``.
+
+Add resource files to the ``src/CORDRobot/rf-resources`` with the extension
+``.resource``
+
+Run ``tox`` to test - see list of test commands run in ``tox.ini``.
diff --git a/cord-robot/requirements.txt b/cord-robot/requirements.txt
new file mode 100644
index 0000000..b068f02
--- /dev/null
+++ b/cord-robot/requirements.txt
@@ -0,0 +1,6 @@
+pexpect
+pyyaml
+requests
+robotframework
+robotframework-requests
+robotframework-sshlibrary
diff --git a/cord-robot/setup.py b/cord-robot/setup.py
new file mode 100644
index 0000000..ce710d9
--- /dev/null
+++ b/cord-robot/setup.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+# Copyright 2020-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.
+
+import os
+from setuptools import setup
+from shutil import copyfile
+
+LIBRARY_NAME = "CORDRobot"
+
+
+def version():
+    # Copy VERSION file of parent to module directory if not found
+    version_path = os.path.join(LIBRARY_NAME, "VERSION")
+    if not os.path.exists(version_path):
+        copyfile("../VERSION", version_path)
+    with open(version_path) as f:
+        return f.read().strip()
+
+
+def parse_requirements(filename):
+    # parse a requirements.txt file, allowing for blank lines and comments
+    requirements = []
+    for line in open(filename):
+        if line and not line.startswith("#"):
+            requirements.append(line)
+    return requirements
+
+
+setup(
+    name="cord-robot",
+    version=version(),
+    description="CORD Project Robot Libraries and common Resources",
+    author="CORD Developers",
+    include_package_data=True,
+    packages=[LIBRARY_NAME],
+    package_data={
+        LIBRARY_NAME: ["rf-resources/*.resource", "VERSION"]
+    },
+    install_requires=parse_requirements("requirements.txt"),
+)
diff --git a/cord-robot/test/test.json b/cord-robot/test/test.json
new file mode 100644
index 0000000..09518ed
--- /dev/null
+++ b/cord-robot/test/test.json
@@ -0,0 +1,3 @@
+{
+  "test" : [ "json1", "json2", "json3"]
+}
diff --git a/cord-robot/test/test.robot b/cord-robot/test/test.robot
new file mode 100644
index 0000000..f537584
--- /dev/null
+++ b/cord-robot/test/test.robot
@@ -0,0 +1,39 @@
+*** Settings ***
+Documentation     Tests for the CORDRobot library
+Library           OperatingSystem
+Library           CORDRobot
+Library           ImportResource    resources=CORDRobot
+
+*** Test Cases ***
+Test list resources
+    [Documentation]    Lists all resources loaded
+    ${res}=    ImportResource.external_resources
+    Log To Console    ${res}
+
+Test loading of CORDRobot Python Functions
+    [Documentation]    Check if __init__.py function work
+    ${ver}=    CR_Version
+    Log To Console    ${ver}
+
+Test loading of testCaseUtils
+    [Documentation]    Check if testCaseUtils.py functions work
+    ${fields}=    CORDRobot.parse_fields    foo,bar    ,
+    Log To Console    ${fields}
+
+Test loading of CORDDictUtils
+    [Documentation]    Check if CORDDictUtils functions work
+    ${json}=    CORDRobot.jsonToList    ${CURDIR}/test.json    test
+    Log To Console    ${json}
+
+Test loading of restApi
+    [Documentation]    Check if restApi functions work
+    ${url1}=    CORDRobot.getURL    CORE_NODES
+    Log To Console    ${url1}
+    Set Environment Variable    CORDROBOT_TEST    /cord_robot_test/
+    ${url2}=    CORDRobot.getURL    CORDROBOT_TEST
+    Log To Console    ${url2}
+
+Test Validate Loading of CORDRobot Resources
+    [Documentation]    Validates that the .resource files distributed by
+    ...    CORDRobot can be invoked
+    Execute Command Locally    echo "Able to run Execute Commnd Locally"
diff --git a/cord-robot/tox.ini b/cord-robot/tox.ini
new file mode 100644
index 0000000..9955929
--- /dev/null
+++ b/cord-robot/tox.ini
@@ -0,0 +1,42 @@
+; 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.
+
+[tox]
+envlist = py35,py36,py37
+skip_missing_interpreters = true
+
+[testenv]
+# remove the git+https link and add to requirements.txt when upstream fixes the pypi package
+deps =
+  -r requirements.txt
+  flake8
+  pylint
+  robotframework-lint
+  git+https://github.com/zdw/robotframework-importresource@b81b87aabaee0594e966687b41e3674b866f28ee
+
+# LineToLong should be much lower
+commands =
+  flake8
+  pylint --py3k CORDRobot
+  rflint \
+     --configure TooFewKeywordSteps:1 \
+     --configure LineTooLong:160 -e LineTooLong \
+     CORDRobot/rf-resources test
+  robot test/test.robot
+
+[flake8]
+exclude =
+  .tox
+  build
+max-line-length = 119