Merge "Landing page adoption"
authorMorgan Richomme <morgan.richomme@orange.com>
Tue, 22 Aug 2017 06:53:44 +0000 (06:53 +0000)
committerGerrit Code Review <gerrit@opnfv.org>
Tue, 22 Aug 2017 06:53:44 +0000 (06:53 +0000)
14 files changed:
jjb/calipso/calipso.yml [new file with mode: 0644]
jjb/dovetail/dovetail-run.sh
jjb/releng/automate.yml
jjb/releng/docker-deploy.sh
jjb/xci/xci-verify-jobs.yml
utils/test/testapi/opnfv_testapi/common/raises.py
utils/test/testapi/opnfv_testapi/resources/handlers.py
utils/test/testapi/opnfv_testapi/resources/models.py
utils/test/testapi/opnfv_testapi/resources/scenario_handlers.py
utils/test/testapi/opnfv_testapi/resources/scenario_models.py
utils/test/testapi/opnfv_testapi/router/url_mappings.py
utils/test/testapi/opnfv_testapi/tests/unit/resources/test_base.py
utils/test/testapi/opnfv_testapi/tests/unit/resources/test_scenario.py
utils/test/testapi/opnfv_testapi/tornado_swagger/swagger.py

diff --git a/jjb/calipso/calipso.yml b/jjb/calipso/calipso.yml
new file mode 100644 (file)
index 0000000..b8d10eb
--- /dev/null
@@ -0,0 +1,59 @@
+- project:
+    name: calipso
+
+    project: '{name}'
+
+    jobs:
+        - 'calipso-verify-{stream}'
+
+    stream:
+        - master:
+            branch: '{stream}'
+            disabled: false
+
+- job-template:
+    name: 'calipso-verify-{stream}'
+
+    disabled: '{obj:disabled}'
+
+    parameters:
+        - project-parameter:
+            project: '{project}'
+            branch: '{branch}'
+
+    scm:
+        - git-scm-gerrit
+
+    triggers:
+        - gerrit:
+            server-name: 'gerrit.opnfv.org'
+            trigger-on:
+                - patchset-created-event:
+                    exclude-drafts: 'false'
+                    exclude-trivial-rebase: 'false'
+                    exclude-no-code-change: 'false'
+                - draft-published-event
+                - comment-added-contains-event:
+                    comment-contains-value: 'recheck'
+                - comment-added-contains-event:
+                    comment-contains-value: 'reverify'
+            projects:
+              - project-compare-type: 'ANT'
+                project-pattern: '{project}'
+                branches:
+                  - branch-compare-type: 'ANT'
+                    branch-pattern: '**/{branch}'
+
+    builders:
+        - verify-unit-tests
+
+- builder:
+    name: verify-unit-tests
+    builders:
+        - shell: |
+            #!/bin/bash
+            set -o errexit
+            set -o nounset
+            set -o pipefail
+            cd $WORKSPACE
+            PYTHONPATH=$PWD/app python3 -m unittest discover -s app/test/fetch
index 7bbecc1..d05b309 100755 (executable)
@@ -160,6 +160,37 @@ if [ $(docker ps | grep "opnfv/dovetail:${DOCKER_TAG}" | wc -l) == 0 ]; then
     exit 1
 fi
 
+# Modify tempest_conf.yaml file
+tempest_conf_file=${DOVETAIL_CONFIG}/tempest_conf.yaml
+if [ ${INSTALLER_TYPE} == 'compass' ]; then
+    volume_device='vdb'
+else
+    volume_device='vdc'
+fi
+
+cat << EOF >$tempest_conf_file
+
+compute:
+    min_compute_nodes: 2
+    volume_device_name: ${volume_device}
+    min_microversion: 2.0
+    max_microversion: latest
+
+compute-feature-enabled:
+    live_migration: True
+    block_migration_for_live_migration: True
+    block_migrate_cinder_iscsi: True
+    attach_encrypted_volume: True
+
+EOF
+
+echo "${tempest_conf_file}..."
+cat ${tempest_conf_file}
+
+cp_tempest_cmd="docker cp ${DOVETAIL_CONFIG}/tempest_conf.yaml $container_id:/home/opnfv/dovetail/dovetail/userconfig"
+echo "exec command: ${cp_tempest_cmd}"
+$cp_tempest_cmd
+
 list_cmd="dovetail list ${TESTSUITE}"
 run_cmd="dovetail run --testsuite ${TESTSUITE} -d"
 echo "Container exec command: ${list_cmd}"
index d12ee5d..c6ca37f 100644 (file)
     name: 'testapi-automate-docker-deploy-macro'
     builders:
         - shell: |
-            bash ./jjb/releng/docker-deploy.sh "sudo docker run -dti -p 8082:8000
+            sudo bash ./jjb/releng/docker-deploy.sh "sudo docker run -dti --name testapi -p 8082:8000
             -e mongodb_url=mongodb://172.17.0.1:27017
             -e base_url=http://testresults.opnfv.org/test opnfv/testapi" \
-            "http://testresults.opnfv.org/test/swagger/APIs" "testapi"
+            "http://testresults.opnfv.org/test/" "testapi"
 
 - builder:
     name: 'reporting-automate-docker-deploy-macro'
     builders:
         - shell: |
-            bash ./jjb/releng/docker-deploy.sh "sudo docker run -itd -p 8084:8000 opnfv/reporting" \
+            sudo bash ./jjb/releng/docker-deploy.sh "sudo docker run -itd --name reporting -p 8084:8000 opnfv/reporting" \
             "http://testresults.opnfv.org/reporting2/reporting/index.html" "reporting"
 
 - builder:
index 2a3e078..1e83577 100644 (file)
 #  specific language governing permissions and limitations      *
 #  under the License.                                           *
 
-# Assigning Variables
+
 command=$1
 url=$2
 module=$3
 
-function check() {
+REPO="opnfv"
+latest_image=$REPO/$module:latest
+old_image=$REPO/$module:old
+latest_container_name=$module
+old_container_name=$module"_old"
+latest_container_id=
+old_container_id=
+new_start_container=
+
+function DEBUG() {
+  echo `date "+%Y-%m-%d %H:%M:%S.%N"` ": $1"
+}
 
-    # Verify hosted
+function check_connectivity() {
+    # check update status via test the connectivity of provide url
     sleep 5
     cmd=`curl -s --head  --request GET ${url} | grep '200 OK' > /dev/null`
     rc=$?
-    echo $rc
-
-    if [[ $rc == 0 ]]
-    then
+    DEBUG $rc
+    if [[ $rc == 0 ]]; then
         return 0
     else
         return 1
     fi
-
 }
 
-echo "Getting contianer Id of the currently running one"
-contId=$(sudo docker ps | grep "opnfv/${module}:latest" | awk '{print $1}')
-
-echo $contId
 
-echo "Pulling the latest image"
-sudo docker pull opnfv/${module}:latest
+function pull_latest_image() {
+    DEBUG "pull latest image $latest_image"
+    docker pull $latest_image
+}
 
-echo "Deleting old containers of opnfv/${module}:old"
-sudo docker ps -a | grep "opnfv/${module}" | grep "old" | awk '{print $1}' | xargs -r sudo docker rm -f
+function get_latest_running_container() {
+    latest_container_id=`docker ps -q --filter name=^/$latest_container_name$`
+}
 
-echo "Deleting old images of opnfv/${module}:latest"
-sudo docker images | grep "opnfv/${module}" | grep "old" | awk '{print $3}' | xargs -r sudo docker rmi -f
+function get_old_running_container() {
+    old_container_id=`docker ps -q --filter name=^/$old_container_name$`
+}
 
+function delete_old_image() {
+    DEBUG "delete old image: $old_image"
+    docker rmi -f $old_image
+}
 
-if [[ -z "$contId" ]]
-then
-    echo "No running ${module} container"
+function delete_old_container() {
+    DEBUG "delete old container: $old_container_name"
+    docker ps -a -q --filter name=^/$old_container_name$ | xargs docker rm -f &>/dev/null
+}
 
-    echo "Removing stopped ${module} containers in the previous iterations"
-    sudo docker ps -f status=exited | grep "opnfv_${module}" | awk '{print $1}' | xargs -r sudo docker rm -f
-else
-    echo $contId
+function delete_latest_container() {
+    DEBUG "delete latest container: $module"
+    docker ps -a -q --filter name=^/$latest_container_name$ | xargs docker rm -f &>/dev/null
+}
 
-    echo "Get the image id of the currently running conatiner"
-    currImgId=$(sudo docker ps | grep "$contId" | awk '{print $2}')
-    echo $currImgId
+function delete_latest_image() {
+    DEBUG "delete latest image: $REPO/$module:latest"
+    docker rmi -f $latest_image
+}
 
-    if [[ -z "$currImgId" ]]
-    then
-        echo "No image id found for the container id"
-        exit 1
-    fi
+function change_image_tag_2_old() {
+    DEBUG "change image tag 2 old"
+    docker tag $latest_image $old_image
+    docker rmi -f $latest_image
+}
 
-    echo "Changing current image tag to old"
-    sudo docker tag "$currImgId" opnfv/${module}:old
+function mark_latest_container_2_old() {
+    DEBUG "mark latest container to be old"
+    docker rename "$latest_container_name" "$old_container_name"
+}
 
-    echo "Removing stopped ${module} containers in the previous iteration"
-    sudo docker ps -f status=exited | grep "opnfv_${module}" | awk '{print $1}' | xargs -r sudo docker rm -f
+function stop_old_container() {
+    DEBUG "stop old container"
+    docker stop "$old_container_name"
+}
 
-    echo "Renaming the running container name to opnfv_${module} as to identify it."
-    sudo docker rename $contId opnfv_${module}
+function run_latest_image() {
+    new_start_container=`$command`
+    DEBUG "run latest image: $new_start_container"
+}
 
-    echo "Stop the currently running container"
-    sudo docker stop $contId
+get_latest_running_container
+get_old_running_container
+
+if [[ ! -z $latest_container_id ]]; then
+    DEBUG "latest container is running: $latest_container_id"
+    delete_old_container
+    delete_old_image
+    change_image_tag_2_old
+    mark_latest_container_2_old
+    pull_latest_image
+    stop_old_container
+    run_latest_image
+
+elif [[ ! -z $old_container_id ]]; then
+    DEBUG "old container is running: $old_container_id"
+    delete_latest_container
+    delete_latest_image
+    pull_latest_image
+    stop_old_container
+    run_latest_image
+else
+    DEBUG "no container is running"
+    delete_old_container
+    delete_old_image
+    delete_latest_container
+    delete_latest_image
+    pull_latest_image
+    run_latest_image
 fi
 
-echo "Running a container with the new image"
-$command:latest
-
-if check; then
-    echo "TestResults Module Hosted."
+if check_connectivity; then
+    DEBUG "CONGRATS: $module update successfully"
 else
-    echo "TestResults Module Failed"
-    if [[ $(sudo docker images | grep "opnfv/${module}" | grep "old" | awk '{print $3}') ]]; then
-        echo "Running old Image"
-        $command:old
-        exit 1
+    DEBUG "ATTENTION: $module update failed"
+    id=`docker ps -a -q --filter name=^/$old_container_name$`
+    if [[ ! -z $id ]]; then
+        DEBUG "start old container instead"
+        docker stop $new_start_container
+        docker start $id
+    fi
+    if ! check_connectivity; then
+        DEBUG "BIG ISSUE: no container is running normally"
     fi
+    exit 1
 fi
 
-# Echo Images and Containers
-sudo docker images
-sudo docker ps -a
+docker images
+docker ps -a
index 6bc0642..8d1ee55 100644 (file)
         - label:
             name: SLAVE_LABEL
             default: 'xci-virtual-{distro}'
+        - string:
+            name: GIT_BASE
+            default: https://gerrit.opnfv.org/gerrit/$PROJECT
+            description: 'Git URL to use on this Jenkins Slave'
 
     builders:
         - description-setter:
             condition: SUCCESSFUL
             projects:
                 - name: 'xci-verify-healthcheck-{type}-{stream}'
-                  current-parameters: false
+                  current-parameters: true
                   predefined-parameters: |
                     DISTRO={distro}
                     DEPLOY_SCENARIO=os-nosdn-nofeature-noha
     parameters:
         - string:
             name: DISTRO
-            default: '{distro}'
+            default: 'xenial'
         - string:
             name: DEPLOY_SCENARIO
             default: 'os-nosdn-nofeature-noha'
             name: XCI_FLAVOR
             default: 'mini'
         - string:
-            name: XCI_DEVEL_ROOT
-            default: $WORKSPACE
+            name: OPNFV_RELENG_DEV_PATH
+            default: $WORKSPACE/
         - string:
             name: ANSIBLE_VERBOSITY
             default: '-vvvv'
+        - string:
+            name: GIT_BASE
+            default: https://gerrit.opnfv.org/gerrit/$PROJECT
+            description: 'Git URL to use on this Jenkins Slave'
 
     wrappers:
         - ssh-agent-wrapper
         - shell: |
             #!/bin/bash
 
-            cd $WORKSPACE
+            # for some reason, the PATH is not set correctly
+            # setting PATH for ansible stuff
+            export PATH=/home/jenkins/.local/bin:$PATH
+
+            cd $WORKSPACE/xci
             ./xci-deploy.sh
 
 - builder:
index ec6b8a5..55c58c9 100644 (file)
@@ -26,6 +26,10 @@ class Forbidden(Raiser):
     code = httplib.FORBIDDEN
 
 
+class Conflict(Raiser):
+    code = httplib.CONFLICT
+
+
 class NotFound(Raiser):
     code = httplib.NOT_FOUND
 
index 474a203..ed55c70 100644 (file)
@@ -50,7 +50,7 @@ class GenericApiHandler(web.RequestHandler):
         self.auth = self.settings["auth"]
 
     def prepare(self):
-        if self.request.method != "GET" and self.request.method != "DELETE":
+        if self.request.body:
             if self.request.headers.get("Content-Type") is not None:
                 if self.request.headers["Content-Type"].startswith(
                         DEFAULT_REPRESENTATION):
@@ -110,22 +110,23 @@ class GenericApiHandler(web.RequestHandler):
         pipelines.append({'$match': query})
 
         total_pages = 0
-        if page > 0:
-            cursor = dbapi.db_list(self.table, query)
-            records_count = yield cursor.count()
-            total_pages, return_nr = self._calc_total_pages(records_count,
-                                                            last,
-                                                            page,
-                                                            per_page)
-            pipelines = self._set_pipelines(pipelines,
-                                            sort,
-                                            return_nr,
-                                            page,
-                                            per_page)
-        cursor = dbapi.db_aggregate(self.table, pipelines)
         data = list()
-        while (yield cursor.fetch_next):
-            data.append(self.format_data(cursor.next_object()))
+        cursor = dbapi.db_list(self.table, query)
+        records_count = yield cursor.count()
+        if records_count > 0:
+            if page > 0:
+                total_pages, return_nr = self._calc_total_pages(records_count,
+                                                                last,
+                                                                page,
+                                                                per_page)
+                pipelines = self._set_pipelines(pipelines,
+                                                sort,
+                                                return_nr,
+                                                page,
+                                                per_page)
+            cursor = dbapi.db_aggregate(self.table, pipelines)
+            while (yield cursor.fetch_next):
+                data.append(self.format_data(cursor.next_object()))
         if res_op is None:
             res = {self.table: data}
         else:
@@ -188,6 +189,16 @@ class GenericApiHandler(web.RequestHandler):
         update_req['_id'] = str(data._id)
         self.finish_request(update_req)
 
+    @check.authenticate
+    @check.no_body
+    @check.not_exist
+    @check.updated_one_not_exist
+    def pure_update(self, data, query=None, **kwargs):
+        data = self.table_cls.from_dict(data)
+        update_req = self._update_requests(data)
+        yield dbapi.db_update(self.table, query, update_req)
+        self.finish_request()
+
     def _update_requests(self, data):
         request = dict()
         for k, v in self.json_args.iteritems():
index e8fc532..6f04cc2 100644 (file)
@@ -48,6 +48,29 @@ class ModelBase(object):
 
         return t
 
+    @classmethod
+    def from_dict_with_raise(cls, a_dict):
+        if a_dict is None:
+            return None
+
+        attr_parser = cls.attr_parser()
+        t = cls()
+        for k, v in a_dict.iteritems():
+            if k not in t.__dict__:
+                raise AttributeError(
+                    '{} has no attribute {}'.format(cls.__name__, k))
+            value = v
+            if isinstance(v, dict) and k in attr_parser:
+                value = attr_parser[k].from_dict(v)
+            elif isinstance(v, list) and k in attr_parser:
+                value = []
+                for item in v:
+                    value.append(attr_parser[k].from_dict(item))
+
+            t.__setattr__(k, value)
+
+        return t
+
     @staticmethod
     def attr_parser():
         return {}
index 66e8559..bd06400 100644 (file)
@@ -1,5 +1,7 @@
 import functools
 
+from opnfv_testapi.common import message
+from opnfv_testapi.common import raises
 from opnfv_testapi.resources import handlers
 import opnfv_testapi.resources.scenario_models as models
 from opnfv_testapi.tornado_swagger import swagger
@@ -138,6 +140,13 @@ class ScenarioUpdater(object):
         updates = {
             ('scores', 'post'): self._update_requests_add_score,
             ('trust_indicators', 'post'): self._update_requests_add_ti,
+            ('customs', 'post'): self._update_requests_add_customs,
+            ('customs', 'put'): self._update_requests_update_customs,
+            ('customs', 'delete'): self._update_requests_delete_customs,
+            ('projects', 'post'): self._update_requests_add_projects,
+            ('projects', 'put'): self._update_requests_update_projects,
+            ('projects', 'delete'): self._update_requests_delete_projects,
+            ('owner', 'put'): self._update_requests_change_owner,
         }
         updates[(item, action)](self.data)
 
@@ -178,6 +187,76 @@ class ScenarioUpdater(object):
         project.trust_indicators.append(
             models.ScenarioTI.from_dict(self.body))
 
+    @iter_installers
+    @iter_versions
+    @iter_projects
+    def _update_requests_add_customs(self, project):
+        project.customs = list(set(project.customs + self.body))
+
+    @iter_installers
+    @iter_versions
+    @iter_projects
+    def _update_requests_update_customs(self, project):
+        project.customs = list(set(self.body))
+
+    @iter_installers
+    @iter_versions
+    @iter_projects
+    def _update_requests_delete_customs(self, project):
+        project.customs = filter(
+            lambda f: f not in self.body,
+            project.customs)
+
+    @iter_installers
+    @iter_versions
+    def _update_requests_add_projects(self, version):
+        exists = list()
+        malformat = list()
+        for n in self.body:
+            try:
+                f_n = models.ScenarioProject.from_dict_with_raise(n)
+                if not any(o.project == f_n.project for o in version.projects):
+                    version.projects.append(f_n)
+                else:
+                    exists.append(n['project'])
+            except Exception as e:
+                malformat.append(e.message)
+        if malformat:
+            raises.BadRequest(message.bad_format(malformat))
+        elif exists:
+            raises.Conflict(message.exist('projects', exists))
+
+    @iter_installers
+    @iter_versions
+    def _update_requests_update_projects(self, version):
+        exists = list()
+        malformat = list()
+        projects = list()
+        for n in self.body:
+            try:
+                f_n = models.ScenarioProject.from_dict_with_raise(n)
+                if not any(o.project == f_n.project for o in projects):
+                    projects.append(models.ScenarioProject.from_dict(n))
+                else:
+                    exists.append(n['project'])
+            except:
+                malformat.append(n)
+        if malformat:
+            raises.BadRequest(message.bad_format(malformat))
+        elif exists:
+            raises.Forbidden(message.exist('projects', exists))
+        version.projects = projects
+
+    @iter_installers
+    @iter_versions
+    def _update_requests_delete_projects(self, version):
+        version.projects = self._remove_projects(version.projects)
+
+    @iter_installers
+    @iter_versions
+    def _update_requests_change_owner(self, version):
+        version.owner = self.body
+
     def _filter_installers(self, installers):
         return self._filter('installer', installers)
 
@@ -187,11 +266,19 @@ class ScenarioUpdater(object):
     def _filter_projects(self, projects):
         return self._filter('project', projects)
 
+    def _remove_projects(self, projects):
+        return self._remove('project', projects)
+
     def _filter(self, item, items):
         return filter(
             lambda f: getattr(f, item) == getattr(self, item),
             items)
 
+    def _remove(self, field, fields):
+        return filter(
+            lambda f: getattr(f, field) not in self.body,
+            fields)
+
 
 class GenericScenarioUpdateHandler(GenericScenarioHandler):
     def __init__(self, application, request, **kwargs):
@@ -204,15 +291,15 @@ class GenericScenarioUpdateHandler(GenericScenarioHandler):
         self.item = None
         self.action = None
 
-    def do_post(self, scenario, item, action, locators):
+    def do_update(self, item, action, locators):
         self.item = item
         self.action = action
-        for k in locators.keys():
-            v = self.get_query_argument(k)
-            setattr(self, k, v)
-            locators[k] = v
-        db_keys = ['name']
-        self._update(query=self.set_query(locators=locators), db_keys=db_keys)
+        for k, v in locators.iteritems():
+            if not v:
+                v = self.get_query_argument(k)
+                setattr(self, k, v)
+                locators[k] = v
+        self.pure_update(query=self.set_query(locators=locators))
 
     def _update_requests(self, data):
         return ScenarioUpdater(data,
@@ -247,16 +334,15 @@ class ScenarioScoresHandler(GenericScenarioUpdateHandler):
         @type project: L{string}
         @in project: query
         @required project: True
-        @rtype: L{Scenario}
         @return 200: score is created.
         @raise 404:  scenario/installer/version/project not existed
         """
-        self.do_post(scenario,
-                     'scores',
-                     'post',
-                     locators={'installer': None,
-                               'version': None,
-                               'project': None})
+        self.do_update('scores',
+                       'post',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None,
+                                 'project': None})
 
 
 class ScenarioTIsHandler(GenericScenarioUpdateHandler):
@@ -284,13 +370,235 @@ class ScenarioTIsHandler(GenericScenarioUpdateHandler):
         @type project: L{string}
         @in project: query
         @required project: True
-        @rtype: L{Scenario}
         @return 200: trust indicator is added.
         @raise 404:  scenario/installer/version/project not existed
         """
-        self.do_post(scenario,
-                     'trust_indicators',
-                     'post',
-                     locators={'installer': None,
-                               'version': None,
-                               'project': None})
+        self.do_update('trust_indicators',
+                       'post',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None,
+                                 'project': None})
+
+
+class ScenarioCustomsHandler(GenericScenarioUpdateHandler):
+    @swagger.operation(nickname="addCustomizedTestCases")
+    def post(self, scenario):
+        """
+        @description: add customized test cases
+        @notes: add several test cases to a project
+            POST /api/v1/scenarios/<scenario_name>/customs? \
+                installer=<installer_name>& \
+                version=<version_name>& \
+                project=<project_name>
+        @param body: test cases to be added
+        @type body: C{list} of L{string}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @param project: project name
+        @type project: L{string}
+        @in project: query
+        @required project: True
+        @return 200: test cases are added.
+        @raise 404:  scenario/installer/version/project not existed
+        """
+        self.do_update('customs',
+                       'post',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None,
+                                 'project': None})
+
+    @swagger.operation(nickname="updateCustomizedTestCases")
+    def put(self, scenario):
+        """
+        @description: update customized test cases
+        @notes: substitute all the customized test cases
+            PUT /api/v1/scenarios/<scenario_name>/customs? \
+                installer=<installer_name>& \
+                version=<version_name>& \
+                project=<project_name>
+        @param body: new supported test cases
+        @type body: C{list} of L{string}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @param project: project name
+        @type project: L{string}
+        @in project: query
+        @required project: True
+        @return 200: substitute test cases success.
+        @raise 404:  scenario/installer/version/project not existed
+        """
+        self.do_update('customs',
+                       'put',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None,
+                                 'project': None})
+
+    @swagger.operation(nickname="deleteCustomizedTestCases")
+    def delete(self, scenario):
+        """
+        @description: delete one or several customized test cases
+        @notes: delete one or some customized test cases
+            DELETE /api/v1/scenarios/<scenario_name>/customs? \
+                installer=<installer_name>& \
+                version=<version_name>& \
+                project=<project_name>
+        @param body: test case(s) to be deleted
+        @type body: C{list} of L{string}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @param project: project name
+        @type project: L{string}
+        @in project: query
+        @required project: True
+        @return 200: delete test case(s) success.
+        @raise 404:  scenario/installer/version/project not existed
+        """
+        self.do_update('customs',
+                       'delete',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None,
+                                 'project': None})
+
+
+class ScenarioProjectsHandler(GenericScenarioUpdateHandler):
+    @swagger.operation(nickname="addProjectsUnderScenario")
+    def post(self, scenario):
+        """
+        @description: add projects to scenario
+        @notes: add one or multiple projects
+            POST /api/v1/scenarios/<scenario_name>/projects? \
+                installer=<installer_name>& \
+                version=<version_name>
+        @param body: projects to be added
+        @type body: C{list} of L{ScenarioProject}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @return 200: projects are added.
+        @raise 400: bad schema
+        @raise 409: conflict, project already exists
+        @raise 404:  scenario/installer/version not existed
+        """
+        self.do_update('projects',
+                       'post',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None})
+
+    @swagger.operation(nickname="updateScenarioProjects")
+    def put(self, scenario):
+        """
+        @description: replace all projects
+        @notes: substitute all projects, delete existed ones with new provides
+            PUT /api/v1/scenarios/<scenario_name>/projects? \
+                installer=<installer_name>& \
+                version=<version_name>
+        @param body: new projects
+        @type body: C{list} of L{ScenarioProject}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @return 200: replace projects success.
+        @raise 400: bad schema
+        @raise 404:  scenario/installer/version not existed
+        """
+        self.do_update('projects',
+                       'put',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None})
+
+    @swagger.operation(nickname="deleteProjectsUnderScenario")
+    def delete(self, scenario):
+        """
+        @description: delete one or multiple projects
+        @notes: delete one or multiple projects
+            DELETE /api/v1/scenarios/<scenario_name>/projects? \
+                installer=<installer_name>& \
+                version=<version_name>
+        @param body: projects(names) to be deleted
+        @type body: C{list} of L{string}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @return 200: delete project(s) success.
+        @raise 404:  scenario/installer/version not existed
+        """
+        self.do_update('projects',
+                       'delete',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None})
+
+
+class ScenarioOwnerHandler(GenericScenarioUpdateHandler):
+    @swagger.operation(nickname="changeScenarioOwner")
+    def put(self, scenario):
+        """
+        @description: change scenario owner
+        @notes: substitute all projects, delete existed ones with new provides
+            PUT /api/v1/scenarios/<scenario_name>/owner? \
+                installer=<installer_name>& \
+                version=<version_name>
+        @param body: new owner
+        @type body: L{string}
+        @in body: body
+        @param installer: installer type
+        @type installer: L{string}
+        @in installer: query
+        @required installer: True
+        @param version: version
+        @type version: L{string}
+        @in version: query
+        @required version: True
+        @return 200: change owner success.
+        @raise 404:  scenario/installer/version not existed
+        """
+        self.do_update('owner',
+                       'put',
+                       locators={'scenario': scenario,
+                                 'installer': None,
+                                 'version': None})
index 9f5a074..7d07707 100644 (file)
@@ -74,7 +74,8 @@ class ScenarioVersion(models.ModelBase):
         @property projects:
         @ptype projects: C{list} of L{ScenarioProject}
     """
-    def __init__(self, version=None, projects=None):
+    def __init__(self, owner=None, version=None, projects=None):
+        self.owner = owner
         self.version = version
         self.projects = list_default(projects)
 
@@ -83,7 +84,9 @@ class ScenarioVersion(models.ModelBase):
         return {'projects': ScenarioProject}
 
     def __eq__(self, other):
-        return [self.version == other.version and self._projects_eq(other)]
+        return [self.version == other.version and
+                self.owner == other.owner and
+                self._projects_eq(other)]
 
     def __ne__(self, other):
         return not self.__eq__(other)
index 4589425..9c9556c 100644 (file)
@@ -58,6 +58,12 @@ mappings = [
      scenario_handlers.ScenarioScoresHandler),
     (r"/api/v1/scenarios/([^/]+)/trust_indicators",
      scenario_handlers.ScenarioTIsHandler),
+    (r"/api/v1/scenarios/([^/]+)/customs",
+     scenario_handlers.ScenarioCustomsHandler),
+    (r"/api/v1/scenarios/([^/]+)/projects",
+     scenario_handlers.ScenarioProjectsHandler),
+    (r"/api/v1/scenarios/([^/]+)/owner",
+     scenario_handlers.ScenarioOwnerHandler),
 
     # static path
     (r'/(.*\.(css|png|gif|js|html|json|map|woff2|woff|ttf))',
index aa6b835..77a8d18 100644 (file)
@@ -92,21 +92,35 @@ class TestBase(testing.AsyncHTTPTestCase):
                          headers=self.headers)
         return self._get_return(res, self.list_res)
 
-    def update(self, new=None, *args):
-        if new:
+    def update_direct_url(self, url, new=None):
+        if new and hasattr(new, 'format'):
             new = new.format()
-        res = self.fetch(self._get_uri(*args),
+        res = self.fetch(url,
                          method='PUT',
                          body=json.dumps(new),
                          headers=self.headers)
         return self._get_return(res, self.update_res)
 
-    def delete(self, *args):
-        res = self.fetch(self._get_uri(*args),
-                         method='DELETE',
-                         headers=self.headers)
+    def update(self, new=None, *args):
+        return self.update_direct_url(self._get_uri(*args), new)
+
+    def delete_direct_url(self, url, body):
+        if body:
+            res = self.fetch(url,
+                             method='DELETE',
+                             body=json.dumps(body),
+                             headers=self.headers,
+                             allow_nonstandard_methods=True)
+        else:
+            res = self.fetch(url,
+                             method='DELETE',
+                             headers=self.headers)
+
         return res.code, res.body
 
+    def delete(self, *args):
+        return self.delete_direct_url(self._get_uri(*args), None)
+
     @staticmethod
     def _get_valid_args(*args):
         new_args = tuple(['%s' % arg for arg in args if arg is not None])
@@ -132,7 +146,10 @@ class TestBase(testing.AsyncHTTPTestCase):
     def _get_return(self, res, cls):
         code = res.code
         body = res.body
-        return code, self._get_return_body(code, body, cls)
+        if body:
+            return code, self._get_return_body(code, body, cls)
+        else:
+            return code, None
 
     @staticmethod
     def _get_return_body(code, body, cls):
index 0558ea3..466caaf 100644 (file)
@@ -168,15 +168,30 @@ class TestScenarioUpdate(TestScenarioBase):
             self.version,
             'functest')
 
+    def update_url_fixture(item):
+        def _update_url_fixture(xstep):
+            def wrapper(self, *args, **kwargs):
+                locator = None
+                if item in ['projects', 'owner']:
+                    locator = 'installer={}&version={}'.format(
+                        self.installer,
+                        self.version)
+                self.update_url = '{}/{}?{}'.format(self.scenario_url,
+                                                    item,
+                                                    locator)
+                xstep(self, *args, **kwargs)
+            return wrapper
+        return _update_url_fixture
+
     def update_partial(operate, expected):
-        def _update(set_update):
+        def _update_partial(set_update):
             @functools.wraps(set_update)
-            def wrap(self):
+            def wrapper(self):
                 update, scenario = set_update(self, deepcopy(self.req_d))
                 code, body = getattr(self, operate)(update, self.scenario)
                 getattr(self, expected)(code, scenario)
-            return wrap
-        return _update
+            return wrapper
+        return _update_partial
 
     @update_partial('_add', '_success')
     def test_addScore(self, scenario):
@@ -200,9 +215,101 @@ class TestScenarioUpdate(TestScenarioBase):
 
         return add, scenario
 
+    @update_partial('_add', '_success')
+    def test_addCustoms(self, scenario):
+        add = ['odl', 'parser', 'vping_ssh']
+        projects = scenario['installers'][0]['versions'][0]['projects']
+        functest = filter(lambda f: f['project'] == 'functest', projects)[0]
+        functest['customs'] = list(set(functest['customs'] + add))
+        self.update_url = '{}/customs?{}'.format(self.scenario_url,
+                                                 self.locate_project)
+        return add, scenario
+
+    @update_partial('_update', '_success')
+    def test_updateCustoms(self, scenario):
+        news = ['odl', 'parser', 'vping_ssh']
+        projects = scenario['installers'][0]['versions'][0]['projects']
+        functest = filter(lambda f: f['project'] == 'functest', projects)[0]
+        functest['customs'] = news
+        self.update_url = '{}/customs?{}'.format(self.scenario_url,
+                                                 self.locate_project)
+
+        return news, scenario
+
+    @update_partial('_delete', '_success')
+    def test_deleteCustoms(self, scenario):
+        obsoletes = ['vping_ssh']
+        projects = scenario['installers'][0]['versions'][0]['projects']
+        functest = filter(lambda f: f['project'] == 'functest', projects)[0]
+        functest['customs'] = ['healthcheck']
+        self.update_url = '{}/customs?{}'.format(self.scenario_url,
+                                                 self.locate_project)
+
+        return obsoletes, scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_add', '_success')
+    def test_addProjects_succ(self, scenario):
+        add = models.ScenarioProject(project='qtip').format()
+        scenario['installers'][0]['versions'][0]['projects'].append(add)
+        return [add], scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_add', '_conflict')
+    def test_addProjects_already_exist(self, scenario):
+        add = models.ScenarioProject(project='functest').format()
+        scenario['installers'][0]['versions'][0]['projects'].append(add)
+        return [add], scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_add', '_bad_request')
+    def test_addProjects_bad_schema(self, scenario):
+        add = models.ScenarioProject(project='functest').format()
+        add['score'] = None
+        scenario['installers'][0]['versions'][0]['projects'].append(add)
+        return [add], scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_update', '_success')
+    def test_updateProjects_succ(self, scenario):
+        update = models.ScenarioProject(project='qtip').format()
+        scenario['installers'][0]['versions'][0]['projects'] = [update]
+        return [update], scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_update', '_bad_request')
+    def test_updateProjects_bad_schema(self, scenario):
+        update = models.ScenarioProject(project='functest').format()
+        update['score'] = None
+        scenario['installers'][0]['versions'][0]['projects'] = [update]
+        return [update], scenario
+
+    @update_url_fixture('projects')
+    @update_partial('_delete', '_success')
+    def test_deleteProjects(self, scenario):
+        deletes = ['functest']
+        projects = scenario['installers'][0]['versions'][0]['projects']
+        scenario['installers'][0]['versions'][0]['projects'] = filter(
+            lambda f: f['project'] != 'functest',
+            projects)
+        return deletes, scenario
+
+    @update_url_fixture('owner')
+    @update_partial('_update', '_success')
+    def test_changeOwner(self, scenario):
+        new_owner = 'new_owner'
+        scenario['installers'][0]['versions'][0]['owner'] = 'www'
+        return new_owner, scenario
+
     def _add(self, update_req, new_scenario):
         return self.post_direct_url(self.update_url, update_req)
 
+    def _update(self, update_req, new_scenario):
+        return self.update_direct_url(self.update_url, update_req)
+
+    def _delete(self, update_req, new_scenario):
+        return self.delete_direct_url(self.update_url, update_req)
+
     def _success(self, status, new_scenario):
         self.assertEqual(status, httplib.OK)
         self._get_and_assert(new_scenario.get('name'), new_scenario)
@@ -212,3 +319,6 @@ class TestScenarioUpdate(TestScenarioBase):
 
     def _bad_request(self, status, new_scenario):
         self.assertEqual(status, httplib.BAD_REQUEST)
+
+    def _conflict(self, status, new_scenario):
+        self.assertEqual(status, httplib.CONFLICT)
index 83f389a..6125c95 100644 (file)
@@ -94,11 +94,18 @@ class DocParser(object):
 
     def _parse_type(self, **kwargs):
         arg = kwargs.get('arg', None)
-        body = self._get_body(**kwargs)
-        self.params.setdefault(arg, {}).update({
-            'name': arg,
-            'dataType': body
-        })
+        code = self._parse_epytext_para('code', **kwargs)
+        link = self._parse_epytext_para('link', **kwargs)
+        if code is None:
+            self.params.setdefault(arg, {}).update({
+                'name': arg,
+                'type': link
+            })
+        elif code == 'list':
+            self.params.setdefault(arg, {}).update({
+                'type': 'array',
+                'items': {'type': link}
+            })
 
     def _parse_in(self, **kwargs):
         arg = kwargs.get('arg', None)