Beispiel #1
0
class TestResourcesWorkflow(Base):
    def setUp(self):
        super(TestResourcesWorkflow, self).setUp()
        priv_key_path = set_private_key(
            config.USERS[config.ADMIN_USER]["privkey"])
        self.gitu_admin = GerritGitUtils(
            config.ADMIN_USER, priv_key_path,
            config.USERS[config.ADMIN_USER]['email'])
        self.gu = GerritUtils(
            config.GATEWAY_URL,
            auth_cookie=config.USERS[config.ADMIN_USER]['auth_cookie'])
        self.ju = JenkinsUtils()

        self.dirs_to_delete = []

    def tearDown(self):
        super(TestResourcesWorkflow, self).tearDown()
        for dirs in self.dirs_to_delete:
            shutil.rmtree(dirs)

    def ssh_run_cmd(self, sshkey_priv_path, user, host, subcmd):
        host = '%s@%s' % (user, host)
        sshcmd = [
            'ssh', '-o', 'LogLevel=ERROR', '-o', 'StrictHostKeyChecking=no',
            '-o', 'UserKnownHostsFile=/dev/null', '-i', sshkey_priv_path, host
        ]
        cmd = sshcmd + subcmd

        devnull = open(os.devnull, 'wb')
        p = Popen(cmd, stdout=devnull, stderr=devnull)
        return p.communicate(), p.returncode

    def clone_as_admin(self, pname):
        url = "ssh://%s@%s:29418/%s" % (config.ADMIN_USER, config.GATEWAY_HOST,
                                        pname)
        clone_dir = self.gitu_admin.clone(url, pname)
        if os.path.dirname(clone_dir) not in self.dirs_to_delete:
            self.dirs_to_delete.append(os.path.dirname(clone_dir))
        return clone_dir

    def commit_direct_push_as_admin(self, clone_dir, msg):
        # Stage, commit and direct push the additions on master
        self.gitu_admin.add_commit_for_all_new_additions(clone_dir, msg)
        return self.gitu_admin.direct_push_branch(clone_dir, 'master')

    def set_resources_then_direct_push(self,
                                       fpath,
                                       resources=None,
                                       mode='add'):
        config_clone_dir = self.clone_as_admin("config")
        path = os.path.join(config_clone_dir, fpath)
        if mode == 'add':
            file(path, 'w').write(resources)
        elif mode == 'del':
            os.unlink(path)
        change_sha = self.commit_direct_push_as_admin(
            config_clone_dir, "Add new resources for functional tests")
        config_update_log = self.ju.wait_for_config_update(change_sha)
        self.assertIn("SUCCESS", config_update_log)

    def wait_for_jenkins_note(self, change_id):
        attempt = 0
        while "jenkins" not in self.gu.get_reviewers(change_id):
            if attempt >= 90:
                break
            time.sleep(1)
            attempt += 1

    def propose_resources_change_check_ci(self,
                                          fpath,
                                          resources=None,
                                          mode='add',
                                          expected_note=1,
                                          msg=None):

        config_clone_dir = self.clone_as_admin("config")
        path = os.path.join(config_clone_dir, fpath)
        if mode == 'add':
            file(path, 'w').write(resources)
        elif mode == 'del':
            os.unlink(path)

        if not msg:
            msg = "Validate resources"
        if mode == 'add':
            change_sha = self.gitu_admin.add_commit_and_publish(
                config_clone_dir, "master", msg, fnames=[path])
        if mode == 'del':
            change_sha = self.gitu_admin.add_commit_for_all_new_additions(
                config_clone_dir, msg, publish=True)

        change_nr = self.gu.get_change_number(change_sha)
        note = self.gu.wait_for_verify(change_nr)
        self.assertEqual(note, expected_note)

    def get_resources(self):
        gateau = config.USERS[config.ADMIN_USER]['auth_cookie']
        resp = requests.get("%s/manage/resources/" % config.GATEWAY_URL,
                            cookies={'auth_pubtkt': gateau})
        return resp.json()

    def test_validate_wrong_resource_workflow(self):
        """ Check resources - wrong model is detected by config-check """
        # This resource is not correct
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  groups:
    %s:
      unknown-key: value
      description: test for functional test
"""
        # Add the resource file with review then check CI
        resources = resources % name
        self.propose_resources_change_check_ci(fpath,
                                               resources=resources,
                                               mode='add',
                                               expected_note=-1)

    def test_validate_correct_resource_workflow(self):
        """ Check resources - good model is detected by config-check """
        # This resource is correct
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  groups:
    %s:
      description: test for functional test
      members:
        - [email protected]
"""
        # Add the resource file with review then check CI
        resources = resources % name
        self.propose_resources_change_check_ci(fpath,
                                               resources=resources,
                                               mode='add')

    def test_validate_resources_deletion(self):
        """ Check resources - deletions detected and authorized via flag """
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  groups:
    %s:
      description: test for functional test
      members: []
"""
        # Add the resources file w/o review
        resources = resources % name
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')

        # Remove the resource file via the review
        self.propose_resources_change_check_ci(fpath,
                                               mode='del',
                                               expected_note=-1)

        # Remove the resource file with "allow-delete" flag via the review
        shutil.rmtree(os.path.join(self.gitu_admin.tempdir, 'config'))
        msg = "Remove resource with flag\nsf-resources: allow-delete"
        self.propose_resources_change_check_ci(fpath, mode='del', msg=msg)

    @skipIfServiceMissing('storyboard')
    def test_CUD_project(self):
        """ Check resources - ops on project work as expected """
        sclient = SFStoryboard(config.GATEWAY_URL + "/storyboard_api",
                               config.USERS[config.USER_4]['auth_cookie'])
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  projects:
    %(pname)s:
      description: An awesome project
      issue-tracker: SFStoryboard
      source-repositories:
        - %(pname)s/%(r1name)s
  repos:
    %(pname)s/%(r1name)s:
      description: The server part
      acl: %(pname)s
  acls:
    %(pname)s:
      file: |
        [access "refs/*"]
          read = group Anonymous Users
"""
        tmpl_keys = {
            'pname': create_random_str(),
            'r1name': create_random_str()
        }

        resources = resources % tmpl_keys
        # Add the resources file w/o review
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')

        # Some checks to validate stuff have been created
        projects = [p.name for p in sclient.projects.get_all()]
        self.assertIn("%s/%s" % (tmpl_keys['pname'], tmpl_keys['r1name']),
                      projects)
        project_groups = [p.name for p in sclient.project_groups.get_all()]
        self.assertIn(tmpl_keys['pname'], project_groups)

        # Modify the project resource
        resources = """resources:
  projects:
    %(pname)s:
      description: An awesome project
      issue-tracker: SFStoryboard
      source-repositories:
        - %(pname)s/%(r1name)s
        - %(pname)s/%(r2name)s
  repos:
    %(pname)s/%(r1name)s:
      description: The server part
      acl: %(pname)s
    %(pname)s/%(r2name)s:
      description: The server part
      acl: %(pname)s
  acls:
    %(pname)s:
      file: |
        [access "refs/*"]
          read = group Anonymous Users
"""
        tmpl_keys.update({'r2name': create_random_str()})
        resources = resources % tmpl_keys
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Some checks to validate stuff have been updated
        projects = [p.name for p in sclient.projects.get_all()]
        for name in (tmpl_keys['r1name'], tmpl_keys['r2name']):
            self.assertIn("%s/%s" % (tmpl_keys['pname'], name), projects)
        project_groups = [p.name for p in sclient.project_groups.get_all()]
        self.assertIn(tmpl_keys['pname'], project_groups)

        # Del the resources file w/o review
        self.set_resources_then_direct_push(fpath, mode='del')

        # Check the project group has been deleted
        # Note the project (in storyboard) is not deleted
        # this is a current limitation of the API (01/13/2017)
        project_groups = [p.name for p in sclient.project_groups.get_all()]
        self.assertFalse(tmpl_keys['pname'] in project_groups)

    def test_CUD_group(self):
        """ Check resources - ops on group work as expected """
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  groups:
    %s:
      description: test for functional test
      members:
        - [email protected]
        - [email protected]
"""
        # Add the resources file w/o review
        resources = resources % name
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Check members on Gerrit
        gid = self.gu.get_group_id(name)
        members = [m['email'] for m in self.gu.get_group_members(gid)]
        self.assertIn("*****@*****.**", members)
        self.assertIn("*****@*****.**", members)
        # Modify resources Add/Remove members w/o review
        resources = """resources:
  groups:
    %s:
      description: test for functional test
      members:
        - [email protected]
        - [email protected]
"""
        resources = resources % name
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Check members on Gerrit
        gid = self.gu.get_group_id(name)
        members = [m['email'] for m in self.gu.get_group_members(gid)]
        self.assertIn("*****@*****.**", members)
        self.assertIn("*****@*****.**", members)
        self.assertNotIn("*****@*****.**", members)
        # Del the resources file w/o review
        self.set_resources_then_direct_push(fpath, mode='del')
        # Check the group has been deleted
        self.assertFalse(self.gu.get_group_id(name))

    def test_CD_repo(self):
        """ Check resources - ops on git repositories work as expected """
        fpath = "resources/%s.yaml" % create_random_str()
        name = create_random_str()
        resources = """resources:
  repos:
    %s:
      description: test for functional test
      default-branch: br1
      branches:
        br1: HEAD
        br2: HEAD
        master: '0'
"""
        # Add the resources file w/o review
        resources = resources % name
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Check the project has been created
        self.assertTrue(self.gu.project_exists(name))
        # Check branches
        branches = self.gu.g.get('/projects/%s/branches/' % name)
        for wref in ("HEAD", "br1", "br2"):
            found = False
            for ref in branches:
                if found:
                    continue
                if ref['ref'].endswith(wref):
                    found = True
                    if ref['ref'] == 'HEAD' and ref['revision'] != "br1":
                        raise Exception("Wrong default branch")
            if not found:
                raise Exception("Requested branch %s not found" % wref)
        # Del the resources file w/o review
        self.set_resources_then_direct_push(fpath, mode='del')
        # Check the project has been deleted
        self.assertFalse(self.gu.project_exists(name))

    def test_CRUD_resources(self):
        """ Check resources - bulk ops on resources work as expected """
        fpath = "resources/%s.yaml" % create_random_str()
        tmpl_keys = {
            'pname': create_random_str(),
            'r1name': create_random_str(),
            'r2name': create_random_str(),
            'aname': create_random_str(),
            'g1name': create_random_str(),
            'g2name': create_random_str()
        }
        resources = """resources:
  projects:
    %(pname)s:
      description: An awesome project
      contacts:
        - [email protected]
      source-repositories:
        - %(pname)s/%(r1name)s
        - %(pname)s/%(r2name)s
      website: http://ichiban-cloud.io
      documentation: http://ichiban-cloud.io/docs
      issue-tracker-url: http://ichiban-cloud.bugtrackers.io
  repos:
    %(pname)s/%(r1name)s:
      description: The server part
      acl: %(aname)s
    %(pname)s/%(r2name)s:
      description: The client part
      acl: %(aname)s
  acls:
    %(aname)s:
      file: |
        [access "refs/*"]
          read = group Anonymous Users
          read = group %(pname)s/%(g2name)s
          owner = group %(pname)s/%(g1name)s
        [access "refs/heads/*"]
          label-Code-Review = -2..+2 group %(pname)s/%(g2name)s
          label-Verified = -2..+2 group %(pname)s/%(g1name)s
          label-Workflow = -1..+1 group %(pname)s/%(g2name)s
          submit = group %(pname)s/%(g2name)s
          read = group Anonymous Users
          read = group %(pname)s/%(g2name)s
        [access "refs/meta/config"]
          read = group %(pname)s/%(g2name)s
        [receive]
          requireChangeId = true
        [submit]
          mergeContent = false
          action = fast forward only
      groups:
        - %(pname)s/%(g1name)s
        - %(pname)s/%(g2name)s
  groups:
    %(pname)s/%(g1name)s:
      members:
        - [email protected]
    %(pname)s/%(g2name)s:
      members:
        - [email protected]
        - [email protected]
"""
        # Add the resources file w/o review
        resources = resources % tmpl_keys
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Check resources have been created
        self.assertTrue(
            self.gu.project_exists(
                os.path.join(tmpl_keys['pname'], tmpl_keys['r1name'])))
        self.assertTrue(
            self.gu.project_exists(
                os.path.join(tmpl_keys['pname'], tmpl_keys['r2name'])))
        gid = self.gu.get_group_id(
            os.path.join(tmpl_keys['pname'], tmpl_keys['g1name']))
        members = [m['email'] for m in self.gu.get_group_members(gid)]
        self.assertEqual(len(members), 1)
        self.assertIn("*****@*****.**", members)
        gid2 = self.gu.get_group_id(
            os.path.join(tmpl_keys['pname'], tmpl_keys['g2name']))
        members = [m['email'] for m in self.gu.get_group_members(gid2)]
        self.assertEqual(len(members), 2)
        self.assertIn("*****@*****.**", members)
        self.assertIn("*****@*****.**", members)
        # Verify ACLs have been written for both repo
        for r in ('r1name', 'r2name'):
            rname = os.path.join(tmpl_keys['pname'], tmpl_keys[r])
            acl = self.gu.g.get('access/?project=%s' % rname)
            self.assertIn(
                gid2, acl[rname]['local']['refs/heads/*']['permissions']
                ['submit']['rules'].keys())
        # Verify the resources endpoint know about what we pushed
        res = self.get_resources()
        self.assertIn(tmpl_keys['pname'], res['resources']['projects'].keys())
        self.assertIn(tmpl_keys['aname'], res['resources']['acls'].keys())
        self.assertIn(os.path.join(tmpl_keys['pname'], tmpl_keys['g1name']),
                      res['resources']['groups'].keys())
        self.assertIn(os.path.join(tmpl_keys['pname'], tmpl_keys['g2name']),
                      res['resources']['groups'].keys())
        self.assertIn(os.path.join(tmpl_keys['pname'], tmpl_keys['r1name']),
                      res['resources']['repos'].keys())
        self.assertIn(os.path.join(tmpl_keys['pname'], tmpl_keys['r2name']),
                      res['resources']['repos'].keys())
        # Modify the ACL to verify repos ACL are updated
        resources = re.sub(
            'submit = group .*', 'submit = group %s' %
            os.path.join(tmpl_keys['pname'], tmpl_keys['g1name']), resources)
        self.set_resources_then_direct_push(fpath,
                                            resources=resources,
                                            mode='add')
        # Verify ACLs have been updated for both repo
        for r in ('r1name', 'r2name'):
            rname = os.path.join(tmpl_keys['pname'], tmpl_keys[r])
            acl = self.gu.g.get('access/?project=%s' % rname)
            self.assertIn(
                gid, acl[rname]['local']['refs/heads/*']['permissions']
                ['submit']['rules'].keys())
        # Now let's remove all that awesome resources
        self.set_resources_then_direct_push(fpath, mode='del')
        # Check resources have been deleted
        self.assertFalse(
            self.gu.project_exists(
                os.path.join(tmpl_keys['pname'], tmpl_keys['r1name'])))
        self.assertFalse(
            self.gu.project_exists(
                os.path.join(tmpl_keys['pname'], tmpl_keys['r2name'])))
        self.assertFalse(
            self.gu.get_group_id(
                os.path.join(tmpl_keys['pname'], tmpl_keys['g1name'])))
        self.assertFalse(
            self.gu.get_group_id(
                os.path.join(tmpl_keys['pname'], tmpl_keys['g2name'])))
        res = self.get_resources()
        projects = res['resources'].get('projects', {})
        acls = res['resources'].get('acls', {})
        groups = res['resources'].get('groups', {})
        repos = res['resources'].get('repos', {})
        self.assertNotIn(tmpl_keys['pname'], projects.keys())
        self.assertNotIn(tmpl_keys['aname'], acls.keys())
        self.assertNotIn(os.path.join(tmpl_keys['pname'], tmpl_keys['g1name']),
                         groups.keys())
        self.assertNotIn(os.path.join(tmpl_keys['pname'], tmpl_keys['g2name']),
                         groups.keys())
        self.assertNotIn(os.path.join(tmpl_keys['pname'], tmpl_keys['r1name']),
                         repos.keys())
        self.assertNotIn(os.path.join(tmpl_keys['pname'], tmpl_keys['r2name']),
                         repos.keys())

    def test_GET_resources(self):
        """ Check resources - GET resources works as expected"""
        cookies = dict(auth_pubtkt=config.USERS[config.USER_1]['auth_cookie'])
        ret = requests.get("%s/manage/resources/" % config.GATEWAY_URL,
                           cookies=cookies)
        self.assertIn('resources', ret.json())

    def test_GET_missing_resources(self):
        """ Check resources - GET missing resources works as expected"""
        token = config.USERS[config.ADMIN_USER]['auth_cookie']
        prev = "resources: {}"
        new = """resources:
  groups:
    %(gname)s:
      description: A test group
      members: ['*****@*****.**']
"""
        group_name = create_random_str()
        data = {'prev': prev, 'new': new % {'gname': group_name}}
        # Direct PUT resources bypassing the config repo workflow
        requests.put("%s/manage/resources/" % config.GATEWAY_URL,
                     json=data,
                     cookies={'auth_pubtkt': token})
        # Verify managesf detects diff and propose a re-sync resource struct
        ret = requests.get("%s/manage/resources/?get_missing_"
                           "resources=true" % config.GATEWAY_URL,
                           cookies={'auth_pubtkt': token})
        logs, resources = ret.json()
        self.assertListEqual(logs, [])
        self.assertIn(group_name, resources['resources']['groups'])
        # Call the resources.sh script on managesf node to propose
        # a review on the config repo to re-sync with the reality
        cmd = [
            '/usr/local/bin/resources.sh', 'get_missing_resources', 'submit'
        ]
        self.ssh_run_cmd(config.SERVICE_PRIV_KEY_PATH, 'root',
                         config.GATEWAY_HOST, cmd)
        # Get change id of the submitted review
        search_string = "Propose missing resources to the config repo"
        r = requests.get('%s/r/changes/?q=%s' %
                         (config.GATEWAY_URL, search_string))
        lastid = 0
        for r in json.loads(r.content[4:]):
            if r['_number'] > lastid:
                lastid = r['_number']
        self.assertEqual(self.gu.wait_for_verify(lastid), 1)
        # Check flag "sf-resources: skip-apply" in the commit msg
        change = self.gu.g.get(
            'changes/?q=%s&o=CURRENT_REVISION&o=CURRENT_COMMIT' % lastid)[0]
        revision = change["current_revision"]
        commit = change['revisions'][revision]["commit"]
        self.assertEqual(commit["message"].split('\n')[0],
                         'Propose missing resources to the config repo')
        self.assertTrue(commit["message"].find('sf-resources: skip-apply') > 0)
        # Approve the change and wait for the +2
        self.gu.submit_change_note(change['id'], "current", "Code-Review", "2")
        self.gu.submit_change_note(change['id'], "current", "Workflow", "1")
        # Check config-update return a success
        # The flag sf-resources: skip-apply should be detected
        # by the config update. Then missing resources won't
        # by concidered new and the resources apply will be skipped.
        # This tests (checking config-update succeed) confirm
        # resource apply have been skipped if not managesf resources
        # apply would have return 409 error making config-update failed too.
        # If not True then we cannot concider config-update succeed
        config_update_log = self.ju.wait_for_config_update(revision)
        self.assertIn("Skip resources apply.", config_update_log)
        self.assertIn("SUCCESS", config_update_log)
        # Checking again missing resources  must return nothing
        ret = requests.get("%s/manage/resources/?get_missing_"
                           "resources=true" % config.GATEWAY_URL,
                           cookies={'auth_pubtkt': token})
        logs, resources = ret.json()
        self.assertListEqual(logs, [])
        self.assertEqual(len(resources['resources']), 0)
Beispiel #2
0
class TestProjectTestsWorkflow(Base):
    """ Functional tests to verify the configuration of a project test
    """
    @classmethod
    def setUpClass(cls):
        cls.ru = ResourcesUtils()
        cls.sample_project_dir = \
            os.path.join(config.SF_TESTS_DIR, "sample_project/")

    @classmethod
    def tearDownClass(cls):
        pass

    def setUp(self):
        super(TestProjectTestsWorkflow, self).setUp()
        self.projects = []
        self.dirs_to_delete = []
        self.un = config.ADMIN_USER
        self.gu = GerritUtils(
            config.GATEWAY_URL,
            auth_cookie=config.USERS[self.un]['auth_cookie'])
        self.gu2 = GerritUtils(
            config.GATEWAY_URL,
            auth_cookie=config.USERS[config.USER_2]['auth_cookie'])
        self.ju = JenkinsUtils()
        self.gu.add_pubkey(config.USERS[self.un]["pubkey"])
        priv_key_path = set_private_key(config.USERS[self.un]["privkey"])
        self.gitu_admin = GerritGitUtils(self.un,
                                         priv_key_path,
                                         config.USERS[self.un]['email'])
        # Clone the config repo and keep job/zuul config content
        self.config_clone_dir = self.clone_as_admin("config")
        self.original_zuul_projects = file(os.path.join(
            self.config_clone_dir, "zuul/projects.yaml")).read()
        self.original_project = file(os.path.join(
            self.config_clone_dir, "jobs/projects.yaml")).read()
        self.need_restore_config_repo = False
        # Put USER_2 as core for config project
        self.gu.add_group_member(config.USER_2, "config-core")

    def tearDown(self):
        super(TestProjectTestsWorkflow, self).tearDown()
        if self.need_restore_config_repo:
            self.restore_config_repo(self.original_project,
                                     self.original_zuul_projects)
        for name in self.projects:
            self.ru.direct_delete_repo(name)
        for dirs in self.dirs_to_delete:
            shutil.rmtree(dirs)

    def assert_reviewer_approvals(self, change_id, value):
        approvals = {}
        for _ in range(300):
            approvals = self.gu.get_reviewer_approvals(change_id,
                                                       'jenkins')
            if approvals and approvals.get('Verified') == value:
                break
            time.sleep(1)
        self.assertEqual(value, approvals.get('Verified'))

    def clone_as_admin(self, pname):
        url = "ssh://%s@%s:29418/%s" % (self.un, config.GATEWAY_HOST,
                                        pname)
        clone_dir = self.gitu_admin.clone(url, pname)
        if os.path.dirname(clone_dir) not in self.dirs_to_delete:
            self.dirs_to_delete.append(os.path.dirname(clone_dir))
        return clone_dir

    def restore_config_repo(self, project, zuul):
        logger.info("Restore {zuul,jobs}/projects.yaml")
        file(os.path.join(
            self.config_clone_dir, "zuul/projects.yaml"), 'w').write(
            zuul)
        file(os.path.join(
            self.config_clone_dir, "jobs/projects.yaml"), 'w').write(
            project)
        change_sha = self.commit_direct_push_as_admin(
            self.config_clone_dir,
            "Restore {zuul,jobs}/projects.yaml")
        logger.info("Waiting for config-update on %s" % change_sha)
        self.ju.wait_for_config_update(change_sha)

    def commit_direct_push_as_admin(self, clone_dir, msg):
        # Stage, commit and direct push the additions on master
        self.gitu_admin.add_commit_for_all_new_additions(clone_dir, msg)
        return self.gitu_admin.direct_push_branch(clone_dir, 'master')

    def push_review_as_admin(self, clone_dir, msg):
        # Stage, commit and direct push the additions on master
        self.gitu_admin.add_commit_for_all_new_additions(clone_dir, msg)
        return self.gitu_admin.review_push_branch(clone_dir, 'master')

    def create_project(self, name):
        self.ru.direct_create_repo(name)
        self.projects.append(name)

    def test_timestamped_logs(self):
        """Test that jenkins timestamps logs"""
        # Done here to make sure a config-update job was run and to avoid
        # duplicating code
        timestamp_re = re.compile('\d{2}:\d{2}:\d{2}.\d{0,3}')
        n = self.ju.get_last_build_number("config-update",
                                          "lastBuild")
        cu_logs = self.ju.get_job_logs("config-update",
                                       n)
        self.assertTrue(cu_logs is not None)
        for l in cu_logs.split('\n'):
            if l:
                self.assertRegexpMatches(l, timestamp_re, msg=l)

    def test_check_project_test_workflow(self):
        """ Validate new project to test via zuul
        """
        # We want to create a project, provide project source
        # code with tests. We then configure zuul/jjb to handle the
        # run of the test cases. We then validate Gerrit has been
        # updated about the test results
        # We use the sample-project (that already exists)

        pname = 'test_workflow_%s' % create_random_str()
        logger.info("Creating project %s" % pname)

        # Create it
        self.create_project(pname)

        logger.info("Populating the project with %s" %
                    self.sample_project_dir)
        # Add the sample-project to the empty repository
        clone_dir = self.clone_as_admin(pname)
        copytree(self.sample_project_dir, clone_dir)
        self.commit_direct_push_as_admin(clone_dir, "Add the sample project")

        # Change to config/{zuul,jobs}/projects.yaml
        # in order to test the new project
        logger.info("Adding config-repo configuration")
        ycontent = file(os.path.join(
            self.config_clone_dir, "zuul/projects.yaml")).read()
        file(os.path.join(
            self.config_clone_dir, "zuul/projects.yaml"), 'w').write(
            ycontent.replace("zuul-demo", pname),
        )
        ycontent2 = load(file(os.path.join(
            self.config_clone_dir, "jobs/projects.yaml")).read())
        sp2 = copy.deepcopy(
            [p for p in ycontent2 if 'project' in p and
                p['project']['name'] == 'zuul-demo'][0])
        sp2['project']['name'] = pname
        ycontent2.append(sp2)
        file(os.path.join(
            self.config_clone_dir, "jobs/projects.yaml"), 'w').write(
            dump(ycontent2))

        # Send review (config-check) will be triggered
        logger.info("Submitting the config review")
        change_sha = self.push_review_as_admin(
            self.config_clone_dir,
            "Add config definition in Zuul/JJB config for %s" % pname)

        change_nr = self.gu.get_change_number(change_sha)

        logger.info("Waiting for verify +1 on change %d" % change_nr)
        self.assertEquals(self.gu.wait_for_verify(change_nr), 1)

        # review the config change as a member from the config-core group
        logger.info("Approving and waiting for verify +2")
        self.gu2.submit_change_note(change_nr, "current", "Code-Review", "2")
        self.gu2.submit_change_note(change_nr, "current", "Workflow", "1")

        for retry in xrange(60):
            jenkins_vote = self.gu.get_vote(change_nr, "Verified")
            if jenkins_vote == 2:
                break
            time.sleep(1)
        self.assertEquals(jenkins_vote, 2)

        # verify whether zuul merged the patch
        logger.info("Waiting for change to be merged")
        for retry in xrange(60):
            change_status = self.gu.get_info(change_nr)['status']
            if change_status == "MERGED":
                break
            time.sleep(1)
        self.assertEqual(change_status, 'MERGED')
        self.need_restore_config_repo = True

        logger.info("Waiting for config-update")
        config_update_log = self.ju.wait_for_config_update(change_sha)
        self.assertIn("Finished: SUCCESS", config_update_log)

        # Propose a change on a the repo and expect a Verified +1
        logger.info("Submiting a test change to %s" % pname)
        change_sha = self.gitu_admin.add_commit_and_publish(
            clone_dir, 'master', "Add useless file",
            self.un)

        change_nr = self.gu.get_change_number(change_sha)

        logger.info("Waiting for verify +1 on change %d" % change_nr)
        self.assertEquals(self.gu.wait_for_verify(change_nr), 1)

        # Update the change on a the repo and expect a Verified -1
        logger.info("Submiting a test change to %s suppose to fail" % pname)
        data = "#!/bin/bash\nexit 1\n"
        file(os.path.join(clone_dir, "run_tests.sh"), 'w').write(data)
        os.chmod(os.path.join(clone_dir, "run_tests.sh"), 0755)
        self.gitu_admin.add_commit_and_publish(
            clone_dir, "master", None, fnames=["run_tests.sh"])

        logger.info("Waiting for verify -1 on change %d" % change_nr)
        self.assertEquals(self.gu.wait_for_verify(change_nr), -1)

        logger.info("Validate jobs ran via the job api %s" % pname)
        # This piece of code is there by convenience ...
        # TODO: Should be moved in the job api tests file.
        # Test the manageSF jobs API: query per patch & revision
        change_ids = self.gu.get_my_changes_for_project(pname)
        self.assertGreater(len(change_ids), 0)
        change_id = change_ids[0]
        patch = self.gu.get_change_last_patchset(change_id)['_number']
        cookie = get_cookie(config.ADMIN_USER, config.ADMIN_PASSWORD)
        cookies = {"auth_pubtkt": cookie}
        base_url = config.GATEWAY_URL + "/manage/jobs/"
        for j in ["%s-functional-tests" % pname, "%s-unit-tests" % pname]:
            job = requests.get(base_url + '%s/?change=%s' % (j, patch),
                               cookies=cookies).json()
            self.assertTrue("jenkins" in job.keys(),
                            job)
            self.assertTrue(len(job["jenkins"]) > 1,
                            job)