summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdashboard/backend/dovetail/__init__.py8
-rwxr-xr-xdashboard/backend/dovetail/api/__init__.py29
-rwxr-xr-xdashboard/backend/dovetail/api/api.py182
-rwxr-xr-xdashboard/backend/dovetail/api/exception_handler.py93
-rwxr-xr-xdashboard/backend/dovetail/api/utils.py20
-rwxr-xr-xdashboard/backend/dovetail/db/__init__.py8
-rwxr-xr-xdashboard/backend/dovetail/db/api.py72
-rwxr-xr-xdashboard/backend/dovetail/db/database.py182
-rwxr-xr-xdashboard/backend/dovetail/db/exception.py121
-rwxr-xr-xdashboard/backend/dovetail/db/models.py105
-rwxr-xr-xdashboard/backend/dovetail/db/utils.py478
-rwxr-xr-xdashboard/backend/dovetail/utils/__init__.py8
-rwxr-xr-xdashboard/backend/dovetail/utils/flags.py82
-rwxr-xr-xdashboard/backend/dovetail/utils/logsetting.py98
-rwxr-xr-xdashboard/backend/dovetail/utils/setting_wrapper.py18
-rwxr-xr-xdashboard/backend/dovetail/utils/util.py71
-rwxr-xr-xdashboard/backend/install_db.py55
-rwxr-xr-xdashboard/backend/wsgi.py35
-rw-r--r--docker/Dockerfile6
-rw-r--r--docs/dovetailtool/dovetail.tool.installation.rst26
-rw-r--r--docs/dovetailtool/dovetail.tool.overview.rst2
-rw-r--r--dovetail/compliance/compliance_set.yml (renamed from dovetail/cert/compliance_set.yml)4
-rw-r--r--dovetail/compliance/debug.yml9
-rw-r--r--dovetail/compliance/proposed_tests.yml (renamed from dovetail/cert/proposed_tests.yml)4
-rw-r--r--dovetail/conf/cmd_config.yml18
-rw-r--r--dovetail/conf/dovetail_config.py16
-rw-r--r--dovetail/conf/dovetail_config.yml2
-rw-r--r--dovetail/container.py30
-rw-r--r--dovetail/parser.py19
-rw-r--r--dovetail/report.py221
-rwxr-xr-xdovetail/run.py93
-rw-r--r--dovetail/testcase.py44
-rw-r--r--dovetail/tests/unit/test_parser.py3
-rw-r--r--dovetail/utils/dovetail_logger.py14
-rw-r--r--ez_setup.py5
35 files changed, 2010 insertions, 171 deletions
diff --git a/dashboard/backend/dovetail/__init__.py b/dashboard/backend/dovetail/__init__.py
new file mode 100755
index 00000000..6dbd8d79
--- /dev/null
+++ b/dashboard/backend/dovetail/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/dashboard/backend/dovetail/api/__init__.py b/dashboard/backend/dovetail/api/__init__.py
new file mode 100755
index 00000000..f9c4e5a2
--- /dev/null
+++ b/dashboard/backend/dovetail/api/__init__.py
@@ -0,0 +1,29 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import datetime
+import logging
+
+from flask import Flask
+
+from dovetail.utils import util
+
+logging.info('flask app: begin to init')
+
+app = Flask(__name__)
+app.debug = True
+logging.info('flask app config:%s', app.config)
+
+app.config['REMEMBER_COOKIE_DURATION'] = (
+ datetime.timedelta(
+ seconds=util.parse_time_interval('2h')
+ )
+)
+
+logging.info('flask app: finish init')
diff --git a/dashboard/backend/dovetail/api/api.py b/dashboard/backend/dovetail/api/api.py
new file mode 100755
index 00000000..0f405f23
--- /dev/null
+++ b/dashboard/backend/dovetail/api/api.py
@@ -0,0 +1,182 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+
+from dovetail.api import utils
+from dovetail.api import exception_handler
+from dovetail.db import api as db_api
+
+from flask import Flask
+from flask import request
+
+import json
+
+app = Flask(__name__)
+
+
+@app.after_request
+def after_request(response):
+ response.headers.add('Access-Control-Allow-Origin', '*')
+ response.headers.add(
+ 'Access-Control-Allow-Headers',
+ 'Content-Type, Authorization')
+ response.headers.add('Aceess-Control-Allow-Methods', 'GET,PUT,DELETE,POST')
+ return response
+
+# test
+
+
+@app.route("/test", methods=['GET'])
+def test():
+ """backend api test"""
+ logging.info('test functest')
+ resp = utils.make_json_response(
+ 200, {'test': 20}
+ )
+ return resp
+
+
+# settings
+@app.route("/clear", methods=['POST'])
+def clear_settings():
+ """ clear all settings data on backend server """
+ logging.info('clear all settings')
+
+ return utils.make_json_response(
+ 200, {}
+ )
+
+
+@app.route("/settings", methods=['GET'])
+def list_settings():
+ """list settings"""
+ logging.info('list settings')
+ global settings
+ return utils.make_json_response(200, settings)
+
+
+@app.route("/settings", methods=['POST'])
+def add_settings():
+ pass
+
+
+@app.route("/settings", methods=['POST'])
+def remove_settings():
+ pass
+
+
+@app.route("/testcases", methods=['GET'])
+def get_testcases():
+ pass
+
+
+@app.route("/results/<test_id>", methods=['GET'])
+def show_result(test_id):
+ data = _get_request_args()
+ return utils.make_json_response(
+ 200,
+ db_api.get_result(
+ test_id, **data
+ )
+ )
+
+
+@app.route("/results", methods=['GET'])
+def list_results():
+ data = _get_request_args()
+ return utils.make_json_response(
+ 200,
+ db_api.list_results(
+ **data
+ )
+ )
+
+
+@app.route("/results", methods=['POST'])
+def add_result():
+ data = _get_request_data()
+ ret_code = 200
+ json_object = json.loads(data)
+ logging.debug('json_object:%s' % (json_object))
+ if not db_api.store_result(**json_object):
+ ret_code = 500
+ resp = utils.make_json_response(
+ ret_code, data
+ )
+ return resp
+
+
+@app.route("/results/<test_id>", methods=['DELETE'])
+def remove_results(test_id):
+ data = _get_request_data()
+ logging.debug('data:%s' % data)
+ response = db_api.del_result(
+ test_id, **data
+ )
+ return utils.make_json_response(
+ 200, response
+ )
+
+
+def _get_request_data():
+ """Convert reqeust data from string to python dict.
+
+ If the request data is not json formatted, raises
+ exception_handler.BadRequest.
+ If the request data is not json formatted dict, raises
+ exception_handler.BadRequest
+ If the request data is empty, return default as empty dict.
+
+ Usage: It is used to add or update a single resource.
+ """
+ if request.data:
+ try:
+ data = json.loads(request.data)
+ except Exception:
+ raise exception_handler.BadRequest(
+ 'request data is not json formatted: %s' % request.data
+ )
+ if not isinstance(data, dict):
+ raise exception_handler.BadRequest(
+ 'request data is not json formatted dict: %s' % request.data
+ )
+
+ return request.data
+ else:
+ return {}
+
+
+def _get_request_args(**kwargs):
+ """Get request args as dict.
+
+ The value in the dict is converted to expected type.
+
+ Args:
+ kwargs: for each key, the value is the type converter.
+ """
+ args = dict(request.args)
+ for key, value in args.items():
+ if key in kwargs:
+ converter = kwargs[key]
+ if isinstance(value, list):
+ args[key] = [converter(item) for item in value]
+ else:
+ args[key] = converter(value)
+ return args
+
+'''
+@app.teardown_appcontext
+def shutdown_session(exception=None):
+ db_session.remove()
+'''
+# user login/logout
+
+if __name__ == '__main__':
+ app.run(host='127.0.0.1')
diff --git a/dashboard/backend/dovetail/api/exception_handler.py b/dashboard/backend/dovetail/api/exception_handler.py
new file mode 100755
index 00000000..b7ce592a
--- /dev/null
+++ b/dashboard/backend/dovetail/api/exception_handler.py
@@ -0,0 +1,93 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+"""Exceptions for RESTful API."""
+import traceback
+
+from dovetail.api import app
+from dovetail.api import utils
+
+
+class HTTPException(Exception):
+
+ def __init__(self, message, status_code):
+ super(HTTPException, self).__init__(message)
+ self.traceback = traceback.format_exc()
+ self.status_code = status_code
+
+ def to_dict(self):
+ return {'message': str(self)}
+
+
+class ItemNotFound(HTTPException):
+ """Define the exception for referring non-existing object."""
+
+ def __init__(self, message):
+ super(ItemNotFound, self).__init__(message, 410)
+
+
+class BadRequest(HTTPException):
+ """Define the exception for invalid/missing parameters.
+
+ User making a request in invalid state cannot be processed.
+ """
+
+ def __init__(self, message):
+ super(BadRequest, self).__init__(message, 400)
+
+
+class Unauthorized(HTTPException):
+ """Define the exception for invalid user login."""
+
+ def __init__(self, message):
+ super(Unauthorized, self).__init__(message, 401)
+
+
+class UserDisabled(HTTPException):
+ """Define the exception for disabled users."""
+
+ def __init__(self, message):
+ super(UserDisabled, self).__init__(message, 403)
+
+
+class Forbidden(HTTPException):
+ """Define the exception for invalid permissions."""
+
+ def __init__(self, message):
+ super(Forbidden, self).__init__(message, 403)
+
+
+class BadMethod(HTTPException):
+ """Define the exception for invoking unsupported methods."""
+
+ def __init__(self, message):
+ super(BadMethod, self).__init__(message, 405)
+
+
+class ConflictObject(HTTPException):
+ """Define the exception for creating an existing object."""
+
+ def __init__(self, message):
+ super(ConflictObject, self).__init__(message, 409)
+
+
+@app.errorhandler(Exception)
+def handle_exception(error):
+ if hasattr(error, 'to_dict'):
+ response = error.to_dict()
+ else:
+ response = {'message': str(error)}
+ if app.debug and hasattr(error, 'traceback'):
+ response['traceback'] = error.traceback
+
+ status_code = 400
+ if hasattr(error, 'status_code'):
+ status_code = error.status_code
+
+ return utils.make_json_response(status_code, response)
diff --git a/dashboard/backend/dovetail/api/utils.py b/dashboard/backend/dovetail/api/utils.py
new file mode 100755
index 00000000..dbe8d082
--- /dev/null
+++ b/dashboard/backend/dovetail/api/utils.py
@@ -0,0 +1,20 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import json
+from flask import make_response
+
+
+def make_json_response(status_code, data):
+ """Wrap json format to the reponse object."""
+
+ result = json.dumps(data, indent=4, default=lambda x: None) + '\r\n'
+ resp = make_response(result, status_code)
+ resp.headers['Content-type'] = 'application/json'
+ return resp
diff --git a/dashboard/backend/dovetail/db/__init__.py b/dashboard/backend/dovetail/db/__init__.py
new file mode 100755
index 00000000..6dbd8d79
--- /dev/null
+++ b/dashboard/backend/dovetail/db/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/dashboard/backend/dovetail/db/api.py b/dashboard/backend/dovetail/db/api.py
new file mode 100755
index 00000000..a522a481
--- /dev/null
+++ b/dashboard/backend/dovetail/db/api.py
@@ -0,0 +1,72 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+"""
+Defines interface for DB access.
+"""
+
+import logging
+
+from dovetail.db import database
+from dovetail.db import utils
+from dovetail.db import models
+
+
+@database.run_in_session()
+def store_result(exception_when_existing=True,
+ session=None, **kwargs):
+ """Storing results into database.
+
+ :param data: Dict describes test results.
+ """
+ logging.debug('store_result:%s' % kwargs)
+ result = utils.add_db_object(
+ session, models.Result, exception_when_existing,
+ **kwargs)
+
+ return result
+
+
+@database.run_in_session()
+@utils.wrap_to_dict()
+def list_results(session=None, **filters):
+ """Get all results
+ """
+ logging.debug('session:%s' % session)
+ results = utils.list_db_objects(
+ session, models.Result, **filters
+ )
+ return results
+
+
+@database.run_in_session()
+@utils.wrap_to_dict()
+def get_result(test_id, exception_when_missing=True,
+ session=None, **kwargs):
+ """Get specific result with the test_id
+
+ :param test_id: the unique serial number for the test
+ """
+ return _get_result(test_id, session,
+ exception_when_missing=exception_when_missing, **kwargs)
+
+
+def _get_result(test_id, session=None, **kwargs):
+ return utils.get_db_object(
+ session, models.Result, test_id=test_id, **kwargs)
+
+
+@database.run_in_session()
+def del_result(test_id, session=None, **kwargs):
+ """Delete a results from database
+
+ :param test_id: the unique serial number for the test
+ """
+ return utils.del_db_objects(session, models.Result,
+ test_id=test_id, **kwargs)
diff --git a/dashboard/backend/dovetail/db/database.py b/dashboard/backend/dovetail/db/database.py
new file mode 100755
index 00000000..bc09d3bd
--- /dev/null
+++ b/dashboard/backend/dovetail/db/database.py
@@ -0,0 +1,182 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+import functools
+
+from threading import local
+
+from sqlalchemy import create_engine
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.exc import OperationalError
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.pool import StaticPool
+
+from contextlib import contextmanager
+from dovetail.db import exception
+from dovetail.db import models
+
+ENGINE = None
+SESSION = sessionmaker(autocommit=False, autoflush=False)
+SCOPED_SESSION = None
+SESSION_HOLDER = local()
+
+SQLALCHEMY_DATABASE_URI = "mysql://root:%s@localhost:3306/dovetail" % ('root')
+
+
+def init(database_url=None):
+ """Initialize database.
+
+ :param database_url: string, database url.
+ """
+ global ENGINE
+ global SCOPED_SESSION
+ if not database_url:
+ database_url = SQLALCHEMY_DATABASE_URI
+ logging.info('init database %s', database_url)
+ print("database init %s" % database_url)
+ ENGINE = create_engine(
+ database_url, convert_unicode=True,
+ poolclass=StaticPool
+ )
+ SESSION.configure(bind=ENGINE)
+ SCOPED_SESSION = scoped_session(SESSION)
+ models.BASE.query = SCOPED_SESSION.query_property()
+
+
+def in_session():
+ """check if in database session scope."""
+ bool(hasattr(SESSION_HOLDER, 'session'))
+
+
+@contextmanager
+def session(exception_when_in_session=True):
+ """database session scope.
+
+ To operate database, it should be called in database session.
+ If not exception_when_in_session, the with session statement support
+ nested session and only the out most session commit/rollback the
+ transaction.
+ """
+ if not ENGINE:
+ init()
+
+ nested_session = False
+ if hasattr(SESSION_HOLDER, 'session'):
+ if exception_when_in_session:
+ logging.error('we are already in session')
+ raise exception.DatabaseException('session already exist')
+ else:
+ new_session = SESSION_HOLDER.session
+ nested_session = True
+ logging.log(
+ logging.DEBUG,
+ 'reuse session %s', nested_session
+ )
+ else:
+ new_session = SCOPED_SESSION()
+ setattr(SESSION_HOLDER, 'session', new_session)
+ logging.log(
+ logging.DEBUG,
+ 'enter session %s', new_session
+ )
+ try:
+ yield new_session
+ if not nested_session:
+ new_session.commit()
+ except Exception as error:
+ if not nested_session:
+ new_session.rollback()
+ logging.error('failed to commit session')
+ logging.exception(error)
+ if isinstance(error, IntegrityError):
+ for item in error.statement.split():
+ if item.islower():
+ object = item
+ break
+ raise exception.DuplicatedRecord(
+ '%s in %s' % (error.orig, object)
+ )
+ elif isinstance(error, OperationalError):
+ raise exception.DatabaseException(
+ 'operation error in database'
+ )
+ elif isinstance(error, exception.DatabaseException):
+ raise error
+ else:
+ raise exception.DatabaseException(str(error))
+ finally:
+ if not nested_session:
+ new_session.close()
+ SCOPED_SESSION.remove()
+ delattr(SESSION_HOLDER, 'session')
+ logging.log(
+ logging.DEBUG,
+ 'exit session %s', new_session
+ )
+
+
+def current_session():
+ """Get the current session scope when it is called.
+
+ :return: database session.
+ :raises: DatabaseException when it is not in session.
+ """
+ try:
+ return SESSION_HOLDER.session
+ except Exception as error:
+ logging.error('It is not in the session scope')
+ logging.exception(error)
+ if isinstance(error, exception.DatabaseException):
+ raise error
+ else:
+ raise exception.DatabaseException(str(error))
+
+
+def run_in_session(exception_when_in_session=True):
+ """Decorator to make sure the decorated function run in session.
+
+ When not exception_when_in_session, the run_in_session can be
+ decorated several times.
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ try:
+ my_session = kwargs.get('session')
+ if my_session is not None:
+ return func(*args, **kwargs)
+ else:
+ with session(
+ exception_when_in_session=exception_when_in_session
+ ) as my_session:
+ kwargs['session'] = my_session
+ return func(*args, **kwargs)
+ except Exception as error:
+ logging.error(
+ 'got exception with func %s args %s kwargs %s',
+ func, args, kwargs
+ )
+ logging.exception(error)
+ raise error
+ return wrapper
+ return decorator
+
+
+@run_in_session()
+def create_db(session=None):
+ """Create database."""
+ models.BASE.metadata.create_all(bind=ENGINE)
+ print('create_db')
+
+
+def drop_db():
+ """Drop database."""
+ models.BASE.metadata.drop_all(bind=ENGINE)
diff --git a/dashboard/backend/dovetail/db/exception.py b/dashboard/backend/dovetail/db/exception.py
new file mode 100755
index 00000000..4acc5fbd
--- /dev/null
+++ b/dashboard/backend/dovetail/db/exception.py
@@ -0,0 +1,121 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+"""Custom exception"""
+import traceback
+
+
+class DatabaseException(Exception):
+ """Base class for all database exceptions."""
+
+ def __init__(self, message):
+ super(DatabaseException, self).__init__(message)
+ self.traceback = traceback.format_exc()
+ self.status_code = 400
+
+ def to_dict(self):
+ return {'message': str(self)}
+
+
+class RecordNotExists(DatabaseException):
+ """Define the exception for referring non-existing object in DB."""
+
+ def __init__(self, message):
+ super(RecordNotExists, self).__init__(message)
+ self.status_code = 404
+
+
+class DuplicatedRecord(DatabaseException):
+ """Define the exception for trying to insert an existing object in DB."""
+
+ def __init__(self, message):
+ super(DuplicatedRecord, self).__init__(message)
+ self.status_code = 409
+
+
+class Unauthorized(DatabaseException):
+ """Define the exception for invalid user login."""
+
+ def __init__(self, message):
+ super(Unauthorized, self).__init__(message)
+ self.status_code = 401
+
+
+class UserDisabled(DatabaseException):
+ """Define the exception that a disabled user tries to do some operations.
+
+ """
+
+ def __init__(self, message):
+ super(UserDisabled, self).__init__(message)
+ self.status_code = 403
+
+
+class Forbidden(DatabaseException):
+ """Define the exception that a user is trying to make some action
+
+ without the right permission.
+
+ """
+
+ def __init__(self, message):
+ super(Forbidden, self).__init__(message)
+ self.status_code = 403
+
+
+class NotAcceptable(DatabaseException):
+ """The data is not acceptable."""
+
+ def __init__(self, message):
+ super(NotAcceptable, self).__init__(message)
+ self.status_code = 406
+
+
+class InvalidParameter(DatabaseException):
+ """Define the exception that the request has invalid or missing parameters.
+
+ """
+
+ def __init__(self, message):
+ super(InvalidParameter, self).__init__(message)
+ self.status_code = 400
+
+
+class InvalidResponse(DatabaseException):
+ """Define the exception that the response is invalid.
+
+ """
+
+ def __init__(self, message):
+ super(InvalidResponse, self).__init__(message)
+ self.status_code = 400
+
+
+class MultiDatabaseException(DatabaseException):
+ """Define the exception composites with multi exceptions."""
+
+ def __init__(self, exceptions):
+ super(MultiDatabaseException, self).__init__('multi exceptions')
+ self.exceptions = exceptions
+ self.status_code = 400
+
+ @property
+ def traceback(self):
+ tracebacks = []
+ for exception in self.exceptions:
+ tracebacks.append(exception.trackback)
+
+ def to_dict(self):
+ dict_info = super(MultiDatabaseException, self).to_dict()
+ dict_info.update({
+ 'exceptions': [
+ exception.to_dict() for exception in self.exceptions
+ ]
+ })
+ return dict_info
diff --git a/dashboard/backend/dovetail/db/models.py b/dashboard/backend/dovetail/db/models.py
new file mode 100755
index 00000000..e0f3ffa3
--- /dev/null
+++ b/dashboard/backend/dovetail/db/models.py
@@ -0,0 +1,105 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import datetime
+
+from sqlalchemy import Column, Integer, String, DateTime
+from sqlalchemy.ext.declarative import declarative_base
+
+from dovetail.utils import util
+from dovetail.db import exception
+
+BASE = declarative_base()
+
+
+class MarkTimestamp(object):
+ created = Column(DateTime, default=lambda: datetime.datetime.now())
+ updated = Column(DateTime, default=lambda: datetime.datetime.now(),
+ onupdate=lambda: datetime.datetime.now())
+
+
+class ModelHandler(object):
+
+ def initialize(self):
+ self.update()
+
+ def update(self):
+ pass
+
+ @staticmethod
+ def type_check(value, column_type):
+ if value is None:
+ return True
+ if not hasattr(column_type, 'python_type'):
+ return True
+ column_python_type = column_type.python_type
+ if isinstance(value, column_python_type):
+ return True
+ if issubclass(column_python_type, basestring):
+ return isinstance(value, basestring)
+ if column_python_type in [int, long]:
+ return type(value) in [int, long]
+ if column_python_type in [float]:
+ return type(value) in [float]
+ if column_python_type in [bool]:
+ return type(value) in [bool]
+ return False
+
+ def validate(self):
+ columns = self.__mapper__.columns
+ for key, column in columns.items():
+ value = getattr(self, key)
+ if not self.type_check(value, column.type):
+ raise exception.InvalidParameter(
+ 'column %s value %r type is unexpected: %s' % (
+ key, value, column.type
+ )
+ )
+
+ def to_dict(self):
+ """General function to convert record to dict.
+
+ Convert all columns not starting with '_' to
+ {<column_name>: <column_value>}
+ """
+ keys = self.__mapper__.columns.keys()
+ dict_info = {}
+ for key in keys:
+ if key.startswith('_'):
+ continue
+ value = getattr(self, key)
+ if value is not None:
+ if isinstance(value, datetime.datetime):
+ value = util.format_datetime(value)
+ dict_info[key] = value
+ return dict_info
+
+
+class Result(BASE, MarkTimestamp, ModelHandler):
+ __tablename__ = 'result'
+ id = Column(Integer, primary_key=True)
+ test_id = Column(String(120), unique=True)
+ name = Column(String(120))
+ data = Column(String(64000))
+
+ def __init__(self, **kwargs):
+ super(Result, self).__init__(**kwargs)
+
+ def __repr__(self):
+ return '<Result %r>' % (self.name)
+
+ def __str__(self):
+ return 'Result[%s:%s]' % (self.name, self.test_id)
+
+ def to_dict(self):
+ dict_info = super(Result, self).to_dict()
+ dict_info['name'] = self.name
+ dict_info['test_id'] = self.test_id
+ dict_info['data'] = self.data
+ return dict_info
diff --git a/dashboard/backend/dovetail/db/utils.py b/dashboard/backend/dovetail/db/utils.py
new file mode 100755
index 00000000..5e788a71
--- /dev/null
+++ b/dashboard/backend/dovetail/db/utils.py
@@ -0,0 +1,478 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+"""Utilities for database."""
+
+
+import functools
+import inspect
+import logging
+
+from sqlalchemy import and_
+from sqlalchemy import or_
+
+from dovetail.db import exception
+from dovetail.db import models
+
+
+def add_db_object(session, table, exception_when_existing=True,
+ *args, **kwargs):
+ """Create db object.
+
+ If not exception_when_existing and the db object exists,
+ Instead of raising exception, updating the existing db object.
+ """
+ if not session:
+ raise exception.DatabaseException('session param is None')
+ with session.begin(subtransactions=True):
+ logging.debug(
+ 'session %s add object %s atributes %s to table %s',
+ id(session), args, kwargs, table.__name__)
+ argspec = inspect.getargspec(table.__init__)
+ arg_names = argspec.args[1:]
+ arg_defaults = argspec.defaults
+ if not arg_defaults:
+ arg_defaults = []
+ if not (
+ len(arg_names) - len(arg_defaults) <= len(args) <= len(arg_names)
+ ):
+ raise exception.InvalidParameter(
+ 'arg names %s does not match arg values %s' % (
+ arg_names, args)
+ )
+ db_keys = dict(zip(arg_names, args))
+ logging.debug('db_keys:%s' % db_keys)
+ if db_keys:
+ db_object = session.query(table).filter_by(**db_keys).first()
+ else:
+ logging.debug('db object is None')
+ db_object = None
+
+ new_object = False
+ if db_object:
+ logging.debug(
+ 'got db object %s: %s', db_keys, db_object
+ )
+ if exception_when_existing:
+ raise exception.DuplicatedRecord(
+ '%s exists in table %s' % (db_keys, table.__name__)
+ )
+ else:
+ db_object = table(**db_keys)
+ new_object = True
+
+ for key, value in kwargs.items():
+ setattr(db_object, key, value)
+
+ logging.debug('db_object:%s' % db_object)
+ if new_object:
+ session.add(db_object)
+ session.flush()
+ db_object.initialize()
+ db_object.validate()
+ logging.debug(
+ 'session %s db object %s added', id(session), db_object
+ )
+ return db_object
+
+
+def list_db_objects(session, table, order_by=[], **filters):
+ """List db objects.
+
+ If order by given, the db objects should be sorted by the ordered keys.
+ """
+ if not session:
+ raise exception.DatabaseException('session param is None')
+ with session.begin(subtransactions=True):
+ logging.debug(
+ 'session %s list db objects by filters %s in table %s',
+ id(session), filters, table.__name__
+ )
+ db_objects = model_order_by(
+ model_filter(
+ model_query(session, table),
+ table,
+ **filters
+ ),
+ table,
+ order_by
+ ).all()
+ logging.debug(
+ 'session %s got listed db objects: %s',
+ id(session), db_objects
+ )
+ return db_objects
+
+
+def get_db_object(session, table, exception_when_missing=True, **kwargs):
+ """Get db object.
+
+ If not exception_when_missing and the db object can not be found,
+ return None instead of raising exception.
+ """
+ if not session:
+ raise exception.DatabaseException('session param is None')
+ with session.begin(subtransactions=True):
+ logging.debug(
+ 'session %s get db object %s from table %s',
+ id(session), kwargs, table.__name__)
+ db_object = model_filter(
+ model_query(session, table), table, **kwargs
+ ).first()
+ logging.debug(
+ 'session %s got db object %s', id(session), db_object
+ )
+ if db_object:
+ return db_object
+
+ if not exception_when_missing:
+ return None
+
+ raise exception.RecordNotExists(
+ 'Cannot find the record in table %s: %s' % (
+ table.__name__, kwargs
+ )
+ )
+
+
+def del_db_objects(session, table, **filters):
+ """delete db objects."""
+ if not session:
+ raise exception.DatabaseException('session param is None')
+ with session.begin(subtransactions=True):
+ logging.debug(
+ 'session %s delete db objects by filters %s in table %s',
+ id(session), filters, table.__name__
+ )
+ query = model_filter(
+ model_query(session, table), table, **filters
+ )
+ db_objects = query.all()
+ query.delete(synchronize_session=False)
+ logging.debug(
+ 'session %s db objects %s deleted', id(session), db_objects
+ )
+ return db_objects
+
+
+def model_order_by(query, model, order_by):
+ """append order by into sql query model."""
+ if not order_by:
+ return query
+ order_by_cols = []
+ for key in order_by:
+ if isinstance(key, tuple):
+ key, is_desc = key
+ else:
+ is_desc = False
+ if isinstance(key, basestring):
+ if hasattr(model, key):
+ col_attr = getattr(model, key)
+ else:
+ continue
+ else:
+ col_attr = key
+ if is_desc:
+ order_by_cols.append(col_attr.desc())
+ else:
+ order_by_cols.append(col_attr)
+ return query.order_by(*order_by_cols)
+
+
+def _model_condition(col_attr, value):
+ """Generate condition for one column.
+
+ Example for col_attr is name:
+ value is 'a': name == 'a'
+ value is ['a']: name == 'a'
+ value is ['a', 'b']: name == 'a' or name == 'b'
+ value is {'eq': 'a'}: name == 'a'
+ value is {'lt': 'a'}: name < 'a'
+ value is {'le': 'a'}: name <= 'a'
+ value is {'gt': 'a'}: name > 'a'
+ value is {'ge': 'a'}: name >= 'a'
+ value is {'ne': 'a'}: name != 'a'
+ value is {'in': ['a', 'b']}: name in ['a', 'b']
+ value is {'notin': ['a', 'b']}: name not in ['a', 'b']
+ value is {'startswith': 'abc'}: name like 'abc%'
+ value is {'endswith': 'abc'}: name like '%abc'
+ value is {'like': 'abc'}: name like '%abc%'
+ value is {'between': ('a', 'c')}: name >= 'a' and name <= 'c'
+ value is [{'lt': 'a'}]: name < 'a'
+ value is [{'lt': 'a'}, {'gt': c'}]: name < 'a' or name > 'c'
+ value is {'lt': 'c', 'gt': 'a'}: name > 'a' and name < 'c'
+
+ If value is a list, the condition is the or relationship among
+ conditions of each item.
+ If value is dict and there are multi keys in the dict, the relationship
+ is and conditions of each key.
+ Otherwise the condition is to compare the column with the value.
+ """
+ if isinstance(value, list):
+ basetype_values = []
+ composite_values = []
+ for item in value:
+ if isinstance(item, (list, dict)):
+ composite_values.append(item)
+ else:
+ basetype_values.append(item)
+ conditions = []
+ if basetype_values:
+ if len(basetype_values) == 1:
+ condition = (col_attr == basetype_values[0])
+ else:
+ condition = col_attr.in_(basetype_values)
+ conditions.append(condition)
+ for composite_value in composite_values:
+ condition = _model_condition(col_attr, composite_value)
+ if condition is not None:
+ conditions.append(condition)
+ if not conditions:
+ return None
+ if len(conditions) == 1:
+ return conditions[0]
+ return or_(*conditions)
+ elif isinstance(value, dict):
+ conditions = []
+ if 'eq' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['eq'],
+ lambda attr, data: attr == data,
+ lambda attr, data, item_condition_func: attr.in_(data)
+ ))
+ if 'lt' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['lt'],
+ lambda attr, data: attr < data,
+ _one_item_list_condition_func
+ ))
+ if 'gt' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['gt'],
+ lambda attr, data: attr > data,
+ _one_item_list_condition_func
+ ))
+ if 'le' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['le'],
+ lambda attr, data: attr <= data,
+ _one_item_list_condition_func
+ ))
+ if 'ge' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['ge'],
+ lambda attr, data: attr >= data,
+ _one_item_list_condition_func
+ ))
+ if 'ne' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['ne'],
+ lambda attr, data: attr != data,
+ lambda attr, data, item_condition_func: attr.notin_(data)
+ ))
+ if 'in' in value:
+ conditions.append(col_attr.in_(value['in']))
+ if 'notin' in value:
+ conditions.append(col_attr.notin_(value['notin']))
+ if 'startswith' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['startswith'],
+ lambda attr, data: attr.like('%s%%' % data)
+ ))
+ if 'endswith' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['endswith'],
+ lambda attr, data: attr.like('%%%s' % data)
+ ))
+ if 'like' in value:
+ conditions.append(_model_condition_func(
+ col_attr, value['like'],
+ lambda attr, data: attr.like('%%%s%%' % data)
+ ))
+ conditions = [
+ condition
+ for condition in conditions
+ if condition is not None
+ ]
+ if not conditions:
+ return None
+ if len(conditions) == 1:
+ return conditions[0]
+ return and_(conditions)
+ else:
+ condition = (col_attr == value)
+ return condition
+
+
+def _default_list_condition_func(col_attr, value, condition_func):
+ """The default condition func for a list of data.
+
+ Given the condition func for single item of data, this function
+ wrap the condition_func and return another condition func using
+ or_ to merge the conditions of each single item to deal with a
+ list of data item.
+
+ Args:
+ col_attr: the colomn name
+ value: the column value need to be compared.
+ condition_func: the sqlalchemy condition object like ==
+
+ Examples:
+ col_attr is name, value is ['a', 'b', 'c'] and
+ condition_func is ==, the returned condition is
+ name == 'a' or name == 'b' or name == 'c'
+ """
+ conditions = []
+ for sub_value in value:
+ condition = condition_func(col_attr, sub_value)
+ if condition is not None:
+ conditions.append(condition)
+ if conditions:
+ return or_(*conditions)
+ else:
+ return None
+
+
+def _one_item_list_condition_func(col_attr, value, condition_func):
+ """The wrapper condition func to deal with one item data list.
+
+ For simplification, it is used to reduce generating too complex
+ sql conditions.
+ """
+ if value:
+ return condition_func(col_attr, value[0])
+ else:
+ return None
+
+
+def _model_condition_func(
+ col_attr, value,
+ item_condition_func,
+ list_condition_func=_default_list_condition_func
+):
+ """Return sql condition based on value type."""
+ if isinstance(value, list):
+ if not value:
+ return None
+ if len(value) == 1:
+ return item_condition_func(col_attr, value)
+ return list_condition_func(
+ col_attr, value, item_condition_func
+ )
+ else:
+ return item_condition_func(col_attr, value)
+
+
+def model_filter(query, model, **filters):
+ """Append conditons to query for each possible column."""
+ for key, value in filters.items():
+ if isinstance(key, basestring):
+ if hasattr(model, key):
+ col_attr = getattr(model, key)
+ else:
+ continue
+ else:
+ col_attr = key
+
+ condition = _model_condition(col_attr, value)
+ if condition is not None:
+ query = query.filter(condition)
+ return query
+
+
+def model_query(session, model):
+ """model query.
+
+ Return sqlalchemy query object.
+ """
+ if not issubclass(model, models.BASE):
+ raise exception.DatabaseException("model should be sublass of BASE!")
+
+ return session.query(model)
+
+
+def wrap_to_dict(support_keys=[], **filters):
+ """Decrator to convert returned object to dict.
+
+ The details is decribed in _wrapper_dict.
+ """
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ return _wrapper_dict(
+ func(*args, **kwargs), support_keys, **filters
+ )
+ return wrapper
+ return decorator
+
+
+def _wrapper_dict(data, support_keys, **filters):
+ """Helper for warpping db object into dictionary.
+
+ If data is list, convert it to a list of dict
+ If data is Base model, convert it to dict
+ for the data as a dict, filter it with the supported keys.
+ For each filter_key, filter_value in filters, also filter
+ data[filter_key] by filter_value recursively if it exists.
+
+ Example:
+ data is models.Switch, it will be converted to
+ {
+ 'id': 1, 'ip': '10.0.0.1', 'ip_int': 123456,
+ 'credentials': {'version': 2, 'password': 'abc'}
+ }
+ Then if support_keys are ['id', 'ip', 'credentials'],
+ it will be filtered to {
+ 'id': 1, 'ip': '10.0.0.1',
+ 'credentials': {'version': 2, 'password': 'abc'}
+ }
+ Then if filters is {'credentials': ['version']},
+ it will be filtered to {
+ 'id': 1, 'ip': '10.0.0.1',
+ 'credentials': {'version': 2}
+ }
+ """
+ logging.debug(
+ 'wrap dict %s by support_keys=%s filters=%s',
+ data, support_keys, filters
+ )
+ if isinstance(data, list):
+ return [
+ _wrapper_dict(item, support_keys, **filters)
+ for item in data
+ ]
+ if isinstance(data, models.ModelHandler):
+ data = data.to_dict()
+ if not isinstance(data, dict):
+ raise exception.InvalidResponse(
+ 'response %s type is not dict' % data
+ )
+ info = {}
+ try:
+ if len(support_keys) == 0:
+ support_keys = data.keys()
+ for key in support_keys:
+ if key in data and data[key] is not None:
+ if key in filters:
+ filter_keys = filters[key]
+ if isinstance(filter_keys, dict):
+ info[key] = _wrapper_dict(
+ data[key], filter_keys.keys(),
+ **filter_keys
+ )
+ else:
+ info[key] = _wrapper_dict(
+ data[key], filter_keys
+ )
+ else:
+ info[key] = data[key]
+ return info
+ except Exception as error:
+ logging.exception(error)
+ raise error
diff --git a/dashboard/backend/dovetail/utils/__init__.py b/dashboard/backend/dovetail/utils/__init__.py
new file mode 100755
index 00000000..6dbd8d79
--- /dev/null
+++ b/dashboard/backend/dovetail/utils/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/dashboard/backend/dovetail/utils/flags.py b/dashboard/backend/dovetail/utils/flags.py
new file mode 100755
index 00000000..dd10670b
--- /dev/null
+++ b/dashboard/backend/dovetail/utils/flags.py
@@ -0,0 +1,82 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import sys
+
+from optparse import OptionParser
+
+
+class Flags(object):
+ """Class to store flags."""
+
+ PARSER = OptionParser()
+ PARSED_OPTIONS = None
+
+ @classmethod
+ def parse_args(cls):
+ """parse args."""
+ (options, argv) = Flags.PARSER.parse_args()
+ sys.argv = [sys.argv[0]] + argv
+ Flags.PARSED_OPTIONS = options
+
+ def __getattr__(self, name):
+ if Flags.PARSED_OPTIONS and hasattr(Flags.PARSED_OPTIONS, name):
+ return getattr(Flags.PARSED_OPTIONS, name)
+
+ for option in Flags.PARSER.option_list:
+ if option.dest == name:
+ return option.default
+
+ raise AttributeError('Option instance has no attribute %s' % name)
+
+ def __setattr__(self, name, value):
+ if Flags.PARSED_OPTIONS and hasattr(Flags.PARSED_OPTIONS, name):
+ setattr(Flags.PARSED_OPTIONS, name, value)
+ return
+
+ for option in Flags.PARSER.option_list:
+ if option.dest == name:
+ option.default = value
+ return
+
+ object.__setattr__(self, name, value)
+
+
+OPTIONS = Flags()
+
+
+def init():
+ """Init flag parsing."""
+ OPTIONS.parse_args()
+
+
+def add(flagname, **kwargs):
+ """Add a flag name and its setting.
+
+ :param flagname: flag name declared in cmd as --<flagname>=...
+ :type flagname: str
+ """
+ Flags.PARSER.add_option('--%s' % flagname,
+ dest=flagname, **kwargs)
+
+
+def add_bool(flagname, default=True, **kwargs):
+ """Add a bool flag name and its setting.
+
+ :param flagname: flag name declared in cmd as --[no]<flagname>.
+ :type flagname: str
+ :param default: default value
+ :type default: bool
+ """
+ Flags.PARSER.add_option('--%s' % flagname,
+ dest=flagname, default=default,
+ action="store_true", **kwargs)
+ Flags.PARSER.add_option('--no%s' % flagname,
+ dest=flagname,
+ action="store_false", **kwargs)
diff --git a/dashboard/backend/dovetail/utils/logsetting.py b/dashboard/backend/dovetail/utils/logsetting.py
new file mode 100755
index 00000000..27255688
--- /dev/null
+++ b/dashboard/backend/dovetail/utils/logsetting.py
@@ -0,0 +1,98 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+import logging.handlers
+import os
+import os.path
+import sys
+
+from dovetail.utils import flags
+from dovetail.utils import setting_wrapper as setting
+
+
+flags.add('loglevel',
+ help='logging level', default=setting.DEFAULT_LOGLEVEL)
+flags.add('logdir',
+ help='logging directory', default=setting.DEFAULT_LOGDIR)
+flags.add('logfile',
+ help='logging filename', default=None)
+flags.add('log_interval', type='int',
+ help='log interval', default=setting.DEFAULT_LOGINTERVAL)
+flags.add('log_interval_unit',
+ help='log interval unit', default=setting.DEFAULT_LOGINTERVAL_UNIT)
+flags.add('log_format',
+ help='log format', default=setting.DEFAULT_LOGFORMAT)
+flags.add('log_backup_count', type='int',
+ help='log backup count', default=setting.DEFAULT_LOGBACKUPCOUNT)
+
+
+# mapping str setting in flag --loglevel to logging level.
+LOGLEVEL_MAPPING = {
+ 'finest': logging.DEBUG - 2, # more detailed log.
+ 'fine': logging.DEBUG - 1, # detailed log.
+ 'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+ 'critical': logging.CRITICAL,
+}
+
+
+logging.addLevelName(LOGLEVEL_MAPPING['fine'], 'fine')
+logging.addLevelName(LOGLEVEL_MAPPING['finest'], 'finest')
+
+
+# disable logging when logsetting.init not called
+logging.getLogger().setLevel(logging.CRITICAL)
+
+
+def getLevelByName(level_name):
+ """Get log level by level name."""
+ return LOGLEVEL_MAPPING[level_name]
+
+
+def init():
+ """Init loggsetting. It should be called after flags.init."""
+ loglevel = flags.OPTIONS.loglevel.lower()
+ logdir = flags.OPTIONS.logdir
+ logfile = flags.OPTIONS.logfile
+ logger = logging.getLogger()
+ if logger.handlers:
+ for handler in logger.handlers:
+ logger.removeHandler(handler)
+
+ if logdir:
+ if not logfile:
+ logfile = './%s.log' % os.path.basename(sys.argv[0])
+
+ handler = logging.handlers.TimedRotatingFileHandler(
+ os.path.join(logdir, logfile),
+ when=flags.OPTIONS.log_interval_unit,
+ interval=flags.OPTIONS.log_interval,
+ backupCount=flags.OPTIONS.log_backup_count)
+ else:
+ if not logfile:
+ handler = logging.StreamHandler(sys.stderr)
+ else:
+ handler = logging.handlers.TimedRotatingFileHandler(
+ logfile,
+ when=flags.OPTIONS.log_interval_unit,
+ interval=flags.OPTIONS.log_interval,
+ backupCount=flags.OPTIONS.log_backup_count)
+
+ if loglevel in LOGLEVEL_MAPPING:
+ logger.setLevel(LOGLEVEL_MAPPING[loglevel])
+ handler.setLevel(LOGLEVEL_MAPPING[loglevel])
+
+ formatter = logging.Formatter(
+ flags.OPTIONS.log_format)
+
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
diff --git a/dashboard/backend/dovetail/utils/setting_wrapper.py b/dashboard/backend/dovetail/utils/setting_wrapper.py
new file mode 100755
index 00000000..bb390ada
--- /dev/null
+++ b/dashboard/backend/dovetail/utils/setting_wrapper.py
@@ -0,0 +1,18 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+DEFAULT_LOGLEVEL = 'debug'
+DEFAULT_LOGDIR = '/var/log/dovetail/'
+DEFAULT_LOGINTERVAL = 30
+DEFAULT_LOGINTERVAL_UNIT = 'M'
+DEFAULT_LOGFORMAT = (
+ '%(asctime)s - %(filename)s - %(lineno)d - %(levelname)s - %(message)s')
+DEFAULT_LOGBACKUPCOUNT = 10
+WEB_LOGFILE = 'dovetail_web.log'
diff --git a/dashboard/backend/dovetail/utils/util.py b/dashboard/backend/dovetail/utils/util.py
new file mode 100755
index 00000000..bfd257d7
--- /dev/null
+++ b/dashboard/backend/dovetail/utils/util.py
@@ -0,0 +1,71 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import datetime
+import re
+import sys
+
+
+def format_datetime(date_time):
+ """Generate string from datetime object."""
+ return date_time.strftime("%Y-%m-%d %H:%M:%S")
+
+
+def parse_time_interval(time_interval_str):
+ """parse string of time interval to time interval.
+
+ supported time interval unit: ['d', 'w', 'h', 'm', 's']
+ Examples:
+ time_interval_str: '3d 2h' time interval to 3 days and 2 hours.
+ """
+ if not time_interval_str:
+ return 0
+
+ time_interval_tuple = [
+ time_interval_element
+ for time_interval_element in time_interval_str.split(' ')
+ if time_interval_element
+ ]
+ time_interval_dict = {}
+ time_interval_unit_mapping = {
+ 'd': 'days',
+ 'w': 'weeks',
+ 'h': 'hours',
+ 'm': 'minutes',
+ 's': 'seconds'
+ }
+ for time_interval_element in time_interval_tuple:
+ mat = re.match(r'^([+-]?\d+)(w|d|h|m|s).*', time_interval_element)
+ if not mat:
+ continue
+
+ time_interval_value = int(mat.group(1))
+ time_interval_unit = time_interval_unit_mapping[mat.group(2)]
+ time_interval_dict[time_interval_unit] = (
+ time_interval_dict.get(time_interval_unit, 0) + time_interval_value
+ )
+
+ time_interval = datetime.timedelta(**time_interval_dict)
+ if sys.version_info[0:2] > (2, 6):
+ return time_interval.total_seconds()
+ else:
+ return (
+ time_interval.microseconds + (
+ time_interval.seconds + time_interval.days * 24 * 3600
+ ) * 1e6
+ ) / 1e6
+
+
+def pretty_print(*contents):
+ """pretty print contents."""
+ if len(contents) == 0:
+ print ""
+ else:
+ print "\n".join(content for content in contents)
diff --git a/dashboard/backend/install_db.py b/dashboard/backend/install_db.py
new file mode 100755
index 00000000..d37a4099
--- /dev/null
+++ b/dashboard/backend/install_db.py
@@ -0,0 +1,55 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+# create db in new env
+from dovetail.utils import flags
+from dovetail.utils import logsetting
+from dovetail.utils import setting_wrapper as setting
+
+from flask_script import Manager
+
+from dovetail.db import database
+from dovetail.api.api import app
+
+import os
+
+app_manager = Manager(app, usage="Perform database operations")
+
+# flags.init()
+curr_path = os.path.dirname(os.path.abspath(__file__))
+logdir = os.path.join(curr_path, 'log')
+if not os.path.exists(logdir):
+ os.makedirs(logdir)
+
+flags.OPTIONS.logdir = logdir
+flags.OPTIONS.logfile = setting.WEB_LOGFILE
+logsetting.init()
+
+
+@app_manager.command
+def createdb():
+ """Creates database from sqlalchemy models."""
+ database.init()
+ try:
+ database.drop_db()
+ except Exception:
+ pass
+
+ database.create_db()
+
+
+@app_manager.command
+def dropdb():
+ """Drops database from sqlalchemy models."""
+ database.init()
+ database.drop_db()
+
+
+if __name__ == "__main__":
+ app_manager.run()
diff --git a/dashboard/backend/wsgi.py b/dashboard/backend/wsgi.py
new file mode 100755
index 00000000..088299d7
--- /dev/null
+++ b/dashboard/backend/wsgi.py
@@ -0,0 +1,35 @@
+##############################################################################
+# Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+from dovetail.utils import flags
+from dovetail.utils import logsetting
+from dovetail.utils import setting_wrapper as setting
+
+from dovetail.api.api import app
+
+import os
+import logging
+
+gunicorn_error_logger = logging.getLogger('gunicorn.error')
+app.logger.handlers.extend(gunicorn_error_logger.handlers)
+app.logger.setLevel(logging.DEBUG)
+
+# flags.init()
+# logdir = setting.DEFAULT_LOGDIR
+curr_path = os.path.dirname(os.path.abspath(__file__))
+logdir = os.path.join(curr_path, 'log')
+if not os.path.exists(logdir):
+ os.makedirs(logdir)
+
+flags.OPTIONS.logdir = logdir
+flags.OPTIONS.logfile = setting.WEB_LOGFILE
+logsetting.init()
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/docker/Dockerfile b/docker/Dockerfile
index cc21203d..19dc3619 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -2,6 +2,8 @@ FROM ubuntu:14.04
MAINTAINER Leo Wang <grakiss.wanglei@huawei.com>
LABEL version="0.1" description="OPNFV Dovetail Docker Container"
+ARG BRANCH=master
+
RUN \
apt-get update \
&& \
@@ -17,7 +19,7 @@ RUN \
&& \
echo deb https://apt.dockerproject.org/repo ubuntu-trusty main > /etc/apt/sources.list.d/docker.list \
&& \
- apt-get update && apt-get install -y docker-engine=1.9.1-0~trusty \
+ apt-get update && apt-get install -y docker-engine=1.12.3-0~trusty \
&& \
pip install pyyaml \
click \
@@ -30,7 +32,7 @@ WORKDIR /home/opnfv
RUN \
git config --global http.sslVerify false \
&& \
- git clone https://gerrit.opnfv.org/gerrit/dovetail.git ${REPOS_DIR} \
+ git clone --depth 1 -b $BRANCH https://gerrit.opnfv.org/gerrit/dovetail.git ${REPOS_DIR} \
&& \
mkdir -p ${REPOS_DIR}/results
diff --git a/docs/dovetailtool/dovetail.tool.installation.rst b/docs/dovetailtool/dovetail.tool.installation.rst
index 17290a35..2f544480 100644
--- a/docs/dovetailtool/dovetail.tool.installation.rst
+++ b/docs/dovetailtool/dovetail.tool.installation.rst
@@ -69,22 +69,19 @@ At this point the environment is now ready for Dovetail execution.
Compliance and certification test cases
----------------------------------------
-The compliance and certification test cases can be defined under the ``/dovetail/cert``
+The compliance and certification test cases can be defined under the ``/dovetail/compliance``
directory, which is defined in yaml format.
A sample file named ``compliance_set.yml`` is provided as follows:
::
- certification_compliance_set:
- name: certification_compliance_set
+ compliance_set:
+ name: compliance_set
testcases_list:
- dovetail.ipv6.tc001
The testcase listed here is dovetail.ipv6.tc001, defined within ``dovetail/testcase``.
-Note: if a new test case yaml file is created, its name should start with ``certification_``,
-in similiar fashion as the sample file ``certification_compliance_set``.
-
Running Dovetail tool
---------------------
@@ -92,10 +89,17 @@ After environment preparation is complete and test cases added, the Dovetail too
::
- python run.py --scenario compliance_set
+ python run.py --testsuite compliance_set
+
+The value ``compliance_set`` passed to the ``testsuite`` flag can be replaced with the test cases yaml file.
+If not argument is given, the compliance_set testsuite will be run as the default.
+
+Moreover, the testcases in given testarea can be run with ``testarea`` command line argument, such as
+testarea ``ipv6`` in ``compliance_set``
+
+::
-The value ``compliance_set`` passed to the ``scenario`` flag can be replaced with the test cases yaml file.
-If not argument is given, the compliance_set scenario will be run as the default.
+ python run.py --testsuite compliance_set --testarea ipv6
Running Dovetail in a Docker container
########################################
@@ -145,10 +149,10 @@ Attach to the container by starting it and obtaining a bash prompt with ::
Inside the container the following commands can be executed to trigger the testcases ::
cd /home/opnfv/dovetail/dovetail
- python run.py --scenario compliance_set
+ python run.py --testsuite compliance_set
Results Output
###############
The running log is stored in ``/home/opnfv/dovetail/results/dovetail.log``.
-The certification report is stored in ``/home/opnfv/dovetail/results/dovetail_report.txt``.
+The compliance report is stored in ``/home/opnfv/dovetail/results/dovetail_report.txt``.
diff --git a/docs/dovetailtool/dovetail.tool.overview.rst b/docs/dovetailtool/dovetail.tool.overview.rst
index a1133f34..c4b7fa22 100644
--- a/docs/dovetailtool/dovetail.tool.overview.rst
+++ b/docs/dovetailtool/dovetail.tool.overview.rst
@@ -10,7 +10,7 @@ Dovetail Tool
What is Dovetail tool
#####################
-A toolset for providing an evaluation of a deployed cloud environment for OPNFV certification.
+A toolset for providing an evaluation of a deployed cloud environment for OPNFV compliance.
Overview
########
diff --git a/dovetail/cert/compliance_set.yml b/dovetail/compliance/compliance_set.yml
index b4108b4f..df0d427d 100644
--- a/dovetail/cert/compliance_set.yml
+++ b/dovetail/compliance/compliance_set.yml
@@ -1,5 +1,5 @@
-certification_compliance_set:
- name: certification_compliance_set
+compliance_set:
+ name: compliance_set
testcases_list:
# Temporarily, one test case kept here as default to run
# for use of software development/debug
diff --git a/dovetail/compliance/debug.yml b/dovetail/compliance/debug.yml
new file mode 100644
index 00000000..a0a2d3ee
--- /dev/null
+++ b/dovetail/compliance/debug.yml
@@ -0,0 +1,9 @@
+# only used for dovetail tool development debug
+# nfvi.tc001 tc002 running time is shorter, about 3 minutes
+# ipv6.tc001 about 20 minutes
+debug:
+ name: debug
+ testcases_list:
+ - dovetail.ipv6.tc001
+ - dovetail.nfvi.tc001
+ - dovetail.nfvi.tc002
diff --git a/dovetail/cert/proposed_tests.yml b/dovetail/compliance/proposed_tests.yml
index 6d6c8d19..fcb7e03a 100644
--- a/dovetail/cert/proposed_tests.yml
+++ b/dovetail/compliance/proposed_tests.yml
@@ -1,5 +1,5 @@
-certification_proposed_tests:
- name: certification_proposed_tests
+proposed_tests:
+ name: proposed_tests
testcases_list:
# TO DO: will adjust the dovetail tool to support in later patches
# run.py --name1 {**/proposed/compliance} --name2 {**/vim/ipv6,etc}
diff --git a/dovetail/conf/cmd_config.yml b/dovetail/conf/cmd_config.yml
index c2108c58..4e3d0110 100644
--- a/dovetail/conf/cmd_config.yml
+++ b/dovetail/conf/cmd_config.yml
@@ -31,18 +31,22 @@ cli:
- '--DEPLOY_TYPE'
- '-T'
help: 'DEPLOY_TYPE of the system under test (SUT).'
- CI_DEBUG:
+ DEBUG:
flags:
- - '--CI_DEBUG'
+ - '--DEBUG'
- '-d'
- help: 'CI_DEBUG for showing debug log.'
+ help: 'DEBUG for showing debug log.'
non-envs:
- scenario:
+ testsuite:
flags:
- - '--scenario'
- - '-s'
+ - '--testsuite'
default: 'compliance_set'
- help: 'certification scenario.'
+ help: 'compliance testsuite.'
+ testarea:
+ flags:
+ - '--testarea'
+ default: 'full'
+ help: 'compliance testarea within testsuite'
tag:
flags:
- '--tag'
diff --git a/dovetail/conf/dovetail_config.py b/dovetail/conf/dovetail_config.py
index 5ac23c43..e5d0608e 100644
--- a/dovetail/conf/dovetail_config.py
+++ b/dovetail/conf/dovetail_config.py
@@ -14,9 +14,12 @@ import re
class DovetailConfig:
- CERT_PATH = './cert/'
+ COMPLIANCE_PATH = './compliance/'
TESTCASE_PATH = './testcase/'
- SCENARIO_NAMING_FMT = 'certification_%s'
+ # testsuite supported tuple, should adjust accordingly
+ testsuite_supported = ('compliance_set', 'proposed_tests', 'debug')
+ # testarea supported tuple, should adjust accordingly
+ testarea_supported = ('vimops', 'nfvi', 'ipv6')
curr_path = os.path.dirname(os.path.abspath(__file__))
@@ -44,14 +47,17 @@ class DovetailConfig:
@classmethod
def update_envs(cls, options):
for item in options:
- if options[item] is not None:
- key = cls.cmd_name_trans(item)
- os.environ[key] = options[item]
+ key = cls.cmd_name_trans(item)
+ if not options[item] and key in os.environ:
+ options[item] = os.environ[key]
+ if options[item]:
cls.update_config_envs('functest', key)
cls.update_config_envs('yardstick', key)
@classmethod
def update_config_envs(cls, script_type, key):
+ if key == 'DEBUG':
+ os.environ['CI_DEBUG'] = os.environ[key]
envs = cls.dovetail_config[script_type]['envs']
old_value = re.findall(r'\s+%s=(.*?)(\s+|$)' % key, envs)
if old_value == []:
diff --git a/dovetail/conf/dovetail_config.yml b/dovetail/conf/dovetail_config.yml
index 1f5de672..c4131a91 100644
--- a/dovetail/conf/dovetail_config.yml
+++ b/dovetail/conf/dovetail_config.yml
@@ -3,6 +3,8 @@ work_dir: /home/opnfv/dovetail
result_dir: /home/opnfv/dovetail/results
report_file: 'dovetail_report.txt'
cli_file_name: 'cmd_config.yml'
+# TO DO: once version scheme settled, adjust accordingly
+repo: 'https://github.com/opnfv/dovetail/tree/master/'
# used for testcase cmd template in jinja2 format
# we have two variables available now
diff --git a/dovetail/container.py b/dovetail/container.py
index 15ccc800..2716a089 100644
--- a/dovetail/container.py
+++ b/dovetail/container.py
@@ -11,7 +11,6 @@
import utils.dovetail_logger as dt_logger
import utils.dovetail_utils as dt_utils
from conf.dovetail_config import DovetailConfig as dt_config
-logger = dt_logger.Logger('container.py').getLogger()
class Container:
@@ -19,6 +18,8 @@ class Container:
container_list = {}
has_pull_latest_image = {'yardstick': False, 'functest': False}
+ logger = None
+
def __init__(cls):
pass
@@ -26,6 +27,10 @@ class Container:
pass
@classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.Container').getLogger()
+
+ @classmethod
def get(cls, type):
return cls.container_list[type]
@@ -46,10 +51,10 @@ class Container:
dovetail_config[type]['result']['dir'])
cmd = 'sudo docker run %s %s %s %s %s /bin/bash' % \
(opts, envs, sshkey, result_volume, docker_image)
- dt_utils.exec_cmd(cmd, logger)
+ dt_utils.exec_cmd(cmd, cls.logger)
ret, container_id = \
dt_utils.exec_cmd("sudo docker ps | grep " + docker_image +
- " | awk '{print $1}' | head -1", logger)
+ " | awk '{print $1}' | head -1", cls.logger)
cls.container_list[type] = container_id
return container_id
@@ -57,20 +62,21 @@ class Container:
def pull_image(cls, type):
docker_image = cls.get_docker_image(type)
if cls.has_pull_latest_image[type] is True:
- logger.debug('%s is already the newest version.' % (docker_image))
+ cls.logger.debug('%s is already the newest version.' %
+ (docker_image))
else:
cmd = 'sudo docker pull %s' % (docker_image)
- dt_utils.exec_cmd(cmd, logger)
+ dt_utils.exec_cmd(cmd, cls.logger)
cls.has_pull_latest_image[type] = True
- @staticmethod
- def clean(container_id):
+ @classmethod
+ def clean(cls, container_id):
cmd1 = 'sudo docker stop %s' % (container_id)
- dt_utils.exec_cmd(cmd1, logger)
+ dt_utils.exec_cmd(cmd1, cls.logger)
cmd2 = 'sudo docker rm %s' % (container_id)
- dt_utils.exec_cmd(cmd2, logger)
+ dt_utils.exec_cmd(cmd2, cls.logger)
- @staticmethod
- def exec_cmd(container_id, sub_cmd, exit_on_error=False):
+ @classmethod
+ def exec_cmd(cls, container_id, sub_cmd, exit_on_error=False):
cmd = 'sudo docker exec %s /bin/bash -c "%s"' % (container_id, sub_cmd)
- dt_utils.exec_cmd(cmd, logger, exit_on_error)
+ dt_utils.exec_cmd(cmd, cls.logger, exit_on_error)
diff --git a/dovetail/parser.py b/dovetail/parser.py
index 1740944a..d8f9fa0a 100644
--- a/dovetail/parser.py
+++ b/dovetail/parser.py
@@ -14,28 +14,33 @@ import utils.dovetail_logger as dt_logger
import utils.dovetail_utils as dt_utils
from conf.dovetail_config import DovetailConfig as dt_config
-logger = dt_logger.Logger('parser.py').getLogger()
-
class Parser:
'''preprocess configuration files'''
- @staticmethod
- def parse_cmd(cmd, testcase):
+ logger = None
+
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.Parser').getLogger()
+
+ @classmethod
+ def parse_cmd(cls, cmd, testcase):
cmd_lines = None
try:
template = jinja2.Template(cmd, undefined=jinja2.StrictUndefined)
kwargs = {}
for arg in dt_config.dovetail_config['parameters']:
path = eval(arg['path'])
- logger.debug('name: %s, eval path: %s ' % (arg['name'], path))
+ cls.logger.debug('name: %s, eval path: %s ' %
+ (arg['name'], path))
kwargs[arg['name']] = \
dt_utils.get_obj_by_path(testcase.testcase, path)
- logger.debug('kwargs: %s' % kwargs)
+ cls.logger.debug('kwargs: %s' % kwargs)
cmd_lines = template.render(**kwargs)
except Exception as e:
- logger.error('failed to parse cmd %s, exception:%s' % (cmd, e))
+ cls.logger.error('failed to parse cmd %s, exception:%s' % (cmd, e))
return None
return cmd_lines
diff --git a/dovetail/report.py b/dovetail/report.py
index 7dd8ba2d..7fd4076d 100644
--- a/dovetail/report.py
+++ b/dovetail/report.py
@@ -6,18 +6,20 @@
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
#
+from __future__ import division
+
import json
import urllib2
import re
import os
+import datetime
+import uuid
import utils.dovetail_logger as dt_logger
-from conf.dovetail_config import DovetailConfig as dt_config
+from conf.dovetail_config import DovetailConfig as dt_cfg
from testcase import Testcase
-logger = dt_logger.Logger('report.py').getLogger()
-
def get_pass_str(passed):
if passed:
@@ -30,78 +32,133 @@ class Report:
results = {'functest': {}, 'yardstick': {}}
+ logger = None
+
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.Report').getLogger()
+
@staticmethod
def check_result(testcase, db_result):
checker = CheckerFactory.create(testcase.script_type())
checker.check(testcase, db_result)
@classmethod
- def generate_json(cls, scenario_yaml):
+ def generate_json(cls, testsuite_yaml, testarea, duration):
report_obj = {}
- report_obj['scenario'] = scenario_yaml['name']
+ # TO DO: once version scheme settled, adjust accordingly
+ report_obj['version'] = '1.0'
+ report_obj['testsuite'] = testsuite_yaml['name']
+ # TO DO: once dashboard url settled, adjust accordingly
+ report_obj['dashboard'] = None
+ report_obj['validation_ID'] = str(uuid.uuid4())
+ report_obj['upload_date'] =\
+ datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
+ report_obj['duration'] = duration
+
report_obj['testcases_list'] = []
- for testcase_name in scenario_yaml['testcases_list']:
+ testarea_list = []
+ for value in testsuite_yaml['testcases_list']:
+ if value is not None and (testarea == 'full' or testarea in value):
+ testarea_list.append(value)
+ for testcase_name in testarea_list:
testcase = Testcase.get(testcase_name)
- testcase_in_rpt = {}
- testcase_in_rpt['name'] = testcase_name
+ testcase_inreport = {}
+ testcase_inreport['name'] = testcase_name
if testcase is None:
- testcase_in_rpt['result'] = 'Undefined'
- testcase_in_rpt['objective'] = ''
- testcase_in_rpt['sub_testcase'] = []
- report_obj['testcases_list'].append(testcase_in_rpt)
+ testcase_inreport['result'] = 'Undefined'
+ testcase_inreport['objective'] = ''
+ testcase_inreport['sub_testcase'] = []
+ report_obj['testcases_list'].append(testcase_inreport)
continue
- testcase_in_rpt['result'] = get_pass_str(testcase.passed())
- testcase_in_rpt['objective'] = testcase.objective()
- testcase_in_rpt['sub_testcase'] = []
+ testcase_inreport['result'] = get_pass_str(testcase.passed())
+ testcase_inreport['objective'] = testcase.objective()
+ testcase_inreport['sub_testcase'] = []
if testcase.sub_testcase() is not None:
for sub_test in testcase.sub_testcase():
- testcase_in_rpt['sub_testcase'].append({
+ testcase_inreport['sub_testcase'].append({
'name': sub_test,
'result': get_pass_str(
testcase.sub_testcase_passed(sub_test))
})
- report_obj['testcases_list'].append(testcase_in_rpt)
- logger.info(json.dumps(report_obj))
+ report_obj['testcases_list'].append(testcase_inreport)
+ cls.logger.info(json.dumps(report_obj))
return report_obj
@classmethod
- def generate(cls, scenario_yaml):
- rpt_data = cls.generate_json(scenario_yaml)
- rpt_text = ''
- split_line = '+-----------------------------------------------------'
- split_line += '---------------------+\n'
-
- rpt_text += '\n\
-+==========================================================================+\n\
-| report |\n'
- rpt_text += split_line
- rpt_text += '|scenario: %s\n' % rpt_data['scenario']
- for testcase in rpt_data['testcases_list']:
- rpt_text += '| [testcase]: %s\t\t\t\t[%s]\n' % \
- (testcase['name'], testcase['result'])
- rpt_text += '| |-objective: %s\n' % testcase['objective']
- if 'sub_testcase' in testcase:
- for sub_test in testcase['sub_testcase']:
- rpt_text += '| |-%s \t\t [%s]\n' % \
- (sub_test['name'], sub_test['result'])
- rpt_text += split_line
-
- logger.info(rpt_text)
- cls.save(rpt_text)
- return rpt_text
+ def generate(cls, testsuite_yaml, testarea, duration):
+ report_data = cls.generate_json(testsuite_yaml, testarea, duration)
+ report_txt = ''
+ report_txt += '\n\nDovetail Report\n'
+ report_txt += 'Version: %s\n' % report_data['version']
+ report_txt += 'TestSuite: %s\n' % report_data['testsuite']
+ report_txt += 'Result Dashboard: %s\n' % report_data['dashboard']
+ report_txt += 'Validation ID: %s\n' % report_data['validation_ID']
+ report_txt += 'Upload Date: %s\n' % report_data['upload_date']
+ if report_data['duration'] == 0:
+ report_txt += 'Duration: %s\n\n' % 'NA'
+ else:
+ report_txt += 'Duration: %.2f s\n\n' % report_data['duration']
+
+ total_num = 0
+ pass_num = 0
+ sub_report = {}
+ testcase_num = {}
+ testcase_passnum = {}
+ for area in dt_cfg.testarea_supported:
+ sub_report[area] = ''
+ testcase_num[area] = 0
+ testcase_passnum[area] = 0
+
+ # TO DO: once version scheme settled, adjust accordingly
+ spec_link = dt_cfg.dovetail_config['repo'] + 'dovetail/testcase'
+ for testcase in report_data['testcases_list']:
+ pattern = re.compile('|'.join(dt_cfg.testarea_supported))
+ area = pattern.findall(testcase['name'])[0]
+ result_dir = dt_cfg.dovetail_config['result_dir']
+ sub_report[area] += '- <%s> %s result: <%s>\n' %\
+ (spec_link, testcase['name'], result_dir)
+ testcase_num[area] += 1
+ total_num += 1
+ if testcase['result'] == 'PASS':
+ testcase_passnum[area] += 1
+ pass_num += 1
+
+ if total_num != 0:
+ pass_rate = pass_num/total_num
+ report_txt += 'Pass Rate: %.2f%% (%s/%s)\n' %\
+ (pass_rate*100, pass_num, total_num)
+ report_txt += 'Assessed test areas:\n'
+ for key in sub_report:
+ if testcase_num[key] != 0:
+ pass_rate = testcase_passnum[key]/testcase_num[key]
+ # TO DO: once version scheme settled, adjust accordingly
+ doc_link = dt_cfg.dovetail_config['repo'] +\
+ ('docs/testsuites/%s' % key)
+ report_txt += '- %s results: <%s> pass %.2f%%\n' %\
+ (key, doc_link, pass_rate*100)
+ for key in sub_report:
+ if testcase_num[key] != 0:
+ pass_rate = testcase_passnum[key]/testcase_num[key]
+ report_txt += '%s: pass rate %.2f%%\n' % (key, pass_rate*100)
+ report_txt += sub_report[key]
+
+ cls.logger.info(report_txt)
+ cls.save(report_txt)
+ return report_txt
# save to disk as default
- @staticmethod
- def save(report):
- report_file_name = dt_config.dovetail_config['report_file']
+ @classmethod
+ def save(cls, report):
+ report_file_name = dt_cfg.dovetail_config['report_file']
try:
- with open(os.path.join(dt_config.dovetail_config['result_dir'],
+ with open(os.path.join(dt_cfg.dovetail_config['result_dir'],
report_file_name), 'w') as report_file:
report_file.write(report)
- logger.info('save report to %s' % report_file_name)
+ cls.logger.info('save report to %s' % report_file_name)
except Exception:
- logger.error('Failed to save: %s' % report_file_name)
+ cls.logger.error('Failed to save: %s' % report_file_name)
@classmethod
def get_result(cls, testcase):
@@ -117,11 +174,12 @@ class Report:
if result is not None:
cls.results[type][script_testcase] = result
testcase.script_result_acquired(True)
- logger.debug('testcase: %s -> result acquired' % script_testcase)
+ cls.logger.debug('testcase: %s -> result acquired' %
+ script_testcase)
else:
retry = testcase.increase_retry()
- logger.debug('testcase: %s -> result acquired retry:%d' %
- (script_testcase, retry))
+ cls.logger.debug('testcase: %s -> result acquired retry:%d' %
+ (script_testcase, retry))
return result
@@ -140,12 +198,18 @@ class CrawlerFactory:
class FunctestCrawler:
+ logger = None
+
def __init__(self):
self.type = 'functest'
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.FunctestCrawler').getLogger()
+
def crawl(self, testcase=None):
store_type = \
- dt_config.dovetail_config[self.type]['result']['store_type']
+ dt_cfg.dovetail_config[self.type]['result']['store_type']
if store_type == 'file':
return self.crawl_from_file(testcase)
@@ -153,12 +217,12 @@ class FunctestCrawler:
return self.crawl_from_url(testcase)
def crawl_from_file(self, testcase=None):
- dovetail_config = dt_config.dovetail_config
+ dovetail_config = dt_cfg.dovetail_config
file_path = \
os.path.join(dovetail_config['result_dir'],
dovetail_config[self.type]['result']['file_path'])
if not os.path.exists(file_path):
- logger.info('result file not found: %s' % file_path)
+ self.logger.info('result file not found: %s' % file_path)
return None
try:
@@ -180,34 +244,40 @@ class FunctestCrawler:
"duration": int(dur_sec_int),
"tests": int(num_tests), "failures": failed_num,
"errors": error_logs}}
- logger.debug('Results: %s' % str(json_results))
+ self.logger.debug('Results: %s' % str(json_results))
return json_results
except Exception as e:
- logger.error('Cannot read content from the file: %s, exception: %s'
- % (file_path, e))
+ self.logger.error('Cannot read content from the file: %s, '
+ 'exception: %s' % (file_path, e))
return None
def crawl_from_url(self, testcase=None):
url = \
- dt_config.dovetail_config[self.type]['result']['db_url'] % testcase
- logger.debug("Query to rest api: %s" % url)
+ dt_cfg.dovetail_config[self.type]['result']['db_url'] % testcase
+ self.logger.debug("Query to rest api: %s" % url)
try:
data = json.load(urllib2.urlopen(url))
return data['results'][0]
except Exception as e:
- logger.error("Cannot read content from the url: %s, exception: %s"
- % (url, e))
+ self.logger.error("Cannot read content from the url: %s, "
+ "exception: %s" % (url, e))
return None
class YardstickCrawler:
+ logger = None
+
def __init__(self):
self.type = 'yardstick'
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.YardstickCrawler').getLogger()
+
def crawl(self, testcase=None):
store_type = \
- dt_config.dovetail_config[self.type]['result']['store_type']
+ dt_cfg.dovetail_config[self.type]['result']['store_type']
if store_type == 'file':
return self.crawl_from_file(testcase)
@@ -215,21 +285,21 @@ class YardstickCrawler:
return self.crawl_from_url(testcase)
def crawl_from_file(self, testcase=None):
- file_path = os.path.join(dt_config.dovetail_config['result_dir'],
+ file_path = os.path.join(dt_cfg.dovetail_config['result_dir'],
testcase+'.out')
if not os.path.exists(file_path):
- logger.info('result file not found: %s' % file_path)
+ self.logger.info('result file not found: %s' % file_path)
return None
try:
with open(file_path, 'r') as myfile:
myfile.read()
criteria = 'PASS'
json_results = {'criteria': criteria}
- logger.debug('Results: %s' % str(json_results))
+ self.logger.debug('Results: %s' % str(json_results))
return json_results
except Exception as e:
- logger.error('Cannot read content from the file: %s, exception: %s'
- % (file_path, e))
+ self.logger.error('Cannot read content from the file: %s, '
+ 'exception: %s' % (file_path, e))
return None
def crawl_from_url(self, testcase=None):
@@ -258,8 +328,13 @@ class ResultChecker:
class FunctestChecker:
- @staticmethod
- def check(testcase, db_result):
+ logger = None
+
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.FunctestChecker').getLogger()
+
+ def check(self, testcase, db_result):
sub_testcase_list = testcase.sub_testcase()
if not db_result:
@@ -280,7 +355,7 @@ class FunctestChecker:
all_passed = True
for sub_testcase in sub_testcase_list:
- logger.debug('check sub_testcase:%s' % sub_testcase)
+ self.logger.debug('check sub_testcase:%s' % sub_testcase)
if sub_testcase in db_result['details']['errors']:
testcase.sub_testcase_passed(sub_testcase, False)
all_passed = False
@@ -292,6 +367,12 @@ class FunctestChecker:
class YardstickChecker:
+ logger = None
+
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.YardstickChecker').getLogger()
+
@staticmethod
def check(testcase, result):
if not result:
diff --git a/dovetail/run.py b/dovetail/run.py
index 86194389..d24919b6 100755
--- a/dovetail/run.py
+++ b/dovetail/run.py
@@ -10,22 +10,25 @@
import click
import sys
+import os
+import time
import utils.dovetail_logger as dt_logger
+import utils.dovetail_utils as dt_utils
-
+from parser import Parser
from container import Container
from testcase import Testcase
-from testcase import Scenario
+from testcase import Testsuite
from report import Report
+from report import FunctestCrawler, YardstickCrawler
+from report import FunctestChecker, YardstickChecker
from conf.dovetail_config import DovetailConfig as dt_config
-logger = dt_logger.Logger('run.py').getLogger()
-
-def load_scenario(scenario):
- Scenario.load()
- return Scenario.get(dt_config.SCENARIO_NAMING_FMT % scenario)
+def load_testsuite(testsuite):
+ Testsuite.load()
+ return Testsuite.get(testsuite)
def set_container_tags(option_str):
@@ -40,12 +43,18 @@ def load_testcase():
Testcase.load()
-def run_test(scenario):
- for testcase_name in scenario['testcases_list']:
+def run_test(testsuite, testarea, logger):
+ testarea_list = []
+ for value in testsuite['testcases_list']:
+ if value is not None and (testarea == 'full' or testarea in value):
+ testarea_list.append(value)
+
+ duration = 0
+ for testcase_name in testarea_list:
logger.info('>>[testcase]: %s' % (testcase_name))
testcase = Testcase.get(testcase_name)
if testcase is None:
- logger.error('testcase %s is not defined in testcase folder, \
+ logger.error('test case %s is not defined in testcase folder, \
skipping' % (testcase_name))
continue
run_testcase = True
@@ -71,8 +80,11 @@ def run_test(scenario):
if not testcase.prepare_cmd():
logger.error('failed to prepare testcase:%s' % testcase.name())
else:
+ start_time = time.time()
for cmd in testcase.cmds:
Container.exec_cmd(container_id, cmd)
+ end_time = time.time()
+ duration = end_time - start_time
# testcase.post_condition()
@@ -81,8 +93,10 @@ def run_test(scenario):
db_result = Report.get_result(testcase)
Report.check_result(testcase, db_result)
+ return duration
-def validate_options(input_dict):
+
+def validate_options(input_dict, logger):
# for 'tag' option
for key, value in input_dict.items():
if key == 'tag' and value is not None:
@@ -101,24 +115,63 @@ def filter_env_options(input_dict):
return envs_options
+def create_logs():
+ Container.create_log()
+ Parser.create_log()
+ Report.create_log()
+ FunctestCrawler.create_log()
+ YardstickCrawler.create_log()
+ FunctestChecker.create_log()
+ YardstickChecker.create_log()
+ Testcase.create_log()
+ Testsuite.create_log()
+
+
+def clean_results_dir():
+ result_path = dt_config.dovetail_config['result_dir']
+ if os.path.exists(result_path):
+ if os.path.isdir(result_path):
+ cmd = 'sudo rm -rf %s/*' % (result_path)
+ dt_utils.exec_cmd(cmd, exit_on_error=False)
+ else:
+ print "result_dir in dovetail_config.yml is not a directory."
+ sys.exit(-1)
+
+
def main(*args, **kwargs):
- """Dovetail certification test entry!"""
- logger.info('=======================================')
- logger.info('Dovetail certification: %s!' % (kwargs['scenario']))
- logger.info('=======================================')
- validate_options(kwargs)
+ """Dovetail compliance test entry!"""
+ clean_results_dir()
+ create_logs()
+ logger = dt_logger.Logger('run').getLogger()
+ logger.info('================================================')
+ logger.info('Dovetail compliance: %s!' % (kwargs['testsuite']))
+ logger.info('================================================')
+ validate_options(kwargs, logger)
envs_options = filter_env_options(kwargs)
dt_config.update_envs(envs_options)
logger.info('Your new envs for functest: %s' %
dt_config.dovetail_config['functest']['envs'])
logger.info('Your new envs for yardstick: %s' %
dt_config.dovetail_config['yardstick']['envs'])
- load_testcase()
- scenario_yaml = load_scenario(kwargs['scenario'])
+
if 'tag' in kwargs and kwargs['tag'] is not None:
set_container_tags(kwargs['tag'])
- run_test(scenario_yaml)
- Report.generate(scenario_yaml)
+
+ testarea = kwargs['testarea']
+ testsuite_validation = False
+ testarea_validation = False
+ if (testarea == 'full') or (testarea in dt_config.testarea_supported):
+ testarea_validation = True
+ if kwargs['testsuite'] in dt_config.testsuite_supported:
+ testsuite_validation = True
+ if testsuite_validation and testarea_validation:
+ testsuite_yaml = load_testsuite(kwargs['testsuite'])
+ load_testcase()
+ duration = run_test(testsuite_yaml, testarea, logger)
+ Report.generate(testsuite_yaml, testarea, duration)
+ else:
+ logger.error('invalid input commands, testsuite %s testarea %s' %
+ (kwargs['testsuite'], testarea))
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
diff --git a/dovetail/testcase.py b/dovetail/testcase.py
index 6f0552b6..60ce2b28 100644
--- a/dovetail/testcase.py
+++ b/dovetail/testcase.py
@@ -15,11 +15,11 @@ import utils.dovetail_logger as dt_logger
from parser import Parser
from conf.dovetail_config import DovetailConfig as dt_config
-logger = dt_logger.Logger('testcase.py').getLogger()
-
class Testcase:
+ logger = None
+
def __init__(self, testcase_yaml):
self.testcase = testcase_yaml.values()[0]
self.testcase['passed'] = False
@@ -28,6 +28,10 @@ class Testcase:
self.update_script_testcase(self.script_type(),
self.script_testcase())
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.Testcase').getLogger()
+
def prepare_cmd(self):
script_type = self.script_type()
for cmd in dt_config.dovetail_config[script_type]['testcase']['cmds']:
@@ -52,7 +56,7 @@ class Testcase:
def sub_testcase_passed(self, name, passed=None):
if passed is not None:
- logger.debug('sub_testcase_passed:%s %s' % (name, passed))
+ self.logger.debug('sub_testcase_passed:%s %s' % (name, passed))
self.sub_testcase_status[name] = passed
return self.sub_testcase_status[name]
@@ -146,7 +150,7 @@ class Testcase:
testcase_yaml = yaml.safe_load(f)
cls.testcase_list[testcase_yaml.keys()[0]] = \
cls(testcase_yaml)
- logger.debug(cls.testcase_list)
+ cls.logger.debug(cls.testcase_list)
@classmethod
def get(cls, testcase_name):
@@ -155,31 +159,37 @@ class Testcase:
return None
-class Scenario:
+class Testsuite:
- def __init__(self, scenario):
- self.scenario = scenario
+ logger = None
+
+ def __init__(self, testsuite):
+ self.testsuite = testsuite
self.testcase_list = {}
+ @classmethod
+ def create_log(cls):
+ cls.logger = dt_logger.Logger(__name__+'.Testsuite').getLogger()
+
def get_test(self, testcase_name):
if testcase_name in self.testcase_list:
return self.testcase_list[testcase_name]
return None
- scenario_list = {}
+ testsuite_list = {}
@classmethod
def load(cls):
- for root, dirs, files in os.walk(dt_config.CERT_PATH):
- for scenario_yaml in files:
- with open(os.path.join(root, scenario_yaml)) as f:
- scenario_yaml = yaml.safe_load(f)
- cls.scenario_list.update(scenario_yaml)
+ for root, dirs, files in os.walk(dt_config.COMPLIANCE_PATH):
+ for testsuite_yaml in files:
+ with open(os.path.join(root, testsuite_yaml)) as f:
+ testsuite_yaml = yaml.safe_load(f)
+ cls.testsuite_list.update(testsuite_yaml)
- logger.debug(cls.scenario_list)
+ cls.logger.debug(cls.testsuite_list)
@classmethod
- def get(cls, scenario_name):
- if scenario_name in cls.scenario_list:
- return cls.scenario_list[scenario_name]
+ def get(cls, testsuite_name):
+ if testsuite_name in cls.testsuite_list:
+ return cls.testsuite_list[testsuite_name]
return None
diff --git a/dovetail/tests/unit/test_parser.py b/dovetail/tests/unit/test_parser.py
index 5b003d1a..b4331ea1 100644
--- a/dovetail/tests/unit/test_parser.py
+++ b/dovetail/tests/unit/test_parser.py
@@ -17,7 +17,7 @@ import unittest
import yaml
-import dovetail.parser as dovetail_parser
+import parser as dovetail_parser
class TestParser(unittest.TestCase):
@@ -27,6 +27,7 @@ class TestParser(unittest.TestCase):
def setUp(self):
"""Test case setup"""
logging.disable(logging.CRITICAL)
+ dovetail_parser.Parser.create_log()
def test_parser_cmd(self):
"""Test whether the command is correctly parsed."""
diff --git a/dovetail/utils/dovetail_logger.py b/dovetail/utils/dovetail_logger.py
index 6a2d38d5..8afa08a1 100644
--- a/dovetail/utils/dovetail_logger.py
+++ b/dovetail/utils/dovetail_logger.py
@@ -25,22 +25,12 @@ import logging
import os
from conf.dovetail_config import DovetailConfig as dt_config
-import dovetail_utils as dt_utils
-
-
-def clean_results_dir():
- result_path = dt_config.dovetail_config['result_dir']
- if os.path.exists(result_path):
- cmd = 'sudo rm -rf %s/*' % (result_path)
- dt_utils.exec_cmd(cmd, exit_on_error=False)
-
-clean_results_dir()
class Logger:
def __init__(self, logger_name):
- CI_DEBUG = os.getenv('CI_DEBUG')
+ DEBUG = os.getenv('DEBUG')
self.logger = logging.getLogger(logger_name)
self.logger.propagate = 0
@@ -50,7 +40,7 @@ class Logger:
formatter = logging.Formatter('%(asctime)s - %(name)s - '
'%(levelname)s - %(message)s')
ch.setFormatter(formatter)
- if CI_DEBUG is not None and CI_DEBUG.lower() == "true":
+ if DEBUG is not None and DEBUG.lower() == "true":
ch.setLevel(logging.DEBUG)
else:
ch.setLevel(logging.INFO)
diff --git a/ez_setup.py b/ez_setup.py
index 4ef3ee01..36da1e62 100644
--- a/ez_setup.py
+++ b/ez_setup.py
@@ -125,7 +125,7 @@ def _do_download(version, download_base, to_dir, download_delay):
egg = os.path.join(to_dir, tp.format(**locals()))
if not os.path.exists(egg):
archive = download_setuptools(version, download_base,
- to_dir, download_delay)
+ to_dir, download_delay)
_build_egg(egg, archive, to_dir)
sys.path.insert(0, egg)
@@ -235,7 +235,8 @@ def download_file_powershell(url, target):
ps_cmd = (
"[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
"[System.Net.CredentialCache]::DefaultCredentials; "
- '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")'
+ '(new-object System.Net.WebClient).DownloadFile("%(url)s", \
+ "%(target)s")'
% locals()
)
cmd = [