Add gate-quickstart role

This role lets arbitrary tripleo-quickstart and
tripleo-quickstart-extras changes be gated together through the
"Depends-On" mechanism.

Change-Id: If5b747b1bba17dd20efa9e7caee6fbc638dbb716
This commit is contained in:
Attila Darazs 2016-11-15 14:38:38 +01:00
parent 661358a588
commit ffb30adaea
5 changed files with 261 additions and 0 deletions

View File

@ -0,0 +1,8 @@
---
# This playbooks is responsible for gating changes in tripleo-quickstart or
# tripleo-quickstart-extras with a possibility to depend on changes from the
# other repo
- name: Gate quickstart changes
hosts: localhost
roles:
- gate-quickstart

View File

@ -0,0 +1,3 @@
gated_projects:
- openstack/tripleo-quickstart
- openstack/tripleo-quickstart-extras

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python
DOCUMENTATION = '''
---
module: jenkins_deps
version_added: "2.0"
short_description: Parses the Gerrit commit message and identifies cross repo depenency changes
description:
- Parses the Gerrit commit message and identifies cross repo depenency changes.
The expected format in the commit message is:
Depends-On: <change-id>[@<gerrit-instance-shorthand>]
Where <change-id> is the gerrit Change-Id of the dependent change,
<gerrit-instance> should be a part of a hostname in ALLOWED_HOSTS.
options:
host:
description:
- The hostname of the Gerrit server.
required: True
change_id:
description:
- The change-id of the Gerrit change, starting with I...
required: True
branch:
description:
- The branch of the change.
required: True
patchset_rev:
description:
- The sha hash of the patchset to be tested. Latest will be used if omitted.
required: False
'''
EXAMPLES = '''
- jenkins-deps:
host: review.openstack.org
change_id: I387b6bfd763d2d86cad68a3119b0edd0caa237b0
patchset_rev: d18f21853e2f3be7382a20d0f42232ff3a78b348
'''
import json
import logging
import re
import sys
import requests
# we ignore any other host reference
ALLOWED_HOSTS = ['review.openstack.org',
'review.gerrithub.io',
'review.rdoproject.org']
def parse_commit_msg(current_host, msg):
'''Look for dependency links in the commit message.'''
tags = []
for line in iter(msg.splitlines()):
# note: this regexp takes care of sanitizing the input
tag = re.search(r'Depends-On: *(I[0-9a-f]+)@?([0-9a-z\.\-:]*)',
line, re.IGNORECASE)
if tag:
change_id = tag.group(1)
target = tag.group(2)
if target == '':
host = current_host
else:
# match a shorthand hostname for our allowed list
for hostname in ALLOWED_HOSTS:
if target in hostname:
host = hostname
break
else:
logging.warning('Cannot resolve "%s" to a host from the '
'ALLOWED HOSTS list', target)
continue
tags.append({'host': host,
'change_id': change_id,
'branch': None,
'revision': None})
return tags
def get_details(host, change_id, branch, revision):
'''Get the details of a specific change'''
url = ''.join(['https://', host, '/changes/?q=change:', change_id])
try:
req = requests.get(url)
req.raise_for_status()
except requests.exceptions.HTTPError:
return {'fail_msg': ''.join(['warning: failed to query change '
'details from ', url])}
# strip XSSI attack prevention prefix
data = json.loads(req.text[4:])
if len(data) == 0:
return {'fail_msg': ''.join(['warning: no change found with id ',
change_id, ' at ', url])}
elif len(data) == 1:
# not filtering by branch if not necessary
full_id = data[0]['id']
else:
# there are more than one change with the same ID
try:
full_id = [change['id'] for change
in data if change['branch'] == branch][0]
except IndexError:
return {'fail_msg': ''.join(['warning: no change found with id ',
change_id, ' for branch ', branch,
' at ', url])}
url = ''.join(['https://', host, '/changes/', full_id,
'?o=ALL_REVISIONS&o=ALL_COMMITS'])
try:
req = requests.get(url)
req.raise_for_status()
except requests.exceptions.HTTPError:
return {'fail_msg': ''.join(['warning: failed to fetch details of ',
change_id, ' from ', url])}
# strip XSSI attack prevention prefix
data = json.loads(req.text[4:])
if revision is None:
revision = data['current_revision']
if revision not in data['revisions']:
return {'fail_msg': ''.join(['warning: cannot find revision ',
revision, ' of change ', change_id,
' at ', url])}
return {'host': host,
'change_id': str(data['change_id']),
'project': str(data['project']),
'branch': str(data['branch']),
'refspec': str(data['revisions'][revision]['ref']),
'commit_msg':
str(data['revisions'][revision]['commit']['message'])}
def resolve_dep(host, change_id, branch, revision):
'''
Resolve the dependencies in the target commits until there are no more
dependent changes. If the branch or revision is None, it can still resolve
the dependencies. It only uses the branch when the change_id is ambigiuous
and by default uses the latest patchset's revision.
The function avoids circular dependencies and only allows one change per
project to be added to the output list.
Returns a list of dictionaries with the dependent changes.
'''
resolved_ids = []
deps = []
to_resolve = [{'host': host,
'change_id': change_id,
'branch': branch,
'revision': revision}]
output_msg = []
while len(to_resolve) > 0:
change = to_resolve.pop()
# use the original branch as default
if change['branch'] is None:
change['branch'] = branch
# avoid circular dependencies
if change['change_id'] in resolved_ids:
continue
details = get_details(**change)
if 'fail_msg' in details:
output_msg.append(details['fail_msg'])
continue
resolved_ids.append(details['change_id'])
# allow only one of each project as a dependency
if details['project'] not in (d['project'] for d in deps):
deps.append({'host': change['host'],
'project': details['project'],
'branch': details['branch'],
'refspec': details['refspec']})
else:
output_msg.append(
''.join(['warning: skipping ', change['change_id'], ' on ',
change['host'], ' because project "',
details['project'], '" is already a dependency']))
continue
new_deps = parse_commit_msg(change['host'], details['commit_msg'])
to_resolve.extend(new_deps)
if len(deps) == 0:
output_msg.append('error: failed to resolve the target change')
return {'failed': True,
'msg': ', '.join(output_msg)}
else:
return {'changed': True,
'ansible_facts': {'artg_change_list': deps},
'msg': ', '.join(output_msg)}
def main():
module = AnsibleModule(
argument_spec=dict(
host=dict(required=True, type='str'),
change_id=dict(required=True, type='str'),
branch=dict(required=False, default=None, type='str'),
patchset_rev=dict(required=False, default=None, type='str')
)
)
result = resolve_dep(module.params['host'],
module.params['change_id'],
module.params['branch'],
module.params['patchset_rev'])
module.exit_json(**result)
# see http://docs.ansible.com/developing_modules.html#common-module-boilerplate
from ansible.module_utils.basic import *
if __name__ == "__main__":
main()

View File

@ -0,0 +1,23 @@
---
- name: Get the project name
set_fact:
gated_project_name: '{{ gated_change.project | regex_replace("^.*/", "") }}'
- name: Check out the gated change
git:
repo: 'https://{{ gated_change.host }}/{{ gated_change.project }}'
dest: '{{ lookup("env", "WORKSPACE") }}/{{ gated_project_name }}'
refspec: '{{ gated_change.refspec }}'
version: 'FETCH_HEAD'
- when: gated_project_name != "tripleo-quickstart"
block:
- name: Modify requirements
replace:
dest: '{{ lookup("env", "WORKSPACE") }}/tripleo-quickstart/quickstart-extras-requirements.txt'
regexp: '^.*egg={{ gated_project_name }}$'
replace: 'file://{{ lookup("env", "WORKSPACE") }}/{{ gated_project_name }}/#egg={{ gated_project_name }}'
- name: Make sure the package is not installed in our venv
pip:
name: '{{ gated_project_name }}'
state: absent

View File

@ -0,0 +1,15 @@
---
# This playbooks is responsible for gating quickstart and the extra roles
# together with optional dependencies between them.
- name: Parse Jenkins changes
jenkins_deps:
host: "{{ lookup('env', 'GERRIT_HOST') }}"
change_id: "{{ lookup('env', 'GERRIT_CHANGE_ID') }}"
branch: "{{ lookup('env', 'GERRIT_BRANCH') }}"
patchset_rev: "{{ lookup('env', 'GERRIT_PATCHSET_REVISION') }}"
when: artg_change_list is not defined and "{{ lookup('env', 'GERRIT_HOST') }}" != ""
- name: Check out the specific changes
include: checkout.yml gated_change={{ item }}
with_items: '{{ artg_change_list }}'
when: item.project in gated_projects and item.project != "{{ lookup('env', 'GERRIT_PROJECT') }}"