# 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"
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/
#
# 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
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 /
---
-tag: 0.0.1
+tag: 0.0.2
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 \
net-tools \
php \
php-cgi \
- python2.7 \
+ python3-pip \
python-dev \
python-setuptools \
- python-wheel \
- python-pip \
python-redis \
unzip \
vim \
xxd
-RUN pip install robotframework==3.0.4 \
+RUN pip3 install robotframework==3.0.4 \
&& python --version
# Copy the robot code
${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" . }}
#
--- /dev/null
+# 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}
--- /dev/null
+# 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}
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
user: test
password: test
submgr:
+ o1mediator:
+ user: test
+ password: test
+ xapp:
+ name: robot-xapp
+ version: 1.0
xapp:
mcxapp:
listener:
#
# 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
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))
# 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.
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':
{'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)
--- /dev/null
+# 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