IMPL: RICPLT-3081 Automate O1 med flow 45/2945/16
authordave kormann <dk3239@att.com>
Sun, 22 Mar 2020 00:44:36 +0000 (20:44 -0400)
committerdave kormann <dk3239@att.com>
Mon, 4 May 2020 17:24:16 +0000 (13:24 -0400)
This change adds support for testing the O1 Mediator via netconf;
this includes adding netconf-ssh libraries to the robot.

Issue-ID: RICPLT-3081
signed-off-by: dave kormann  <dk3239@att.com>
Change-Id: I33082cc5e668e01516d8163c2f0da8e385b309cc

ric_robot_suite/docker/nanobot/Dockerfile
ric_robot_suite/docker/nanobot/container-tag.yaml
ric_robot_suite/docker/ric-robot/Dockerfile
ric_robot_suite/helm/nanobot/configmap-src/public/properties/global_properties.robot
ric_robot_suite/helm/nanobot/configmap-src/public/resources/mcxapp_interface.robot [new file with mode: 0644]
ric_robot_suite/helm/nanobot/configmap-src/public/testsuites/o1mediator.robot [new file with mode: 0644]
ric_robot_suite/helm/nanobot/values.yaml
ric_robot_suite/ric-python-utils/ricutils/E2SimUtils.py
ric_robot_suite/ric-python-utils/ricutils/KubernetesEntity.py
ric_robot_suite/robot/resources/o1mediator/o1mediator_interface.robot [new file with mode: 0644]

index dcdfed2..f87df67 100644 (file)
@@ -1,5 +1,4 @@
 #   Copyright (c) 2019 AT&T Intellectual Property.
-#   Copyright (c) 2019 Nokia.
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
 #   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 python:3-alpine
 
-MAINTAINER "RIC"
+FROM python:3-alpine AS nanobot-build
+
+RUN apk update && apk add git build-base libffi-dev libxml2 libxslt libxml2-dev libxslt-dev openssl-dev
+
+RUN pip install UUID
+RUN pip install kubernetes
+RUN pip install redis
+RUN pip install asyncio
+RUN pip install websockets
+RUN pip install robotframework
+RUN pip install robotframework-requests
+RUN pip install robotframework-ncclient
+
+WORKDIR /tmp/
+RUN git clone -b 3.0.1-ONAP https://gerrit.onap.org/r/testsuite/python-testing-utils.git
 
+FROM python:3-alpine
+MAINTAINER "RIC"
 LABEL name="Docker image for the RIC Robot Testing Framework"
 
 ENV ROBOT_HOME="/robot"
@@ -26,20 +40,16 @@ ENV RICPLT_NAMESPACE=ricplt
 ENV RICPLT_RELEASE_NAME=ric-full
 ENV RICPLT_COMPONENTS="a1mediator appmgr dbaas e2mgr e2term rtmgr"
 
+RUN apk update && apk add libxslt
+COPY --from=nanobot-build /usr/local/lib/python3.8 /usr/local/lib/python3.8
+COPY --from=nanobot-build /usr/local/bin/robot /usr/local/bin
+
 RUN  mkdir -p /robot/lib/python
 
 # ONAP eteutils
-# we only need a few things from this so we won't install
-# the whole thing.
-# we could do this in a builder image but that seems execssive.
-RUN apk update && apk add git
-WORKDIR /tmp/
-RUN git clone -b 3.0.1-ONAP https://gerrit.onap.org/r/testsuite/python-testing-utils.git
-RUN cp /tmp/python-testing-utils/eteutils/StringTemplater.py /robot/lib/python
-RUN cp /tmp/python-testing-utils/eteutils/UUID.py /robot/lib/python
-RUN rm -rf /tmp/python-testing-utils
-RUN apk del --purge  git
-
+# we only need a few things from this so we won't install the whole thing.
+COPY --from=nanobot-build /tmp/python-testing-utils/eteutils/StringTemplater.py /robot/lib/python
+COPY --from=nanobot-build /tmp/python-testing-utils/eteutils/UUID.py /robot/lib/python
 COPY ric-python-utils/ricutils/*.py /robot/lib/python/
 
 #
@@ -47,12 +57,12 @@ COPY ric-python-utils/ricutils/*.py /robot/lib/python/
 # files/libraries, so we pick and choose what to copy..
 RUN mkdir /robot/resources
 COPY robot/resources/json_templater.robot /robot/resources
-COPY robot/resources/a1mediator /robot/resources/a1mediator
 COPY robot/resources/appmgr /robot/resources/appmgr
 COPY robot/resources/dashboard /robot/resources/dashboard
 COPY robot/resources/e2mgr /robot/resources/e2mgr
 COPY robot/resources/e2sim /robot/resources/e2sim
 COPY robot/resources/e2term /robot/resources/e2term
+COPY robot/resources/o1mediator /robot/resources/o1mediator
 COPY robot/resources/ric /robot/resources/ric
 COPY robot/resources/rnib /robot/resources/rnib
 COPY robot/resources/rtmgr /robot/resources/rtmgr
@@ -64,12 +74,6 @@ COPY robot/resources/global_properties.robot /robot/resources
 RUN mkdir -p /robot/assets/templates
 COPY robot/assets/templates/e2mgr_setup_nodeb.template /robot/assets/templates
 
-RUN pip install UUID
-RUN pip install kubernetes
-RUN pip install redis
-RUN pip install robotframework
-RUN pip install robotframework-requests
-
 RUN python -m compileall /robot/lib/python
 
 WORKDIR /
index ea1b13e..aa8e0a4 100644 (file)
@@ -31,12 +31,16 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 ENV BUILDTIME=true
 
 # Install Python, Pip, Robot framework, chromium, lighttpd web server
+
+# temporarily removed chromium-browser and chromium-chromedriver as
+# they've moved to snaps and we don't have snapd support in this container
+# yet. This will break robot tests that depend on the browser, but we don't
+# have those in the RIC at this point anyway.
+
 RUN apt-get update \
     && apt-get install \
         --no-install-recommends \
         --assume-yes \
-            chromium-browser \
-            chromium-chromedriver \
             dnsutils \
             git \
             gcc \
@@ -47,11 +51,9 @@ RUN apt-get update \
             net-tools \
             php  \
             php-cgi \
-            python2.7 \
+            python3-pip \
             python-dev \
             python-setuptools \
-            python-wheel \
-            python-pip \
             python-redis \
             unzip \
             vim   \
@@ -61,7 +63,7 @@ RUN apt-get update \
             xxd
 
 
-RUN pip install robotframework==3.0.4 \
+RUN pip3 install robotframework==3.0.4 \
     && python --version
 
 # Copy the robot code
index 83e96bf..704112f 100644 (file)
@@ -70,6 +70,16 @@ ${GLOBAL_A1MEDIATOR_POLICY_ID}             {{ default "6266268" .Values.ric.plat
 ${GLOBAL_A1MEDIATOR_TARGET_XAPP}           {{ default $testxapp .Values.ric.platform.components.a1mediator.xappName }}
 {{- end }}
 #
+{{- if .Values.ric.platform.components.o1mediator }}
+${GLOBAL_O1MEDIATOR_HOST}               {{ printf "%s.%s" (include "common.servicename.o1mediator.tcp.netconf" .) $ricplt }}
+${GLOBAL_O1MEDIATOR_PORT}               {{ include "common.serviceport.o1mediator.tcp.netconf" .  }}
+${GLOBAL_O1MEDIATOR_USER}               {{ .Values.ric.platform.components.o1mediator.user }}
+${GLOBAL_O1MEDIATOR_PASSWORD}           {{ .Values.ric.platform.components.o1mediator.password }}
+${GLOBAL_O1MEDIATOR_TARGET_XAPP}        {{ default $testxapp .Values.ric.platform.components.o1mediator.xapp.name }}
+${GLOBAL_O1MEDIATOR_XAPP_VERSION}       {{ default "1.0" .Values.ric.platform.components.o1mediator.xapp.version }}
+${GLOBAL_O1MEDIATOR_DEPLOYMENT_WAIT}    {{ default "180" .Values.ric.platform.components.o1mediator.xapp.wait }}
+{{- end }}
+#
 ${GLOBAL_INJECTED_DBAAS_IP_ADDR}      {{ printf "%s.%s" (include "common.servicename.dbaas.tcp" .) $ricplt  }}
 ${GLOBAL_DBAAS_SERVER_PORT}           {{ include "common.serviceport.dbaas.tcp" .  }}
 #
diff --git a/ric_robot_suite/helm/nanobot/configmap-src/public/resources/mcxapp_interface.robot b/ric_robot_suite/helm/nanobot/configmap-src/public/resources/mcxapp_interface.robot
new file mode 100644 (file)
index 0000000..3194aad
--- /dev/null
@@ -0,0 +1,64 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#
+#   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  Tools for interacting with the MC XApp
+
+Resource       /robot/resources/global_properties.robot
+Resource       /robot/resources/mcxapp_properties.robot
+
+Library        Collections
+Library        String
+Library        KubernetesEntity       ${GLOBAL_XAPP_NAMESPACE}
+
+*** Variables ***
+
+${listenerStatSubstring}              (mcl) mtype=
+${listenerStatRegex}                  ^\\s*([0-9]+)\\s+\\[STAT\\]\\s+\\(mcl\\)\\s+mtype=([^\\s]+)\\s+total\\s+writes=([0-9]+)\\s+total\\s+drops=([0-9]+);.*writes=([0-9]+)\\s+drops=([0-9]+)
+
+*** Keywords ***
+Retrieve Listener Message Counts
+  ${pods} =       Retrieve Pods For Deployment  ${MCDeployment}
+  # for now, i'm just going to completely ignore the possibility
+  # of multiple MC XApp pods.  that seems safe, i think.
+  ${pod} =        Get From List         ${pods}  0
+  ${log} =        Retrieve Log For Pod  ${pod}   tail=500
+  ${statLogs} =   Get Matches           ${log}   glob=*${listenerStatSubstring}*
+  ${stats} =      Parse Listener Statistics      ${statLogs}
+  [Return]        ${stats}
+
+Parse Listener Statistics
+  [Arguments]  ${logLines}
+  # while it's almost certainly safe to assume the log
+  # lines are sorted by timestamp, it's not something i'm
+  # going to take for granted.
+  ${stats} =         Create Dictionary
+  :FOR  ${statLine}  IN  @{logLines}
+  \  ${match}  ${ts}  ${mtype}  ${tWrites}  ${tDrops}  ${rWrites}  ${rDrops} =
+  ...  Should Match Regexp   ${statLine}  ${listenerStatRegex}
+  \  ${stat} =        Create Dictionary
+  ...                 timestamp=${ts}
+  ...                 totalWrites=${tWrites}
+  ...                 totalDrops=${tDrops}
+  ...                 recentWrites=${rWrites}
+  ...                 recentDrops=${rDrops}
+  \  ${s}  ${d} =     Run Keyword And Ignore Error
+  ...                 Get From Dictionary  ${stats}  ${mtype}
+  \  ${prevTS} =      Run Keyword If       "${s}" == "PASS"
+  ...                 Get From Dictionary  ${d}  timestamp
+  ...  ELSE
+  ...                 Set Variable         -1
+  \  Run Keyword If   ${ts} > ${prevTS}
+  ...                 Set To Dictionary  ${stats}  ${mtype}  ${stat}
+  [Return]     ${stats}
diff --git a/ric_robot_suite/helm/nanobot/configmap-src/public/testsuites/o1mediator.robot b/ric_robot_suite/helm/nanobot/configmap-src/public/testsuites/o1mediator.robot
new file mode 100644 (file)
index 0000000..35a5b09
--- /dev/null
@@ -0,0 +1,92 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#
+#   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  Tests for the UE Event Collector XApp
+
+Resource       /robot/resources/global_properties.robot
+
+Resource       /robot/resources/o1mediator/o1mediator_interface.robot
+Resource       /robot/resources/ric/ric_utils.robot
+
+Library        String
+Library        Collections
+Library        XML
+
+Library        KubernetesEntity  ${GLOBAL_RICPLT_NAMESPACE}
+
+*** Variables ***
+${sessionPfx} =   nanobot-O1
+
+*** Test Cases ***
+O1 Mediator Should Be Available
+  [Tags]   etetests  o1mediatortests
+  ${o1} =        Get From Dictionary  ${GLOBAL_RICPLT_COMPONENTS}  o1mediator
+  ${deploy} =    Deployment           ${o1}
+  ${status} =    Most Recent Availability Condition  @{deploy.status.conditions}
+  Should Be Equal As Strings          ${status}  True  ignore_case=True
+
+Connect To O1 Mediator
+   [tags]  etetests  o1mediatortests
+   ${sessionGensym} =    Generate Random String
+   ${O1Session} =        Set Variable  ${sessionPfx}-${sessionGensym}
+   Set Suite Variable    ${O1Session}
+   Set Suite Variable    ${O1Session}
+   ${status} =           Establish O1 Session
+   ...                   ${GLOBAL_O1MEDIATOR_USER}
+   ...                   ${GLOBAL_O1MEDIATOR_PASSWORD}
+   ...                   ${O1Session}
+   ...                   ${GLOBAL_O1MEDIATOR_HOST}
+   ...                   ${GLOBAL_O1MEDIATOR_PORT}
+   Should Be True        ${status}
+
+Get O1 State
+   [tags]  etetests  o1mediatortests
+   ${conf} =         Retrieve O1 State  ${O1Session}
+   # just going to let this bail at a lower layer if
+   # the Get fails.  Might be better to look for ric stuff
+   # in the active modules, though.
+   ${confXML} =      Element To String  ${conf}
+   
+Deploy XApp Via O1
+   [tags]  etetests  o1mediatortests  intrusive
+   Deploy An XApp Using O1  ${O1Session}
+   ...                      ${GLOBAL_O1MEDIATOR_TARGET_XAPP}
+   ...                      ${GLOBAL_O1MEDIATOR_XAPP_VERSION}
+
+XApp Should Be Running
+   [tags]  etetests  o1mediatortests
+   Wait For Deployment      ${GLOBAL_XAPP_NAMESPACE}-${GLOBAL_O1MEDIATOR_TARGET_XAPP}
+   ...                      timeout=${GLOBAL_O1MEDIATOR_DEPLOYMENT_WAIT}
+   ...                      namespace=${GLOBAL_XAPP_NAMESPACE}
+
+Undeploy XApp Via O1
+   [tags]  etetests  o1mediatortests  intrusive
+   Remove An XApp Using O1  ${O1Session}
+   ...                      ${GLOBAL_O1MEDIATOR_TARGET_XAPP}
+   ...                      ${GLOBAL_O1MEDIATOR_XAPP_VERSION}
+
+XApp Should Not Be Running
+   [tags]  etetests  o1mediatortests
+   ${status}  ${deploy} =      Run Keyword And Ignore Error
+   ...                         Deployment  ${GLOBAL_XAPP_NAMESPACE}-${GLOBAL_O1MEDIATOR_TARGET_XAPP}
+   ${status} =                 Run Keyword If  '${status}' == 'PASS'
+   ...                         Most Recent Availablity Condition  @{deploy.status.conditions}
+   ...  ELSE
+   ...                         Set Variable   False
+   Should Be Equal As Strings  '${status}'  'False'
+   
+Disconnect From O1
+   [tags]  etetests  o1mediatortests
+   Close O1 Session  ${O1Session}
index 78f622e..c2a224c 100644 (file)
@@ -29,7 +29,7 @@ images:
     run:
      repository: nexus3.o-ran-sc.org:10004
      name: o-ran-sc/it-test-nanobot
-     tag: 0.0.1
+     tag: 0.0.2
      # note: the helm chart does not create repository credential secrets.
      # If your repository requires authentication, create a docker-registry 
      # secret with
@@ -69,6 +69,12 @@ ric:
     user: test
     password: test
    submgr:
+   o1mediator:
+    user: test
+    password: test
+    xapp:
+     name: robot-xapp
+     version: 1.0
  xapp:
   mcxapp:
    listener:
@@ -92,7 +98,7 @@ ric:
   #
   # If specified, a host filesystem
   # path where robot output will be stored 
-  log: /opt/ric/robot/log
+  log: /opt/ric/robot/log
   #
   # Active testsuites can be chosen by
   # tag, testuite names, or both.  leaving
index d5f3fa1..409ad65 100644 (file)
@@ -56,3 +56,22 @@ class E2SimUtils(object):
  def randomRANName(self, prefix=""):
   prefix = prefix + ''.join(random.choice(string.ascii_uppercase) for _ in range(4-(min(4, len(prefix)))))
   return prefix[0:4].upper() + ''.join(random.choice(string.digits) for _ in range(6))
+
+ def TranslategNodeBID(self, prefix, plmn, bits):
+  # given a gNodeB type prefix, a plmn (as a string, no spaces), and a bitstring
+  # return a gNodeB ID
+  plmn = ''.join(filter(lambda c: c in list(string.hexdigits), plmn))
+  bits = ''.join(filter(lambda c: c in ['0', '1'], bits))
+  mnc3 = (int(plmn[2:4],16) & 0xf0) >> 4
+  if mnc3 == 15:
+    return '%s:%d%d%d-0%d%d-%x' % \
+     (prefix,\
+      int(plmn[0:2],16) & 0xf, (int(plmn[0:2],16) & 0xf0) >> 4, int(plmn[2:4],16) & 0xf,\
+      int(plmn[4:6],16) & 0x0f, (int(plmn[4:6],16) & 0xf0) >> 4, \
+      int((bits + "0" * (len(bits)%4)),2))
+  else:
+    return '%s:%d%d%d-%d%d%d-%x' % \
+     (prefix,\
+      int(plmn[0:2],16) & 0xf, (int(plmn[0:2],16) & 0xf0) >> 4, int(plmn[2:4],16) & 0xf,\
+      int(plmn[4:6],16) & 0x0f, (int(plmn[4:6],16) & 0xf0) >> 4, mnc3,\
+      int((bits + "0" * (len(bits)%4)),2))
index 8944167..f4583bc 100644 (file)
 #   limitations under the License.
 
 from kubernetes import client, config
+import sys
 import string
 import random
 import time
-import sys
+import ssl
+import asyncio
+import websockets
+import urllib.parse
 
 # This library provides a massively-simplified interface to the kubernetes
 # API library to reduce bloat in robot tests.
@@ -34,30 +38,30 @@ class KubernetesEntity(object):
   self._k8sCore = client.CoreV1Api()
   self._k8sEV1B1 = client.ExtensionsV1beta1Api()
 
- def Deployment(self, name):
+ def Deployment(self, name, namespace=None):
   # this will throw kubernetes.client.rest.ApiException if
   # the deployment doesn't exist.  we'll let robot cope with
   # that.
 
   # calling code will most likely want to check that
   # deploy.status.replicas == deploy.status.available_replicas
-  return self._k8sApp.read_namespaced_deployment(namespace=self._ns,
+  return self._k8sApp.read_namespaced_deployment(namespace=namespace or self._ns,
                                                  name=name)
 
- def Service(self, name):
+ def Service(self, name, namespace=None):
   # as above, we'll rely on this to throw if the svc dne.
 
   # not much to check directly here.  calling code will want
   # to hit svc.spec.cluster_ip:r.spec.ports[0..n] with some
   # sort of health-check request
-  return self._k8sCore.read_namespaced_service(namespace=self._ns,
+  return self._k8sCore.read_namespaced_service(namespace=namespace or self._ns,
                                                name=name)
 
- def Pod(self, name):
-  return self._k8sCore.read_namespaced_pod(namespace=self._ns,
+ def Pod(self, name, namepsace=None):
+  return self._k8sCore.read_namespaced_pod(namespace=namespace or self._ns,
                                            name=name)
 
- def Redeploy(self, name, wait=True, timeout=30):
+ def Redeploy(self, name, wait=True, timeout=30, namespace=None):
   # restart an existing deployment by doing a nonsense update
   # to its spec.
   body = {'spec':
@@ -66,36 +70,72 @@ class KubernetesEntity(object):
             {'annotations':
              { self._annotationGensym: str(time.time()) }}}}}
 
-  r = self._k8sEV1B1.patch_namespaced_deployment(namespace=self._ns,
+  r = self._k8sEV1B1.patch_namespaced_deployment(namespace=namespace or self._ns,
                                                  name=name,
                                                  body=body)
   if wait:
-   r = self.WaitForDeployment(name, timeout)
+   r = self.WaitForDeployment(name, timeout, namespace=namespace or self._ns)
   return r
 
- def WaitForDeployment(self, name, timeout=30):
+ def WaitForDeployment(self, name, timeout=30, namespace=None):
   # block until a deployment is available
   while timeout > 0:
-   dep = self.Deployment(name)
+   dep = self.Deployment(name, namespace=namespace or self._ns)
    if dep and dep.status.conditions[-1].type == 'Available':
     return True
    time.sleep(1)
    timeout -= 1
   raise TimeoutError('Kubernetes timeout waiting for ' + name + ' to become available')
 
- def RetrievePodsForDeployment(self, name):
+ def RetrievePodsForDeployment(self, name, namespace=None):
   # return the pod names associated with a deployment
-  d = self.Deployment(name)
+  d = self.Deployment(name, namespace or self._ns)
   labels = d.spec.selector.match_labels
-  pods = self._k8sCore.list_namespaced_pod(self._ns,
+  pods = self._k8sCore.list_namespaced_pod(namespace or self._ns,
                                            label_selector=",".join(map(lambda k: k + "=" + labels[k], 
                                                                        labels)))
-  return map(lambda i: i.metadata.name, pods.items)
- def RetrieveLogForPod(self, pod, container='', tail=sys.maxsize):
+  return list(map(lambda i: i.metadata.name, pods.items))
+
+ def RetrieveLogForPod(self, pod, container='', tail=sys.maxsize, namespace=None):
   # not really an "entity" thing per se, but.
   # kinda want to include timestamps, but i don't have a use case for them.
-  return self._k8sCore.read_namespaced_pod_log(namespace=self._ns,
+  return self._k8sCore.read_namespaced_pod_log(namespace=namespace or self._ns,
                                                name=pod,
                                                container=container,
                                                tail_lines=tail).split('\n')[0:-1]
+
+ def ExecuteCommandInPod(self, pod, cmd, strip_newlines=True, namespace=None):
+   # near as i can tell, the python k8s client doesn't implement
+   # 'kubectl exec'.  this is near enough for our purposes.
+   # 'cmd' is an argv list.
+   channels={1: 'stdout', 2: 'stderr', 3: 'k8s'}
+   output={'stdout': [], 'stderr': [], 'k8s': []}
+   path='/api/v1/namespaces/%s/pods/%s/exec?%s&stdin=false&stderr=true&stdout=true&tty=false' % \
+        (namespace or self._ns, pod, urllib.parse.urlencode({'command': cmd}, doseq=True))
+   # we could probably cache and reuse the sslcontext, but meh, we're not
+   # after performance here.
+   ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+   c = client.Configuration()
+   
+   async def ExecCoroutine():
+      # base64.channel.k8s.io is also a valid subprotocol, but i don't see any
+      # reason to support it.
+      async with websockets.connect(uri,\
+                                    ssl=ctx,\
+                                    subprotocols=["channel.k8s.io"],\
+                                    extra_headers=c.api_key) as ws:
+        async for message in ws:
+           if message[0] in channels and (not strip_newlines or len(message) > 1):
+             # we probably should throw up if we get an unrecognized channel, but
+             # i really don't want to be bothered with asyncio exception handling
+             # for that vanishingly improbable case.
+             output[channels[message[0]]].extend(message[1:-1].decode('utf-8').split('\n'))
+            
+   ctx.load_verify_locations(c.ssl_ca_cert)
+   if(c.cert_file and c.key_file):
+     ctx.load_cert_chain(c.cert_file, c.key_file)
+   uri = 'wss://%s%s' % (c.host.lstrip('https://'), path)
+
+   asyncio.get_event_loop().run_until_complete(ExecCoroutine())
+  
+   return(output)
diff --git a/ric_robot_suite/robot/resources/o1mediator/o1mediator_interface.robot b/ric_robot_suite/robot/resources/o1mediator/o1mediator_interface.robot
new file mode 100644 (file)
index 0000000..f869d95
--- /dev/null
@@ -0,0 +1,89 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#
+#   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 ***
+Library  XML              use_lxml=True
+Library  NcclientLibrary
+
+*** Variables ***
+${XAppNS}                 urn:o-ran:ric:xapp-desc:1.0
+${NetconfNS}              urn:ietf:params:xml:ns:netconf:base:1.0
+${ricXML} =               <ric xmlns="${XAppNS}"></ric>
+${configXML}              <config xmlns="${NetconfNS}">${ricXML}</config>
+
+
+*** Keywords ***
+Establish O1 Session
+  [Arguments]  ${user}
+  ...          ${password}
+  ...          ${session}
+  ...          ${host}=service-ricplt-o1mediator-tcp-netconf.ricplt
+  ...          ${port}=830
+  ...          ${hostkey_verify}=${False}
+  ...          ${key}=/dev/null
+  ${status} =  Connect      host=${host}
+  ...           port=${port}
+  ...           username=${user}
+  ...           password=${password}
+  ...           key_filename=${key}
+  ...           look_for_keys=False
+  ...           alias=${session}
+  [Return]     ${status}
+
+Retrieve O1 State
+  [Arguments]     ${session}
+  # this doesn't actually seem to result in filtered XML,
+  # but it matches what the O1 CLI does.
+  ${filter} =     Parse XML   ${ricXML}
+  ${config} =     Get         ${session}  filter_criteria=${filter}
+  [Return]        ${config}
+
+Retrieve O1 Running Configuration
+  [Arguments]     ${session}
+  ${config} =     Get Config  ${session}  running
+  [Return]        ${config}
+
+Deploy An XApp Using O1
+  [Arguments]         ${session}   ${app}   ${version}
+  ${xappCreateXML} =  Generate XApp Deployment XML  ${app}  ${version}  create
+  Edit Config         ${session}   running  ${xappCreateXML}
+
+Remove An XApp Using O1
+  [Arguments]         ${session}   ${app}   ${version}
+  ${xappDeleteXML} =  Generate XApp Deployment XML  ${app}  ${version}  delete
+  Edit Config         ${session}   running  ${xappDeleteXML}
+  
+Close O1 Session
+  [Arguments]         ${session}
+  Close Session       ${session}
+  
+*** Keywords ***
+Generate XApp Deployment XML
+  [Arguments]  ${name}    ${version}  ${operation}
+  ${XML} =     Parse XML  ${configXML}
+  Add Element  ${XML}
+  ...                     <xapps xmlns="${XAppNS}"></xapps>
+  ...                     xpath=ric
+  Add Element  ${XML}     <xapp xmlns:xc="${NetconfNS}" xc:operation="${operation}"></xapp>
+  ...                     xpath=ric/xapps
+  Add Element  ${XML}
+  ...                     <name>${name}</name>
+  ...                     xpath=ric/xapps/xapp
+  Add Element  ${XML}     <release-name>xapp-${name}</release-name>
+  ...                     xpath=ric/xapps/xapp
+  Add Element  ${XML}     <version>${version}</version>
+  ...                     xpath=ric/xapps/xapp
+  Add Element  ${XML}     <namespace>${GLOBAL_XAPP_NAMESPACE}</namespace>
+  ...                     xpath=ric/xapps/xapp
+  [Return]                ${XML}
\ No newline at end of file