diff options
30 files changed, 1879 insertions, 114 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/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/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..8fa1a6d2 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') + # testarea supported tuple, should adjust accordingly + testarea_supported = ('vimops', 'nfvi', 'ipv6') curr_path = os.path.dirname(os.path.abspath(__file__)) @@ -52,6 +55,8 @@ class DovetailConfig: @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/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..52c8ebf9 100644 --- a/dovetail/report.py +++ b/dovetail/report.py @@ -16,8 +16,6 @@ import utils.dovetail_logger as dt_logger from conf.dovetail_config import DovetailConfig as dt_config from testcase import Testcase -logger = dt_logger.Logger('report.py').getLogger() - def get_pass_str(passed): if passed: @@ -30,17 +28,27 @@ 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): report_obj = {} - report_obj['scenario'] = scenario_yaml['name'] + report_obj['testsuite'] = testsuite_yaml['name'] 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 @@ -62,12 +70,12 @@ class Report: testcase.sub_testcase_passed(sub_test)) }) report_obj['testcases_list'].append(testcase_in_rpt) - logger.info(json.dumps(report_obj)) + cls.logger.info(json.dumps(report_obj)) return report_obj @classmethod - def generate(cls, scenario_yaml): - rpt_data = cls.generate_json(scenario_yaml) + def generate(cls, testsuite_yaml, testarea): + rpt_data = cls.generate_json(testsuite_yaml, testarea) rpt_text = '' split_line = '+-----------------------------------------------------' split_line += '---------------------+\n' @@ -76,7 +84,7 @@ class Report: +==========================================================================+\n\ | report |\n' rpt_text += split_line - rpt_text += '|scenario: %s\n' % rpt_data['scenario'] + rpt_text += '|testsuite: %s\n' % rpt_data['testsuite'] for testcase in rpt_data['testcases_list']: rpt_text += '| [testcase]: %s\t\t\t\t[%s]\n' % \ (testcase['name'], testcase['result']) @@ -87,21 +95,21 @@ class Report: (sub_test['name'], sub_test['result']) rpt_text += split_line - logger.info(rpt_text) + cls.logger.info(rpt_text) cls.save(rpt_text) return rpt_text # save to disk as default - @staticmethod - def save(report): + @classmethod + def save(cls, report): report_file_name = dt_config.dovetail_config['report_file'] try: with open(os.path.join(dt_config.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 +125,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,9 +149,15 @@ 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'] @@ -158,7 +173,7 @@ class FunctestCrawler: 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,31 +195,37 @@ 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) + 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'] @@ -218,18 +239,18 @@ class YardstickCrawler: file_path = os.path.join(dt_config.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 +279,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 +306,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 +318,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..e908f3ab 100755 --- a/dovetail/run.py +++ b/dovetail/run.py @@ -13,19 +13,21 @@ import sys import utils.dovetail_logger as dt_logger - +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 +from report import YardstickCrawler +from report import FunctestChecker +from report import 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 +42,17 @@ 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) + + 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 @@ -82,7 +89,7 @@ def run_test(scenario): Report.check_result(testcase, db_result) -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 +108,51 @@ 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 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!""" + 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() + run_test(testsuite_yaml, testarea, logger) + Report.generate(testsuite_yaml, testarea) + 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/utils/dovetail_logger.py b/dovetail/utils/dovetail_logger.py index 6a2d38d5..e725e612 100644 --- a/dovetail/utils/dovetail_logger.py +++ b/dovetail/utils/dovetail_logger.py @@ -40,7 +40,7 @@ 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 +50,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) |