Add playbook_context
zuul variable
This adds a variable which may be useful for debugging or auditing the repo state of playbooks or roles for a job. Change-Id: I86429a06ed8625faa72db6a19630de633f1694b6
This commit is contained in:
parent
36086ae5a5
commit
476800d382
@ -405,6 +405,20 @@ of item.
|
||||
This may be influenced by the branch or tag associated with
|
||||
the item as well as the job configuration.
|
||||
|
||||
.. var:: checkout_description
|
||||
|
||||
A human-readable description of why Zuul chose this
|
||||
particular branch or tag to be checked out. This is intended
|
||||
as a debugging aid in the case of complex jobs. The specific
|
||||
text is not defined and is subject to change.
|
||||
|
||||
.. var:: commit
|
||||
|
||||
The hex SHA of the commit checked out. This commit may
|
||||
appear in the upstream repository, or if it the result of a
|
||||
speculative merge, it may only exist during the run of this
|
||||
job.
|
||||
|
||||
For example, to access the source directory of a single known
|
||||
project, you might use::
|
||||
|
||||
@ -418,6 +432,95 @@ of item.
|
||||
msg: "Project {{ item.name }} is at {{ item.src_dir }}
|
||||
with_items: {{ zuul.projects.values() | list }}
|
||||
|
||||
.. var:: playbook_context
|
||||
:type: dict
|
||||
|
||||
This dictionary contains information about the execution of each
|
||||
playbook in the job. This may be useful for understanding
|
||||
exactly what playbooks and roles Zuul executed.
|
||||
|
||||
All paths herein are located under the root of the build
|
||||
directory (note that is one level higher than the workspace
|
||||
directory accessible to jobs on the executor).
|
||||
|
||||
.. var:: playbook_projects
|
||||
:type: dict
|
||||
|
||||
A dictionary of projects that have been checked out for
|
||||
playbook execution. When used in the trusted execution
|
||||
context, these will contain only merged commits in upstream
|
||||
repositories. In the case of the untrusted context, they may
|
||||
contain speculatively merged code.
|
||||
|
||||
The key is the path and each value is another dictionary with
|
||||
the following keys:
|
||||
|
||||
.. var:: canonical_name
|
||||
|
||||
The canonical name of the repository.
|
||||
|
||||
.. var:: checkout
|
||||
|
||||
The branch or tag checked out.
|
||||
|
||||
.. var:: commit
|
||||
|
||||
The hex SHA of the commit checked out. As above, this
|
||||
commit may or may not exist in the upstream repository
|
||||
depending on whether it was the result of a speculative
|
||||
merge.
|
||||
|
||||
.. var:: playbooks
|
||||
:type: list
|
||||
|
||||
An ordered list of playbooks executed for the job. Each item
|
||||
is a dictionary with the following keys:
|
||||
|
||||
.. var:: path
|
||||
|
||||
The path to the playbook.
|
||||
|
||||
.. var:: roles
|
||||
:type: list
|
||||
|
||||
Information about the roles available to the playbook.
|
||||
The actual `role path` supplied to Ansible is the
|
||||
concatenation of the ``role_path`` entry in each of the
|
||||
following dictionaries. The rest of the information
|
||||
describes what is in the role path.
|
||||
|
||||
In order to deal with the many possible role layouts and
|
||||
aliases, each element in the role path gets its own
|
||||
directory. Depending on the contents and alias
|
||||
configuration for that role repo, a symlink is added to
|
||||
one of the repo checkouts in
|
||||
:var:`zuul.playbook_context.playbook_projects` so that the
|
||||
role may be supplied to Ansible with the correct name.
|
||||
|
||||
.. var:: checkout
|
||||
|
||||
The branch or tag checked out.
|
||||
|
||||
.. var:: checkout_description
|
||||
|
||||
A human-readable description of why Zuul chose this
|
||||
particular branch or tag to be checked out. This is
|
||||
intended as a debugging aid in the case of complex
|
||||
jobs. The specific text is not defined and is subject
|
||||
to change.
|
||||
|
||||
.. var:: link_name
|
||||
|
||||
The name of the symbolic link.
|
||||
|
||||
.. var:: link_target
|
||||
|
||||
The target of the symbolic_link.
|
||||
|
||||
.. var:: role_path
|
||||
|
||||
The role path passed to Ansible.
|
||||
|
||||
.. var:: tenant
|
||||
|
||||
The name of the current Zuul tenant.
|
||||
|
11
releasenotes/notes/audit-vars-4dcb0d3c7bf87ef2.yaml
Normal file
11
releasenotes/notes/audit-vars-4dcb0d3c7bf87ef2.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The new variable :var:`zuul.playbook_context` as well as new
|
||||
variables under :var:`zuul.projects` have been added to help debug
|
||||
or audit the playbooks and roles used by Ansible when running
|
||||
jobs.
|
||||
|
||||
These variables describe the repo configuration used for the
|
||||
playbooks and roles of each Ansible execution. These repos may
|
||||
have a different state than the workspace repos.
|
@ -29,6 +29,7 @@ import git
|
||||
import paramiko
|
||||
|
||||
import zuul.configloader
|
||||
from zuul.lib import yamlutil as yaml
|
||||
from zuul.model import MergeRequest
|
||||
|
||||
from tests.base import (
|
||||
@ -4425,10 +4426,76 @@ class TestRoleBranches(RoleTestCase):
|
||||
self._assertInFile(build.jobdir.pre_playbooks[1].path,
|
||||
'parent-stable-role')
|
||||
|
||||
inventory = self.getBuildInventory('child-job-override')
|
||||
zuul = inventory['all']['vars']['zuul']
|
||||
|
||||
expected = {
|
||||
'playbook_projects': {
|
||||
'trusted/project_0/review.example.com/common-config': {
|
||||
'canonical_name': 'review.example.com/common-config',
|
||||
'checkout': 'master',
|
||||
'commit': self.getCheckout(
|
||||
build,
|
||||
'trusted/project_0/review.example.com/common-config')},
|
||||
'untrusted/project_0/review.example.com/project1': {
|
||||
'canonical_name': 'review.example.com/project1',
|
||||
'checkout': 'stable',
|
||||
'commit': self.getCheckout(
|
||||
build,
|
||||
'untrusted/project_0/review.example.com/project1')},
|
||||
'untrusted/project_1/review.example.com/common-config': {
|
||||
'canonical_name': 'review.example.com/common-config',
|
||||
'checkout': 'master',
|
||||
'commit': self.getCheckout(
|
||||
build,
|
||||
'untrusted/project_1/review.example.com/common-config'
|
||||
)},
|
||||
'untrusted/project_2/review.example.com/project2': {
|
||||
'canonical_name': 'review.example.com/project2',
|
||||
'checkout': 'master',
|
||||
'commit': self.getCheckout(
|
||||
build,
|
||||
'untrusted/project_2/review.example.com/project2')}},
|
||||
'playbooks': [
|
||||
{'path': 'untrusted/project_2/review.example.com/'
|
||||
'project2/playbooks/child-job.yaml',
|
||||
'roles': [
|
||||
{'checkout': 'stable',
|
||||
'checkout_description': 'job override ref',
|
||||
'link_name': 'ansible/playbook_0/role_1/project1',
|
||||
'link_target': 'untrusted/project_0/'
|
||||
'review.example.com/project1',
|
||||
'role_path': 'ansible/playbook_0/role_1/project1/roles'
|
||||
},
|
||||
{'checkout': 'master',
|
||||
'checkout_description': 'zuul branch',
|
||||
'link_name': 'ansible/playbook_0/role_2/common-config',
|
||||
'link_target': 'untrusted/project_1/'
|
||||
'review.example.com/common-config',
|
||||
'role_path': 'ansible/playbook_0/role_2/'
|
||||
'common-config/roles'
|
||||
}
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
self.assertEqual(expected, zuul['playbook_context'])
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
def getBuildInventory(self, name):
|
||||
build = self.getBuildByName(name)
|
||||
inv_path = os.path.join(build.jobdir.root, 'ansible', 'inventory.yaml')
|
||||
inventory = yaml.safe_load(open(inv_path, 'r'))
|
||||
return inventory
|
||||
|
||||
def getCheckout(self, build, path):
|
||||
root = os.path.join(build.jobdir.root, path)
|
||||
repo = git.Repo(root)
|
||||
return repo.head.commit.hexsha
|
||||
|
||||
|
||||
class TestRoles(RoleTestCase):
|
||||
tenant_config_file = 'config/roles/main.yaml'
|
||||
|
@ -417,6 +417,30 @@ class KubeFwd(object):
|
||||
pass
|
||||
|
||||
|
||||
class JobDirPlaybookRole(object):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.link_src = None
|
||||
self.link_target = None
|
||||
self.role_path = None
|
||||
self.checkout_description = None
|
||||
self.checkout = None
|
||||
|
||||
def toDict(self, jobdir_root=None):
|
||||
# This is serialized to the zuul.playbook_context variable
|
||||
if jobdir_root:
|
||||
strip = len(jobdir_root) + 1
|
||||
else:
|
||||
strip = 0
|
||||
return dict(
|
||||
link_name=self.link_name[strip:],
|
||||
link_target=self.link_target[strip:],
|
||||
role_path=self.role_path[strip:],
|
||||
checkout_description=self.checkout_description,
|
||||
checkout=self.checkout,
|
||||
)
|
||||
|
||||
|
||||
class JobDirPlaybook(object):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
@ -440,8 +464,25 @@ class JobDirPlaybook(object):
|
||||
count = len(self.roles)
|
||||
root = os.path.join(self.root, 'role_%i' % (count,))
|
||||
os.makedirs(root)
|
||||
self.roles.append(root)
|
||||
return root
|
||||
role_info = JobDirPlaybookRole(root)
|
||||
self.roles.append(role_info)
|
||||
return role_info
|
||||
|
||||
|
||||
class JobDirProject(object):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.canonical_name = None
|
||||
self.checkout = None
|
||||
self.commit = None
|
||||
|
||||
def toDict(self):
|
||||
# This is serialized to the zuul.playbook_context variable
|
||||
return dict(
|
||||
canonical_name=self.canonical_name,
|
||||
checkout=self.checkout,
|
||||
commit=self.commit,
|
||||
)
|
||||
|
||||
|
||||
class JobDir(object):
|
||||
@ -589,10 +630,8 @@ class JobDir(object):
|
||||
job_output.write("{now} | Job console starting...\n".format(
|
||||
now=datetime.datetime.now()
|
||||
))
|
||||
self.trusted_projects = []
|
||||
self.trusted_project_index = {}
|
||||
self.untrusted_projects = []
|
||||
self.untrusted_project_index = {}
|
||||
self.trusted_projects = {}
|
||||
self.untrusted_projects = {}
|
||||
|
||||
# Create a JobDirPlaybook for the Ansible setup run. This
|
||||
# doesn't use an actual playbook, but it lets us use the same
|
||||
@ -618,12 +657,14 @@ class JobDir(object):
|
||||
count = len(self.trusted_projects)
|
||||
root = os.path.join(self.trusted_root, 'project_%i' % (count,))
|
||||
os.makedirs(root)
|
||||
self.trusted_projects.append(root)
|
||||
self.trusted_project_index[(canonical_name, branch)] = root
|
||||
return root
|
||||
project_info = JobDirProject(root)
|
||||
project_info.canonical_name = canonical_name
|
||||
project_info.checkout = branch
|
||||
self.trusted_projects[(canonical_name, branch)] = project_info
|
||||
return project_info
|
||||
|
||||
def getTrustedProject(self, canonical_name, branch):
|
||||
return self.trusted_project_index.get((canonical_name, branch))
|
||||
return self.trusted_projects.get((canonical_name, branch))
|
||||
|
||||
def addUntrustedProject(self, canonical_name, branch):
|
||||
# Similar to trusted projects, but these hold checkouts of
|
||||
@ -635,12 +676,14 @@ class JobDir(object):
|
||||
count = len(self.untrusted_projects)
|
||||
root = os.path.join(self.untrusted_root, 'project_%i' % (count,))
|
||||
os.makedirs(root)
|
||||
self.untrusted_projects.append(root)
|
||||
self.untrusted_project_index[(canonical_name, branch)] = root
|
||||
return root
|
||||
project_info = JobDirProject(root)
|
||||
project_info.canonical_name = canonical_name
|
||||
project_info.checkout = branch
|
||||
self.untrusted_projects[(canonical_name, branch)] = project_info
|
||||
return project_info
|
||||
|
||||
def getUntrustedProject(self, canonical_name, branch):
|
||||
return self.untrusted_project_index.get((canonical_name, branch))
|
||||
return self.untrusted_projects.get((canonical_name, branch))
|
||||
|
||||
def addPrePlaybook(self):
|
||||
count = len(self.pre_playbooks)
|
||||
@ -1331,12 +1374,14 @@ class AnsibleJob(object):
|
||||
self.log.info("Checking out %s %s %s",
|
||||
project['canonical_name'], selected_desc,
|
||||
selected_ref)
|
||||
repo.checkout(selected_ref)
|
||||
commit = repo.checkout(selected_ref)
|
||||
|
||||
# Update the inventory variables to indicate the ref we
|
||||
# checked out
|
||||
p = args['zuul']['projects'][project['canonical_name']]
|
||||
p['checkout'] = selected_ref
|
||||
p['checkout_description'] = selected_desc
|
||||
p['commit'] = commit.hexsha
|
||||
|
||||
# Set the URL of the origin remote for each repo to a bogus
|
||||
# value. Keeping the remote allows tools to use it to determine
|
||||
@ -2024,43 +2069,44 @@ class AnsibleJob(object):
|
||||
return ret
|
||||
|
||||
def checkoutTrustedProject(self, project, branch, args):
|
||||
root = self.jobdir.getTrustedProject(project.canonical_name,
|
||||
branch)
|
||||
if not root:
|
||||
root = self.jobdir.addTrustedProject(project.canonical_name,
|
||||
branch)
|
||||
pi = self.jobdir.getTrustedProject(project.canonical_name,
|
||||
branch)
|
||||
if not pi:
|
||||
pi = self.jobdir.addTrustedProject(project.canonical_name,
|
||||
branch)
|
||||
self.log.debug("Cloning %s@%s into new trusted space %s",
|
||||
project, branch, root)
|
||||
project, branch, pi.root)
|
||||
# We always use the golang scheme for playbook checkouts
|
||||
# (so that the path indicates the canonical repo name for
|
||||
# easy debugging; there are no concerns with collisions
|
||||
# since we only have one repo in the working dir).
|
||||
merger = self.executor_server._getMerger(
|
||||
root,
|
||||
pi.root,
|
||||
self.executor_server.merge_root,
|
||||
logger=self.log,
|
||||
scheme=zuul.model.SCHEME_GOLANG)
|
||||
merger.checkoutBranch(
|
||||
commit = merger.checkoutBranch(
|
||||
project.connection_name, project.name,
|
||||
branch,
|
||||
repo_state=args['repo_state'],
|
||||
process_worker=self.executor_server.process_worker,
|
||||
zuul_event_id=self.zuul_event_id)
|
||||
pi.commit = commit.hexsha
|
||||
else:
|
||||
self.log.debug("Using existing repo %s@%s in trusted space %s",
|
||||
project, branch, root)
|
||||
project, branch, pi.root)
|
||||
|
||||
path = os.path.join(root,
|
||||
path = os.path.join(pi.root,
|
||||
project.canonical_hostname,
|
||||
project.name)
|
||||
return path
|
||||
|
||||
def checkoutUntrustedProject(self, project, branch, args):
|
||||
root = self.jobdir.getUntrustedProject(project.canonical_name,
|
||||
branch)
|
||||
if not root:
|
||||
root = self.jobdir.addUntrustedProject(project.canonical_name,
|
||||
branch)
|
||||
pi = self.jobdir.getUntrustedProject(project.canonical_name,
|
||||
branch)
|
||||
if not pi:
|
||||
pi = self.jobdir.addUntrustedProject(project.canonical_name,
|
||||
branch)
|
||||
# If the project is in the dependency chain, clone from
|
||||
# there so we pick up any speculative changes, otherwise,
|
||||
# clone from the cache.
|
||||
@ -2076,7 +2122,7 @@ class AnsibleJob(object):
|
||||
# We already have this repo prepared
|
||||
self.log.debug("Found workdir repo for untrusted project")
|
||||
merger = self.executor_server._getMerger(
|
||||
root,
|
||||
pi.root,
|
||||
self.jobdir.src_root,
|
||||
logger=self.log,
|
||||
scheme=zuul.model.SCHEME_GOLANG,
|
||||
@ -2086,7 +2132,7 @@ class AnsibleJob(object):
|
||||
repo_state = None
|
||||
if merger is None:
|
||||
merger = self.executor_server._getMerger(
|
||||
root,
|
||||
pi.root,
|
||||
self.executor_server.merge_root,
|
||||
logger=self.log,
|
||||
scheme=zuul.model.SCHEME_GOLANG)
|
||||
@ -2097,17 +2143,18 @@ class AnsibleJob(object):
|
||||
repo_state = args['repo_state']
|
||||
|
||||
self.log.debug("Cloning %s@%s into new untrusted space %s",
|
||||
project, branch, root)
|
||||
merger.checkoutBranch(
|
||||
project, branch, pi.root)
|
||||
commit = merger.checkoutBranch(
|
||||
project.connection_name, project.name,
|
||||
branch, repo_state=repo_state,
|
||||
process_worker=self.executor_server.process_worker,
|
||||
zuul_event_id=self.zuul_event_id)
|
||||
pi.commit = commit.hexsha
|
||||
else:
|
||||
self.log.debug("Using existing repo %s@%s in trusted space %s",
|
||||
project, branch, root)
|
||||
project, branch, pi.root)
|
||||
|
||||
path = os.path.join(root,
|
||||
path = os.path.join(pi.root,
|
||||
project.canonical_hostname,
|
||||
project.name)
|
||||
return path
|
||||
@ -2149,8 +2196,8 @@ class AnsibleJob(object):
|
||||
|
||||
def prepareRole(self, jobdir_playbook, role, args):
|
||||
if role['type'] == 'zuul':
|
||||
root = jobdir_playbook.addRole()
|
||||
self.prepareZuulRole(jobdir_playbook, role, args, root)
|
||||
role_info = jobdir_playbook.addRole()
|
||||
self.prepareZuulRole(jobdir_playbook, role, args, role_info)
|
||||
|
||||
def findRole(self, path, trusted=False):
|
||||
d = os.path.join(path, 'tasks')
|
||||
@ -2173,7 +2220,7 @@ class AnsibleJob(object):
|
||||
# It is neither a bare role, nor a collection of roles
|
||||
raise RoleNotFoundError("Unable to find role in %s" % (path,))
|
||||
|
||||
def prepareZuulRole(self, jobdir_playbook, role, args, root):
|
||||
def prepareZuulRole(self, jobdir_playbook, role, args, role_info):
|
||||
self.log.debug("Prepare zuul role for %s" % (role,))
|
||||
# Check out the role repo if needed
|
||||
source = self.executor_server.connections.getSource(
|
||||
@ -2190,6 +2237,8 @@ class AnsibleJob(object):
|
||||
branch = jobdir_playbook.branch
|
||||
self.log.debug("Role project is playbook project, "
|
||||
"using playbook branch %s", branch)
|
||||
role_info.checkout_description = 'playbook branch'
|
||||
role_info.checkout = branch
|
||||
else:
|
||||
# Find if the project is one of the job-specified projects.
|
||||
# If it is, we can honor the project checkout-override options.
|
||||
@ -2209,6 +2258,8 @@ class AnsibleJob(object):
|
||||
args_project.get('override_checkout'),
|
||||
role['project_default_branch'])
|
||||
self.log.debug("Role using %s %s", selected_desc, branch)
|
||||
role_info.checkout_description = selected_desc
|
||||
role_info.checkout = branch
|
||||
|
||||
if not jobdir_playbook.trusted:
|
||||
path = self.checkoutUntrustedProject(project, branch, args)
|
||||
@ -2218,12 +2269,14 @@ class AnsibleJob(object):
|
||||
# The name of the symlink is the requested name of the role
|
||||
# (which may be the repo name or may be something else; this
|
||||
# can come into play if this is a bare role).
|
||||
link = os.path.join(root, name)
|
||||
link = os.path.join(role_info.root, name)
|
||||
link = os.path.realpath(link)
|
||||
if not link.startswith(os.path.realpath(root)):
|
||||
if not link.startswith(os.path.realpath(role_info.root)):
|
||||
raise ExecutorError("Invalid role name %s" % name)
|
||||
os.symlink(path, link)
|
||||
|
||||
role_info.link_name = link
|
||||
role_info.link_target = path
|
||||
try:
|
||||
role_path = self.findRole(link, trusted=jobdir_playbook.trusted)
|
||||
except RoleNotFoundError:
|
||||
@ -2239,7 +2292,8 @@ class AnsibleJob(object):
|
||||
raise
|
||||
if role_path is None:
|
||||
# In the case of a bare role, add the containing directory
|
||||
role_path = root
|
||||
role_path = role_info.root
|
||||
role_info.role_path = role_path
|
||||
self.log.debug("Adding role path %s", role_path)
|
||||
jobdir_playbook.roles_path.append(role_path)
|
||||
|
||||
@ -2387,6 +2441,27 @@ class AnsibleJob(object):
|
||||
result_data_file=self.jobdir.result_data_file,
|
||||
inventory_file=self.jobdir.inventory)
|
||||
|
||||
# Add playbook_context info
|
||||
zuul_vars['playbook_context'] = dict(
|
||||
playbook_projects={},
|
||||
playbooks=[],
|
||||
)
|
||||
strip = len(self.jobdir.root) + 1
|
||||
for pi in self.jobdir.trusted_projects.values():
|
||||
root = os.path.join(pi.root[strip:], pi.canonical_name)
|
||||
zuul_vars['playbook_context']['playbook_projects'][
|
||||
root] = pi.toDict()
|
||||
for pi in self.jobdir.untrusted_projects.values():
|
||||
root = os.path.join(pi.root[strip:], pi.canonical_name)
|
||||
zuul_vars['playbook_context']['playbook_projects'][
|
||||
root] = pi.toDict()
|
||||
for pb in self.jobdir.playbooks:
|
||||
zuul_vars['playbook_context']['playbooks'].append(dict(
|
||||
path=pb.path[strip:],
|
||||
roles=[ri.toDict(self.jobdir.root) for ri in pb.roles
|
||||
if ri.role_path is not None],
|
||||
))
|
||||
|
||||
with open(self.jobdir.zuul_vars, 'w') as zuul_vars_yaml:
|
||||
zuul_vars_yaml.write(
|
||||
yaml.safe_dump({'zuul': zuul_vars}, default_flow_style=False))
|
||||
|
@ -930,7 +930,7 @@ class Merger(object):
|
||||
self._restoreRepoState(connection_name, project_name, repo,
|
||||
repo_state, zuul_event_id,
|
||||
process_worker=process_worker)
|
||||
repo.checkout(branch, zuul_event_id=zuul_event_id)
|
||||
return repo.checkout(branch, zuul_event_id=zuul_event_id)
|
||||
|
||||
def _saveRepoState(self, connection_name, project_name, repo,
|
||||
repo_state, recent, branches):
|
||||
|
Loading…
x
Reference in New Issue
Block a user