Add OpenStack fcaps module 89/78989/3
authorBin Yang <bin.yang@windriver.com>
Fri, 22 Feb 2019 08:46:53 +0000 (08:46 +0000)
committerBin Yang <bin.yang@windriver.com>
Wed, 27 Feb 2019 04:46:55 +0000 (04:46 +0000)
Change-Id: Ie05808a199edf1203ed0c7663031c7065dcb1512
Issue-ID: MULTICLOUD-499
Signed-off-by: Bin Yang <bin.yang@windriver.com>
40 files changed:
fcaps/.gitignore [new file with mode: 0644]
fcaps/README.md [new file with mode: 0644]
fcaps/assembly.xml [new file with mode: 0644]
fcaps/docker/Dockerfile [new file with mode: 0644]
fcaps/docker/build_image.sh [new file with mode: 0644]
fcaps/fcaps/__init__.py [new file with mode: 0644]
fcaps/fcaps/celery.py [new file with mode: 0644]
fcaps/fcaps/middleware.py [new file with mode: 0644]
fcaps/fcaps/pub/__init__.py [new file with mode: 0644]
fcaps/fcaps/pub/config/__init__.py [new file with mode: 0644]
fcaps/fcaps/pub/config/config.py [new file with mode: 0644]
fcaps/fcaps/pub/config/log.yml [new file with mode: 0644]
fcaps/fcaps/samples/__init__.py [new file with mode: 0644]
fcaps/fcaps/samples/tests_sample.py [new file with mode: 0644]
fcaps/fcaps/samples/urls.py [new file with mode: 0644]
fcaps/fcaps/samples/views.py [new file with mode: 0644]
fcaps/fcaps/settings.py [new file with mode: 0644]
fcaps/fcaps/urls.py [new file with mode: 0644]
fcaps/fcaps/vesagent/__init__.py [new file with mode: 0644]
fcaps/fcaps/vesagent/event_domain/__init__.py [new file with mode: 0644]
fcaps/fcaps/vesagent/event_domain/fault_vm.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tasks.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tests/__init__.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tests/tests_fault_vm.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tests/tests_tasks.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tests/tests_vesagent_ctrl.py [new file with mode: 0644]
fcaps/fcaps/vesagent/tests/tests_vespublish.py [new file with mode: 0644]
fcaps/fcaps/vesagent/vesagent_ctrl.py [new file with mode: 0644]
fcaps/fcaps/vesagent/vespublish.py [new file with mode: 0644]
fcaps/fcaps/wsgi.py [new file with mode: 0644]
fcaps/initialize.sh [new file with mode: 0644]
fcaps/logs/empty.txt [new file with mode: 0644]
fcaps/manage.py [new file with mode: 0644]
fcaps/mvn-phase-script.sh [new file with mode: 0755]
fcaps/pom.xml [new file with mode: 0644]
fcaps/requirements.txt [new file with mode: 0644]
fcaps/run.sh [new file with mode: 0644]
fcaps/stop.sh [new file with mode: 0644]
fcaps/test-requirements.txt [new file with mode: 0644]
fcaps/tox.ini [new file with mode: 0644]

diff --git a/fcaps/.gitignore b/fcaps/.gitignore
new file mode 100644 (file)
index 0000000..17e6ddd
--- /dev/null
@@ -0,0 +1,12 @@
+.project
+.classpath
+.settings/
+.checkstyle
+target/
+logs/*.log
+*.pyc
+.tox
+.coverage
+htmlcov/
+coverage.xml
+test-reports/
diff --git a/fcaps/README.md b/fcaps/README.md
new file mode 100644 (file)
index 0000000..8005e17
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+# Micro service of MultiCloud plugin for Wind River Titanium Cloud.
diff --git a/fcaps/assembly.xml b/fcaps/assembly.xml
new file mode 100644 (file)
index 0000000..bfa14be
--- /dev/null
@@ -0,0 +1,77 @@
+<!--
+ Copyright (c) 2017-2019 Wind River Systems, Inc.
+
+ 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.
+-->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" 
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+    <id>fcaps</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>fcaps</directory>
+            <outputDirectory>/fcaps</outputDirectory>
+            <includes>
+                <include>**/*.py</include>
+                <include>**/*.json</include>
+                <include>**/*.xml</include>
+                <include>**/*.wsdl</include>
+                <include>**/*.xsd</include>
+                <include>**/*.bpel</include>
+                <include>**/*.yml</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>../share</directory>
+            <outputDirectory>/lib/share</outputDirectory>
+            <includes>
+                <include>**/*.py</include>
+                <include>**/*.json</include>
+                <include>**/*.xml</include>
+                <include>**/*.wsdl</include>
+                <include>**/*.xsd</include>
+                <include>**/*.bpel</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>logs</directory>
+            <outputDirectory>/logs</outputDirectory>
+            <includes>
+                <include>*.txt</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>docker</directory>
+            <outputDirectory>/docker</outputDirectory>
+            <includes>
+                <include>*.sh</include>
+                <include>Dockerfile</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>.</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>*.py</include>
+                <include>*.txt</include>
+                <include>*.sh</include>
+                <include>*.ini</include>
+                <include>*.md</include>
+            </includes>
+        </fileSet>
+    </fileSets>
+    <baseDirectory>fcaps</baseDirectory>
+</assembly>
diff --git a/fcaps/docker/Dockerfile b/fcaps/docker/Dockerfile
new file mode 100644 (file)
index 0000000..8a3c6b2
--- /dev/null
@@ -0,0 +1,36 @@
+FROM python:2
+
+ARG HTTP_PROXY=${HTTP_PROXY}
+ARG HTTPS_PROXY=${HTTPS_PROXY}
+
+ENV http_proxy $HTTP_PROXY
+ENV https_proxy $HTTPS_PROXY
+
+ENV MSB_ADDR "127.0.0.1"
+ENV MSB_PORT "80"
+ENV AAI_ADDR "aai.api.simpledemo.openecomp.org"
+ENV AAI_PORT "8443"
+ENV AAI_SCHEMA_VERSION "v13"
+ENV AAI_USERNAME "AAI"
+ENV AAI_PASSWORD "AAI"
+
+EXPOSE 9011
+
+RUN groupadd -r onap && useradd -r -g onap onap
+# COPY ./ /opt/fcaps/
+
+RUN apt-get update && \
+    apt-get install -y memcached && \
+    apt-get install -y unzip && \
+    cd /opt/ && \
+    wget -O multicloud-openstack-fcaps.zip "https://nexus.onap.org/service/local/artifact/maven/redirect?r=snapshots&g=org.onap.multicloud.openstack&a=multicloud-openstack-fcaps&e=zip&v=1.3.0-SNAPSHOT" && \
+    unzip -q -o -B multicloud-openstack-fcaps.zip && \
+    chmod +x /opt/fcaps/*.sh && \
+    rm -f multicloud-openstack-fcaps.zip && \
+    pip install -r /opt/fcaps/requirements.txt && \
+    chown onap:onap /opt/fcaps -R
+
+USER onap
+
+WORKDIR /opt/fcaps
+CMD /bin/sh -c /opt/fcaps/run.sh
diff --git a/fcaps/docker/build_image.sh b/fcaps/docker/build_image.sh
new file mode 100644 (file)
index 0000000..050777b
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/bash
+DIRNAME=`dirname $0`
+DOCKER_BUILD_DIR=`cd $DIRNAME/; pwd`
+echo "DOCKER_BUILD_DIR=${DOCKER_BUILD_DIR}"
+cd ${DOCKER_BUILD_DIR}
+
+BUILD_ARGS="--no-cache"
+ORG="onap"
+VERSION="1.3.0-SNAPSHOT"
+STAGING="1.3.0-STAGING"
+PROJECT="multicloud"
+IMAGE="openstack-fcaps"
+DOCKER_REPOSITORY="nexus3.onap.org:10003"
+IMAGE_NAME="${DOCKER_REPOSITORY}/${ORG}/${PROJECT}/${IMAGE}"
+
+if [ $HTTP_PROXY ]; then
+    BUILD_ARGS+=" --build-arg HTTP_PROXY=${HTTP_PROXY}"
+fi
+if [ $HTTPS_PROXY ]; then
+    BUILD_ARGS+=" --build-arg HTTPS_PROXY=${HTTPS_PROXY}"
+fi
+
+function build_image {
+    docker build ${BUILD_ARGS} -t ${IMAGE_NAME}:${VERSION} -t ${IMAGE_NAME}:latest -t ${IMAGE_NAME}:${STAGING} .
+}
+
+function push_image {
+    docker push ${IMAGE_NAME}:${VERSION}
+    docker push ${IMAGE_NAME}:latest
+    docker push ${IMAGE_NAME}:${STAGING}
+}
+
+build_image
+push_image
diff --git a/fcaps/fcaps/__init__.py b/fcaps/fcaps/__init__.py
new file mode 100644 (file)
index 0000000..899993f
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, unicode_literals
+
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app
+
+__all__ = ['celery_app']
diff --git a/fcaps/fcaps/celery.py b/fcaps/fcaps/celery.py
new file mode 100644 (file)
index 0000000..ca3b2e5
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, unicode_literals
+import os
+from celery import Celery
+import logging
+
+# set the default Django settings module for the 'celery' program.
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fcaps.settings')
+
+app = Celery('fcaps')
+
+# Using a string here means the worker doesn't have to serialize
+# the configuration object to child processes.
+# - namespace='CELERY' means all celery-related configuration keys
+#   should have a `CELERY_` prefix.
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+# Load task modules from all registered Django app configs.
+app.autodiscover_tasks()
+
+logger = logging.getLogger(__name__)
+
+
+@app.task(bind=True)
+def debug_task(self):
+    logger.debug("self.request")
diff --git a/fcaps/fcaps/middleware.py b/fcaps/fcaps/middleware.py
new file mode 100644 (file)
index 0000000..e6804f0
--- /dev/null
@@ -0,0 +1,64 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 uuid
+from django.conf import settings
+from onaplogging.mdcContext import MDC
+
+FORWARDED_FOR_FIELDS = ["HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_HOST",
+                        "HTTP_X_FORWARDED_SERVER"]
+
+
+class LogContextMiddleware(object):
+
+    #  the last IP behind multiple proxies,  if no exist proxies
+    #  get local host ip.
+    def _getLastIp(self, request):
+
+        ip = ""
+        try:
+            for field in FORWARDED_FOR_FIELDS:
+                if field in request.META:
+                    if ',' in request.META[field]:
+                        parts = request.META[field].split(',')
+                        ip = parts[-1].strip().split(":")[0]
+                    else:
+                        ip = request.META[field].split(":")[0]
+
+            if ip == "":
+                ip = request.META.get("HTTP_HOST").split(":")[0]
+
+        except Exception:
+            pass
+
+        return ip
+
+    def process_request(self, request):
+        # fetch propageted Id from other component. if do not fetch id,
+        # generate one.
+        ReqeustID = request.META.get("HTTP_X_TRANSACTIONID", None)
+        if ReqeustID is None:
+            ReqeustID = str(uuid.uuid3(uuid.NAMESPACE_URL, settings.MULTIVIM_VERSION))
+        MDC.put("requestID", ReqeustID)
+        # generate the reqeust id
+        InvocationID = str(uuid.uuid4())
+        MDC.put("invocationID", InvocationID)
+        MDC.put("serviceName", settings.MULTIVIM_VERSION)
+        MDC.put("serviceIP", self._getLastIp(request))
+        return None
+
+    def process_response(self, request, response):
+
+        MDC.clear()
+        return response
diff --git a/fcaps/fcaps/pub/__init__.py b/fcaps/fcaps/pub/__init__.py
new file mode 100644 (file)
index 0000000..d0f2d86
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/pub/config/__init__.py b/fcaps/fcaps/pub/config/__init__.py
new file mode 100644 (file)
index 0000000..d0f2d86
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/pub/config/config.py b/fcaps/fcaps/pub/config/config.py
new file mode 100644 (file)
index 0000000..d0f2d86
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/pub/config/log.yml b/fcaps/fcaps/pub/config/log.yml
new file mode 100644 (file)
index 0000000..ad27b50
--- /dev/null
@@ -0,0 +1,38 @@
+version: 1
+disable_existing_loggers: False
+
+loggers:
+    fcaps:
+      handlers: [fcaps_handler]
+      level: "DEBUG"
+      propagate: False
+    newton_base:
+      handlers: [fcaps_handler]
+      level: "DEBUG"
+      propagate: False
+    common:
+      handlers: [fcaps_handler]
+      level: "DEBUG"
+      propagate: False
+    starlingx_base:
+      handlers: [fcaps_handler]
+      level: "DEBUG"
+      propagate: False
+handlers:
+    fcaps_handler:
+        level: "DEBUG"
+        class: "logging.handlers.RotatingFileHandler"
+        filename: "/var/log/onap/multicloud/openstack/fcaps/fcaps.log"
+        formatter: "mdcFormat"
+        maxBytes: 52428800
+        backupCount: 10
+formatters:
+    standard:
+        format: "%(asctime)s|||||%(name)s||%(thread)||%(funcName)s||%(levelname)s||%(message)s"
+    mdcFormat:
+        format: "%(asctime)s|||||%(name)s||%(thread)s||%(funcName)s||%(levelname)s||%(message)s||||%(mdc)s \t"
+        mdcfmt: "{requestID} {invocationID} {serviceName} {serviceIP}"
+        datefmt: "%Y-%m-%d %H:%M:%S"
+        (): onaplogging.mdcformatter.MDCFormatter
+
+
diff --git a/fcaps/fcaps/samples/__init__.py b/fcaps/fcaps/samples/__init__.py
new file mode 100644 (file)
index 0000000..d0f2d86
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/samples/tests_sample.py b/fcaps/fcaps/samples/tests_sample.py
new file mode 100644 (file)
index 0000000..f010d63
--- /dev/null
@@ -0,0 +1,31 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 unittest
+from django.test import Client
+from rest_framework import status
+
+
+class SampleViewTest(unittest.TestCase):
+    def setUp(self):
+        self.client = Client()
+
+    def tearDown(self):
+        pass
+
+    def test_sample(self):
+        response = self.client.get("/samples/")
+        self.assertEqual(status.HTTP_200_OK, response.status_code, response.content)
+        resp_data = response.json()
+        self.assertEqual({"status": "active"}, resp_data)
diff --git a/fcaps/fcaps/samples/urls.py b/fcaps/fcaps/samples/urls.py
new file mode 100644 (file)
index 0000000..3a9b758
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 django.conf.urls import url
+from fcaps.samples import views
+
+urlpatterns = [
+    url(r'^samples/?$', views.SampleList.as_view()), ]
diff --git a/fcaps/fcaps/samples/views.py b/fcaps/fcaps/samples/views.py
new file mode 100644 (file)
index 0000000..94ab28f
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 rest_framework.views import APIView
+from rest_framework.response import Response
+
+logger = logging.getLogger(__name__)
+
+
+class SampleList(APIView):
+    """
+    List all samples.
+    """
+    def get(self, request, format=None):
+        logger.debug("get")
+        return Response({"status": "active"})
diff --git a/fcaps/fcaps/settings.py b/fcaps/fcaps/settings.py
new file mode 100644 (file)
index 0000000..f7306b7
--- /dev/null
@@ -0,0 +1,141 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 sys
+
+from logging import config
+from onaplogging import monkey
+monkey.patch_all()
+
+
+CACHE_EXPIRATION_TIME = 3600
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '3o-wney!99y)^h3v)0$j16l9=fdjxcb+a8g+q3tfbahcnu2b0o'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+# DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+]
+
+MIDDLEWARE_CLASSES = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'fcaps.middleware.LogContextMiddleware',
+]
+
+ROOT_URLCONF = 'fcaps.urls'
+
+WSGI_APPLICATION = 'fcaps.wsgi.application'
+
+REST_FRAMEWORK = {
+    'DEFAULT_RENDERER_CLASSES': (
+        'rest_framework.renderers.JSONRenderer',
+    ),
+
+    'DEFAULT_PARSER_CLASSES': (
+        'rest_framework.parsers.JSONParser',
+        'rest_framework.parsers.MultiPartParser',
+    )
+}
+
+TIME_ZONE = 'UTC'
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.6/howto/static-files/
+
+STATIC_URL = '/static/'
+
+
+DEFAULT_MSB_ADDR = "127.0.0.1"
+DEFAULT_CACHE_BACKEND_LOCATION = '127.0.0.1:11211'
+
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+        'LOCATION': DEFAULT_CACHE_BACKEND_LOCATION,
+    }
+}
+
+# [MSB]
+MSB_SERVICE_ADDR = os.environ.get('MSB_ADDR', DEFAULT_MSB_ADDR)
+MSB_SERVICE_PORT = os.environ.get('MSB_PORT', "80")
+
+# [Multicloud]
+MULTICLOUD_PREFIX = "http://%s:%s/api/multicloud-fcaps/v0" % (
+    MSB_SERVICE_ADDR, MSB_SERVICE_PORT)
+
+MULTICLOUD_API_V1_PREFIX = "http://%s:%s/api/multicloud-fcaps/v1" % (
+    MSB_SERVICE_ADDR, MSB_SERVICE_PORT)
+
+# [A&AI]
+AAI_ADDR = os.environ.get('AAI_ADDR', "aai.api.simpledemo.openecomp.org")
+AAI_PORT = os.environ.get('AAI_PORT', "8443")
+AAI_SERVICE_URL = 'https://%s:%s/aai' % (AAI_ADDR, AAI_PORT)
+AAI_SCHEMA_VERSION = os.environ.get('AAI_SCHEMA_VERSION', "v13")
+AAI_USERNAME = os.environ.get('AAI_USERNAME', "AAI")
+AAI_PASSWORD = os.environ.get('AAI_PASSWORD', "AAI")
+
+AAI_BASE_URL = "%s/%s" % (AAI_SERVICE_URL, AAI_SCHEMA_VERSION)
+
+MULTICLOUD_APP_ID = 'MultiCloud-FCAPS'
+
+# [IMAGE LOCAL PATH]
+ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+OPENSTACK_VERSION = "fcaps"
+MULTIVIM_VERSION = "multicloud-" + OPENSTACK_VERSION
+
+
+LOGGING_CONFIG = None
+# yaml configuration of logging
+LOGGING_FILE = os.path.join(BASE_DIR, 'fcaps/pub/config/log.yml')
+config.yamlConfig(filepath=LOGGING_FILE, watchDog=True)
+
+if 'test' in sys.argv:
+
+    # LOGGING['handlers']['titanium_cloud_handler']['filename'] = 'logs/fcaps.log'
+
+    REST_FRAMEWORK = {}
+    import platform
+
+    if platform.system() == 'Linux':
+        TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
+        TEST_OUTPUT_VERBOSE = True
+        TEST_OUTPUT_DESCRIPTIONS = True
+        TEST_OUTPUT_DIR = 'test-reports'
diff --git a/fcaps/fcaps/urls.py b/fcaps/fcaps/urls.py
new file mode 100644 (file)
index 0000000..3662ed8
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 django.conf.urls import include, url
+from fcaps.vesagent import vesagent_ctrl
+
+urlpatterns = [
+    url(r'^', include('fcaps.samples.urls')),
+
+    url(r'^api/multicloud-fcaps/v0/(?P<vimid>[0-9a-zA-Z_-]+)/vesagent/?$',
+        vesagent_ctrl.VesAgentCtrl.as_view()),
+
+    url(r'^api/multicloud-fcaps/v1/(?P<cloud_owner>[0-9a-zA-Z_-]+)/(?P<cloud_region_id>[0-9a-zA-Z_-]+)/vesagent/?$',
+        vesagent_ctrl.APIv1VesAgentCtrl.as_view()),
+
+]
diff --git a/fcaps/fcaps/vesagent/__init__.py b/fcaps/fcaps/vesagent/__init__.py
new file mode 100644 (file)
index 0000000..46a7d44
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/vesagent/event_domain/__init__.py b/fcaps/fcaps/vesagent/event_domain/__init__.py
new file mode 100644 (file)
index 0000000..46a7d44
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/vesagent/event_domain/fault_vm.py b/fcaps/fcaps/vesagent/event_domain/fault_vm.py
new file mode 100644 (file)
index 0000000..11db643
--- /dev/null
@@ -0,0 +1,325 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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
+import uuid
+
+from django.conf import settings
+from fcaps.vesagent.vespublish import publishAnyEventToVES
+from common.utils import restcall
+# from common.msapi.helper import Helper as helper
+
+import datetime
+import time
+
+logger = logging.getLogger(__name__)
+
+
+def get_epoch_now_usecond():
+    '''
+    get epoch timestamp of this moment in usecond
+    :return:
+    '''
+    now_time = datetime.datetime.now()
+    epoch_time_sec = time.mktime(now_time.timetuple())
+    return int(epoch_time_sec * 1e6 + now_time.microsecond)
+
+
+def buildBacklog_fault_vm(vimid, backlog_input):
+    # build backlog with domain:"fault", type:"vm"
+
+    logger.info("vimid: %s" % vimid)
+    logger.debug("with input: %s" % backlog_input)
+
+    try:
+
+        # must resolve the tenant id and server id while building the backlog
+        tenant_id = backlog_input.get("tenantid", None)
+        server_id = backlog_input.get("sourceid", None)
+        server_name = backlog_input.get("source", None)
+
+        # should resolve the name to id later
+        if tenant_id is None:
+            tenant_name = backlog_input["tenant"]
+
+            # get token
+            # resolve tenant_name to tenant_id
+            auth_api_url_format = "/{f_vim_id}/identity/v2.0/tokens"
+            auth_api_url = auth_api_url_format.format(f_vim_id=vimid)
+            auth_api_data = {"auth": {"tenantName": tenant_name}}
+            base_url = settings.MULTICLOUD_PREFIX
+            extra_headers = ''
+            ret = restcall._call_req(base_url, "", "", 0, auth_api_url,
+                                     "POST", extra_headers, json.dumps(auth_api_data))
+            if ret[0] > 0 or ret[1] is None:
+                logger.critical("call url %s failed with status %s" % (auth_api_url, ret[0]))
+                return None
+
+            token_resp = json.JSONDecoder().decode(ret[1])
+            token = token_resp["access"]["token"]["id"]
+            tenant_id = token_resp["access"]["token"]["tenant"]["id"]
+
+            if server_id is None and server_name:
+                # resolve server_name to server_id in case no wildcast in server_name
+                vserver_api_url_format \
+                    = "/{f_vim_id}/compute/v2.1/{f_tenant_id}/servers?name={f_server_name}"
+                vserver_api_url = vserver_api_url_format.format(f_vim_id=vimid,
+                                                                f_tenant_id=tenant_id,
+                                                                f_server_name=server_name)
+                base_url = settings.MULTICLOUD_PREFIX
+                extra_headers = {'X-Auth-Token': token}
+                ret = restcall._call_req(base_url, "", "", 0, vserver_api_url, "GET", extra_headers, "")
+                if ret[0] > 0 or ret[1] is None:
+                    logger.critical("call url %s failed with status %s" % (vserver_api_url, ret[0]))
+                    return None
+
+                server_resp = json.JSONDecoder().decode(ret[1])
+                # find out the server wanted
+                for s in server_resp.get("servers", []):
+                    if s["name"] == server_name:
+                        server_id = s["id"]
+                        break
+                if server_id is None:
+                    logger.warn("source %s cannot be found under tenant id %s "
+                                % (server_name, tenant_id))
+                    return None
+
+        # m.c. proxied OpenStack API
+        if server_id is None and server_name is None:
+            # monitor all VMs of the specified VIMs since no server_id can be resolved
+            api_url_fmt = "/{f_vim_id}/compute/v2.1/{f_tenant_id}/servers/detail"
+            api_url = api_url_fmt.format(
+                f_vim_id=vimid, f_tenant_id=tenant_id)
+        else:
+            api_url_fmt = "/{f_vim_id}/compute/v2.1/{f_tenant_id}/servers/{f_server_id}"
+            api_url = api_url_fmt.format(
+                f_vim_id=vimid, f_tenant_id=tenant_id, f_server_id=server_id)
+
+        backlog = {
+            "backlog_uuid":
+                str(uuid.uuid3(uuid.NAMESPACE_URL,
+                               str("%s-%s-%s" % (vimid, tenant_id, server_id)))),
+            "tenant_id": tenant_id,
+            "server_id": server_id,
+            "api_method": "GET",
+            "api_link": api_url,
+        }
+        backlog.update(backlog_input)
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+        return None
+
+    logger.info("return")
+    logger.debug("with backlog: %s" % backlog)
+    return backlog
+
+
+# process backlog with domain:"fault", type:"vm"
+
+
+def processBacklog_fault_vm(vesAgentConfig, vesAgentState, oneBacklog):
+    logger.debug("vesAgentConfig:%s, vesAgentState:%s, oneBacklog: %s"
+                 % (vesAgentConfig, vesAgentState, oneBacklog))
+
+    try:
+        vimid = vesAgentConfig["vimid"]
+        tenant_name = oneBacklog["tenant"]
+
+        # get token
+        auth_api_url_format = "/{f_vim_id}/identity/v2.0/tokens"
+        auth_api_url = auth_api_url_format.format(f_vim_id=vimid)
+        auth_api_data = {"auth": {"tenantName": tenant_name}}
+        base_url = settings.MULTICLOUD_PREFIX
+        extra_headers = ''
+        logger.debug("authenticate with url:%s" % auth_api_url)
+        ret = restcall._call_req(base_url, "", "", 0, auth_api_url,
+                                 "POST", extra_headers, json.dumps(auth_api_data))
+        if ret[0] > 0 or ret[1] is None:
+            logger.critical("call url %s failed with status %s" %
+                            (auth_api_url, ret[0]))
+
+        token_resp = json.JSONDecoder().decode(ret[1])
+        logger.debug("authenticate resp: %s" % token_resp)
+        token = token_resp["access"]["token"]["id"]
+
+        # collect data by issue API
+        api_link = oneBacklog["api_link"]
+        method = oneBacklog["api_method"]
+        base_url = settings.MULTICLOUD_PREFIX
+        data = ''
+        extra_headers = {'X-Auth-Token': token}
+        # which one is correct? extra_headers = {'HTTP_X_AUTH_TOKEN': token}
+        logger.debug("authenticate with url:%s, header:%s" %
+                     (auth_api_url, extra_headers))
+        ret = restcall._call_req(base_url, "", "", 0, api_link, method, extra_headers, data)
+        if ret[0] > 0 or ret[1] is None:
+            logger.critical("call url %s failed with status %s" % (api_link, ret[0]))
+
+        server_resp = json.JSONDecoder().decode(ret[1])
+        logger.debug("collected data: %s" % server_resp)
+
+        # encode data
+        backlog_uuid = oneBacklog.get("backlog_uuid", None)
+        backlogState = vesAgentState.get("%s" % (backlog_uuid), None)
+
+        # iterate all VMs
+        all_events = []
+        server_1 = server_resp.get("server", None)  # in case querying single server
+        for s in server_resp.get("servers", [server_1] if server_1 else []):
+            server_id = s.get("id", None)
+            server_name = s.get("name", None)
+            if not server_id:
+                continue
+
+            last_event = backlogState.get("last_event_%s" % (server_id), None)
+            logger.debug("last event for server name %s: %s" % (server_name, last_event))
+
+            this_event = data2event_fault_vm(vimid, oneBacklog, last_event, s)
+            if this_event is not None:
+                logger.debug("this event: %s" % this_event)
+                all_events.append(this_event.get("event", None))
+                backlogState["last_event_%s" % (server_id)] = this_event
+
+        # report data to VES
+        if len(all_events) > 0:
+            ves_subscription = vesAgentConfig.get("subscription", None)
+            publishAnyEventToVES(ves_subscription, all_events)
+            # store the latest data into cache, never expire
+
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+        return
+
+    logger.info("return")
+    return
+
+
+def data2event_fault_vm(vimid, oneBacklog, last_event, vm_data):
+    VES_EVENT_VERSION = 3.0
+    VES_EVENT_FAULT_VERSION = 2.0
+    VES_EVENT_FAULT_DOMAIN = "fault"
+
+    try:
+
+        if vm_status_is_fault(vm_data["status"]):
+            if last_event is not None \
+                    and last_event['event']['commonEventHeader']['eventName'] == 'Fault_MultiCloud_VMFailure':
+                # asserted alarm already, so no need to assert it again
+                return None
+
+            eventName = "Fault_MultiCloud_VMFailure"
+            priority = "High"
+            eventSeverity = "CRITICAL"
+            alarmCondition = "Guest_Os_Failure"
+            # vfStatus = "Active"
+            specificProblem = "Fault_MultiCloud_VMFailure"
+            eventType = ''
+            reportingEntityId = vimid
+            reportingEntityName = vimid
+            sequence = 0
+
+            startEpochMicrosec = get_epoch_now_usecond()
+            lastEpochMicrosec = startEpochMicrosec
+
+            eventId = str(uuid.uuid4())
+            pass
+        else:
+            if last_event is None \
+                    or last_event['event']['commonEventHeader']['eventName'] != 'Fault_MultiCloud_VMFailure':
+                # not assert alarm yet, so no need to clear it
+                return None
+
+            eventName = "Fault_MultiCloud_VMFailureCleared"
+            priority = "Normal"
+            eventSeverity = "NORMAL"
+            alarmCondition = "Vm_Restart"
+            # vfStatus = "Active"
+            specificProblem = "Fault_MultiCloud_VMFailure"
+            eventType = ''
+            reportingEntityId = vimid
+            reportingEntityName = vimid
+            sequence = 1  # last_event['event']['commonEventHeader']['sequence'] + 1
+
+            startEpochMicrosec = last_event['event']['commonEventHeader']['startEpochMicrosec']
+            lastEpochMicrosec = get_epoch_now_usecond()
+            # holmes requires that eventId must be unique for each event!
+            eventId = str(uuid.uuid4())
+
+            pass
+
+        # now populate the event structure
+        this_event = {
+            'event': {
+                'commonEventHeader': {
+                    'version': VES_EVENT_VERSION,
+                    'eventName': eventName,
+                    'domain': VES_EVENT_FAULT_DOMAIN,
+                    'eventId': eventId,
+                    'eventType': eventType,
+                    'sourceId': vm_data['id'],
+                    'sourceName': vm_data['name'],
+                    'reportingEntityId': reportingEntityId,
+                    'reportingEntityName': reportingEntityName,
+                    'priority': priority,
+                    'startEpochMicrosec': startEpochMicrosec,
+                    'lastEpochMicrosec': lastEpochMicrosec,
+                    'sequence': sequence
+                },
+                'faultFields': {
+                    'faultFieldsVersion': VES_EVENT_FAULT_VERSION,
+                    'eventSeverity': eventSeverity,
+                    'eventSourceType': 'virtualMachine',
+                    'alarmCondition': alarmCondition,
+                    'specificProblem': specificProblem,
+                    'vfStatus': 'Active',
+                    "alarmInterfaceA": "aaaa",
+                    "alarmAdditionalInformation": [
+                        {
+                            "name": "objectType",
+                            "value": "VIM"
+                        },
+                        {
+                            "name": "eventTime",
+                            "value": str(datetime.datetime.now())
+                        }
+                    ],
+                }
+
+            }
+
+        }
+
+        return this_event
+
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+        return None
+
+
+def vm_status_is_fault(status):
+    '''
+    report VM fault when status falls into one of following state
+        ['ERROR', 'DELETED', 'PAUSED', 'REBUILD', 'RESCUE',
+                  'RESIZE','REVERT_RESIZE', 'SHELVED', 'SHELVED_OFFLOADED',
+                  'SHUTOFF', 'SOFT_DELETED','SUSPENDED', 'UNKNOWN', 'VERIFY_RESIZE']
+    :param status:
+    :return:
+    '''
+    if status in ['BUILD', 'ACTIVE', 'HARD_REBOOT', 'REBOOT', 'MIGRATING', 'PASSWORD']:
+        return False
+    else:
+        return True
diff --git a/fcaps/fcaps/vesagent/tasks.py b/fcaps/fcaps/vesagent/tasks.py
new file mode 100644 (file)
index 0000000..6a24a65
--- /dev/null
@@ -0,0 +1,197 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+# VES agent workers
+from __future__ import absolute_import, unicode_literals
+from fcaps.celery import app
+# import os
+import logging
+import json
+import time
+
+from django.core.cache import cache
+
+from fcaps.vesagent.event_domain.fault_vm import processBacklog_fault_vm
+
+logger = logging.getLogger(__name__)
+
+
+@app.task(bind=True)
+def scheduleBacklogs(self, vimid):
+    # make sure only one task runs here
+    # cannot get vimid ? logger.info("schedule with vimid:%" % (vimid))
+
+    logger.debug("scheduleBacklogs starts")
+    backlog_count, next_time_slot = processBacklogs()
+    logger.debug("processBacklogs return with %s, %s" % (backlog_count, next_time_slot))
+
+    # sleep for next_time_slot
+    while backlog_count > 0:
+        time.sleep(next_time_slot)
+        backlog_count, next_time_slot = processBacklogs()
+
+    logger.debug("scheduleBacklogs stops")
+
+
+def processBacklogs():
+    # find out count of valid backlog and the next time slot
+    backlog_count = 0
+    next_time_slot = 10
+    try:
+        # get the whole list of backlog
+        VesAgentBacklogsVimListStr = cache.get("VesAgentBacklogs.vimlist")
+        if VesAgentBacklogsVimListStr is None:
+            logger.warn("VesAgentBacklogs.vimlist cannot be found in cache")
+            return 0, next_time_slot
+
+        logger.debug("VesAgentBacklogs.vimlist: %s" % VesAgentBacklogsVimListStr)
+
+        backlogsAllVims = json.loads(VesAgentBacklogsVimListStr)
+        if backlogsAllVims is None:
+            logger.warn("VesAgentBacklogs.vimlist is empty")
+            return 0, next_time_slot
+
+        for vimid in backlogsAllVims:
+            # iterate each backlogs
+            backlog_count_tmp, next_time_slot_tmp = processBacklogsOfOneVIM(vimid)
+            logger.debug("vimid:%s, backlog_count,next_time_slot:%s,%s"
+                         % (vimid, backlog_count_tmp, next_time_slot_tmp))
+            backlog_count += backlog_count_tmp
+            next_time_slot = next_time_slot_tmp if next_time_slot > next_time_slot_tmp else next_time_slot
+            pass
+
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+
+    return backlog_count, next_time_slot
+
+    pass
+
+
+def processBacklogsOfOneVIM(vimid):
+    '''
+    process all backlogs for a VIM, return count of valid backlogs
+    :param vimid:
+    :return:
+    '''
+    backlog_count = 0
+    next_time_slot = 10
+
+    try:
+        vesAgentConfigStr = cache.get("VesAgentBacklogs.config.%s" % vimid)
+        if vesAgentConfigStr is None:
+            logger.warn("VesAgentBacklogs.config.%s cannot be found in cache" % vimid)
+            return 0, next_time_slot
+
+        logger.debug("VesAgentBacklogs.config.%s: %s" % (vimid, vesAgentConfigStr))
+
+        vesAgentConfig = json.loads(vesAgentConfigStr)
+        if vesAgentConfig is None:
+            logger.warn("VesAgentBacklogs.config.%s corrupts" % vimid)
+            return 0, next_time_slot
+
+        vesAgentStateStr = cache.get("VesAgentBacklogs.state.%s" % vimid)
+        vesAgentState = json.loads(vesAgentStateStr) if vesAgentStateStr is not None else {}
+
+        ves_info = vesAgentConfig.get("subscription", None)
+        if ves_info is None:
+            logger.warn("VesAgentBacklogs.config.%s: ves subscription corrupts:%s" % (vimid, vesAgentConfigStr))
+            return 0, next_time_slot
+
+        poll_interval_default = vesAgentConfig.get("poll_interval_default", None)
+        if poll_interval_default is None:
+            logger.warn("VesAgentBacklogs.config.%s: poll_interval_default corrupts:%s" % (vimid, vesAgentConfigStr))
+            return 0, next_time_slot
+
+        if poll_interval_default == 0:
+            # invalid interval value
+            logger.warn("VesAgentBacklogs.config.%s: poll_interval_default invalid:%s" % (vimid, vesAgentConfigStr))
+            return 0, next_time_slot
+
+        backlogs_list = vesAgentConfig.get("backlogs", None)
+        if backlogs_list is None:
+            logger.warn("VesAgentBacklogs.config.%s: backlogs corrupts:%s" % (vimid, vesAgentConfigStr))
+            return 0, next_time_slot
+
+        for backlog in backlogs_list:
+            backlog_count_tmp, next_time_slot_tmp = \
+                processOneBacklog(
+                    vesAgentConfig, vesAgentState, poll_interval_default, backlog)
+            logger.debug("processOneBacklog return with %s,%s" % (backlog_count_tmp, next_time_slot_tmp))
+            backlog_count += backlog_count_tmp
+            next_time_slot = next_time_slot_tmp if next_time_slot > next_time_slot_tmp else next_time_slot
+
+            pass
+
+        # save back the updated backlogs state
+        vesAgentStateStr = json.dumps(vesAgentState)
+        cache.set("VesAgentBacklogs.state.%s" % vimid, vesAgentStateStr, None)
+
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+
+    return backlog_count, next_time_slot
+
+
+def processOneBacklog(vesAgentConfig, vesAgentState, poll_interval_default, oneBacklog):
+    logger.info("Process one backlog")
+    # logger.debug("vesAgentConfig:%s, vesAgentState:%s, poll_interval_default:%s, oneBacklog: %s"
+    #             % (vesAgentConfig, vesAgentState, poll_interval_default, oneBacklog))
+
+    backlog_count = 1
+    next_time_slot = 10
+    try:
+        timestamp_now = int(time.time())
+        backlog_uuid = oneBacklog.get("backlog_uuid", None)
+        if backlog_uuid is None:
+            # warning: uuid is None, omit this backlog
+            logger.warn("backlog without uuid: %s" % oneBacklog)
+            return 0, next_time_slot
+
+        backlogState = vesAgentState.get("%s" % (backlog_uuid), None)
+        if backlogState is None:
+            initialBacklogState = {
+                "timestamp": timestamp_now
+            }
+            vesAgentState["%s" % (backlog_uuid)] = initialBacklogState
+            backlogState = initialBacklogState
+
+        time_expiration = \
+            backlogState["timestamp"] + \
+            oneBacklog.get("poll_interval", poll_interval_default)
+
+        # check if poll interval expires
+        if timestamp_now < time_expiration:
+            # not expired yet
+            logger.info("return without dispatching, not expired yet")
+            return backlog_count, next_time_slot
+
+        logger.info("Dispatching backlog")
+
+        # collect data in case of expiration
+        if oneBacklog["domain"] == "fault" and oneBacklog["type"] == "vm":
+            processBacklog_fault_vm(vesAgentConfig, vesAgentState, oneBacklog)
+        else:
+            logger.warn("Dispatching backlog fails due to unsupported backlog domain %s,type:%s"
+                        % (oneBacklog["domain"], oneBacklog["type"]))
+            backlog_count = 0
+            pass
+
+        # update timestamp and internal state
+        backlogState["timestamp"] = timestamp_now
+    except Exception as e:
+        logger.error("exception:%s" % str(e))
+
+    logger.info("return")
+    return backlog_count, next_time_slot
diff --git a/fcaps/fcaps/vesagent/tests/__init__.py b/fcaps/fcaps/vesagent/tests/__init__.py
new file mode 100644 (file)
index 0000000..46a7d44
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
diff --git a/fcaps/fcaps/vesagent/tests/tests_fault_vm.py b/fcaps/fcaps/vesagent/tests/tests_fault_vm.py
new file mode 100644 (file)
index 0000000..37e8371
--- /dev/null
@@ -0,0 +1,228 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 mock
+
+import unittest
+import json
+
+from fcaps.vesagent import vespublish
+from common.utils import restcall
+from fcaps.vesagent.event_domain import fault_vm
+
+MOCK_TOKEN_RESPONSE = {
+    "access":
+        {"token": {"issued_at": "2018-05-10T16:56:56.000000Z",
+                   "expires": "2018-05-10T17:56:56.000000Z",
+                   "id": "4a832860dd744306b3f66452933f939e",
+                   "tenant": {"domain": {"id": "default", "name": "Default"},
+                              "enabled": "true", "id": "0e148b76ee8c42f78d37013bf6b7b1ae", "name": "VIM"}},
+         "serviceCatalog": [], "user": {"domain": {"id": "default", "name": "Default"},
+                                        "id": "ba76c94eb5e94bb7bec6980e5507aae2", "name": "demo"}}
+}
+
+MOCK_SERVERS_GET_RESPONSE = {
+    "servers": [
+        {"id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+         "links": [{
+             "href": "http://10.12.25.2:8774/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+             "rel": "self"},
+             {
+                 "href": "http://10.12.25.2:8774/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                 "rel": "bookmark"}],
+         "name": "onap-aaf"}]
+}
+
+MOCK_BACKLOG_INPUT = {
+    "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+    "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+    "source": "onap-aaf",
+    "api_link":
+        "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "domain": "fault", "type": "vm", "tenant": "VIM"
+}
+
+MOCK_BACKLOG_INPUT_wo_tenant_id = {
+    "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+    "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "source": "onap-aaf",
+    "api_link":
+        "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "domain": "fault", "type": "vm", "tenant": "VIM"
+}
+
+MOCK_BACKLOG_INPUT_wo_tenant = {
+    "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+    "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "source": "onap-aaf",
+    "domain": "fault", "type": "vm", }
+
+MOCK_BACKLOG_INPUT_wo_server_id = {
+    "source": "onap-aaf",
+    "domain": "fault", "type": "vm", "tenant": "VIM"}
+
+MOCK_BACKLOG_INPUT_wo_server = {"domain": "fault", "type": "vm", "tenant": "VIM"}
+
+MOCK_SERVER_GET_RESPONSE = {
+    "server": {"wrs-res:topology": "node:0,  4096MB, pgsize:2M, vcpus:0,1, pol:sha",
+               "OS-EXT-STS:task_state": None,
+               "addresses": {
+                   "oam_onap_BTHY": [{"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:6c:0d:6b",
+                                      "version": 4, "addr": "10.0.13.1", "OS-EXT-IPS:type": "fixed"},
+                                     {"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:6c:0d:6b", "version": 4,
+                                      "addr": "10.12.5.185", "OS-EXT-IPS:type": "floating"}]},
+               "links": [], "image": {"id": "6e219e86-cd94-4989-9119-def29aa10b12", "links": []},
+               "wrs-if:nics": [], "wrs-sg:server_group": "",
+               "OS-EXT-STS:vm_state": "active", "OS-SRV-USG:launched_at": "2018-04-26T08:01:28.000000",
+               "flavor": {}, "id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+               "security_groups": [{"name": "onap_sg_BTHY"}],
+               "user_id": "ba76c94eb5e94bb7bec6980e5507aae2",
+               "OS-DCF:diskConfig": "MANUAL", "accessIPv4": "",
+               "accessIPv6": "", "progress": 0, "OS-EXT-STS:power_state": 1,
+               "OS-EXT-AZ:availability_zone": "nova", "metadata": {},
+               "status": "ACTIVE", "updated": "2018-04-26T08:01:28Z",
+               "hostId": "17acc9f2ae4f618c314e4cdf0c206585b895bc72a9ec57e57b254133",
+               "OS-SRV-USG:terminated_at": None, "wrs-res:pci_devices": "",
+               "wrs-res:vcpus": [2, 2, 2], "key_name": "onap_key_BTHY", "name": "onap-aaf",
+               "created": "2018-04-26T08:01:20Z", "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae",
+               "os-extended-volumes:volumes_attached": [], "config_drive": ""}}
+
+MOCK_SERVER_GET_RESPONSE_empty = {}
+
+MOCK_vesAgentConfig = {
+    "backlogs": [
+        {"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+         "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+         "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+         "source": "onap-aaf",
+         "api_link":
+             "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+         "domain": "fault", "type": "vm", "tenant": "VIM"}
+    ],
+    "poll_interval_default": 10, "vimid": "fcaps-hudson-dc_RegionOne",
+    "ves_subscription": {"username": "user", "password": "password",
+                         "endpoint": "http://127.0.0.1:9011/sample"}}
+
+MOCK_vesAgentState = {"ce2d7597-22e1-4239-890f-bc303bd67076": {"timestamp": 1525975400}}
+MOCK_oneBacklog = {
+    "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+    "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae",
+    "api_method": "GET", "source": "onap-aaf",
+    "api_link":
+        "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+    "domain": "fault", "type": "vm", "tenant": "VIM"}
+
+
+class FaultVMTest(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_get_epoch_now_usecond(self):
+        epoch = fault_vm.get_epoch_now_usecond()
+        self.assertGreater(epoch, 1)
+
+    @mock.patch.object(restcall, '_call_req')
+    def test_buildBacklog_fault_vm(self, mock_call_req):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVERS_GET_RESPONSE), "MOCKED response body")
+        ]
+        backlog = fault_vm.buildBacklog_fault_vm(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=MOCK_BACKLOG_INPUT)
+
+        self.assertIsNotNone(backlog)
+
+    @mock.patch.object(restcall, '_call_req')
+    def test_buildBacklog_fault_vm_wo_tenant_id(self, mock_call_req):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVERS_GET_RESPONSE), "MOCKED response body")
+        ]
+        backlog = fault_vm.buildBacklog_fault_vm(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=MOCK_BACKLOG_INPUT_wo_tenant_id)
+        self.assertIsNotNone(backlog)
+
+    @mock.patch.object(restcall, '_call_req')
+    def test_buildBacklog_fault_vm_wo_tenant(self, mock_call_req):
+        mock_call_req.side_effect = [
+            (1, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body: failed"),
+            (0, json.dumps(MOCK_SERVERS_GET_RESPONSE), "MOCKED response body")
+        ]
+        backlog = fault_vm.buildBacklog_fault_vm(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=MOCK_BACKLOG_INPUT_wo_tenant)
+        self.assertIsNone(backlog)
+
+    @mock.patch.object(restcall, '_call_req')
+    def test_buildBacklog_fault_vm_wo_server_id(self, mock_call_req):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVERS_GET_RESPONSE), "MOCKED response body")
+        ]
+        backlog = fault_vm.buildBacklog_fault_vm(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=MOCK_BACKLOG_INPUT_wo_server_id)
+        self.assertIsNotNone(backlog)
+
+    @mock.patch.object(restcall, '_call_req')
+    def test_buildBacklog_fault_vm_wo_server(self, mock_call_req):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVERS_GET_RESPONSE), "MOCKED response body")
+        ]
+        backlog = fault_vm.buildBacklog_fault_vm(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=MOCK_BACKLOG_INPUT_wo_server)
+        self.assertIsNotNone(backlog)
+
+    @mock.patch.object(vespublish, 'publishAnyEventToVES')
+    @mock.patch.object(restcall, '_call_req')
+    def test_processBacklog_fault_vm(
+            self, mock_call_req, mock_publishAnyEventToVES):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVER_GET_RESPONSE), "MOCKED response body")
+        ]
+        mock_publishAnyEventToVES.return_value = "mocked return value"
+
+        result = fault_vm.processBacklog_fault_vm(
+            vesAgentConfig=MOCK_vesAgentConfig,
+            vesAgentState=MOCK_vesAgentState,
+            oneBacklog=MOCK_oneBacklog)
+        self.assertIsNone(result)
+        pass
+
+    @mock.patch.object(vespublish, 'publishAnyEventToVES')
+    @mock.patch.object(restcall, '_call_req')
+    def test_processBacklog_fault_vm_wo_server(
+            self, mock_call_req, mock_publishAnyEventToVES):
+        mock_call_req.side_effect = [
+            (0, json.dumps(MOCK_TOKEN_RESPONSE), "MOCKED response body"),
+            (0, json.dumps(MOCK_SERVER_GET_RESPONSE_empty), "MOCKED response body")
+        ]
+        mock_publishAnyEventToVES.return_value = "mocked return value"
+
+        result = fault_vm.processBacklog_fault_vm(
+            vesAgentConfig=MOCK_vesAgentConfig,
+            vesAgentState=MOCK_vesAgentState,
+            oneBacklog=MOCK_oneBacklog)
+
+        self.assertIsNone(result)
diff --git a/fcaps/fcaps/vesagent/tests/tests_tasks.py b/fcaps/fcaps/vesagent/tests/tests_tasks.py
new file mode 100644 (file)
index 0000000..23cd182
--- /dev/null
@@ -0,0 +1,143 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 mock
+
+import unittest
+import json
+from django.test import Client
+# from rest_framework import status
+
+from django.core.cache import cache
+from common.msapi import extsys
+
+from fcaps.vesagent import tasks
+from fcaps.vesagent.event_domain import fault_vm
+
+MOCK_VIM_INFO = {
+    "createTime": "2017-04-01 02:22:27",
+    "domain": "Default",
+    "name": "TiS_R4",
+    "password": "admin",
+    "tenant": "admin",
+    "type": "openstack",
+    "url": "http://128.224.180.14:5000/v3",
+    "userName": "admin",
+    "vendor": "WindRiver",
+    "version": "newton",
+    "vimId": "fcaps-hudson-dc_RegionOne",
+    'cloud_owner': 'fcaps-hudson-dc',
+    'cloud_region_id': 'RegionOne',
+    'cloud_extra_info':
+        '{"vesagent_config":{"backlogs":[{"source":"onap-aaf","domain":"fault","type":"vm","tenant":"VIM"}],"poll_interval_default":10,"ves_subscription":{"username":"user","password":"password","endpoint":"http://127.0.0.1:9011/sample"}}}',
+    'insecure': 'True',
+}
+
+COUNT_TIME_SLOT1 = (1, 1)
+COUNT_TIME_SLOT2 = (0, 1)
+
+
+class VesTaskTest(unittest.TestCase):
+    def setUp(self):
+        self.client = Client()
+
+    def tearDown(self):
+        pass
+
+    @mock.patch.object(tasks, 'processBacklogs')
+    @mock.patch.object(extsys, 'get_vim_by_id')
+    def test_tasks_scheduleBacklogs(self, mock_get_vim_by_id, mock_processBacklogs):
+        mock_get_vim_by_id.return_value = MOCK_VIM_INFO
+        mock_processBacklogs.side_effect = [
+            COUNT_TIME_SLOT1,
+            COUNT_TIME_SLOT2
+        ]
+        result = tasks.scheduleBacklogs(vimid="fcaps-hudson-dc_RegionOne")
+        self.assertEquals(None, result)
+
+    @mock.patch.object(tasks, 'processBacklogsOfOneVIM')
+    @mock.patch.object(cache, 'get')
+    def test_tasks_processBacklogs(
+            self, mock_cache_get, mock_tasks_processBacklogsOfOneVIM):
+        mock_VesAgentBacklogs_vimlist = ["fcaps-hudson-dc_RegionOne"]
+        COUNT_TIME_SLOT_ONE_VIM = (1, 1)
+        mock_tasks_processBacklogsOfOneVIM.return_value = COUNT_TIME_SLOT_ONE_VIM
+        mock_cache_get.side_effect = [
+            json.dumps(mock_VesAgentBacklogs_vimlist),
+        ]
+        result = tasks.processBacklogs()
+        self.assertEquals(COUNT_TIME_SLOT_ONE_VIM, result)
+
+    @mock.patch.object(tasks, 'processOneBacklog')
+    @mock.patch.object(cache, 'set')
+    @mock.patch.object(cache, 'get')
+    def test_tasks_processBacklogsOfOneVIM(
+            self, mock_cache_get, mock_cache_set, mock_tasks_processOneBacklog):
+        # mock_VesAgentBacklogs_vimlist = ["fcaps-hudson-dc_RegionOne"]
+        mock_vesagent_config = {
+            "backlogs":
+                [{"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+                  "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+                  "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+                  "source": "onap-aaf",
+                  "api_link":
+                      "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                  "domain": "fault", "type": "vm", "tenant": "VIM"}],
+            "poll_interval_default": 10, "vimid": "onaplab_RegionOne",
+            "subscription": {"username": "user", "password": "password",
+                             "endpoint": "http://127.0.0.1:9011/sample"}}
+        mock_cache_get.side_effect = [
+            json.dumps(mock_vesagent_config),
+            json.dumps({})
+        ]
+        mock_tasks_processOneBacklog.return_value = (1, 11)
+        mock_cache_set.return_value = "mocked cache set"
+        result = tasks.processBacklogsOfOneVIM(vimid="fcaps-hudson-dc_RegionOne")
+        COUNT_TIME_SLOT = (1, 10)
+        self.assertEquals(COUNT_TIME_SLOT, result)
+
+    @mock.patch.object(fault_vm, 'processBacklog_fault_vm')
+    def test_tasks_processOneBacklog(
+            self, mock_fault_vm_processBacklog_fault_vm):
+        mock_fault_vm_processBacklog_fault_vm.return_value = None
+        vesagent_config = {
+            "backlogs":
+                [{"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+                  "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+                  "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+                  "source": "onap-aaf",
+                  "api_link":
+                      "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                  "domain": "fault", "type": "vm", "tenant": "VIM"}],
+            "poll_interval_default": 10, "vimid": "onaplab_RegionOne",
+            "subscription": {"username": "user", "password": "password",
+                             "endpoint": "http://127.0.0.1:9011/sample"}}
+
+        vesagent_onebacklog = {
+            "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+            "poll_interval": 10,
+            "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+            "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+            "source": "onap-aaf",
+            "api_link": "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+            "domain": "fault", "type": "vm", "tenant": "VIM"
+        }
+
+        result = tasks.processOneBacklog(
+            vesAgentConfig=vesagent_config,
+            vesAgentState={},
+            poll_interval_default=10,
+            oneBacklog=vesagent_onebacklog)
+        COUNT_TIME_SLOT = (1, 10)
+        self.assertEquals(COUNT_TIME_SLOT, result)
diff --git a/fcaps/fcaps/vesagent/tests/tests_vesagent_ctrl.py b/fcaps/fcaps/vesagent/tests/tests_vesagent_ctrl.py
new file mode 100644 (file)
index 0000000..760c44d
--- /dev/null
@@ -0,0 +1,179 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 mock
+
+import unittest
+import json
+from django.test import Client
+from rest_framework import status
+
+from django.core.cache import cache
+from common.msapi import extsys
+from fcaps.vesagent import vesagent_ctrl
+from fcaps.vesagent.event_domain import fault_vm
+from fcaps.vesagent.tasks import scheduleBacklogs
+
+MOCK_VIM_INFO = {
+    "createTime": "2017-04-01 02:22:27",
+    "domain": "Default",
+    "name": "TiS_R4",
+    "password": "admin",
+    "tenant": "admin",
+    "type": "openstack",
+    "url": "http://128.224.180.14:5000/v3",
+    "userName": "admin",
+    "vendor": "WindRiver",
+    "version": "newton",
+    "vimId": "fcaps-hudson-dc_RegionOne",
+    'cloud_owner': 'fcaps-hudson-dc',
+    'cloud_region_id': 'RegionOne',
+    'cloud_extra_info':
+        '{"vesagent_config":{"backlogs":[{"source": "onap-aaf","domain": "fault","type": "vm","tenant": "VIM"}],"poll_interval_default":10,"ves_subscription":{"username": "user","password": "password","endpoint": "http://127.0.0.1:9011/sample"}}}',
+    'insecure': 'True',
+}
+
+
+class VesAgentCtrlTest(unittest.TestCase):
+    def setUp(self):
+        self.client = Client()
+        self.view = vesagent_ctrl.VesAgentCtrl()
+
+    def tearDown(self):
+        pass
+
+    @mock.patch.object(cache, 'get')
+    @mock.patch.object(extsys, 'get_vim_by_id')
+    def test_get(self, mock_get_vim_by_id, mock_get):
+        mock_get_vim_by_id.return_value = MOCK_VIM_INFO
+        mock_get.return_value = \
+            '{"backlogs": [{"backlog_uuid": "2b8f6ff8-bc64-339b-a714-155909db937f", "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721", "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET", "source": "onap-aaf", "api_link": "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721", "domain": "fault", "type": "vm", "tenant": "VIM"}], "poll_interval_default": 10, "vimid": "onaplab_RegionOne", "subscription": {"username": "user", "password": "password", "endpoint": "http://127.0.0.1:9011/sample"}}'
+
+        response = self.client.get("/api/multicloud-fcaps/v0/fcaps-hudson-dc_RegionOne/vesagent")
+        self.assertEqual(status.HTTP_200_OK, response.status_code, response.content)
+
+    @mock.patch.object(vesagent_ctrl.VesAgentCtrl, 'buildBacklogsOneVIM')
+    @mock.patch.object(extsys, 'get_vim_by_id')
+    def test_post(self, mock_get_vim_by_id, mock_buildBacklogsOneVIM):
+        mock_get_vim_by_id.return_value = MOCK_VIM_INFO
+        mock_buildBacklogsOneVIM.return_value = "mocked vesagent_backlogs"
+        mock_request = mock.Mock()
+        mock_request.META = {"testkey": "testvalue"}
+        mock_request.data = {"testdatakey": "testdatavalue"}
+
+        response = self.view.post(request=mock_request, vimid="fcaps-hudson-dc_RegionOne")
+        self.assertEquals(status.HTTP_201_CREATED, response.status_code)
+
+    @mock.patch.object(vesagent_ctrl.VesAgentCtrl, 'clearBacklogsOneVIM')
+    @mock.patch.object(extsys, 'get_vim_by_id')
+    def test_delete(self, mock_get_vim_by_id, mock_clearBacklogsOneVIM):
+        mock_get_vim_by_id.return_value = MOCK_VIM_INFO
+        mock_clearBacklogsOneVIM.return_value = "mocked vesagent_backlogs"
+        mock_request = mock.Mock()
+        mock_request.META = {"testkey": "testvalue"}
+
+        response = self.view.delete(request=mock_request, vimid="fcaps-hudson-dc_RegionOne")
+        self.assertEquals(status.HTTP_200_OK, response.status_code)
+
+    @mock.patch.object(cache, 'get')
+    def test_getBacklogsOneVIM(self, mock_get):
+        mock_vesagent_config = {"backlogs": [{"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+                                              "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+                                              "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+                                              "source": "onap-aaf",
+                                              "api_link": "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                                              "domain": "fault", "type": "vm", "tenant": "VIM"}],
+                                "poll_interval_default": 10, "vimid": "onaplab_RegionOne",
+                                "subscription": {"username": "user", "password": "password",
+                                                 "endpoint": "http://127.0.0.1:9011/sample"}}
+        mock_get.return_value = json.dumps(mock_vesagent_config)
+
+        vesAgentConfig = self.view.getBacklogsOneVIM(vimid="fcaps-hudson-dc_RegionOne")
+        self.assertEquals(vesAgentConfig, mock_vesagent_config)
+
+    @mock.patch.object(cache, 'set')
+    @mock.patch.object(cache, 'get')
+    def test_clearBacklogsOneVIM(self, mock_get, mock_set):
+        mock_VesAgentBacklogs_vimlist = ["fcaps-hudson-dc_RegionOne"]
+        mock_vesagent_config = {
+            "backlogs": [
+                {"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+                 "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+                 "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+                 "source": "onap-aaf",
+                 "api_link":
+                     "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                 "domain": "fault", "type": "vm", "tenant": "VIM"}
+            ],
+            "poll_interval_default": 10, "vimid": "onaplab_RegionOne",
+            "subscription": {"username": "user", "password": "password",
+                             "endpoint": "http://127.0.0.1:9011/sample"}}
+
+        mock_get.side_effect = [
+            json.dumps(mock_VesAgentBacklogs_vimlist),
+            json.dumps(mock_vesagent_config)
+        ]
+
+        mock_set.return_value = "mocked cache set"
+
+        result = self.view.clearBacklogsOneVIM(
+            vimid="fcaps-hudson-dc_RegionOne")
+        self.assertEquals(0, result)
+
+    @mock.patch.object(scheduleBacklogs, 'delay')
+    @mock.patch.object(cache, 'set')
+    @mock.patch.object(cache, 'get')
+    def test_buildBacklogsOneVIM(
+            self, mock_get, mock_set, mock_scheduleBacklogs_delay):
+        mock_VesAgentBacklogs_vimlist = ["fcaps-hudson-dc_RegionOne"]
+        mock_vesagent_config = {
+            "backlogs": [{"backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+                          "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+                          "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+                          "source": "onap-aaf",
+                          "api_link": "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+                          "domain": "fault", "type": "vm", "tenant": "VIM"}],
+            "poll_interval_default": 10, "vimid": "fcaps-hudson-dc_RegionOne",
+            "ves_subscription": {"username": "user", "password": "password",
+                                 "endpoint": "http://127.0.0.1:9011/sample"}}
+
+        mock_get.side_effect = [
+            json.dumps(mock_VesAgentBacklogs_vimlist),
+        ]
+
+        mock_set.return_value = "mocked cache set"
+        mock_scheduleBacklogs_delay.return_value = "mocked delay"
+
+        VesAgentBacklogsConfig = self.view.buildBacklogsOneVIM(
+            vimid="fcaps-hudson-dc_RegionOne",
+            vesagent_config=mock_vesagent_config)
+        self.assertIsNotNone(VesAgentBacklogsConfig)
+
+    @mock.patch.object(fault_vm, 'buildBacklog_fault_vm')
+    def test_buildBacklog(self, mock_buildBacklog_fault_vm):
+        mock_backlog_input = {
+            "backlog_uuid": "ce2d7597-22e1-4239-890f-bc303bd67076",
+            "server_id": "c4b575fa-ed85-4642-ab4b-335cb5744721",
+            "tenant_id": "0e148b76ee8c42f78d37013bf6b7b1ae", "api_method": "GET",
+            "source": "onap-aaf",
+            "api_link":
+                "/onaplab_RegionOne/compute/v2.1/0e148b76ee8c42f78d37013bf6b7b1ae/servers/c4b575fa-ed85-4642-ab4b-335cb5744721",
+            "domain": "fault", "type": "vm", "tenant": "VIM"}
+
+        mock_buildBacklog_fault_vm.return_value = "mocked buildBacklog_fault_vm"
+
+        VesAgentBacklogsConfig = self.view.buildBacklog(
+            vimid="fcaps-hudson-dc_RegionOne",
+            backlog_input=mock_backlog_input)
+        self.assertIsNotNone(VesAgentBacklogsConfig)
diff --git a/fcaps/fcaps/vesagent/tests/tests_vespublish.py b/fcaps/fcaps/vesagent/tests/tests_vespublish.py
new file mode 100644 (file)
index 0000000..791b0ca
--- /dev/null
@@ -0,0 +1,54 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 mock
+
+import unittest
+# import json
+import urllib2
+
+from fcaps.vesagent import vespublish
+
+MOCK_VESENDPOINT = {
+    "endpoint": "MOCKED_VES_COLLECTOR_EP1",
+    "username": "MOCKED_VES_COLLECTOR_USER1",
+    "password": "MOCKED_VES_COLLECTOR_PASSWD1",
+}
+
+MOCK_VESPUBLISH_EVENT1 = [{"name": "event1"}]
+
+
+class VespublishTest(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    @mock.patch.object(urllib2, 'urlopen')
+    @mock.patch.object(urllib2, 'Request')
+    def test_publishAnyEventToVES(self, mock_Request, mock_urlopen):
+        mock_request = mock.Mock()
+
+        mock_Request.side_effect = [
+            mock_request
+        ]
+
+        mock_response = mock.Mock(["read"])
+        mock_response.read.return_value = "MOCKED_VESPUBLISH_RESPONSE_MESSAGE"
+        mock_urlopen.side_effect = [
+            mock_response
+        ]
+
+        vespublish.publishAnyEventToVES(MOCK_VESENDPOINT, MOCK_VESPUBLISH_EVENT1)
diff --git a/fcaps/fcaps/vesagent/vesagent_ctrl.py b/fcaps/fcaps/vesagent/vesagent_ctrl.py
new file mode 100644 (file)
index 0000000..c332d48
--- /dev/null
@@ -0,0 +1,452 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 traceback
+import json
+
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from django.conf import settings
+from common.msapi import extsys
+from fcaps.vesagent.tasks import scheduleBacklogs
+from fcaps.vesagent.event_domain import fault_vm
+
+from django.core.cache import cache
+
+logger = logging.getLogger(__name__)
+
+
+class VesAgentCtrl(APIView):
+    '''
+    control plane of VesAgent
+    Design tips:
+    1, vesagent are multiple processing workers
+    2, the runtime logic is simple: a vesagent worker polls the data source (vm/hypervisor/host/vim/etc.)
+    and then feeds the encoded data to VES.
+    3, the vesagent workers can be distributed to different clouds while latency/throughput is concerned,
+    this distributed deployment usually comes along with the distributed VES deployment.
+    So it is very likely that the collected data from different VIM/Cloud instance will be fed into
+    different VES endpoint, however, assuming that there will be at most one VES endpoint serving
+    any single VIM/Cloud instance.
+    4, According to VES specs, the collected data can be cataloged by domain:
+        domain : fault, heartbeat, measurementsForVfScaling, other, stateChange, syslog, thresholdCrossingAlert
+        As far as VIM/Cloud concerned, fault, heartbeat, measurementsForVfScaling, TCAalert are relevant.
+    5, the source of the collected data can be cataloged by eventSourceType:
+        eventSourceType: VNF/VNFC/VM
+        As far as VIM/Cloud concerned, only VM is relevant. This eventSourceType should be extended to cover
+        the data source of hypervisor, VIM, Host,Controller, PIM, etc.
+
+    6, the source of collected data should be specified explicitly,so is the domain of the collected data.
+        To specify the source: eventSourceType, uuid or name of the source
+        To specify the domain: domain
+        the specifications above will be provisioned as a vesagent backlog entry to a VIM/Cloud instance
+        to tell a vesagent worker that :
+        with regarding to that VIM/Cloud instance, what kind of data to be collected from which source .
+
+    7,the VES endpoint will be also specified for a VIM/Cloud instance, so that all collected data
+    will be fed into this VES endpoint
+
+    8, the vesagent backlog are stored into the respective cloud_region's property "cloud-extra-info",
+     which implies that those specifications can be CRUD either by ESR portal or the RestAPIs in this view, e.g.
+        "cloud-extra-info": {
+            ...,
+            "vesagent_config":
+            {
+                "ves_subscription":{
+                    "endpoint":"http://{VES IP}:{VES port}/{URI}",
+                    "username":"{VES username}",
+                    "password":"{VES password}",
+                },
+                "poll_interval_default" : "{default interval for polling}",
+                "backlogs":[
+                    {
+                        "domain":"fault"
+                        "type":"vm",
+                        "tenant":"{tenant name1}",
+                        "source":"{VM name1}",
+                        "poll_interval" : "{optional, interval for polling}",
+                    },
+                    {
+                        "domain":"fault"
+                        "type":"vm",
+                        "tenant":"{tenant name2}",
+                        "source":"{VM name2}",
+                        "poll_interval" : "{optional, interval for polling}",
+                    }
+                ]
+            }
+        }
+
+        Idea: API dispatching to distributed M.C. service can be determined by Complex Object in AAI:
+            cloud-region has been assoicated to a Complex Object
+            M.C. plugin service instance should refer to the same Complex Object (by physical_locaton_id ?)
+            So the M.C. broker/API distributor/other approach will correlate the cloud-region with
+            corresponding M.C. plugin service instance.
+
+
+    Backlog built in cache:
+
+        maintain backlog in cache and VES agent workers
+        cache objects:
+            "VesAgentBacklogs.vimlist": [ list of vimid] ### will not expire forever
+            "VesAgentBacklogs.state.{vimdid}":
+            ### will expire eventually to eliminate the garbage, expiration duration: 1hour?
+            {
+                "{backlog_uuid}": {
+                    "timestamp": "{timestamp for last time of data collecting}",
+                    "api_data": [list of data to populate the format string of the API link]
+                    "last_event": {object, event reported to ves last time}"
+                }
+            }
+            "VesAgentBacklogs.config.{vimdid}": ### will not expire forever
+            {
+                "vimid": "{vim id}",
+                "subscription": {
+                    "endpoint": "{ves endpoint, e.g. http://ves_ip:ves_port/eventListener/v5}",
+                    "username": "{username}",
+                    "password": "{password}"
+                }
+                "poll_interval_default" : "{default interval for polling}",
+                "backlogs":[
+                    {
+                        "backlog_uuid": "{uuid to identify the backlog}"
+                        "domain":"fault"
+                        "type":"vm",
+                        "tenant":"{tenant name1}",
+                        "source":"{VM name1}",
+                        "poll_interval" : "{optional, interval in second for polling}",
+                        "api_method": "{GET/POST/PUT/etc.}",
+                        "api_link":"{API link to collect data, could be format string}",
+                        "tenant_id": tenant_id,
+                        "server_id": server_id,
+                    },
+                    {
+                        "domain":"fault"
+                        "type":"vm",
+                        "tenant":"{tenant name2}",
+                        "source":"{VM name2}",
+                        "poll_interval" : "{optional, interval in second for polling}",
+                        "api_method": "{GET/POST/PUT/etc.}",
+                        "api_link":"{API link to collect data, could be format string}",
+                        "tenant_id": tenant_id,
+                        "server_id": server_id,
+                    }
+                ]
+            }
+    '''
+
+    def __init__(self):
+        self._logger = logger
+        self.proxy_prefix = settings.MULTICLOUD_PREFIX
+
+    def get(self, request, vimid=""):
+        '''
+        get blob of vesagent-config
+        :param request:
+        :param vimid:
+        :return:
+        '''
+        self._logger.info("vimid: %s" % vimid)
+        self._logger.debug("with META: %s" % request.META)
+        try:
+            # get vesagent_config from cloud region
+            try:
+                viminfo = extsys.get_vim_by_id(vimid)
+                cloud_extra_info_str = viminfo.get('cloud_extra_info', '')
+                cloud_extra_info = json.loads(cloud_extra_info_str) if cloud_extra_info_str != '' else None
+                vesagent_config = cloud_extra_info.get("vesagent_config", None) if cloud_extra_info is not None else None
+            except Exception:
+                # ignore this error
+                self._logger.warn("cloud extra info is provided with data in  bad format: %s" % cloud_extra_info_str)
+                pass
+
+            vesagent_backlogs = self.getBacklogsOneVIM(vimid)
+
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            return Response(data={'error': str(e)},
+                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+        self._logger.info("return with %s" % status.HTTP_200_OK)
+        return Response(data={"vesagent_config": vesagent_config,
+                              "vesagent_backlogs": vesagent_backlogs},
+                        status=status.HTTP_200_OK)
+
+    def post(self, request, vimid=""):
+        '''
+        update the blob of vesagent-config, rebuild the backlog for the vesagent workers,
+        and start the vesagent workers if not started yet
+        Implication: the request to this API endpoint will build the backlog locally, hence only local VES agent workers
+        will process these backlogs, which conforms to distributed deployment of M.C. services which includes VES agents
+        :param request:{"vesagent_config":
+                         {"ves_subscription":
+                           {"endpoint":"http://127.0.0.1:9011/sample",
+                            "username":"user","password":"password"},
+                            "poll_interval_default":10,
+                            "backlogs":
+                            [
+                            {"domain":"fault","type":"vm","tenant":"VIM","source":"onap-aaf"}
+                            ]
+                           }
+                         }
+        :param vimid:
+        :return:
+        '''
+
+        self._logger.info("vimid: %s" % vimid)
+        self._logger.debug("with META: %s, with data: %s" % (request.META, request.data))
+        try:
+            vesagent_config = None
+            if request.data is None or request.data.get("vesagent_config", None) is None:
+                # Try to load the vesagent_config out of cloud_region["cloud_extra_info"]
+                viminfo = extsys.get_vim_by_id(vimid)
+                cloud_extra_info_str = viminfo.get('cloud_extra_info', None)
+                cloud_extra_info = json.loads(cloud_extra_info_str) if cloud_extra_info_str is not None else None
+                vesagent_config = cloud_extra_info.get("vesagent_config", None) if cloud_extra_info is not None else None
+            else:
+                vesagent_config = request.data.get("vesagent_config", None)
+
+            if vesagent_config is None:
+                return Response(data={'vesagent_config is not provided'},
+                                status=status.HTTP_400_BAD_REQUEST)
+
+            vesagent_backlogs = self.buildBacklogsOneVIM(vimid, vesagent_config)
+
+            # store back to cloud_extra_info
+            # tbd
+
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            return Response(data={'error': str(e)},
+                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+        self._logger.info("return with %s" % status.HTTP_201_CREATED)
+        return Response(data={"vesagent_config": vesagent_config,
+                              "vesagent_backlogs": vesagent_backlogs},
+                        status=status.HTTP_201_CREATED)
+
+    def delete(self, request, vimid=""):
+        '''
+        delete the blob of vesagent-config, remove it from backlog and stop the vesagent worker if no backlog
+        :param request:
+        :param vimid:
+        :return:
+        '''
+        self._logger.info("vimid: %s" % vimid)
+        self._logger.debug("with META: %s" % request.META)
+        try:
+            # tbd
+            self.clearBacklogsOneVIM(vimid)
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            return Response(data={'error': str(e)},
+                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+        self._logger.info("return with %s" % status.HTTP_200_OK)
+        return Response(status=status.HTTP_200_OK)
+
+    def getBacklogsOneVIM(self, vimid):
+        '''
+        remove the specified backlogs for a VIM
+        :param vimid:
+        :return:
+        '''
+        self._logger.debug("vimid: %s" % vimid)
+
+        vesAgentConfig = None
+        try:
+            # retrive the backlogs
+            vesAgentConfigStr = cache.get("VesAgentBacklogs.config.%s" % (vimid))
+            if vesAgentConfigStr is None:
+                logger.warn("VesAgentBacklogs.config.%s cannot be found in cache" % (vimid))
+                return None
+
+            logger.debug("VesAgentBacklogs.config.%s: %s" % (vimid, vesAgentConfigStr))
+
+            vesAgentConfig = json.loads(vesAgentConfigStr)
+            if vesAgentConfig is None:
+                logger.warn("VesAgentBacklogs.config.%s corrupts" % (vimid))
+                return None
+
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            vesAgentConfig = {"error": "exception occurs"}
+
+        self._logger.debug("return")
+        return vesAgentConfig
+
+    def clearBacklogsOneVIM(self, vimid):
+        '''
+        remove the specified backlogs for a VIM
+        :param vimid:
+        :param vesagent_config:
+        :return:
+        '''
+        self._logger.debug("vimid: %s" % vimid)
+
+        try:
+            # remove vimid from "VesAgentBacklogs.vimlist"
+            VesAgentBacklogsVimListStr = cache.get("VesAgentBacklogs.vimlist")
+            VesAgentBacklogsVimList = []
+            if VesAgentBacklogsVimListStr is not None:
+                VesAgentBacklogsVimList = json.loads(VesAgentBacklogsVimListStr)
+                VesAgentBacklogsVimList = [v for v in VesAgentBacklogsVimList if v != vimid]
+
+            logger.debug("VesAgentBacklogs.vimlist is %s" % VesAgentBacklogsVimList)
+
+            # cache forever
+            cache.set("VesAgentBacklogs.vimlist", json.dumps(VesAgentBacklogsVimList), None)
+
+            # retrieve the backlogs
+            vesAgentConfigStr = cache.get("VesAgentBacklogs.config.%s" % (vimid))
+            if vesAgentConfigStr is None:
+                logger.warn("VesAgentBacklogs.config.%s cannot be found in cache" % (vimid))
+                return 0
+
+            logger.debug("VesAgentBacklogs.config.%s: %s" % (vimid, vesAgentConfigStr))
+
+            vesAgentConfig = json.loads(vesAgentConfigStr)
+            if vesAgentConfig is None:
+                logger.warn("VesAgentBacklogs.config.%s corrupts" % (vimid))
+                return 0
+
+            # iterate all backlog and remove the associate state!
+            # tbd
+
+            # clear the whole backlogs for a VIM
+            cache.set("VesAgentBacklogs.config.%s" % vimid, "deleting the backlogs", 1)
+
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+
+        self._logger.debug("return")
+        return 0
+
+    def buildBacklogsOneVIM(self, vimid, vesagent_config=None):
+        '''
+        build and cache backlog for specific cloud region,spawn vesagent workers if needed
+        :param vimid:
+        :param vesagent_config: vesagent_config data in json object
+        :return:
+        '''
+        self._logger.debug("vimid: %s" % vimid)
+        self._logger.debug("config data: %s" % vesagent_config)
+
+        VesAgentBacklogsConfig = None
+        try:
+            if vesagent_config:
+                # now rebuild the backlog
+                VesAgentBacklogsConfig = {
+                    "vimid": vimid,
+                    "poll_interval_default": vesagent_config.get("poll_interval_default", 0),
+                    "subscription": vesagent_config.get("ves_subscription", None),
+                    "backlogs": [self.buildBacklog(vimid, b) for b in vesagent_config.get("backlogs", [])]
+                }
+
+                # add/update the backlog into cache
+                VesAgentBacklogsConfigStr = json.dumps(VesAgentBacklogsConfig)
+                # cache forever
+                cache.set("VesAgentBacklogs.config.%s" % vimid, VesAgentBacklogsConfigStr, None)
+
+                # update list of vimid for vesagent
+                # get the whole list of backlog
+                VesAgentBacklogsVimListStr = cache.get("VesAgentBacklogs.vimlist")
+                VesAgentBacklogsVimList = [vimid]
+                if VesAgentBacklogsVimListStr is not None:
+                    VesAgentBacklogsVimList = json.loads(VesAgentBacklogsVimListStr)
+                    VesAgentBacklogsVimList = [v for v in VesAgentBacklogsVimList if v != vimid]
+                    VesAgentBacklogsVimList.append(vimid)
+
+                logger.debug("VesAgentBacklogs.vimlist is %s" % VesAgentBacklogsVimList)
+
+                # cache forever
+                cache.set("VesAgentBacklogs.vimlist", json.dumps(VesAgentBacklogsVimList), None)
+
+                # notify schduler
+                scheduleBacklogs.delay(vimid)
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            VesAgentBacklogsConfig = {"error": "exception occurs during build backlogs"}
+
+        self._logger.debug("return")
+        return VesAgentBacklogsConfig
+
+    def buildBacklog(self, vimid, backlog_input):
+        self._logger.debug("build backlog for: %s" % vimid)
+        self._logger.debug("with input: %s" % backlog_input)
+
+        try:
+            if backlog_input["domain"] == "fault" and backlog_input["type"] == "vm":
+                return fault_vm.buildBacklog_fault_vm(vimid, backlog_input)
+            else:
+                self._logger.warn("return with failure: unsupported backlog domain:%s, type:%s"
+                                  % (backlog_input["domain"], backlog_input["type"] == "vm"))
+                return None
+        except Exception as e:
+            self._logger.error("exception:%s" % str(e))
+            return None
+
+        self._logger.debug("return without backlog")
+        return None
+
+
+class APIv1VesAgentCtrl(VesAgentCtrl):
+
+    def __init__(self):
+        super(APIv1VesAgentCtrl, self).__init__()
+        # self._logger = logger
+        self.proxy_prefix = settings.MULTICLOUD_API_V1_PREFIX
+
+    def get(self, request, cloud_owner="", cloud_region_id=""):
+        '''
+        :param request:
+        :param cloud_owner:
+        :param cloud_region_id:
+        :return:
+        '''
+        self._logger.info("cloud_owner, cloud_region_id: %s,%s" % (cloud_owner, cloud_region_id))
+
+        vimid = extsys.encode_vim_id(cloud_owner, cloud_region_id)
+        return super(APIv1VesAgentCtrl, self).get(request, vimid)
+
+    def post(self, request, cloud_owner="", cloud_region_id=""):
+        '''
+        wrapper for inherited API with VIM ID
+        :param request:
+        :param cloud_owner:
+        :param cloud_region_id:
+        :return:
+        '''
+        self._logger.info("cloud_owner, cloud_region_id: %s,%s" % (cloud_owner, cloud_region_id))
+
+        vimid = extsys.encode_vim_id(cloud_owner, cloud_region_id)
+        return super(APIv1VesAgentCtrl, self).post(request, vimid)
+
+    def delete(self, request, cloud_owner="", cloud_region_id=""):
+        '''
+        wrapper of inherited API with VIM ID
+        :param request:
+        :param cloud_owner:
+        :param cloud_region_id:
+        :return:
+        '''
+        self._logger.info(
+            "cloud_owner, cloud_region_id: %s,%s" %
+            (cloud_owner, cloud_region_id))
+
+        vimid = extsys.encode_vim_id(cloud_owner, cloud_region_id)
+        return super(APIv1VesAgentCtrl, self).delete(request, vimid)
diff --git a/fcaps/fcaps/vesagent/vespublish.py b/fcaps/fcaps/vesagent/vespublish.py
new file mode 100644 (file)
index 0000000..a53245c
--- /dev/null
@@ -0,0 +1,53 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, unicode_literals
+
+import time
+import logging
+import json
+import urllib2
+
+logger = logging.getLogger(__name__)
+
+
+def publishAnyEventToVES(ves_subscription, events):
+    if not events or len(events) == 0:
+        return
+
+    logger.info("Start to send single event to VES collector.")
+    endpoint = ves_subscription.get("endpoint", None)
+    # username = ves_subscription.get("username", None)
+    # password = ves_subscription.get("password", None)
+
+    if endpoint:
+        try:
+            if len(events) > 1:
+                endpoint = "%s/eventBatch" % endpoint
+                events = {"eventList": events}
+            elif len(events) == 1:
+                events = {"event": events[0]}
+
+            logger.info("publish event to VES: %s" % endpoint)
+            headers = {'Content-Type': 'application/json'}
+            request = urllib2.Request(url=endpoint, headers=headers, data=json.dumps(events))
+            time.sleep(1)
+            response = urllib2.urlopen(request)
+            logger.info("VES response is: %s" % response.read())
+        except urllib2.URLError as e:
+            logger.critical("Failed to publish to %s: %s" % (endpoint, e.reason))
+        except Exception as e:
+            logger.error("exception:%s" % str(e))
+    else:
+        logger.info("Missing VES info.")
diff --git a/fcaps/fcaps/wsgi.py b/fcaps/fcaps/wsgi.py
new file mode 100644 (file)
index 0000000..894e6f4
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fcaps.settings")
+
+application = get_wsgi_application()
diff --git a/fcaps/initialize.sh b/fcaps/initialize.sh
new file mode 100644 (file)
index 0000000..5c47145
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+pip install -r requirements.txt
diff --git a/fcaps/logs/empty.txt b/fcaps/logs/empty.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/fcaps/manage.py b/fcaps/manage.py
new file mode 100644 (file)
index 0000000..3a3d3bb
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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 sys
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fcaps.settings")
+
+if __name__ == "__main__":
+    from django.core.management import execute_from_command_line
+    execute_from_command_line(sys.argv)
diff --git a/fcaps/mvn-phase-script.sh b/fcaps/mvn-phase-script.sh
new file mode 100755 (executable)
index 0000000..d09584d
--- /dev/null
@@ -0,0 +1,83 @@
+#!/bin/bash
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+
+set -e
+
+echo "running script: [$0] for module [$1] at stage [$2]"
+
+export SETTINGS_FILE=${SETTINGS_FILE:-$HOME/.m2/settings.xml}
+MVN_PROJECT_MODULEID="$1"
+MVN_PHASE="$2"
+
+
+FQDN="${MVN_PROJECT_GROUPID}.${MVN_PROJECT_ARTIFACTID}"
+if [ "$MVN_PROJECT_MODULEID" == "__" ]; then
+  MVN_PROJECT_MODULEID=""
+fi
+
+if [ -z "$WORKSPACE" ]; then
+    WORKSPACE=$(pwd)
+fi
+
+# mvn phase in life cycle
+MVN_PHASE="$2"
+
+
+echo "MVN_PROJECT_MODULEID is            [$MVN_PROJECT_MODULEID]"
+echo "MVN_PHASE is                       [$MVN_PHASE]"
+echo "MVN_PROJECT_GROUPID is             [$MVN_PROJECT_GROUPID]"
+echo "MVN_PROJECT_ARTIFACTID is          [$MVN_PROJECT_ARTIFACTID]"
+echo "MVN_PROJECT_VERSION is             [$MVN_PROJECT_VERSION]"
+
+run_tox_test()
+{
+  set -x
+  echo $PWD
+  CURDIR=$(pwd)
+  TOXINIS=$(find . -name "tox.ini")
+  cd ..
+  for TOXINI in "${TOXINIS[@]}"; do
+    DIR=$(echo "$TOXINI" | rev | cut -f2- -d'/' | rev)
+    cd "${CURDIR}/${DIR}"
+    rm -rf ./venv-tox ./.tox
+    virtualenv ./venv-tox
+    source ./venv-tox/bin/activate
+    pip install --upgrade pip
+    pip install --upgrade tox argparse
+    pip freeze
+    cd ${CURDIR}
+    tox
+    deactivate
+    cd ..
+    rm -rf ./venv-tox ./.tox
+  done
+}
+
+
+case $MVN_PHASE in
+clean)
+  echo "==> clean phase script"
+  rm -rf ./venv-*
+  ;;
+test)
+  echo "==> test phase script"
+  run_tox_test
+  ;;
+*)
+  echo "==> unprocessed phase"
+  ;;
+esac
+
diff --git a/fcaps/pom.xml b/fcaps/pom.xml
new file mode 100644 (file)
index 0000000..98f25db
--- /dev/null
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<!--
+ Copyright (c) 2017-2019 Wind River Systems, Inc.
+
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.onap.oparent</groupId>
+        <artifactId>oparent</artifactId>
+        <version>1.2.0</version>
+        <relativePath>../oparent</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.onap.multicloud.openstack</groupId>
+    <artifactId>multicloud-openstack-fcaps</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+    <name>multicloud-openstack-fcaps</name>
+    <description>multicloud for openstack Wind River Titanium Cloud</description>
+    <properties>
+        <encoding>UTF-8</encoding>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <nexusproxy>https://nexus.onap.org</nexusproxy>
+        <sonar.sources>.</sonar.sources>
+        <sonar.junit.reportsPath>xunit-results.xml</sonar.junit.reportsPath>
+        <sonar.python.coverage.reportPath>coverage.xml</sonar.python.coverage.reportPath>
+        <sonar.language>py</sonar.language>
+        <sonar.pluginName>Python</sonar.pluginName>
+        <sonar.inclusions>**/*.py</sonar.inclusions>
+        <sonar.exclusions>**/venv-tox/**,**/.tox/**, **/tests/**,setup.py</sonar.exclusions>
+    </properties>
+    <build>
+      <pluginManagement>
+        <plugins>
+            <plugin>
+              <groupId>org.codehaus.mojo</groupId>
+              <artifactId>exec-maven-plugin</artifactId>
+              <version>1.2.1</version>
+              <configuration>
+                <executable>${project.basedir}/mvn-phase-script.sh</executable>
+                <environmentVariables>
+                  <!-- make mvn properties as env for our script -->
+                  <MVN_PROJECT_GROUPID>${project.groupId}</MVN_PROJECT_GROUPID>
+                  <MVN_PROJECT_ARTIFACTID>${project.artifactId}</MVN_PROJECT_ARTIFACTID>
+                  <MVN_PROJECT_VERSION>${project.version}</MVN_PROJECT_VERSION>
+                </environmentVariables>
+              </configuration>
+            </plugin>
+        </plugins>
+      </pluginManagement>
+        <plugins>
+        <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <version>1.2.1</version>
+            <executions>
+              <execution>
+                <id>clean phase script</id>
+                <phase>clean</phase>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <configuration>
+                  <arguments>
+                    <argument>__</argument>
+                    <argument>clean</argument>
+                  </arguments>
+                </configuration>
+              </execution>
+              <execution>
+                <id>test script</id>
+                <phase>test</phase>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <configuration>
+                  <arguments>
+                    <argument>__</argument>
+                    <argument>test</argument>
+                  </arguments>
+                </configuration>
+              </execution>
+            </executions>
+        </plugin>
+        <plugin>
+            <artifactId>maven-assembly-plugin</artifactId>
+            <configuration>
+                <appendAssemblyId>false</appendAssemblyId>
+                <descriptors>
+                    <descriptor>assembly.xml</descriptor>
+                </descriptors>
+            </configuration>
+            <executions>
+                <execution>
+                    <id>make-assembly</id>
+                    <phase>package</phase>
+                    <goals>
+                        <goal>single</goal>
+                    </goals>
+                </execution>
+            </executions>
+        </plugin>
+      </plugins>
+    </build>
+</project>
diff --git a/fcaps/requirements.txt b/fcaps/requirements.txt
new file mode 100644 (file)
index 0000000..cec3a5e
--- /dev/null
@@ -0,0 +1,26 @@
+# rest framework
+Django==1.9.6
+djangorestframework==3.3.3
+
+# for call rest api
+httplib2==0.9.2
+
+# for call openstack auth and transport api
+keystoneauth1==2.18.0
+
+#python-memcached
+python-memcached
+
+#uwsgi for parallel processing
+uwsgi
+
+# for unit test
+coverage==4.2
+mock==2.0.0
+unittest_xml_reporting==1.12.0
+
+# for onap logging
+onappylog>=1.0.6
+
+# for background tasks
+celery >= 4.0
diff --git a/fcaps/run.sh b/fcaps/run.sh
new file mode 100644 (file)
index 0000000..bc01c99
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/bash
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+memcached -d -m 2048 -u root -c 1024 -p 11211 -P /tmp/memcached1.pid
+export PYTHONPATH=lib/share
+
+#service rabbitmq-server restart
+# make sure only 1 worker due to missing the synchronization between workers now
+nohup celery -A fcaps worker --concurrency=1 --loglevel=info &
+
+#nohup python manage.py runserver 0.0.0.0:9011 2>&1 &
+nohup uwsgi --http :9011 --module fcaps.wsgi --master --processes 4 &
+
+logDir="/var/log/onap/multicloud/openstack/fcaps"
+if [ ! -x  $logDir  ]; then
+       mkdir -p $logDir
+fi
+while [ ! -f $logDir/fcaps.log ]; do
+    sleep 1
+done
+
+tail -F $logDir/fcaps.log
+
+
diff --git a/fcaps/stop.sh b/fcaps/stop.sh
new file mode 100644 (file)
index 0000000..56104f9
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright (c) 2017-2019 Wind River Systems, Inc.
+#
+# 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.
+
+#ps auxww | grep 'manage.py runserver 0.0.0.0:9011' | awk '{print $2}' | xargs kill -9
+ps auxww |grep 'uwsgi --http :9011 --module fcaps.wsgi --master' |awk '{print $2}' |xargs kill -9
+ps auxww | grep 'memcached -d -m 2048 -u root -c 1024 -p 11211 -P /tmp/memcached1.pid' | awk '{print $2}' | xargs kill -9
diff --git a/fcaps/test-requirements.txt b/fcaps/test-requirements.txt
new file mode 100644 (file)
index 0000000..97044b5
--- /dev/null
@@ -0,0 +1 @@
+pylint # GPLv2
diff --git a/fcaps/tox.ini b/fcaps/tox.ini
new file mode 100644 (file)
index 0000000..7710e46
--- /dev/null
@@ -0,0 +1,34 @@
+[tox]
+envlist = py27,cov,pylint
+skipsdist = true
+
+[tox:jenkins]
+downloadcache = ~/cache/pip
+
+[flake8]
+ignore = E501,E722
+exclude = ./venv-tox,./.tox
+max-complexity = 27
+
+[testenv]
+setenv =
+    PYTHONPATH = {toxinidir}/../share
+deps =
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/test-requirements.txt
+commands =
+    coverage run --branch manage.py test fcaps
+    coverage report --omit="./venv-tox/*,./.tox/*,*tests*,*__init__.py,*newton_base*,*common*,*starlingx_base*" --fail-under=30
+
+[testenv:pep8]
+deps=flake8
+commands=flake8
+
+[testenv:cov]
+commands = coverage xml --omit="./venv-tox/*,./.tox/*,*tests*,*__init__.py,*newton_base*,*common*, *site-packages*"
+
+[testenv:pylint]
+whitelist_externals = bash
+commands =
+  bash -c "\
+    pylint -f parseable --reports=y fcaps | tee pylint.out"