--- /dev/null
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# Pycharm project config
+.idea/
+
+# VIM swap file
+*.swp
+
+# Mac OS
+.DS_Store
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied. See the Documentation License for the specific language governing
permissions and limitations under the Documentation License.
-
+++ /dev/null
-#!/bin/bash
-##############################################################################
-#
-# 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.
-#
-##############################################################################
-
-# Installs well-known RIC charts then verifies specified helm chart
-# Requires chart tgz archives in /tmp
-
-
-while [ -n "$1" ]; do # while loop starts
-
- case "$1" in
-
- -d) DOCKER_REGISTRY=$2
- shift
- ;;
-
- -p) CHART_DIRECTORY_PATH=$2
- shift
- ;;
-
- -h) HELM_REPO=$2
- shift
- ;;
-
- *) echo "Option $1 not recognized. Please specify the docker registry path with the -d option and specify the xApp directory path with the -p option."
- ;; # In case you typed a different option other than -d or -p
-
- esac
-
- shift
-
-done
-
-if [ -z "$DOCKER_REGISTRY" ]; then
- echo "Please specify the docker registry path with the -d option."
- exit 1
-fi
-
-if [ -z "$CHART_DIRECTORY_PATH" ]; then
- echo "Please specify the xApp directory path with the -p option."
- exit 1
-fi
-
-if [ -z "$HELM_REPO" ]; then
- echo "Please specify the helm repo uploading URL with the -h option."
- exit 1
-fi
-
-
-echo "************************************************************"
-
-for image in $CHART_DIRECTORY_PATH/docker_images/*/*; do
- echo "Loading image $image"
- OUTPUT=$(docker load -i $image)
- IMAGE_PATH_ORIGINAL=$(echo $OUTPUT | grep image: | awk '{print $3}' )
- IMAGENAME=$(echo $IMAGE_PATH_ORIGINAL | awk '{ n=split($0, a, "/"); print a[n] }')
- echo "Pushing image $DOCKER_REGISTRY/$IMAGENAME"
- docker tag $IMAGE_PATH_ORIGINAL $DOCKER_REGISTRY/$IMAGENAME
- docker push $DOCKER_REGISTRY/$IMAGENAME || { echo "Failed to push the docker image." ; exit 1; }
-
-done
-
-rm -rf $CHART_DIRECTORY_PATH/chart_packages
-mkdir -p $CHART_DIRECTORY_PATH/chart_packages
-
-echo "************************************************************"
-for chart in $CHART_DIRECTORY_PATH/helm_charts/*; do
- echo "Onboard Helm Charts"
-
- sed -i "s/^ repository: .*/ repository: $DOCKER_REGISTRY/" $chart/values.yaml
- helm package $chart -d $CHART_DIRECTORY_PATH/chart_packages
-
-done
-
-
-
-echo "************************************************************"
-for tarball in $CHART_DIRECTORY_PATH/chart_packages/*; do
- TARBALL_FILE_NAME=$(echo $tarball | awk -F '/' '{print $NF}')
- CHART_NAME=$(echo $TARBALL_FILE_NAME | awk -F '-' '{gsub($NF, ""); print substr($0,1,length($0)-1)}')
- CHART_VERSION=$(echo $TARBALL_FILE_NAME | awk -F '-' '{print substr ($NF,1,length($NF)-4)}')
-
- DELETE_MESSAGE=$(curl -X DELETE --connect-timeout 2 $HELM_REPO/api/charts/$CHART_NAME/$CHART_VERSION 2>/dev/null)
- LOCATE_CHART=$(curl --connect-timeout 2 $HELM_REPO/index.yaml |& grep $TARBALL_FILE_NAME)
-
- if [ ! -z "$LOCATE_CHART" ]; then
- echo "ERROR: Helm chart delete fail."
- echo $DELETE_MESSAGE
- exit 1
- fi
-
- UPLOAD_MESSAGE=$(curl --data-binary "@$tarball" --connect-timeout 2 $HELM_REPO/api/charts 2>/dev/null)
- LOCATE_CHART=$(curl --connect-timeout 2 $HELM_REPO/index.yaml |& grep $TARBALL_FILE_NAME )
-
- if [ -z "$LOCATE_CHART" ]; then
- echo "ERROR: Helm chart upload failed."
- echo $UPLOAD_MESSAGE
- exit 1
- fi
-
- echo "Helm chart $CHART_NAME version $CHART_VERSION uploaded successfully."
-
-done
+++ /dev/null
-#!/bin/bash
-##############################################################################
-#
-# 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.
-#
-##############################################################################
-
-# Installs well-known RIC charts then verifies specified helm chart
-# Requires chart tgz archives in /tmp
-
-
-while [ -n "$1" ]; do # while loop starts
-
- case "$1" in
-
- -f) XAPPLISTFILE=$2
- shift
- ;;
-
- -d) CHART_DIRECTORY_PATH=$2
- shift
- ;;
-
-
- *) echo "Option $1 not recognized. Please use -f to specify the recipe path." ;; # In case you typed a different option other than a,b,c
-
- esac
-
- shift
-
-done
-
-if [ -z "$XAPPLISTFILE" ]; then
- echo "xApp list file is missing. Please use -f to specify the path."
- exit 1
-fi
-
-if [ -z "$CHART_DIRECTORY_PATH" ]; then
- CHART_DIRECTORY_PATH=/tmp/xapp_charts
-fi
-
-
-rm -rf $CHART_DIRECTORY_PATH
-mkdir -p $CHART_DIRECTORY_PATH/helm_charts
-mkdir -p $CHART_DIRECTORY_PATH/docker_images
-
-
-while IFS= read -r chart
-do
- CHARTNAME=$(echo $chart | awk '{ n=split($0, a, "/"); split(a[n],b,":"); print b[1] }')
- CHARTVERSION=$(echo $chart | awk '{ n=split($0, a, "/"); split(a[n],b,":"); print b[2] }')
- HELM_REPO=$(echo $chart | awk -F'/' '{gsub($NF,""); print substr($0,1,length($0)-1)}')
-
- echo "Fetching helm charts $CHARTNAME version $CHARTVERSION from repo $HELM_REPO"
- helm repo add temp $HELM_REPO > /dev/null
- helm fetch temp/$CHARTNAME --version $CHARTVERSION -d $CHART_DIRECTORY_PATH/helm_charts --untar
- helm repo remove temp > /dev/null
-
-
-done < "$XAPPLISTFILE"
-
-echo "************************************************************"
-for chart in $CHART_DIRECTORY_PATH/helm_charts/*; do
-
-
- CHART_NAME=$(echo $chart | awk -F '/' '{print $NF}')
- mkdir -p $CHART_DIRECTORY_PATH/docker_images/$CHART_NAME
-
- IMAGE_ARRAY=$(helm template $chart | grep "image:" | awk '{ gsub(/.*image: /, "", $0); gsub(/"/, "", $0); print $0}' )
-
- while read -r image; do
-
- IMAGENAME=$(echo $image | awk '{ n=split($0, a, "/"); print a[n] }')
- echo "Pulling image $image"
- RESULT=$(docker pull $image |& grep "no basic auth credentials" )
- if [ ! -z "$RESULT" ]; then
- echo "You are not logined to docker registry. Please login by running \"docker login DOCKER_REGISTRY\""
- exit 1
- fi
-
- echo "Saving image $image"
- docker save $image -o $CHART_DIRECTORY_PATH/docker_images/$CHART_NAME/$IMAGENAME
-
-
-
-
- done <<< "$IMAGE_ARRAY"
-
-
-done
-
-
-
-
-echo "************************************************************"
-echo "xApp helm charts are downloaded to: $CHART_DIRECTORY_PATH"
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+FROM python:3.7-alpine
+
+ADD ./xapp_onboarder /opt/xapp_onboarder
+
+RUN cd /opt/xapp_onboarder && pip3 install . && ln -s $(pip show xapp_onboarder | grep Location | awk '{printf $2 "/xapp_onboarder/xapp_onboarder"}') /usr/local/bin/xapp_onboarder && ln -s $(pip show xapp_onboarder | grep Location | awk '{printf $2 "/xapp_onboarder/cli"}') /usr/local/bin/cli && rm -r /opt/xapp_onboarder
+
+ARG HELM_VERSION="2.12.3"
+
+RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && tar -xvf helm-v${HELM_VERSION}-linux-amd64.tar.gz && mv linux-amd64/helm /usr/local/bin/helm && rm -r linux-amd64 && rm helm-v${HELM_VERSION}-linux-amd64.tar.gz
+
+ENTRYPOINT ["xapp_onboarder"]
--- /dev/null
+tag: 1.0.0
+++ /dev/null
-#!/bin/bash
-################################################################################
-# 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. #
-################################################################################
-
-
-while [ -n "$1" ]; do # while loop starts
-
- case "$1" in
-
- -n)
- CHART_NAME=$2
- shift
- ;;
-
- -v) CHART_VERSION=$2
- shift
- ;; # Message for -b option
-
- -f) OVERRIDEYAML=$2
- shift
- ;; # Message for -c option
-
- -i) FULLIMAGE=$2
- shift
- ;;
-
- -d) DESCRIPTOR_PATH=$2
- shift
- ;;
-
- -c) CONFIG_JSON_PATH=$2
- shift
- ;;
-
- -h) HELM_REPO_USERNAME=$2
- shift
- ;;
-
- -p) HELM_REPO_PASSWORD=$2
- shift
- ;;
-
- *) echo "Option $1 not recognized" ;; # In case you typed a different option other than a,b,c
-
- esac
-
- shift
-
-done
-
-
-
-
-if [ -z $CHART_NAME ]; then
- echo "Please specify chart name using -n option."
- exit 1
-fi
-if [ -z $CHART_VERSION ]; then
- echo "Please specify chart version using -v option."
- exit 1
-fi
-if [ -z $FULLIMAGE ]; then
- echo "Please specify image using -i option."
- exit 1
-fi
-if [ -z $DESCRIPTOR_PATH ]; then
- echo "Please specify descriptor file using -d option."
- exit 1
-fi
-if [ -z $CONFIG_JSON_PATH ]; then
- echo "Please specify config json file using -c option."
- exit 1
-fi
-
-
-if [ ! -f $DESCRIPTOR_PATH ]; then
- echo "Descriptor file cannot be founded at $DESCRIPTOR_PATH"
- exit 1
-fi
-if [ ! -f $CONFIG_JSON_PATH ]; then
- echo "Config json file cannot be founded at $CONFIG_JSON_PATH"
- exit 1
-fi
-
-
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-
-
-source $DIR/../etc/xapp.conf
-
-if [ -z $OVERRIDEYAML ]; then
- HELM_REPO=$default_helm_repo
- DOCKER_REGISTRY=$default_docker_registry
-else
- helm_repo_override=$(grep "^ *helmRepository:" $OVERRIDEYAML | awk '{gsub(/ /,""); gsub(/\"/,""); split($0, b, "tory:");split(b[2],c,"#"); print c[1]}')
- docker_reg_override=$(grep "^ *repository:" $OVERRIDEYAML | awk '{ gsub(/ /,""); gsub(/\"/,""); split($0, b, "tory:");split(b[2],c,"#"); print c[1]}')
- if [ -z $helm_repo_override ]; then
- HELM_REPO=$default_helm_repo
- else
- HELM_REPO=$helm_repo_override
- fi
-
- if [ -z $docker_reg_override ]; then
- DOCKER_REGISTRY=$default_docker_registry
- else
- DOCKER_REGISTRY=$docker_reg_override
- fi
-fi
-
-
-
-
-rm -rf /tmp/$CHART_NAME
-
-cp -r $DIR/../helm/xapp-std/ /tmp/$CHART_NAME
-
-
-
-sed -i "s/^name: xapp-std/name: $CHART_NAME/" /tmp/$CHART_NAME/Chart.yaml
-sed -i "s/^version: 0.0.1/version: $CHART_VERSION/" /tmp/$CHART_NAME/Chart.yaml
-
-
-registry_path=$(echo $FULLIMAGE | awk '{n=split($0, a, "/"); if(n>1) print a[1]}')
-
-
-
-tag=$(echo $FULLIMAGE | awk '{n=split($0, a, "/"); split(a[n], b, ":"); print b[2]}')
-
-image=$(echo $FULLIMAGE | awk -v head="$registry_path/" -v tail=":$tag" '{gsub (head, ""); gsub(tail,""); gsub(/\//,"\\/"); print $0}')
-
-
-sed -i "s/^ name: xapp-std/ name: $CHART_NAME/" /tmp/$CHART_NAME/values.yaml
-sed -i "s/^ name: xapp-std/ name: $image/" /tmp/$CHART_NAME/values.yaml
-sed -i "s/^ tag: latest/ tag: $tag/" /tmp/$CHART_NAME/values.yaml
-
-
-if [ -z $registry_path ]; then
- sed -i "s/^ repository: xapp-std-reg/ repository: $DOCKER_REGISTRY/" /tmp/$CHART_NAME/values.yaml
-else
- sed -i "s/^ repository: xapp-std-reg/ repository: $registry_path/" /tmp/$CHART_NAME/values.yaml
-fi
-
-
-mkdir /tmp/$CHART_NAME/config/
-mkdir /tmp/$CHART_NAME/descriptors/
-
-cp $CONFIG_JSON_PATH /tmp/$CHART_NAME/config/
-cp $DESCRIPTOR_PATH /tmp/$CHART_NAME/descriptors/
-
-
-helm package -d /tmp /tmp/$CHART_NAME
-
-
-echo $HELM_REPO
-#curl -k -u $HELM_REPO_USERNAME:$HELM_REPO_PASSWORD $HELM_REPO --upload-file /tmp/$CHART_NAME-$CHART_VERSION.tgz -v
-curl -Lk -u $HELM_REPO_USERNAME:$HELM_REPO_PASSWORD "$HELM_REPO"/api/charts --data-binary "@/tmp/$CHART_NAME-$CHART_VERSION.tgz"
+++ /dev/null
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
-.vscode/
+++ /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. #
-################################################################################'
-
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: {{ include "ricxapp.configmapname" . }}-appconfig
-data:
-{{- $dbaasservice := .Values.ricplt.dbaasService | quote -}}
-{{- $pltingressurl := .Values.ricplt.pltIngressUrl | quote -}}
-{{- $xappingressurl := .Values.ricplt.xappIngressUrl | quote -}}
-{{- $appmgrrmrservice := .Values.ricplt.appmgrRMRService | quote -}}
-{{- $e2mgrrmrservice := .Values.ricplt.e2mgrRMRService | quote -}}
-{{- $e2termrmrservice := .Values.ricplt.e2termRMRService | quote -}}
-{{- $rtmgrrmrservice := .Values.ricplt.rtmgrRMRService | quote -}}
-{{- $a1mediatorrmrservice := .Values.ricplt.a1mediatorRMRService | quote -}}
-
-{{- (.Files.Glob "config/*").AsConfig | replace "__DBAAS_SERVICE__" $dbaasservice | replace "__PLT_INGRESS_URL__" $pltingressurl | replace "__XAPP_INGRESS_URL__" $xappingressurl | replace "__APPMGR_RMR_SERVICE__" $appmgrrmrservice | replace "__E2MGR_RMR_SERVICE__" $e2mgrrmrservice | replace "__E2TERM_RMR_SERVICE__" $e2termrmrservice | replace "__RTMGR_RMR_SERVICE__" $rtmgrrmrservice | replace "__A1MEDIATOR_RMR_SERVICE__" $a1mediatorrmrservice | nindent 2 }}
+++ /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. #
-################################################################################'
-# This is a YAML-formatted file.
-# Declare variables to be passed into your templates.
-ricplt:
- # This section is reserved for values imported from RIC Platform charts
- dbaasService: "service-ricplt-dbaas-tcp.ricplt.svc.cluster.local"
- pltIngressUrl: "ricplt-entry"
- xappIngressUrl: "ricxapp-entry"
-
- appmgrRMRService: "service-ricplt-appmgr-rmr.ricplt.svc.cluster.local"
- e2mgrRMRService: "service-ricplt-e2mgr-rmr.ricplt.svc.cluster.local"
- e2termRMRService: "service-ricplt-e2term-rmr.ricplt.svc.cluster.local"
- rtmgrRMRService: "service-ricplt-rtmgr-rmr.ricplt.svc.cluster.local"
- a1mediatorRMRService: "service-ricplt-a1mediator-rmr.ricplt.svc.cluster.local"
-
-
-
-ricxapp:
- # This section is for xapp. Templates to be resolved from xApp descriptor
- replicaCount: 1
- name: xapp-std
- # Your can specify the chart fullname by using the following option
- #fullname: xapp-std
-
- image:
- pullPolicy: IfNotPresent
- repository: xapp-std-reg
- name: xapp-std
- tag: latest
-
- service:
- http:
- port: 8080
- containerPort: 8080
- rmr:
- route:
- port: 4561
- data:
- port: 4560
-
- livenessProbe: |-
- httpGet:
- path: ric/v1/health/alive
- port: 8080
- initialDelaySeconds: 5
- periodSeconds: 15
-
- readinessProbe: |-
- httpGet:
- path: ric/v1/health/ready
- port: 8080
- initialDelaySeconds: 5
- periodSeconds: 15
-
-
- appconfig:
- path: /opt/ric/config
-
- appenv:
--- /dev/null
+recursive-include * *.py
+recursive-include * *.yaml
+recursive-include * *.tpl
+recursive-include * *.conf
+recursive-include * *.pickle
+include xapp_onboarder/cli
+include xapp_onboarder/xapp_onboarder
+include README.md
+include LICENSES.txt
+include requirements.txt
--- /dev/null
+xApp Onboarder
+==============
+
+xApp onboarder onboards xApp to the near-rt RIC platform. The operators provides the xApp descriptors and their schemas, the xApp onboarder generates the xApp helm charts dynamically.
+
+## Install xapp_onboarder
+
+Run [pip](https://pip.pypa.io/en/stable/) to install xapp_onboarder.
+
+```bash
+pip install xapp_onboarder
+```
+
+## Prerequisite Requirements
+A helm chart repo is needed to store the xApp helm charts. You can use [chartmuseum](https://github.com/helm/chartmuseum) for this purpose.
+
+Environment variables:
+* **FLASK_SERVER_NAME**: Address that the xapp_onboarder is listening on. Default http://0.0.0.0:8888
+* **CHART_REPO_URL**: helm chart repo URL. Default http://0.0.0.0:8080
+* **DBAAS_SERVICE_HOST**: DBAAS service host URL that will be injected into the xApp config
+* **DBAAS_MASTER_NAME**: DBAAS_HA sentinel master URL that will be injected into the xApp config
+* **DBAAS_SERVICE_SENTINEL_PORT**: DBAAS_HA sentinel port that will be injected into the xApp config
+* **DBAAS_SERVICE_PORT**: DBAAS service port that will be injected into the xApp config
+
+## Configurations
+Environment variables:
+* **CHART_WORKSPACE_PATH**: Temporary directory that will store the xApp helm chart artifacts. Default /tmp/xapp_onboarder
+* **CHART_WORKSPACE_SIZE**: Size limit of the temporary directory. Default 500MB
+* **ALLOW_REDEPLOY**: Enable or disable redeploying of xApp helm charts. Default True
+* **HTTP_TIME_OUT**: Timeout of all http requests. Default 10
+* **HTTP_RETRY**: Number of retry xapp_onboarder will use for the http requests. Default 3
+
+## Run the API server
+```bash
+python3 -m xapp_onboarder.server.server
+```
+Or we recommend you can set up the symbolic link in your PATH
+```bash
+ln -s $(pip show xapp_onboarder | grep Location | awk '{printf $2 "/xapp_onboarder/xapp_onboarder"}') /usr/local/bin/xapp_onboarder
+```
+Then you can run the server
+```bash
+xapp_onboarder
+```
+## Run the CLI tool
+```bash
+python3 -m xapp_onboarder.server.cli
+```
+Or we recommend you can set up the symbolic link in your PATH
+```bash
+ln -s $(pip show xapp_onboarder | grep Location | awk '{printf $2 "/xapp_onboarder/cli"}') /usr/local/bin/cli
+```
+Then you can run the server
+```bash
+cli
+```
--- /dev/null
+aniso8601==8.0.0
+attrs==19.3.0
+certifi==2019.11.28
+chardet==3.0.4
+Click==7.0
+fire==0.2.1
+Flask==1.1.1
+flask-restplus==0.13.0
+idna==2.9
+importlib-metadata==1.5.0
+itsdangerous==1.1.0
+Jinja2==2.11.1
+jsonschema==3.2.0
+MarkupSafe==1.1.1
+pyrsistent==0.15.7
+pytz==2019.3
+PyYAML==5.3
+requests==2.23.0
+six==1.14.0
+termcolor==1.1.0
+urllib3==1.25.8
+Werkzeug==0.16.1
+zipp==3.0.0
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+[flake8]
+max-line-length = 160
+max-complexity = 10
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+from setuptools import setup, find_packages
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+with open('requirements.txt') as f:
+ requirements = f.read().splitlines()
+
+
+setup(
+ name='xapp_onboarder',
+ version='1.0.0',
+ description='RIC xApp onboarder',
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url='https://gerrit.o-ran-sc.org/r/admin/repos/it/dev',
+ author='Zhe Huang',
+ author_email='zhehuang@research.att.com',
+ include_package_data=True,
+ packages=find_packages(),
+ package_data={'': ['*.yaml', '*.tpl', '*.conf', 'xapp_onboarder', 'cli']},
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Operating System :: OS Independent",
+ ],
+ python_requires='>=3.6',
+ install_requires=requirements,
+)
--- /dev/null
+# tests/conftest.py
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import pytest
+from flask import Flask
+from xapp_onboarder.server.server import server
+
+@pytest.fixture
+def app() -> Flask:
+ """ Provides an instance of xapp onboarder """
+ server_instance = server()
+ assert isinstance(server_instance.app, Flask)
+ return server_instance.app
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+schema_file = {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://example.com/root.json",
+ "type": "object",
+ "title": "The Root Schema",
+ "required": [
+ "local",
+ "logger",
+ "rmr",
+ "db",
+ "controls",
+ "metrics"
+ ],
+ "properties": {
+ "local": {
+ "$id": "#/properties/local",
+ "type": "object",
+ "title": "The Local Schema",
+ "required": [
+ "host"
+ ],
+ "properties": {
+ "host": {
+ "$id": "#/properties/local/properties/host",
+ "type": "string",
+ "title": "The Host Schema",
+ "default": "",
+ "examples": [
+ ":8080"
+ ],
+ "pattern": "^(.*)$"
+ }
+ }
+ },
+ "logger": {
+ "$id": "#/properties/logger",
+ "type": "object",
+ "title": "The Logger Schema",
+ "required": [
+ "level"
+ ],
+ "properties": {
+ "level": {
+ "$id": "#/properties/logger/properties/level",
+ "type": "integer",
+ "title": "The Level Schema",
+ "default": 0,
+ "examples": [
+ 3
+ ]
+ }
+ }
+ },
+ "rmr": {
+ "$id": "#/properties/rmr",
+ "type": "object",
+ "title": "The Rmr Schema",
+ "required": [
+ "protPort",
+ "maxSize",
+ "numWorkers",
+ "rxMessages",
+ "txMessages"
+ ],
+ "properties": {
+ "protPort": {
+ "$id": "#/properties/rmr/properties/protPort",
+ "type": "string",
+ "title": "The Protport Schema",
+ "default": "",
+ "examples": [
+ "tcp:4560"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "maxSize": {
+ "$id": "#/properties/rmr/properties/maxSize",
+ "type": "integer",
+ "title": "The Maxsize Schema",
+ "default": 0,
+ "examples": [
+ 2072
+ ]
+ },
+ "numWorkers": {
+ "$id": "#/properties/rmr/properties/numWorkers",
+ "type": "integer",
+ "title": "The Numworkers Schema",
+ "default": 0,
+ "examples": [
+ 1
+ ]
+ },
+ "rxMessages": {
+ "$id": "#/properties/rmr/properties/rxMessages",
+ "type": "array",
+ "title": "The Rxmessages Schema",
+ "items": {
+ "$id": "#/properties/rmr/properties/rxMessages/items",
+ "type": "string",
+ "title": "The Items Schema",
+ "default": "",
+ "examples": [
+ "RIC_SUB_RESP",
+ "RIC_SUB_FAILURE",
+ "RIC_SUB_DEL_RESP",
+ "RIC_SUB_DEL_FAILURE",
+ "RIC_INDICATION"
+ ],
+ "pattern": "^(.*)$"
+ }
+ },
+ "txMessages": {
+ "$id": "#/properties/rmr/properties/txMessages",
+ "type": "array",
+ "title": "The Txmessages Schema",
+ "items": {
+ "$id": "#/properties/rmr/properties/txMessages/items",
+ "type": "string",
+ "title": "The Items Schema",
+ "default": "",
+ "examples": [
+ "RIC_SUB_REQ",
+ "RIC_SUB_DEL_REQ",
+ "RIC_SGNB_ADDITION_REQ",
+ "RIC_SGNB_ADDITION_ACK",
+ "RIC_SGNB_ADDITION_REJECT",
+ "RIC_SGNB_MOD_REQUEST",
+ "RIC_SGNB_MOD_REQUEST_ACK",
+ "RIC_SGNB_MOD_REQUEST_REJECT",
+ "RIC_SGNB_MOD_REQUIRED",
+ "RIC_SGNB_MOD_CONFIRM",
+ "RIC_SGNB_MOD_REFUSE",
+ "RIC_SGNB_RECONF_COMPLETE",
+ "RIC_SGNB_RELEASE_REQUEST",
+ "RIC_SGNB_RELEASE_CONFIRM",
+ "RIC_SGNB_RELEASE_REQUIRED",
+ "RIC_SGNB_RELEASE_REQUEST_ACK",
+ "RIC_SECONDARY_RAT_DATA_USAGE_REPORT",
+ "RIC_SN_STATUS_TRANSFER",
+ "RIC_RRC_TRANSFER",
+ "RIC_UE_CONTEXT_RELEASE"
+ ],
+ "pattern": "^(.*)$"
+ }
+ }
+ }
+ },
+ "db": {
+ "$id": "#/properties/db",
+ "type": "object",
+ "title": "The Db Schema",
+ "required": [
+ "host",
+ "port",
+ "namespaces"
+ ],
+ "properties": {
+ "host": {
+ "$id": "#/properties/db/properties/host",
+ "type": "string",
+ "title": "The Host Schema",
+ "default": "",
+ "examples": [
+ "localhost"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "port": {
+ "$id": "#/properties/db/properties/port",
+ "type": "integer",
+ "title": "The Port Schema",
+ "default": 0,
+ "examples": [
+ 6379
+ ]
+ },
+ "namespaces": {
+ "$id": "#/properties/db/properties/namespaces",
+ "type": "array",
+ "title": "The Namespaces Schema",
+ "items": {
+ "$id": "#/properties/db/properties/namespaces/items",
+ "type": "string",
+ "title": "The Items Schema",
+ "default": "",
+ "examples": [
+ "sdl",
+ "rnib"
+ ],
+ "pattern": "^(.*)$"
+ }
+ }
+ }
+ },
+ "controls": {
+ "$id": "#/properties/controls",
+ "type": "object",
+ "title": "The Controls Schema",
+ "required": [
+ "active",
+ "requestorId",
+ "ranFunctionId",
+ "ricActionId",
+ "interfaceId"
+ ],
+ "properties": {
+ "active": {
+ "$id": "#/properties/controls/properties/active",
+ "type": "boolean",
+ "title": "The Active Schema",
+ "default": False,
+ "examples": [
+ True
+ ]
+ },
+ "requestorId": {
+ "$id": "#/properties/controls/properties/requestorId",
+ "type": "integer",
+ "title": "The Requestorid Schema",
+ "default": 0,
+ "examples": [
+ 66
+ ]
+ },
+ "ranFunctionId": {
+ "$id": "#/properties/controls/properties/ranFunctionId",
+ "type": "integer",
+ "title": "The Ranfunctionid Schema",
+ "default": 0,
+ "examples": [
+ 1
+ ]
+ },
+ "ricActionId": {
+ "$id": "#/properties/controls/properties/ricActionId",
+ "type": "integer",
+ "title": "The Ricactionid Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "interfaceId": {
+ "$id": "#/properties/controls/properties/interfaceId",
+ "type": "object",
+ "title": "The Interfaceid Schema",
+ "required": [
+ "globalENBId"
+ ],
+ "properties": {
+ "globalENBId": {
+ "$id": "#/properties/controls/properties/interfaceId/properties/globalENBId",
+ "type": "object",
+ "title": "The Globalenbid Schema",
+ "required": [
+ "plmnId",
+ "eNBId"
+ ],
+ "properties": {
+ "plmnId": {
+ "$id": "#/properties/controls/properties/interfaceId/properties/globalENBId/properties/plmnId",
+ "type": "string",
+ "title": "The Plmnid Schema",
+ "default": "",
+ "examples": [
+ "310150"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "eNBId": {
+ "$id": "#/properties/controls/properties/interfaceId/properties/globalENBId/properties/eNBId",
+ "type": "integer",
+ "title": "The Enbid Schema",
+ "default": 0,
+ "examples": [
+ 202251
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "metrics": {
+ "$id": "#/properties/metrics",
+ "type": "array",
+ "title": "The Metrics Schema",
+ "items": {
+ "$id": "#/properties/metrics/items",
+ "type": "object",
+ "title": "The Items Schema",
+ "required": [
+ "objectName",
+ "objectInstance",
+ "name",
+ "type",
+ "description"
+ ],
+ "properties": {
+ "objectName": {
+ "$id": "#/properties/metrics/items/properties/objectName",
+ "type": "string",
+ "title": "The Objectname Schema",
+ "default": "",
+ "examples": [
+ "UEEventStreamingCounters"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "objectInstance": {
+ "$id": "#/properties/metrics/items/properties/objectInstance",
+ "type": "string",
+ "title": "The Objectinstance Schema",
+ "default": "",
+ "examples": [
+ "SgNBAdditionRequest"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "name": {
+ "$id": "#/properties/metrics/items/properties/name",
+ "type": "string",
+ "title": "The Name Schema",
+ "default": "",
+ "examples": [
+ "SgNBAdditionRequest"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "type": {
+ "$id": "#/properties/metrics/items/properties/type",
+ "type": "string",
+ "title": "The Type Schema",
+ "default": "",
+ "examples": [
+ "counter"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "description": {
+ "$id": "#/properties/metrics/items/properties/description",
+ "type": "string",
+ "title": "The Description Schema",
+ "default": "",
+ "examples": [
+ "The total number of SG addition request events processed"
+ ],
+ "pattern": "^(.*)$"
+ }
+ }
+ }
+ }
+ }
+}
+
+config_file = {
+ "xapp_name": "test_xapp",
+ "version": "1.0.0",
+ "containers": [{
+ "name": "test1",
+ "image": {
+ "registry": "test_repo",
+ "name": "test_name",
+ "tag": "test_tag"
+ },
+ "command": "test command"
+ },
+ {
+ "name": "test2",
+ "image": {
+ "registry": "test2_repo",
+ "name": "test2_name",
+ "tag": "test2:_tag"
+ },
+ "command": "test2 command"
+ }],
+ "local": {
+ "host": ":8080"
+ },
+ "logger": {
+ "level": 3
+ },
+ "db": {
+ "host": "localhost",
+ "port": 6379,
+ "namespaces": ["sdl", "rnib"]
+ },
+ "controls": {
+ "active": True,
+ "requestorId": 66,
+ "ranFunctionId": 1,
+ "ricActionId": 0,
+ "interfaceId": {
+ "globalENBId": {
+ "plmnId": "310150",
+ "eNBId": 202251
+ }
+ }
+ },
+ "rmr": {
+ "protPort": "tcp:4560",
+ "maxSize": 10000,
+ "numWorkers": 1,
+ "rxMessages": [
+ "RIC_SUB_RESP",
+ "RIC_SUB_FAILURE",
+ "RIC_SUB_DEL_RESP",
+ "RIC_SUB_DEL_FAILURE",
+ "RIC_INDICATION"
+ ],
+ "txMessages": [
+ "RIC_SUB_REQ",
+ "RIC_SUB_DEL_REQ",
+ "RIC_SGNB_ADDITION_REQ",
+ "RIC_SGNB_ADDITION_ACK",
+ "RIC_SGNB_ADDITION_REJECT",
+ "RIC_SGNB_MOD_REQUEST",
+ "RIC_SGNB_MOD_REQUEST_ACK",
+ "RIC_SGNB_MOD_REQUEST_REJECT",
+ "RIC_SGNB_MOD_REQUIRED",
+ "RIC_SGNB_MOD_CONFIRM",
+ "RIC_SGNB_MOD_REFUSE",
+ "RIC_SGNB_RELEASE_REQUEST",
+ "RIC_SGNB_RELEASE_CONFIRM",
+ "RIC_SGNB_RELEASE_REQUIRED",
+ "RIC_SGNB_RELEASE_REQUEST_ACK",
+ "RIC_SGNB_RECONF_COMPLETE",
+ "RIC_UE_CONTEXT_RELEASE",
+ "RIC_RRC_TRANSFER",
+ "RIC_SECONDARY_RAT_DATA_USAGE_REPORT",
+ "RIC_SN_STATUS_TRANSFER"
+ ]
+ },
+ "metrics": [
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBAdditionRequest",
+ "name": "SgNBAdditionRequest",
+ "type": "counter",
+ "description": "The total number of SG addition request events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBAdditionRequestAcknowledge",
+ "name": "SgNBAdditionRequestAcknowledge",
+ "type": "counter",
+ "description": "The total number of SG addition request acknowledge events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBAdditionRequestReject",
+ "name": "SgNBAdditionRequestReject",
+ "type": "counter",
+ "description": "The total number of SG addition request reject events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationRequest",
+ "name": "SgNBModificationRequest",
+ "type": "counter",
+ "description": "The total number of SG modification request events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationRequestAcknowledge",
+ "name": "SgNBModificationRequestAcknowledge",
+ "type": "counter",
+ "description": "The total number of SG modification request acknowledge events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationRequestReject",
+ "name": "SgNBModificationRequestReject",
+ "type": "counter",
+ "description": "The total number of SG modification request reject events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationRequired",
+ "name": "SgNBModificationRequired",
+ "type": "counter",
+ "description": "The total number of SG modification required events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationConfirm",
+ "name": "SgNBModificationConfirm",
+ "type": "counter",
+ "description": "The total number of SG modification confirm events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBModificationRefuse",
+ "name": "SgNBModificationRefuse",
+ "type": "counter",
+ "description": "The total number of SG modification refuse events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReleaseRequest",
+ "name": "SgNBReleaseRequest",
+ "type": "counter",
+ "description": "The total number of SG release request events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReleaseRequestAcknowledge",
+ "name": "SgNBReleaseRequestAcknowledge",
+ "type": "counter",
+ "description": "The total number of SG release request acknowledge events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReleaseRequestReject",
+ "name": "SgNBReleaseRequestReject",
+ "type": "counter",
+ "description": "The total number of SG release request reject events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReleaseRequired",
+ "name": "SgNBReleaseRequired",
+ "type": "counter",
+ "description": "The total number of SG release required events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReleasenConfirm",
+ "name": "SgNBReleasenConfirm",
+ "type": "counter",
+ "description": "The total number of SG release confirm events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SgNBReconfigurationComplete",
+ "name": "SgNBReconfigurationComplete",
+ "type": "counter",
+ "description": "The total number of SG reconfiguration complete events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "UEContextRelease",
+ "name": "UEContextRelease",
+ "type": "counter",
+ "description": "The total number of SG UE context release events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "RRCTransfer",
+ "name": "RRCTransfer",
+ "type": "counter",
+ "description": "The total number of SG RRC transfers events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SNStatusTransfer",
+ "name": "SNStatusTransfer",
+ "type": "counter",
+ "description": "The total number of SG SN status transfers events processed"
+ },
+ {
+ "objectName": "UEEventStreamingCounters",
+ "objectInstance": "SecondaryRATDataUsageReport",
+ "name": "SecondaryRATDataUsageReport",
+ "type": "counter",
+ "description": "The total number of SG secondary RAT data usage reports events processed"
+ },
+ {
+ "objectName": "RMRCounters",
+ "objectInstance": "Transmitted",
+ "name": "Transmitted",
+ "type": "counter",
+ "description": "The total number of RMR messages transmited"
+ },
+ {
+ "objectName": "RMRCounters",
+ "objectInstance": "Received",
+ "name": "Received",
+ "type": "counter",
+ "description": "The total number of RMR messages received"
+ },
+ {
+ "objectName": "RMRCounters",
+ "objectInstance": "TransmitError",
+ "name": "TransmitError",
+ "type": "counter",
+ "description": "The total number of RMR messages transmission errors"
+ },
+ {
+ "objectName": "RMRCounters",
+ "objectInstance": "ReceiveError",
+ "name": "ReceiveError",
+ "type": "counter",
+ "description": "The total number of RMR messages receive errors"
+ },
+ {
+ "objectName": "SDLounters",
+ "objectInstance": "Stored",
+ "name": "Stored",
+ "type": "counter",
+ "description": "The total number of stored SDL transactions"
+ },
+ {
+ "objectName": "SDLounters",
+ "objectInstance": "StoreError",
+ "name": "StoreError",
+ "type": "counter",
+ "description": "The total number of SDL store errors"
+ }
+ ]
+}
+
+mock_json_body_url = {
+ 'config-file.json_url': 'http://0.0.0.0:8080/config-file.json',
+ 'schema.json_url': 'http://0.0.0.0:8080/schema.json'
+}
+
+mock_json_body = {
+ "config-file.json": config_file,
+ "schema.json": schema_file
+}
+
+helm_repo_index_response={'apiVersion': 'v1',
+ 'entries':{
+ 'test_xapp':[{
+ 'apiVersion': 'v1',
+ 'appVersion': '1.0',
+ 'created': '2020-03-12T19:10:17.178396719Z',
+ 'description': 'test xApp Helm Chart',
+ 'digest': 'd77dfb3f008e5174e90d79bfe982ef85b5dc5930141f6a1bd9995b2fa35',
+ 'name': 'test_xapp',
+ 'urls':['charts/test-1.0.0.tgz'],
+ 'version': '1.0.0'
+ }]
+ },
+ 'generated': '2020-03-16T16:54:44Z',
+ 'serverInfo':{}
+ }
--- /dev/null
+#!/usr/bin/env python3
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import pkg_resources
+import logging.config
+import yaml
+import json
+import os
+import tarfile
+from flask import Flask, jsonify, send_file
+from tests.constants import config_file, schema_file, helm_repo_index_response
+from xapp_onboarder.server import settings
+
+
+logger_config = pkg_resources.resource_filename("xapp_onboarder", 'logging.conf')
+logging.config.fileConfig(logger_config)
+log = logging.getLogger(__name__)
+
+listen_address = '0.0.0.0:8080'
+app = Flask(__name__)
+app.config['SERVER_NAME'] = listen_address
+
+
+
+
+@app.route('/')
+def root_dir():
+ return {'health': 'OK'}
+
+@app.route('/index.yaml')
+def get_index():
+ return yaml.dump(helm_repo_index_response)
+
+@app.route('/api/charts', methods=['POST'])
+def upload_chart():
+ return {"saved": True}, 201
+
+@app.route('/api/charts/test_xapp/1.0.0', methods=['DELETE'])
+def delete_chart():
+ return {"deleted":True}, 200
+
+@app.route('/api/charts/test_xapp', methods=['GET'])
+def get_all_xapp_version():
+ test_xapp = helm_repo_index_response['entries']['test_xapp']
+ return jsonify(test_xapp), 200
+
+@app.route('/api/charts', methods=['GET'])
+def get_all_xapp():
+ return jsonify(helm_repo_index_response['entries']), 200
+
+@app.route('/charts/test_xapp-1.0.0.tgz', methods=['GET'])
+def download_xapp_helm_package():
+ if not os.path.exists(settings.MOCK_TEST_HELM_REPO_TEMP_DIR):
+ os.makedirs(settings.MOCK_TEST_HELM_REPO_TEMP_DIR)
+ with open(settings.MOCK_TEST_HELM_REPO_TEMP_DIR + '/values.yaml', 'w') as file:
+ file.write(json.dumps(helm_repo_index_response))
+
+ with tarfile.open(settings.MOCK_TEST_HELM_REPO_TEMP_DIR + '/test_xapp-1.0.0.tgz', "w:gz") as tar:
+ tar.addfile(tarfile.TarInfo("test_xapp/values.yaml"), open(settings.MOCK_TEST_HELM_REPO_TEMP_DIR + '/values.yaml'))
+
+ return send_file(settings.MOCK_TEST_HELM_REPO_TEMP_DIR + '/test_xapp-1.0.0.tgz', mimetype='application/zip')
+
+
+
+@app.route('/schema.json', methods=['GET'])
+def get_schema():
+
+
+ return schema_file, 200
+
+@app.route('/config-file.json', methods=['GET'])
+def get_config_file():
+
+
+ return config_file, 200
+
+def run():
+ log.info('>>>>> Starting mock helm_repo at http://{}<<<<<'.format(listen_address))
+ app.run(debug=True)
+
+if __name__ == '__main__':
+ run()
+
+
+
+
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+from http import HTTPStatus
+from tests.constants import mock_json_body, mock_json_body_url, helm_repo_index_response
+
+
+def test_health(client):
+ response = client.get('/api/v1/health')
+ assert response.status_code == HTTPStatus.OK, 'Wrong status code'
+ assert response.json == {'status': 'OK'}, 'Improper response'
+
+def test_get_charts(client):
+ response = client.get('/api/v1/charts')
+ assert response.status_code == HTTPStatus.OK, 'Wrong status code'
+ assert response.content_type == 'application/json', 'Content type error'
+ assert sorted([repr(x) for x in response.json]) == sorted([repr(x) for x in helm_repo_index_response['entries']])
+
+def test_get_test_xapp_charts(client):
+ response = client.get('/api/v1/charts/xapp/test_xapp')
+ assert response.status_code == HTTPStatus.OK, 'Wrong status code'
+ assert response.content_type == 'application/json', 'Content type error'
+ assert sorted([repr(x) for x in response.json]) == sorted([repr(x) for x in helm_repo_index_response['entries']['test_xapp']])
+
+def test_get_test_xapp_charts_package(client):
+ response = client.get('/api/v1/charts/xapp/test_xapp/ver/1.0.0')
+ assert response.status_code == HTTPStatus.OK, 'Wrong status code'
+ assert response.content_type == 'application/gzip', 'Content type error'
+
+def test_get_test_xapp_charts_values_yaml(client):
+ response = client.get('/api/v1/charts/xapp/test_xapp/ver/1.0.0/values.yaml')
+ assert response.status_code == HTTPStatus.OK, 'Wrong status code'
+ assert response.content_type == 'text/x-yaml', 'Content type error'
+
+def test_onboard_post(client):
+ url = '/api/v1/onboard'
+ response = client.post(url, json=mock_json_body)
+ assert response.status_code == HTTPStatus.CREATED, 'Wrong status code'
+ assert response.content_type == 'application/json', 'Content type error'
+ assert response.json == {'status': 'Created'}, 'Onboard failed'
+
+def test_onboard_download_post(client):
+ url = '/api/v1/onboard/download'
+ response = client.post(url, json=mock_json_body_url)
+ assert response.status_code == HTTPStatus.CREATED, 'Wrong status code'
+ assert response.content_type == 'application/json', 'Content type error'
+ assert response.json == {'status': 'Created'}, 'Onboard failed'
\ No newline at end of file
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+import os
+import shutil
+from xapp_onboarder.helm_controller.xApp_builder import xApp, xAppError
+from tests.constants import config_file, schema_file
+from xapp_onboarder.server import settings
+
+
+def test_packaging_xapp(client):
+ chart_workspace_path = settings.CHART_WORKSPACE_PATH + '/test_xapp-1.0.0'
+ if os.path.exists(chart_workspace_path):
+ shutil.rmtree(chart_workspace_path)
+
+ xapp = xApp(config_file, schema_file)
+ xapp.package_chart()
+ assert os.path.isfile(chart_workspace_path + '/test_xapp-1.0.0.tgz'), 'xApp packaging error'
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+import time
+import os
+import shutil
+from xapp_onboarder.server import settings
+from xapp_onboarder.helm_controller.artifacts_manager import format_artifact_dir_size, get_dir_size, trim_artifact_dir
+def test_trim_thread():
+ path = settings.CHART_WORKSPACE_PATH
+ if os.path.exists(path):
+ shutil.rmtree(path)
+ os.makedirs(path)
+
+ size_limit = format_artifact_dir_size()
+ f = open(path+'/mock_size_file',"wb")
+ f.seek(int(size_limit)+100000000)
+ f.write(b"\0")
+ f.close()
+
+ artifact_dir_size = get_dir_size(start_path=path)
+ assert size_limit < artifact_dir_size, 'Fail to write large files'
+
+ trim_artifact_dir()
+ artifact_dir_size = get_dir_size(start_path=path)
+
+ assert size_limit > artifact_dir_size, 'Fail to trim'
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+[tox]
+envlist = py36-{local,lf}
+
+
+[testenv]
+deps=
+ pytest
+ pytest-flask
+ coverage
+ pytest-cov
+setenv =
+ PYTHONUNBUFFERED = 1
+ CHART_REPO_URL = http://0.0.0.0:8080
+ DBAAS_SERVICE_HOST = test_dbaas_host
+ DBAAS_MASTER_NAME = test_master_name
+ DBAAS_SERVICE_SENTINEL_PORT = 29701
+ DBAAS_SERVICE_PORT = 3710
+ MOCK_TEST_MODE = True
+ lf: CHART_WORKSPACE_PATH = {toxworkdir}/.tmp/xapp_onboarder
+ lf: MOCK_TEST_HELM_REPO_TEMP_DIR = {toxworkdir}/.tmp/helm_repo
+
+commands_pre=
+ bash -c 'nohup python -m tests.mock_helm_repo.mock_helm_repo >/dev/null 2>&1 &'
+
+commands=
+ pytest --verbose --cov xapp_onboarder --cov-report xml --cov-report term-missing --cov-report html
+ coverage xml -i
+
+commands_post=
+ local: bash -c 'kill -9 $(ps -x | grep "tests.mock_helm_repo.mock_helm_repo" | grep -v grep | cut -d" " -f1)'
+
+whitelist_externals = bash
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+
+from flask_restplus import Api
+from xapp_onboarder.server import settings
+
+log = logging.getLogger(__name__)
+
+api = Api(version='1.0', title='RIC xApp onboarder API',
+ description='APIs to manage the xApp helm charts')
+
+
+@api.errorhandler
+def default_error_handler(e):
+ message = 'An unhandled exception occurred.'
+ log.exception(message)
+
+ if not settings.FLASK_DEBUG:
+ return {'message': message}, 500
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+import io
+import tarfile
+from xapp_onboarder.repo_manager.repo_manager import repo_manager, RepoManagerError
+from xapp_onboarder.api.models.response_models import error_message_model, response
+
+log = logging.getLogger(__name__)
+
+
+def get_charts_list(xapp_chart_name=None):
+
+ if not repo_manager.is_repo_ready():
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="xapp_onboarder",
+ error_message="Cannot connect to local helm repo.",
+ status="Service not ready.")
+ return response_message.get_return()
+
+ try:
+ content = repo_manager.get_xapp_list(xapp_chart_name=xapp_chart_name)
+ except RepoManagerError as err:
+ log.error(str(err))
+ response_message = response(model=error_message_model, status_code=err.status_code,
+ error_source="charts_fetcher",
+ error_message=str(err),
+ status="Fetching helm chart list failed")
+ return response_message.get_return()
+ return content, 200
+
+
+def download_chart_package(xapp_chart_name, version):
+
+ if not repo_manager.is_repo_ready():
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="xapp_onboarder",
+ error_message="Cannot connect to local helm repo.",
+ status="Service not ready.")
+ return response_message.get_return()
+ try:
+ content = repo_manager.download_xapp_chart(xapp_chart_name=xapp_chart_name, version=version)
+ except RepoManagerError as err:
+ log.error(str(err))
+ response_message = response(model=error_message_model, status_code=err.status_code,
+ error_source="charts_fetcher",
+ error_message=str(err),
+ status="Downloading helm chart package failed")
+ return response_message.get_return()
+ return content, 200
+
+
+def download_values_yaml(xapp_chart_name, version):
+
+ content, status = download_chart_package(xapp_chart_name=xapp_chart_name, version=version)
+
+ if status != 200:
+ return content, status
+
+ file_stream = io.BytesIO(content)
+
+ with tarfile.open(fileobj=file_stream) as tar:
+ values_yaml_file = tar.extractfile(xapp_chart_name + '/values.yaml')
+ return_response = values_yaml_file.read()
+
+ return return_response, 200
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+from flask import make_response
+from flask_restplus import Resource
+from xapp_onboarder.api.api_reference import api
+from xapp_onboarder.api.charts import get_charts_list, download_chart_package, download_values_yaml
+from xapp_onboarder.api.models.response_models import error_message_model
+
+log = logging.getLogger(__name__)
+ns = api.namespace('charts', description='Managing helm charts')
+
+
+@ns.route('')
+class ChartsList(Resource):
+
+ @api.response(200, 'Get helm chart list OK')
+ @api.response(500, 'Get helm chart list failed', error_message_model)
+ def get(self):
+ """
+ Returns the list of charts that have been onboarded.
+ """
+
+ return get_charts_list()
+
+
+@ns.route('/xapp/<string:xapp_chart_name>')
+class VersionList(Resource):
+
+ @api.response(200, 'Get helm chart list OK')
+ @api.response(500, 'Get helm chart list failed', error_message_model)
+ def get(self, xapp_chart_name):
+ """
+ Returns the list of charts that have been onboarded.
+ """
+ return get_charts_list(xapp_chart_name=xapp_chart_name)
+
+
+@ns.route('/xapp/<string:xapp_chart_name>/ver/<string:version>')
+class ChartsFetcher(Resource):
+
+ @api.response(200, 'Get helm chart package OK')
+ @api.response(500, 'Get helm chart package failed', error_message_model)
+ @api.produces(['application/gzip'])
+ def get(self, xapp_chart_name, version):
+ """
+ Returns the helm charts that have been onboarded.
+ """
+
+ content, status = download_chart_package(xapp_chart_name=xapp_chart_name, version=version)
+
+ if status != 200:
+ return content, status
+
+ response = make_response(content)
+ response.headers.set("Content-Type", "application/gzip")
+ response.headers.set("Content-Disposition",
+ "attachment; filename=\"" + xapp_chart_name + "-" + version + ".tgz\"")
+ return response
+
+
+@ns.route('/xapp/<string:xapp_chart_name>/ver/<string:version>/values.yaml')
+class ValuesYamlFetcher(Resource):
+
+ @api.response(200, 'Get helm chart values.yaml OK')
+ @api.response(500, 'Get helm chart values.yaml failed', error_message_model)
+ @api.produces(['text/x-yaml'])
+ def get(self, xapp_chart_name, version):
+ """
+ Returns the values.yaml file of the xApp
+ """
+
+ content, status = download_values_yaml(xapp_chart_name=xapp_chart_name, version=version)
+
+ if status != 200:
+ return content, status
+
+ response = make_response(content)
+ response.headers.set("Content-Type", "text/x-yaml")
+ response.headers.set("Content-Disposition", "attachment; filename=\"values.yaml\"")
+
+ return response
--- /dev/null
+###############################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+
+from flask_restplus import Resource
+from xapp_onboarder.api.api_reference import api
+from xapp_onboarder.api.models.response_models import status_message_model, error_message_model, response
+from xapp_onboarder.repo_manager.repo_manager import repo_manager
+
+log = logging.getLogger(__name__)
+ns = api.namespace('health', description='health check')
+
+
+@ns.route('')
+class HealthCheck(Resource):
+
+ @api.response(200, 'Health check OK', status_message_model)
+ @api.response(500, 'xApp onboarder is not ready', error_message_model)
+ def get(self):
+ """
+ Returns the health condition of the xApp onboarder
+ """
+ if not repo_manager.is_repo_ready():
+ response_message = response( model= error_message_model, status_code = 500,
+ error_source = "xapp_onboarder",
+ error_message = "Cannot connect to local helm repo.",
+ status = "Service not ready.")
+ return response_message.get_return()
+ return response(model = status_message_model, status_code = 200, status= "OK").get_return()
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+from flask import request
+from flask_restplus import Resource
+from xapp_onboarder.api.models import request_models
+from xapp_onboarder.api.api_reference import api
+from xapp_onboarder.api.models.response_models import status_message_model, error_message_model
+from xapp_onboarder.api.onboard import onboard, download_config_and_schema_and_onboard
+
+log = logging.getLogger(__name__)
+ns = api.namespace('onboard', description='onboard xApps')
+
+
+@ns.route('')
+class OnboardxApps(Resource):
+
+ # @api.response(200, 'Everything is fine')
+ # @api.response(500, 'temp is not ready')
+ # def get(self):
+ # """
+ # Return a list of xApp that have been onboarded and their versions.
+ # """
+ # if not repo_manager.is_repo_ready():
+ # return {'status': 'not ready'}, 500
+ # return {'status': 'OK'}, 200
+
+ @api.response(201, 'xApp onboard successfully.', status_message_model)
+ @api.response(400, 'xApp descriptor format error', error_message_model)
+ @api.response(500, 'xApp onboarder is not ready', error_message_model)
+ @api.expect(request_models.xapp_descriptor_post, validate=True)
+ def post(self):
+ """
+ Onboard xApp with the xApp descriptor and its scehma included in the Json body
+ """
+ config_file = request.json.get('config-file.json')
+ schema_file = request.json.get('schema.json')
+
+ return onboard(config_file, schema_file)
+
+
+@ns.route('/download')
+class OnboardxAppsDownload(Resource):
+
+ @api.response(201, 'xApp onboard successfully.', status_message_model)
+ @api.response(400, 'xApp descriptor format error', error_message_model)
+ @api.response(500, 'xApp onboarder is not ready', error_message_model)
+ @api.expect(request_models.xapp_descriptor_download_post, validate=True)
+ def post(self):
+ """
+ Onboard xApp with xApp descriptor and schema downloading URLs
+ """
+ config_file_url = request.json.get('config-file.json_url')
+ schema_url = request.json.get('schema.json_url')
+
+ return download_config_and_schema_and_onboard(config_file_url, schema_url)
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+from flask_restplus import fields
+from xapp_onboarder.api.api_reference import api
+
+xapp_descriptor_post = api.model('descriptor', {
+ 'config-file.json': fields.Nested(
+ api.model('config', {
+ 'xapp_name': fields.String(description='Name of the xApp chart', required=True),
+ 'version': fields.String(description='Version of the xApp chart', required=True,
+ pattern='^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'),
+ }), required=True),
+ 'schema.json': fields.Raw(description='Schema file body', required=True),
+})
+
+
+xapp_descriptor_download_post = api.model('descriptor_remote', {
+ 'config-file.json_url': fields.Url(description='URL to download the config-file.json file', absolute=True, required=True),
+ 'schema.json_url': fields.Url(description='URL to download the schema.json file', absolute=True, required=True),
+})
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+from flask_restplus import fields, marshal
+from xapp_onboarder.api.api_reference import api
+
+error_message_model = api.model('error_message', {
+ 'error_source': fields.String(description='source of the error', required=True),
+ 'error_message': fields.String(description='source of the error', required=True),
+ 'status': fields.String(description='http response message', required=True),
+})
+
+status_message_model = api.model('status', {
+ 'status': fields.String(description='status of the service', required=True)
+})
+
+
+class response():
+
+ def __init__(self, model, status_code, status = "" , error_source = "", error_message = ""):
+ self.model = model
+ self.status = status
+ self.status_code = status_code
+ self.error_source = error_source
+ self.error_message = error_message
+
+ def get_return(self):
+ return marshal(self, self.model), self.status_code
\ No newline at end of file
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+import json
+from jsonschema import ValidationError, SchemaError
+from jsonschema import validate, Draft7Validator
+from xapp_onboarder.helm_controller.xApp_builder import xApp, xAppError
+from xapp_onboarder.server import settings
+from xapp_onboarder.repo_manager.repo_manager import requests_retry_session, repo_manager
+from xapp_onboarder.api.models.response_models import error_message_model, response, status_message_model
+
+
+log = logging.getLogger(__name__)
+
+
+def onboard(config_file, schema_file):
+ if not repo_manager.is_repo_ready():
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="xapp_onboarder",
+ error_message="Cannot connect to local helm repo.",
+ status="Service not ready.")
+ return response_message.get_return()
+
+ try:
+ Draft7Validator.check_schema(schema_file)
+ validate(config_file, schema_file)
+ except ValidationError as err:
+ log.debug(err.message)
+ response_message = response(model=error_message_model, status_code=400,
+ error_source="config-file.json",
+ error_message=err.message,
+ status="Input payload validation failed")
+ return response_message.get_return()
+ except SchemaError as err:
+ log.debug(err.message)
+ response_message = response(model=error_message_model, status_code=400,
+ error_source="schema.json",
+ error_message=err.message,
+ status="Input payload validation failed")
+ return response_message.get_return()
+
+ try:
+ xapp = xApp(config_file, schema_file)
+ xapp.package_chart()
+ xapp.distribute_chart()
+ except xAppError as err:
+ log.error(str(err))
+ response_message = response(model=error_message_model, status_code=err.status_code,
+ error_source="xApp_builder",
+ error_message=str(err),
+ status="xApp onboarding failed")
+ return response_message.get_return()
+ return response(model=status_message_model, status_code=201, status="Created").get_return()
+
+
+def download_config_and_schema_and_onboard(config_file_url, schema_url):
+ if not repo_manager.is_repo_ready():
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="xapp_onboarder",
+ error_message="Cannot connect to local helm repo.",
+ status="Service not ready.")
+ return response_message.get_return()
+
+ session = requests_retry_session()
+ try:
+ response_content = session.get(config_file_url, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ log.error(err.message)
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="config-file.json",
+ error_message=err.message,
+ status="Downloading config-file.json failed")
+ return response_message.get_return()
+ else:
+ if response_content.status_code != 200:
+ error_message = "Wrong response code: {}, {}".format(response_content.status_code, response_content.content.decode("utf-8"))
+ log.error(error_message)
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="config-file.json",
+ error_message=error_message,
+ status="Downloading config-file.json failed")
+ return response_message.get_return()
+ config_file = json.loads(response_content.content)
+
+ try:
+ response_content = session.get(schema_url, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ log.error(err.message)
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="schema.json",
+ error_message=err.message,
+ status="Downloading schema.json failed")
+ return response_message.get_return()
+ else:
+ if response_content.status_code != 200:
+ error_message = "Wrong response code. {}, {}".format(response_content.status_code, response_content.content.decode("utf-8"))
+ log.error(error_message)
+ response_message = response(model=error_message_model, status_code=500,
+ error_source="schema.json",
+ error_message=error_message,
+ status="Downloading schema.json failed")
+ return response_message.get_return()
+ schema_file = json.loads(response_content.content)
+
+ return onboard(config_file, schema_file)
--- /dev/null
+#!/usr/bin/env python3
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+import logging.config
+import pkg_resources
+from xapp_onboarder.server.cli import run
+
+if __name__ == "__main__":
+
+ logger_config = pkg_resources.resource_filename("xapp_onboarder", 'logging.conf')
+ logging.config.fileConfig(logger_config)
+ run()
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import os
+import logging
+import re
+import glob
+import threading
+import shutil
+from xapp_onboarder.server import settings
+
+log = logging.getLogger(__name__)
+
+
+def get_dir_size(start_path='.'):
+ total_size = 0
+
+ if os.path.isfile(start_path):
+ total_size += os.path.getsize(start_path)
+
+ if os.path.isdir(start_path):
+ for dirpath, dirnames, filenames in os.walk(start_path):
+ for f in filenames:
+ fp = os.path.join(dirpath, f)
+ # skip if it is symbolic link
+ if not os.path.islink(fp):
+ total_size += os.path.getsize(fp)
+
+ return total_size
+
+
+def format_artifact_dir_size():
+ """ Converts integers to common size units used in computing """
+ artifact_dir_size_string = settings.CHART_WORKSPACE_SIZE
+ size_unit = re.sub('[0-9\s\.]', '', artifact_dir_size_string)
+ size_limit = re.sub('[A-Za-z\s\.]', '', artifact_dir_size_string)
+
+ bit_shift = {"B": 0,
+ "kb": 7,
+ "KB": 10,
+ "mb": 17,
+ "MB": 20,
+ "gb": 27,
+ "GB": 30,
+ "TB": 40, }
+ return float(size_limit) * float(1 << bit_shift[size_unit])
+
+def trim_artifact_dir():
+ artifact_dir_size = get_dir_size(start_path=settings.CHART_WORKSPACE_PATH)
+ dir_limit = format_artifact_dir_size()
+
+ if artifact_dir_size > dir_limit:
+ dirs = sorted(glob.glob(settings.CHART_WORKSPACE_PATH + '/*'), key=os.path.getctime)
+ trim_dir = list()
+ remain_size = artifact_dir_size
+ for dir in dirs:
+ remain_size = remain_size - get_dir_size(start_path=dir)
+ trim_dir.append(dir)
+ if remain_size < dir_limit:
+ break
+ log.info("Trimming artifact directories: " + str(trim_dir))
+ for dir in trim_dir:
+ if os.path.isfile(dir):
+ os.remove(dir)
+ else:
+ shutil.rmtree(dir)
+
+
+class artifacts_manager():
+
+ def __init__(self):
+ self.timer_thread = threading.Timer(60.0, trim_artifact_dir)
+
+ def start_trim_thread(self):
+ if not settings.MOCK_TEST_MODE:
+ log.info("Artifact directory trimming thread started.")
+ self.timer_thread.start()
+
+
+ def cancel_trim_thread(self):
+ log.info("Artifact directory trimming thread stopped.")
+ self.timer_thread.cancel()
+
+ def start(self):
+ self.start_trim_thread()
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import logging
+import yaml
+import json
+import os
+import io
+import subprocess
+import shutil
+import re
+import copy
+import platform
+import tarfile
+import stat
+from xapp_onboarder.server import settings
+from xapp_onboarder.repo_manager.repo_manager import repo_manager, RepoManagerError
+from pkg_resources import resource_filename
+from subprocess import PIPE
+from xapp_onboarder.repo_manager.repo_manager import requests_retry_session
+log = logging.getLogger(__name__)
+
+
+def indent(text, amount, ch=' '):
+ padding = amount * ch
+ return ''.join(padding + line for line in text.splitlines(True))
+
+
+class xAppError(Exception):
+ def __init__(self, message, status_code):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message)
+ self.status_code = status_code
+
+
+class xApp():
+ def __init__(self, config_file, schema_file):
+ self.config_file = config_file
+ self.schema_file = schema_file
+
+ if 'xapp_name' not in self.config_file:
+ raise xAppError(
+ "xApp chart name not found. (Caused by: config-file.json does not contain xapp_name attribute.)", 500)
+
+ if 'version' not in self.config_file:
+ raise xAppError(
+ "xApp chart version not found. (Caused by: config-file.json does not contain version attribute.)", 500)
+
+ self.chart_name = self.config_file['xapp_name']
+ self.chart_version = self.config_file['version']
+ self.configmap_config_json_file = copy.deepcopy(self.config_file)
+ self.chart_workspace_path = settings.CHART_WORKSPACE_PATH + '/' + self.chart_name + '-' + self.chart_version
+ if os.path.exists(self.chart_workspace_path):
+ shutil.rmtree(self.chart_workspace_path)
+ os.makedirs(self.chart_workspace_path)
+ shutil.copytree(resource_filename( 'xapp_onboarder', 'resources/xapp-std'), self.chart_workspace_path + '/' + self.chart_name)
+ self.helm_client_path = settings.CHART_WORKSPACE_PATH + '/helm'
+ self.setup_helm()
+
+ def setup_helm(self):
+ if not os.path.isfile(settings.CHART_WORKSPACE_PATH + '/helm'):
+ log.info("Helm client missing. Trying to download it.")
+ helm_file_name = "helm-v{}-{}-amd64.tar.gz".format(settings.HELM_VERSION, platform.system().lower())
+ helm_download_link = "https://get.helm.sh/" + helm_file_name
+
+
+ try:
+ response = requests_retry_session().get(helm_download_link, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ error_message = "Download helm client failed. (Caused by: " + str(err)+")"
+ log.error(error_message)
+ raise xAppError(error_message, 500)
+ else:
+ if response.status_code != 200:
+ error_message = "Download helm chart failed. Helm repo return status code: "+ str(response.status_code) +" "+ response.content.decode("utf-8")
+ log.error(error_message)
+ raise xAppError(error_message, 500)
+
+ file_stream = io.BytesIO(response.content)
+
+ with tarfile.open(fileobj=file_stream) as tar:
+ helm_client = tar.extractfile(platform.system().lower() + "-amd64/helm")
+ with open(settings.CHART_WORKSPACE_PATH+'/helm', 'wb') as file:
+ file.write(helm_client.read())
+ st = os.stat(settings.CHART_WORKSPACE_PATH+'/helm')
+ os.chmod(settings.CHART_WORKSPACE_PATH+'/helm', st.st_mode | stat.S_IEXEC)
+
+
+
+
+ def recursive_convert_config_file(self, node_list=list()):
+ current_node = self.configmap_config_json_file
+ helm_value_path = '.Values'
+ for node in node_list:
+ current_node = current_node.get(node)
+ helm_value_path = helm_value_path + '.' + node
+
+ if type(current_node) is not dict:
+ raise TypeError("Recursive write was called on a leaf node.")
+
+ for item in current_node.keys():
+ if type(current_node.get(item)) is not dict:
+ current_node[item] = '{{ '+ helm_value_path +'.'+ item + ' | toJson }}'
+ else:
+ new_node_list = node_list.copy()
+ new_node_list.append(item)
+ self.recursive_convert_config_file(new_node_list)
+
+
+
+ def append_config_to_config_map(self):
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/appconfig.yaml', 'a') as outputfile:
+ self.recursive_convert_config_file()
+ config_file_json_text = json.dumps(self.configmap_config_json_file, indent=4)
+ indented_config_text = indent(config_file_json_text, 4)
+ indented_config_text = re.sub(r"\"{{", '{{', indented_config_text)
+ indented_config_text = re.sub(r"}}\"", '}}', indented_config_text)
+ outputfile.write(" config-file.json: |\n")
+ outputfile.write(indented_config_text)
+ outputfile.write("\n schema.json: |\n")
+ schema_json = json.dumps(self.schema_file, indent=4)
+ indented_schema_text = indent(schema_json, 4)
+ outputfile.write(indented_schema_text)
+
+
+ def add_probes_to_deployment(self):
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/deployment.yaml', 'a') as outputfile:
+
+ for probes in ['readinessProbe', 'livenessProbe']:
+ if self.configmap_config_json_file.get(probes):
+ probe_definition = self.configmap_config_json_file.get(probes)
+ probe_definition_yaml = yaml.dump(probe_definition)
+ indented_probe_definition_yaml = indent(probe_definition_yaml, 12)
+ indented_probe_definition_yaml = re.sub(r" \| toJson", '', indented_probe_definition_yaml)
+ indented_probe_definition_yaml = re.sub(r"'", '', indented_probe_definition_yaml)
+ outputfile.write(" "+probes+":\n")
+ outputfile.write(indented_probe_definition_yaml)
+
+
+
+ def append_config_to_values_yaml(self):
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/values.yaml', 'a') as outputfile:
+ yaml.dump(self.config_file, outputfile, default_flow_style=False)
+
+
+ def append_env_to_config_map(self):
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/appenv.yaml', 'a') as outputfile:
+ append = {}
+ if settings.DBAAS_MASTER_NAME:
+ master_name = settings.DBAAS_MASTER_NAME
+ service_host = settings.DBAAS_SERVICE_HOST
+ sentinel_port = settings.DBAAS_SERVICE_SENTINEL_PORT
+ if not service_host:
+ raise xAppError(
+ "Internal failure. Cannot find environment variable 'DBAAS_SERVICE_HOST'. (Caused by: Misconfiguration of temp deployment)", 500)
+ if not sentinel_port:
+ raise xAppError(
+ "Internal failure. Cannot find environment variable 'DBAAS_SERVICE_SENTINEL_PORT'. (Caused by: Misconfiguration of temp deployment)", 500)
+
+ append['DBAAS_MASTER_NAME'] = master_name
+ append['DBAAS_SERVICE_HOST'] = service_host
+ append['DBAAS_SERVICE_SENTINEL_PORT'] = sentinel_port
+ elif settings.DBAAS_SERVICE_HOST:
+ service_host = settings.DBAAS_SERVICE_HOST
+ service_port = settings.DBAAS_SERVICE_PORT
+ if not service_port:
+ raise xAppError(
+ "Internal failure. Cannot find environment variable 'DBAAS_SERVICE_PORT'. (Caused by: Misconfiguration of temp deployment)", 500)
+ append['DBAAS_SERVICE_HOST'] = service_host
+ append['DBAAS_SERVICE_PORT'] = service_port
+ else:
+ raise xAppError(
+ "Internal failure. Cannot find environment variable 'DBAAS_SERVICE_HOST' or 'DBAAS_MASTER_NAME'. (Caused by: Misconfiguration of temp deployment)",
+ 500)
+ output_yaml = yaml.dump(append)
+ indented_output_yaml = indent(output_yaml, 2)
+ outputfile.write(indented_output_yaml)
+
+
+ def change_chart_name_version(self):
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'r') as inputfile:
+ self.chart_yaml = yaml.load(inputfile, Loader=yaml.FullLoader)
+ self.chart_yaml['version'] = self.chart_version
+ self.chart_yaml['name'] = self.chart_name
+
+ with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'w') as outputfile:
+ yaml.dump(self.chart_yaml, outputfile, default_flow_style=False)
+
+
+ def helm_lint(self):
+ try:
+ process = subprocess.run([self.helm_client_path, "lint", self.chart_workspace_path + "/" + self.chart_name], stdout=PIPE, stderr=PIPE, check=True)
+
+ except OSError as err:
+ raise xAppError(
+ "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " + str(
+ err) + ")", 500)
+ except subprocess.CalledProcessError as err:
+ raise xAppError(
+ "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " +
+ err.stderr.decode("utf-8") + "\n" + err.stdout.decode("utf-8") + ")", 400)
+
+ def package_chart(self):
+ self.append_config_to_config_map()
+ self.append_config_to_values_yaml()
+ self.append_env_to_config_map()
+ self.add_probes_to_deployment()
+ self.change_chart_name_version()
+ self.helm_lint()
+ try:
+ process = subprocess.run([self.helm_client_path, "package", self.chart_workspace_path + "/" + self.chart_name, "-d"
+ ,self.chart_workspace_path,"--save=false"], stdout=PIPE, stderr=PIPE, check=True)
+
+ except OSError as err:
+ raise xAppError("xApp "+ self.chart_name+'-'+self.chart_version +" packaging failed. (Caused by: "+str(err) +")", 500)
+ except subprocess.CalledProcessError as err:
+ raise xAppError(
+ "xApp " + self.chart_name + '-' + self.chart_version + " packaging failed. (Caused by: " +
+ err.stderr.decode("utf-8") + ")", 500)
+
+
+
+ def distribute_chart(self):
+ try:
+ repo_manager.upload_chart(self)
+ except RepoManagerError as err:
+ raise xAppError( "xApp " + self.chart_name + '-' + self.chart_version + " distribution failed. (Caused by: " + str(err) + ")" , err.status_code)
+
--- /dev/null
+[loggers]
+keys=root, xapp_onboarder
+
+[handlers]
+keys=console
+
+[formatters]
+keys=simple
+
+[logger_root]
+level=DEBUG
+handlers=console
+
+[logger_xapp_onboarder]
+level=DEBUG
+handlers=console
+qualname=xapp_onboarder
+propagate=0
+
+[handler_console]
+class=StreamHandler
+level=DEBUG
+formatter=simple
+args=(sys.stdout,)
+
+[formatter_simple]
+format=%(asctime)s - %(levelname)s - %(message)s - %(name)s
+datefmt=
\ No newline at end of file
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import yaml
+import json
+from xapp_onboarder.server import settings
+import logging
+import requests
+import time
+from requests.adapters import HTTPAdapter
+from requests.packages.urllib3.util.retry import Retry
+
+log = logging.getLogger(__name__)
+
+def requests_retry_session(retries=3, backoff_factor=0.3, status_forcelist=(500, 502, 504, 400, 401, 409), session=None,):
+ session = session or requests.Session()
+ retry = Retry(
+ total=retries,
+ read=retries,
+ connect=retries,
+ backoff_factor=backoff_factor,
+ status_forcelist=status_forcelist,
+ )
+ adapter = HTTPAdapter(max_retries=retry)
+ session.mount('http://', adapter)
+ session.mount('https://', adapter)
+ return session
+
+
+
+class RepoManagerError(Exception):
+ def __init__(self, message, status_code):
+ # Call the base class constructor with the parameters it needs
+ super().__init__(message)
+ self.status_code = status_code
+
+
+class repoManager():
+ def __init__(self, repo_url):
+ self.repo_url = repo_url
+ self.__is_repo_ready__ = False
+ log.debug("Initialize connection to helm chart repo at "+self.repo_url)
+ t0 = time.time()
+ self.retry_session = requests_retry_session()
+ try:
+ response = self.retry_session.get(self.repo_url, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ t1 = time.time()
+ log.error('Failed to connect to helm chart repo ' + self.repo_url + ' after ' + str(
+ settings.HTTP_RETRY) + ' retries and ' + str(t1 - t0) + ' seconds. (Caused by: ' + err.__class__.__name__ + ')')
+ else:
+ self.__is_repo_ready__ = True
+
+
+
+ def is_repo_ready(self):
+ return self.__is_repo_ready__
+
+ def get_index(self):
+ try:
+ response = self.retry_session.get(self.repo_url +'/index.yaml', timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ raise RepoManagerError("Get helm repo index failed. (Caused by: " + str(err)+")", 500)
+ else:
+ if response.status_code != 200:
+ raise RepoManagerError("Get helm repo index failed. Helm repo return status code: {}, {}".format(response.status_code, response.content.decode("utf-8")))
+ return yaml.load(response.content, Loader=yaml.FullLoader)
+
+ def upload_chart(self, xapp):
+
+ xapp_chart_index = self.get_index()
+ found_xapp = False
+ for chart in xapp_chart_index.get('entries', {}).get(xapp.chart_name, []):
+ if chart['version'] == xapp.chart_version:
+ found_xapp = True
+
+ if found_xapp:
+ if settings.ALLOW_REDEPLOY:
+ self.delete_chart(xapp)
+ else:
+ raise RepoManagerError("Upload helm chart failed. Redeploy xApp helm chart is not allowed.", 400)
+
+ headers = {'Content-Type': 'application/json'}
+ chart_package_path = xapp.chart_workspace_path + '/' + xapp.chart_name + '-' + xapp.chart_version + '.tgz'
+ with open(chart_package_path, mode='rb') as filereader:
+ fileContent = filereader.read()
+
+ try:
+ response = self.retry_session.post(self.repo_url +'/api/charts', headers=headers, data=fileContent, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ raise RepoManagerError("Upload helm chart failed. (Caused by: " + str(err) + ")", 500)
+ else:
+ if response.status_code != 201:
+ raise RepoManagerError("Upload helm chart failed. Helm repo return status code: "+ str(response.status_code) +" "+ response.content.decode("utf-8"), response.status_code)
+
+
+ def delete_chart(self, xapp):
+
+ headers = {'Content-Type': 'application/json'}
+
+ try:
+ response = self.retry_session.delete(self.repo_url +'/api/charts/' + xapp.chart_name
+ + '/' + xapp.chart_version, headers=headers, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ raise RepoManagerError("Delete helm chart failed." + str(err), 500)
+ else:
+ if response.status_code != 200:
+ response_dict = json.loads(response.content)
+ if xapp.chart_name+'-'+xapp.chart_version+'.tgz' not in response_dict["error"]:
+ raise RepoManagerError("Delete helm chart failed. Helm repo return status code:" + str(response.status_code) +" "+ response.content.decode("utf-8"),response.status_code)
+
+
+ def get_xapp_list(self, xapp_chart_name=None):
+
+ request_path = self.repo_url+'/api/charts'
+ if xapp_chart_name:
+ request_path = request_path +'/' + xapp_chart_name
+
+ try:
+ response = self.retry_session.get(request_path, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ raise RepoManagerError("Get xApp charts list failed. (Caused by: " + str(err)+")", 500)
+ else:
+ if response.status_code != 200:
+ raise RepoManagerError("Get xApp charts list failed. Helm repo return status code: "+ str(response.status_code) +" "+ response.content.decode("utf-8"), response.status_code)
+ return json.loads(response.content)
+
+
+ def download_xapp_chart(self, xapp_chart_name, version):
+
+ request_path = self.repo_url+'/charts/'+xapp_chart_name+'-'+version+'.tgz'
+ try:
+ response = self.retry_session.get(request_path, timeout=settings.HTTP_TIME_OUT)
+ except Exception as err:
+ raise RepoManagerError("Download helm chart failed. (Caused by: " + str(err)+")", 500)
+ else:
+ if response.status_code != 200:
+ raise RepoManagerError("Download helm chart failed. Helm repo return status code: "+ str(response.status_code) +" "+ response.content.decode("utf-8"), response.status_code)
+ return response.content
+
+
+
+
+repo_manager = repoManager(settings.CHART_REPO_URL)
Expand the name of the chart.
*/}}
{{- define "ricxapp.name" -}}
- {{- default .Chart.Name .Values.ricxapp.name | trunc 63 | trimSuffix "-" -}}
+ {{- default .Chart.Name .Values.name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
{{- define "ricxapp.fullname" -}}
{{- $name := ( include "ricxapp.name" . ) -}}
{{- $fullname := ( printf "%s-%s" .Release.Namespace $name ) -}}
- {{- default $fullname .Values.ricxapp.fullname | trunc 63 | trimSuffix "-" -}}
+ {{- default $fullname .Values.fullname | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
{{- printf "container-%s" $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
+
{{- define "ricxapp.imagepullsecret" -}}
- {{- printf "docker-reg-cred" -}}
-{{- end -}}
+ {{- $reponame := .repo -}}
+ {{- $postfix := $reponame | replace "." "-" | replace ":" "-" | replace "/" "-" | trunc 63 | trimSuffix "-" -}}
+ {{- printf "secret-%s" $postfix -}}
+{{- end -}}
\ No newline at end of file
# 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. #
-################################################################################
-
-default_helm_repo=https://$(hostname):32080/helm
-
-default_docker_registry=docker-entry
-
+################################################################################'
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "ricxapp.configmapname" . }}-appconfig
+data:
metadata:
name: {{ include "ricxapp.configmapname" . }}-appenv
data:
- {{- if .Values.ricxapp.appenv }}
- {{- toYaml .Values.ricxapp.appenv | nindent 2 }}
+ {{- if .Values.appenv }}
+ {{- toYaml .Values.appenv | nindent 2 }}
{{- end }}
- DBAAS_SERVICE_HOST: "{{ .Values.ricplt.dbaasService }}"
- DBAAS_SERVICE_PORT: "6379"
- DBAAS_PORT_6379_TCP_ADDR: "{{ .Values.ricplt.dbaasService }}"
- DBAAS_PORT_6379_TCP_PORT: "6379"
- RMR_RTG_SVC: "{{ .Values.ricxapp.service.rmr.route.port }}"
+ RMR_RTG_SVC: "4561"
+ XAPP_DESCRIPTOR_PATH: {{ .Values.appconfig.path }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
- replicas: {{ .Values.ricxapp.replicaCount }}
+ replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "ricxapp.namespace" . }}-{{ include "ricxapp.name" . }}
spec:
hostname: {{ include "ricxapp.name" . }}
imagePullSecrets:
- - name: {{ include "ricxapp.imagepullsecret" . }}
+{{- range .Values.containers }}
+ - name: {{ .image.registry | replace "." "-" | replace ":" "-" | replace "/" "-" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+ volumes:
+ - name: config-volume
+ configMap:
+ name: {{ include "ricxapp.configmapname" . }}-appconfig
containers:
- - name: {{ include "ricxapp.containername" . }}
- image: "{{ .Values.ricxapp.image.repository }}/{{ .Values.ricxapp.image.name }}:{{ .Values.ricxapp.image.tag }}"
- imagePullPolicy: {{ .Values.ricxapp.image.pullPolicy }}
+{{- range .Values.containers }}
+ - name: {{ .name }}
+ image: "{{ .image.registry }}/{{ .image.name }}:{{ .image.tag }}"
+ {{- if .command }}
+ command: [{{ .command }}]
+ {{- end}}
+ imagePullPolicy: {{ $.Values.image_pull_policy }}
+ {{- if .ports }}
ports:
- - name: http
- containerPort: {{ .Values.ricxapp.service.http.containerPort }}
+ {{- if .ports.http }}
+ - name: http_{{ .name }}
+ containerPort: {{ .ports.http }}
protocol: TCP
- - name: rmrroute
- containerPort: {{ .Values.ricxapp.service.rmr.route.port }}
+ {{- end }}
+ {{- if .ports.rmr_data }}
+ - name: rmrdata_{{ .name }}
+ containerPort: {{ .ports.rmr_data }}
protocol: TCP
- - name: rmrdata
- containerPort: {{ .Values.ricxapp.service.rmr.data.port }}
+ {{- end }}
+ {{- if .ports.rmr_route }}
+ - name: rmrroute_{{ .name }}
+ containerPort: {{ .ports.rmr_route }}
protocol: TCP
+ {{- end }}
+ {{- end }}
+{{- end }}
volumeMounts:
- name: config-volume
- mountPath: {{ .Values.ricxapp.appconfig.path }}
+ mountPath: {{ .Values.appconfig.path }}
envFrom:
- configMapRef:
name: {{ include "ricxapp.configmapname" . }}-appenv
- {{- if .Values.ricxapp.livenessProbe }}
- livenessProbe:
- {{- .Values.ricxapp.livenessProbe | nindent 12 -}}
- {{ end }}
- {{- if .Values.ricxapp.readinessProbe }}
- readinessProbe:
- {{- .Values.ricxapp.readinessProbe | nindent 12 -}}
- {{ end }}
restartPolicy: Always
- volumes:
- - name: config-volume
- configMap:
- name: {{ include "ricxapp.configmapname" . }}-appconfig
spec:
type: ClusterIP
ports:
- - port: {{ .Values.ricxapp.service.http.port }}
- targetPort: http
+ {{- range .Values.containers }}
+ {{- if .ports }}
+ {{- if .ports.http }}
+ - port: {{ .ports.http }}
+ targetPort: http_{{ .name }}
protocol: TCP
- name: http
+ name: http_{{ .name }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
selector:
app: {{ include "ricxapp.namespace" . }}-{{ include "ricxapp.name" . }}
release: {{ .Release.Name }}
spec:
type: ClusterIP
ports:
- - port: {{ .Values.ricxapp.service.rmr.data.port }}
- targetPort: rmrdata
+ {{- range .Values.containers }}
+ {{- if .ports }}
+ {{- if .ports.rmr_data }}
+ - port: {{ .ports.rmr_data }}
+ targetPort: rmrdata_{{ .name }}
protocol: TCP
- name: rmrdata
- - port: {{ .Values.ricxapp.service.rmr.route.port }}
- targetPort: rmrroute
+ name: rmrdata_{{ .name }}
+ {{- end }}
+ {{- if .ports.rmr_route }}
+ - port: {{ .ports.rmr_route }}
+ targetPort: rmrroute_{{ .name }}
protocol: TCP
- name: rmrroute
+ name: rmrroute_{{ .name }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
selector:
app: {{ include "ricxapp.namespace" . }}-{{ include "ricxapp.name" . }}
release: {{ .Release.Name }}
--- /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. #
+################################################################################'
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+
+# Location of the xApp config files injected from the xApp descriptor
+appconfig:
+ path: /opt/ric/config
+
+# Number of replica
+replicaCount: 1
+
+# Image pulling policy
+image_pull_policy: IfNotPresent
+
+# Environment variables that will be injected
+appenv: {}
+
+# Liveness probe definition. If empty, liveness probe will be disabled
+livenessProbe: {}
+
+# Readiness probe definition. If empty, readiness probe will be disabled
+readinessProbe: {}
+
+# Instance name. If empty, chart name will be used
+name: {}
+
+# Full instance name. If empty, full name will be constructed from name
+fullname: {}
+############### The following are from the xApp descriptor ###############
+
+
+
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+
+import fire
+import json
+import os
+import pkg_resources
+import logging.config
+
+from xapp_onboarder.api.charts import get_charts_list, download_chart_package, download_values_yaml
+from xapp_onboarder.repo_manager.repo_manager import repo_manager
+from xapp_onboarder.api.onboard import onboard, download_config_and_schema_and_onboard
+
+log = logging.getLogger(__name__)
+
+
+class cli():
+ """This is the cli tool for xapp_onboarder."""
+
+ def get_charts_list(self, xapp_chart_name=''):
+ """Get the list of all onboarded xApps. To show all version of an xApp, use --xapp_chart_name to
+ specify the name."""
+ message, status = get_charts_list(xapp_chart_name=xapp_chart_name)
+
+ return json.dumps(message, indent=4, sort_keys=True)
+
+ def download_helm_chart(self, xapp_chart_name, version, output_path="."):
+ """Download the helm chart package of an xApp. Specify xApp name with --xapp_chart_name, version with
+ --version. Optionally use --output_path to specify the path to save the file."""
+ content, status = download_chart_package(xapp_chart_name=xapp_chart_name, version=version)
+
+ if status != 200:
+ return json.dumps(content, indent=4, sort_keys=True)
+
+ try:
+ if os.path.isabs(output_path):
+ path = output_path + '/' + xapp_chart_name + '-' + version + '.tgz'
+ else:
+ path = os.getcwd() + '/' + output_path + '/' + xapp_chart_name + '-' + version + '.tgz'
+
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path))
+
+ with open(path, 'wb') as outputfile:
+ outputfile.write(content)
+ except Exception as err:
+ return err
+ return {"status": "OK"}
+
+ def download_values_yaml(self, xapp_chart_name, version, output_path="."):
+ """Download the values.yaml file that can be used to override parameters at runtime. Specify xApp name with
+ --xapp_chart_name, version with --version. Optionally use --output_path to specify the path to save the file.
+ """
+ content, status = download_values_yaml(xapp_chart_name=xapp_chart_name, version=version)
+
+ if status != 200:
+ return json.dumps(content, indent=4, sort_keys=True)
+
+ try:
+ if os.path.isabs(output_path):
+ path = output_path + '/values.yaml'
+ else:
+ path = os.getcwd() + '/' + output_path + '/values.yaml'
+
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path))
+
+ with open(path, 'wb') as outputfile:
+ outputfile.write(content)
+ except Exception as err:
+ return err
+ return {"status": "OK"}
+
+ def health(self):
+ """Health check. If xapp onboarder is not ready, it return false."""
+ return repo_manager.is_repo_ready()
+
+ def onboard(self, config_file_path, shcema_file_path):
+ """Onboard an xApp with local descriptor files. Use --config_file_path to specify the path to
+ config-file.json file, --shcema_file_path to specify the path to schema.json file. """
+ try:
+ with open(config_file_path, 'r') as inputfile:
+ config_file = json.load(inputfile)
+
+ with open(shcema_file_path, 'r') as inputfile:
+ schema_file = json.load(inputfile)
+
+ except Exception as err:
+ return err
+
+ message, status = onboard(config_file, schema_file)
+ return json.dumps(message, indent=4, sort_keys=True)
+
+ def download_and_onboard(self, config_file_url, schema_file_url):
+ """Onboard an xApp with URLs pointing to the xApp descriptor files. Use --config_file_url to specify the
+ config-file.json URL, --schema_file_url to specify the schema.json URL. """
+ message, status = download_config_and_schema_and_onboard(config_file_url, schema_file_url)
+ return json.dumps(message, indent=4, sort_keys=True)
+
+def run():
+ fire.Fire(cli(), name='xapp_onboarder')
+
+if __name__ == "__main__":
+ logger_config = pkg_resources.resource_filename("xapp_onboarder", 'logging.conf')
+ logging.config.fileConfig(logger_config)
+ run()
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+import pkg_resources
+import logging.config
+from xapp_onboarder.server import settings
+import logging
+from flask import Flask, Blueprint
+from xapp_onboarder.api.api_reference import api
+from xapp_onboarder.api.endpoints.onboard_ep import ns as onboard_ns
+from xapp_onboarder.api.endpoints.health_check_ep import ns as health_check_ns
+from xapp_onboarder.api.endpoints.charts_ep import ns as charts_ns
+from xapp_onboarder.helm_controller.artifacts_manager import artifacts_manager
+
+log = logging.getLogger(__name__)
+
+class server(object):
+ def __init__(self):
+ self.app = Flask(__name__)
+ self.app.config['SERVER_NAME'] = settings.FLASK_SERVER_NAME
+ self.app.config['SWAGGER_UI_DOC_EXPANSION'] = settings.RESTPLUS_SWAGGER_UI_DOC_EXPANSION
+ self.app.config['RESTPLUS_VALIDATE'] = settings.RESTPLUS_VALIDATE
+ self.app.config['RESTPLUS_MASK_SWAGGER'] = settings.RESTPLUS_MASK_SWAGGER
+ self.app.config['ERROR_404_HELP'] = settings.RESTPLUS_ERROR_404_HELP
+ self.api = api
+ self.api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
+ self.api.init_app(self.api_bp)
+ self.api.add_namespace(onboard_ns)
+ self.api.add_namespace(health_check_ns)
+ self.api.add_namespace(charts_ns)
+ self.app.register_blueprint(self.api_bp)
+ self.artifacts_manager = artifacts_manager()
+ self.artifacts_manager.start()
+
+ def run(self):
+ log.info('>>>>> Starting development xapp_onboarder at http://{}/api/v1/ <<<<<'.format(self.app.config['SERVER_NAME']))
+ self.app.run(debug=settings.FLASK_DEBUG)
+
+if __name__ == "__main__":
+ logger_config = pkg_resources.resource_filename("xapp_onboarder", 'logging.conf')
+ logging.config.fileConfig(logger_config)
+ server().run()
--- /dev/null
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+
+import os
+
+# Flask settings
+FLASK_SERVER_NAME = os.environ.get('FLASK_SERVER_NAME') or '0.0.0.0:8888'
+FLASK_DEBUG = os.environ.get('FLASK_DEBUG') or True # Do not use debug mode in production
+
+# Flask-Restplus settings
+RESTPLUS_SWAGGER_UI_DOC_EXPANSION = os.environ.get('RESTPLUS_SWAGGER_UI_DOC_EXPANSION') or 'list'
+RESTPLUS_VALIDATE = os.environ.get('RESTPLUS_VALIDATE') or True
+RESTPLUS_MASK_SWAGGER = os.environ.get('RESTPLUS_MASK_SWAGGER') or False
+RESTPLUS_ERROR_404_HELP = os.environ.get('RESTPLUS_ERROR_404_HELP') or False
+
+# xapp_onboarder settings
+CHART_WORKSPACE_PATH = os.environ.get('CHART_WORKSPACE_PATH') or '/tmp/xapp_onboarder'
+CHART_REPO_URL = os.environ.get('CHART_REPO_URL') or 'http://0.0.0.0:8080'
+HTTP_TIME_OUT = os.environ.get('HTTP_TIME_OUT') or 10
+HELM_VERSION = os.environ.get('HELM_VERSION') or '2.12.3'
+HTTP_RETRY = os.environ.get('HTTP_RETRY') or 3
+ALLOW_REDEPLOY = os.environ.get('ALLOW_REDEPLOY') or True
+CHART_WORKSPACE_SIZE = os.environ.get('CHART_WORKSPACE_SIZE') or '500 MB'
+MOCK_TEST_MODE = os.environ.get('MOCK_TEST_MODE') or False
+MOCK_TEST_HELM_REPO_TEMP_DIR = os.environ.get('MOCK_TEST_HELM_REPO_TEMP_DIR') or '/tmp/mock_helm_repo'
+
+
+# Environment variables that will be passed into xApp
+DBAAS_MASTER_NAME = os.environ.get('DBAAS_MASTER_NAME')
+DBAAS_SERVICE_HOST = os.environ.get('DBAAS_SERVICE_HOST')
+DBAAS_SERVICE_SENTINEL_PORT = os.environ.get('DBAAS_SERVICE_SENTINEL_PORT')
+DBAAS_SERVICE_PORT = os.environ.get('DBAAS_SERVICE_PORT')
--- /dev/null
+#!/usr/bin/env python3
+################################################################################
+# Copyright (c) 2020 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. #
+################################################################################
+import logging.config
+import pkg_resources
+from xapp_onboarder.server.server import server
+
+if __name__ == "__main__":
+
+ logger_config = pkg_resources.resource_filename("xapp_onboarder", 'logging.conf')
+ logging.config.fileConfig(logger_config)
+ server().run()
+