mirror of
https://github.com/ansible/awx.git
synced 2026-04-05 01:59:25 -02:30
Merge pull request #7643 from AlanCoding/per_update_cache
Implement per-update cache for roles and collections Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -194,6 +194,11 @@ class ProjectOptions(models.Model):
|
|||||||
if not check_if_exists or os.path.exists(smart_str(proj_path)):
|
if not check_if_exists or os.path.exists(smart_str(proj_path)):
|
||||||
return proj_path
|
return proj_path
|
||||||
|
|
||||||
|
def get_cache_path(self):
|
||||||
|
local_path = os.path.basename(self.local_path)
|
||||||
|
if local_path:
|
||||||
|
return os.path.join(settings.PROJECTS_ROOT, '.__awx_cache', local_path)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def playbooks(self):
|
def playbooks(self):
|
||||||
results = []
|
results = []
|
||||||
@@ -418,6 +423,10 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_id(self):
|
||||||
|
return str(self.last_job_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def notification_templates(self):
|
def notification_templates(self):
|
||||||
base_notification_templates = NotificationTemplate.objects
|
base_notification_templates = NotificationTemplate.objects
|
||||||
@@ -455,11 +464,12 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
path_to_delete = self.get_project_path(check_if_exists=False)
|
paths_to_delete = (self.get_project_path(check_if_exists=False), self.get_cache_path())
|
||||||
r = super(Project, self).delete(*args, **kwargs)
|
r = super(Project, self).delete(*args, **kwargs)
|
||||||
if self.scm_type and path_to_delete: # non-manual, concrete path
|
for path_to_delete in paths_to_delete:
|
||||||
from awx.main.tasks import delete_project_files
|
if self.scm_type and path_to_delete: # non-manual, concrete path
|
||||||
delete_project_files.delay(path_to_delete)
|
from awx.main.tasks import delete_project_files
|
||||||
|
delete_project_files.delay(path_to_delete)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@@ -554,6 +564,19 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
|
|||||||
def result_stdout_raw(self):
|
def result_stdout_raw(self):
|
||||||
return self._result_stdout_raw(redact_sensitive=True)
|
return self._result_stdout_raw(redact_sensitive=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def branch_override(self):
|
||||||
|
"""Whether a branch other than the project default is used."""
|
||||||
|
if not self.project:
|
||||||
|
return True
|
||||||
|
return bool(self.scm_branch and self.scm_branch != self.project.scm_branch)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_id(self):
|
||||||
|
if self.branch_override or self.job_type == 'check' or (not self.project):
|
||||||
|
return str(self.id)
|
||||||
|
return self.project.cache_id
|
||||||
|
|
||||||
def result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True):
|
def result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True):
|
||||||
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive=redact_sensitive)
|
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive=redact_sensitive)
|
||||||
|
|
||||||
@@ -597,10 +620,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
added_update_fields = []
|
added_update_fields = []
|
||||||
if not self.job_tags:
|
if not self.job_tags:
|
||||||
job_tags = ['update_{}'.format(self.scm_type)]
|
job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections']
|
||||||
if self.job_type == 'run':
|
|
||||||
job_tags.append('install_roles')
|
|
||||||
job_tags.append('install_collections')
|
|
||||||
self.job_tags = ','.join(job_tags)
|
self.job_tags = ','.join(job_tags)
|
||||||
added_update_fields.append('job_tags')
|
added_update_fields.append('job_tags')
|
||||||
if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check':
|
if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check':
|
||||||
|
|||||||
@@ -1865,44 +1865,31 @@ class RunJob(BaseTask):
|
|||||||
project_path = job.project.get_project_path(check_if_exists=False)
|
project_path = job.project.get_project_path(check_if_exists=False)
|
||||||
job_revision = job.project.scm_revision
|
job_revision = job.project.scm_revision
|
||||||
sync_needs = []
|
sync_needs = []
|
||||||
all_sync_needs = ['update_{}'.format(job.project.scm_type), 'install_roles', 'install_collections']
|
source_update_tag = 'update_{}'.format(job.project.scm_type)
|
||||||
|
branch_override = bool(job.scm_branch and job.scm_branch != job.project.scm_branch)
|
||||||
if not job.project.scm_type:
|
if not job.project.scm_type:
|
||||||
pass # manual projects are not synced, user has responsibility for that
|
pass # manual projects are not synced, user has responsibility for that
|
||||||
elif not os.path.exists(project_path):
|
elif not os.path.exists(project_path):
|
||||||
logger.debug('Performing fresh clone of {} on this instance.'.format(job.project))
|
logger.debug('Performing fresh clone of {} on this instance.'.format(job.project))
|
||||||
sync_needs = all_sync_needs
|
sync_needs.append(source_update_tag)
|
||||||
elif not job.project.scm_revision:
|
elif job.project.scm_type == 'git' and job.project.scm_revision and (not branch_override):
|
||||||
logger.debug('Revision not known for {}, will sync with remote'.format(job.project))
|
|
||||||
sync_needs = all_sync_needs
|
|
||||||
elif job.project.scm_type == 'git':
|
|
||||||
git_repo = git.Repo(project_path)
|
git_repo = git.Repo(project_path)
|
||||||
try:
|
try:
|
||||||
desired_revision = job.project.scm_revision
|
if job_revision == git_repo.head.commit.hexsha:
|
||||||
if job.scm_branch and job.scm_branch != job.project.scm_branch:
|
|
||||||
desired_revision = job.scm_branch # could be commit or not, but will try as commit
|
|
||||||
current_revision = git_repo.head.commit.hexsha
|
|
||||||
if desired_revision == current_revision:
|
|
||||||
job_revision = desired_revision
|
|
||||||
logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format))
|
logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format))
|
||||||
else:
|
else:
|
||||||
sync_needs = all_sync_needs
|
sync_needs.append(source_update_tag)
|
||||||
except (ValueError, BadGitName):
|
except (ValueError, BadGitName):
|
||||||
logger.debug('Needed commit for {} not in local source tree, will sync with remote'.format(job.log_format))
|
logger.debug('Needed commit for {} not in local source tree, will sync with remote'.format(job.log_format))
|
||||||
sync_needs = all_sync_needs
|
sync_needs.append(source_update_tag)
|
||||||
else:
|
else:
|
||||||
sync_needs = all_sync_needs
|
logger.debug('Project not available locally, {} will sync with remote'.format(job.log_format))
|
||||||
# Galaxy requirements are not supported for manual projects
|
sync_needs.append(source_update_tag)
|
||||||
if not sync_needs and job.project.scm_type:
|
|
||||||
# see if we need a sync because of presence of roles
|
|
||||||
galaxy_req_path = os.path.join(project_path, 'roles', 'requirements.yml')
|
|
||||||
if os.path.exists(galaxy_req_path):
|
|
||||||
logger.debug('Running project sync for {} because of galaxy role requirements.'.format(job.log_format))
|
|
||||||
sync_needs.append('install_roles')
|
|
||||||
|
|
||||||
galaxy_collections_req_path = os.path.join(project_path, 'collections', 'requirements.yml')
|
has_cache = os.path.exists(os.path.join(job.project.get_cache_path(), job.project.cache_id))
|
||||||
if os.path.exists(galaxy_collections_req_path):
|
# Galaxy requirements are not supported for manual projects
|
||||||
logger.debug('Running project sync for {} because of galaxy collections requirements.'.format(job.log_format))
|
if job.project.scm_type and ((not has_cache) or branch_override):
|
||||||
sync_needs.append('install_collections')
|
sync_needs.extend(['install_roles', 'install_collections'])
|
||||||
|
|
||||||
if sync_needs:
|
if sync_needs:
|
||||||
pu_ig = job.instance_group
|
pu_ig = job.instance_group
|
||||||
@@ -1920,7 +1907,7 @@ class RunJob(BaseTask):
|
|||||||
execution_node=pu_en,
|
execution_node=pu_en,
|
||||||
celery_task_id=job.celery_task_id
|
celery_task_id=job.celery_task_id
|
||||||
)
|
)
|
||||||
if job.scm_branch and job.scm_branch != job.project.scm_branch:
|
if branch_override:
|
||||||
sync_metafields['scm_branch'] = job.scm_branch
|
sync_metafields['scm_branch'] = job.scm_branch
|
||||||
if 'update_' not in sync_metafields['job_tags']:
|
if 'update_' not in sync_metafields['job_tags']:
|
||||||
sync_metafields['scm_revision'] = job_revision
|
sync_metafields['scm_revision'] = job_revision
|
||||||
@@ -1952,10 +1939,7 @@ class RunJob(BaseTask):
|
|||||||
if job_revision:
|
if job_revision:
|
||||||
job = self.update_model(job.pk, scm_revision=job_revision)
|
job = self.update_model(job.pk, scm_revision=job_revision)
|
||||||
# Project update does not copy the folder, so copy here
|
# Project update does not copy the folder, so copy here
|
||||||
RunProjectUpdate.make_local_copy(
|
RunProjectUpdate.make_local_copy(job.project, private_data_dir, scm_revision=job_revision)
|
||||||
project_path, os.path.join(private_data_dir, 'project'),
|
|
||||||
job.project.scm_type, job_revision
|
|
||||||
)
|
|
||||||
|
|
||||||
if job.inventory.kind == 'smart':
|
if job.inventory.kind == 'smart':
|
||||||
# cache smart inventory memberships so that the host_filter query is not
|
# cache smart inventory memberships so that the host_filter query is not
|
||||||
@@ -1995,10 +1979,7 @@ class RunProjectUpdate(BaseTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def proot_show_paths(self):
|
def proot_show_paths(self):
|
||||||
show_paths = [settings.PROJECTS_ROOT]
|
return [settings.PROJECTS_ROOT]
|
||||||
if self.job_private_data_dir:
|
|
||||||
show_paths.append(self.job_private_data_dir)
|
|
||||||
return show_paths
|
|
||||||
|
|
||||||
def __init__(self, *args, job_private_data_dir=None, **kwargs):
|
def __init__(self, *args, job_private_data_dir=None, **kwargs):
|
||||||
super(RunProjectUpdate, self).__init__(*args, **kwargs)
|
super(RunProjectUpdate, self).__init__(*args, **kwargs)
|
||||||
@@ -2032,12 +2013,6 @@ class RunProjectUpdate(BaseTask):
|
|||||||
credential = project_update.credential
|
credential = project_update.credential
|
||||||
if credential.has_input('ssh_key_data'):
|
if credential.has_input('ssh_key_data'):
|
||||||
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
|
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
|
||||||
|
|
||||||
# Create dir where collections will live for the job run
|
|
||||||
if project_update.job_type != 'check' and getattr(self, 'job_private_data_dir'):
|
|
||||||
for folder_name in ('requirements_collections', 'requirements_roles'):
|
|
||||||
folder_path = os.path.join(self.job_private_data_dir, folder_name)
|
|
||||||
os.mkdir(folder_path, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
|
|
||||||
return private_data
|
return private_data
|
||||||
|
|
||||||
def build_passwords(self, project_update, runtime_passwords):
|
def build_passwords(self, project_update, runtime_passwords):
|
||||||
@@ -2165,8 +2140,7 @@ class RunProjectUpdate(BaseTask):
|
|||||||
extra_vars.update(extra_vars_new)
|
extra_vars.update(extra_vars_new)
|
||||||
|
|
||||||
scm_branch = project_update.scm_branch
|
scm_branch = project_update.scm_branch
|
||||||
branch_override = bool(scm_branch and project_update.scm_branch != project_update.project.scm_branch)
|
if project_update.job_type == 'run' and (not project_update.branch_override):
|
||||||
if project_update.job_type == 'run' and (not branch_override):
|
|
||||||
if project_update.project.scm_revision:
|
if project_update.project.scm_revision:
|
||||||
scm_branch = project_update.project.scm_revision
|
scm_branch = project_update.project.scm_revision
|
||||||
elif not scm_branch:
|
elif not scm_branch:
|
||||||
@@ -2174,7 +2148,9 @@ class RunProjectUpdate(BaseTask):
|
|||||||
elif not scm_branch:
|
elif not scm_branch:
|
||||||
scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
|
scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
|
||||||
extra_vars.update({
|
extra_vars.update({
|
||||||
'project_path': project_update.get_project_path(check_if_exists=False),
|
'projects_root': settings.PROJECTS_ROOT.rstrip('/'),
|
||||||
|
'local_path': os.path.basename(project_update.project.local_path),
|
||||||
|
'project_path': project_update.get_project_path(check_if_exists=False), # deprecated
|
||||||
'insights_url': settings.INSIGHTS_URL_BASE,
|
'insights_url': settings.INSIGHTS_URL_BASE,
|
||||||
'awx_license_type': get_license(show_key=False).get('license_type', 'UNLICENSED'),
|
'awx_license_type': get_license(show_key=False).get('license_type', 'UNLICENSED'),
|
||||||
'awx_version': get_awx_version(),
|
'awx_version': get_awx_version(),
|
||||||
@@ -2184,9 +2160,6 @@ class RunProjectUpdate(BaseTask):
|
|||||||
'roles_enabled': settings.AWX_ROLES_ENABLED,
|
'roles_enabled': settings.AWX_ROLES_ENABLED,
|
||||||
'collections_enabled': settings.AWX_COLLECTIONS_ENABLED,
|
'collections_enabled': settings.AWX_COLLECTIONS_ENABLED,
|
||||||
})
|
})
|
||||||
if project_update.job_type != 'check' and self.job_private_data_dir:
|
|
||||||
extra_vars['collections_destination'] = os.path.join(self.job_private_data_dir, 'requirements_collections')
|
|
||||||
extra_vars['roles_destination'] = os.path.join(self.job_private_data_dir, 'requirements_roles')
|
|
||||||
# apply custom refspec from user for PR refs and the like
|
# apply custom refspec from user for PR refs and the like
|
||||||
if project_update.scm_refspec:
|
if project_update.scm_refspec:
|
||||||
extra_vars['scm_refspec'] = project_update.scm_refspec
|
extra_vars['scm_refspec'] = project_update.scm_refspec
|
||||||
@@ -2322,8 +2295,7 @@ class RunProjectUpdate(BaseTask):
|
|||||||
os.mkdir(settings.PROJECTS_ROOT)
|
os.mkdir(settings.PROJECTS_ROOT)
|
||||||
self.acquire_lock(instance)
|
self.acquire_lock(instance)
|
||||||
self.original_branch = None
|
self.original_branch = None
|
||||||
if (instance.scm_type == 'git' and instance.job_type == 'run' and instance.project and
|
if instance.scm_type == 'git' and instance.branch_override:
|
||||||
instance.scm_branch != instance.project.scm_branch):
|
|
||||||
project_path = instance.project.get_project_path(check_if_exists=False)
|
project_path = instance.project.get_project_path(check_if_exists=False)
|
||||||
if os.path.exists(project_path):
|
if os.path.exists(project_path):
|
||||||
git_repo = git.Repo(project_path)
|
git_repo = git.Repo(project_path)
|
||||||
@@ -2332,17 +2304,48 @@ class RunProjectUpdate(BaseTask):
|
|||||||
else:
|
else:
|
||||||
self.original_branch = git_repo.active_branch
|
self.original_branch = git_repo.active_branch
|
||||||
|
|
||||||
|
stage_path = os.path.join(instance.get_cache_path(), 'stage')
|
||||||
|
if os.path.exists(stage_path):
|
||||||
|
logger.warning('{0} unexpectedly existed before update'.format(stage_path))
|
||||||
|
shutil.rmtree(stage_path)
|
||||||
|
os.makedirs(stage_path) # presence of empty cache indicates lack of roles or collections
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_local_copy(project_path, destination_folder, scm_type, scm_revision):
|
def clear_project_cache(cache_dir, keep_value):
|
||||||
if scm_type == 'git':
|
if os.path.isdir(cache_dir):
|
||||||
|
for entry in os.listdir(cache_dir):
|
||||||
|
old_path = os.path.join(cache_dir, entry)
|
||||||
|
if entry not in (keep_value, 'stage'):
|
||||||
|
# invalidate, then delete
|
||||||
|
new_path = os.path.join(cache_dir,'.~~delete~~' + entry)
|
||||||
|
try:
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
shutil.rmtree(new_path)
|
||||||
|
except OSError:
|
||||||
|
logger.warning(f"Could not remove cache directory {old_path}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_local_copy(p, job_private_data_dir, scm_revision=None):
|
||||||
|
"""Copy project content (roles and collections) to a job private_data_dir
|
||||||
|
|
||||||
|
:param object p: Either a project or a project update
|
||||||
|
:param str job_private_data_dir: The root of the target ansible-runner folder
|
||||||
|
:param str scm_revision: For branch_override cases, the git revision to copy
|
||||||
|
"""
|
||||||
|
project_path = p.get_project_path(check_if_exists=False)
|
||||||
|
destination_folder = os.path.join(job_private_data_dir, 'project')
|
||||||
|
if not scm_revision:
|
||||||
|
scm_revision = p.scm_revision
|
||||||
|
|
||||||
|
if p.scm_type == 'git':
|
||||||
git_repo = git.Repo(project_path)
|
git_repo = git.Repo(project_path)
|
||||||
if not os.path.exists(destination_folder):
|
if not os.path.exists(destination_folder):
|
||||||
os.mkdir(destination_folder, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
|
os.mkdir(destination_folder, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
|
||||||
tmp_branch_name = 'awx_internal/{}'.format(uuid4())
|
tmp_branch_name = 'awx_internal/{}'.format(uuid4())
|
||||||
# always clone based on specific job revision
|
# always clone based on specific job revision
|
||||||
if not scm_revision:
|
if not p.scm_revision:
|
||||||
raise RuntimeError('Unexpectedly could not determine a revision to run from project.')
|
raise RuntimeError('Unexpectedly could not determine a revision to run from project.')
|
||||||
source_branch = git_repo.create_head(tmp_branch_name, scm_revision)
|
source_branch = git_repo.create_head(tmp_branch_name, p.scm_revision)
|
||||||
# git clone must take file:// syntax for source repo or else options like depth will be ignored
|
# git clone must take file:// syntax for source repo or else options like depth will be ignored
|
||||||
source_as_uri = Path(project_path).as_uri()
|
source_as_uri = Path(project_path).as_uri()
|
||||||
git.Repo.clone_from(
|
git.Repo.clone_from(
|
||||||
@@ -2361,19 +2364,48 @@ class RunProjectUpdate(BaseTask):
|
|||||||
else:
|
else:
|
||||||
copy_tree(project_path, destination_folder, preserve_symlinks=1)
|
copy_tree(project_path, destination_folder, preserve_symlinks=1)
|
||||||
|
|
||||||
|
# copy over the roles and collection cache to job folder
|
||||||
|
cache_path = os.path.join(p.get_cache_path(), p.cache_id)
|
||||||
|
subfolders = []
|
||||||
|
if settings.AWX_COLLECTIONS_ENABLED:
|
||||||
|
subfolders.append('requirements_collections')
|
||||||
|
if settings.AWX_ROLES_ENABLED:
|
||||||
|
subfolders.append('requirements_roles')
|
||||||
|
for subfolder in subfolders:
|
||||||
|
cache_subpath = os.path.join(cache_path, subfolder)
|
||||||
|
if os.path.exists(cache_subpath):
|
||||||
|
dest_subpath = os.path.join(job_private_data_dir, subfolder)
|
||||||
|
copy_tree(cache_subpath, dest_subpath, preserve_symlinks=1)
|
||||||
|
logger.debug('{0} {1} prepared {2} from cache'.format(type(p).__name__, p.pk, dest_subpath))
|
||||||
|
|
||||||
def post_run_hook(self, instance, status):
|
def post_run_hook(self, instance, status):
|
||||||
# To avoid hangs, very important to release lock even if errors happen here
|
# To avoid hangs, very important to release lock even if errors happen here
|
||||||
try:
|
try:
|
||||||
if self.playbook_new_revision:
|
if self.playbook_new_revision:
|
||||||
instance.scm_revision = self.playbook_new_revision
|
instance.scm_revision = self.playbook_new_revision
|
||||||
instance.save(update_fields=['scm_revision'])
|
instance.save(update_fields=['scm_revision'])
|
||||||
|
|
||||||
|
# Roles and collection folders copy to durable cache
|
||||||
|
base_path = instance.get_cache_path()
|
||||||
|
stage_path = os.path.join(base_path, 'stage')
|
||||||
|
if status == 'successful' and 'install_' in instance.job_tags:
|
||||||
|
# Clear other caches before saving this one, and if branch is overridden
|
||||||
|
# do not clear cache for main branch, but do clear it for other branches
|
||||||
|
self.clear_project_cache(base_path, keep_value=instance.project.cache_id)
|
||||||
|
cache_path = os.path.join(base_path, instance.cache_id)
|
||||||
|
if os.path.exists(stage_path):
|
||||||
|
if os.path.exists(cache_path):
|
||||||
|
logger.warning('Rewriting cache at {0}, performance may suffer'.format(cache_path))
|
||||||
|
shutil.rmtree(cache_path)
|
||||||
|
os.rename(stage_path, cache_path)
|
||||||
|
logger.debug('{0} wrote to cache at {1}'.format(instance.log_format, cache_path))
|
||||||
|
elif os.path.exists(stage_path):
|
||||||
|
shutil.rmtree(stage_path) # cannot trust content update produced
|
||||||
|
|
||||||
if self.job_private_data_dir:
|
if self.job_private_data_dir:
|
||||||
# copy project folder before resetting to default branch
|
# copy project folder before resetting to default branch
|
||||||
# because some git-tree-specific resources (like submodules) might matter
|
# because some git-tree-specific resources (like submodules) might matter
|
||||||
self.make_local_copy(
|
self.make_local_copy(instance, self.job_private_data_dir)
|
||||||
instance.get_project_path(check_if_exists=False), os.path.join(self.job_private_data_dir, 'project'),
|
|
||||||
instance.scm_type, instance.scm_revision
|
|
||||||
)
|
|
||||||
if self.original_branch:
|
if self.original_branch:
|
||||||
# for git project syncs, non-default branches can be problems
|
# for git project syncs, non-default branches can be problems
|
||||||
# restore to branch the repo was on before this run
|
# restore to branch the repo was on before this run
|
||||||
@@ -2626,13 +2658,21 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
source_project = None
|
source_project = None
|
||||||
if inventory_update.inventory_source:
|
if inventory_update.inventory_source:
|
||||||
source_project = inventory_update.inventory_source.source_project
|
source_project = inventory_update.inventory_source.source_project
|
||||||
if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and source_project):
|
if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and
|
||||||
# In project sync, pulling galaxy roles is not needed
|
source_project and source_project.scm_type): # never ever update manual projects
|
||||||
|
|
||||||
|
# Check if the content cache exists, so that we do not unnecessarily re-download roles
|
||||||
|
sync_needs = ['update_{}'.format(source_project.scm_type)]
|
||||||
|
has_cache = os.path.exists(os.path.join(source_project.get_cache_path(), source_project.cache_id))
|
||||||
|
# Galaxy requirements are not supported for manual projects
|
||||||
|
if not has_cache:
|
||||||
|
sync_needs.extend(['install_roles', 'install_collections'])
|
||||||
|
|
||||||
local_project_sync = source_project.create_project_update(
|
local_project_sync = source_project.create_project_update(
|
||||||
_eager_fields=dict(
|
_eager_fields=dict(
|
||||||
launch_type="sync",
|
launch_type="sync",
|
||||||
job_type='run',
|
job_type='run',
|
||||||
job_tags='update_{},install_collections'.format(source_project.scm_type), # roles are never valid for inventory
|
job_tags=','.join(sync_needs),
|
||||||
status='running',
|
status='running',
|
||||||
execution_node=inventory_update.execution_node,
|
execution_node=inventory_update.execution_node,
|
||||||
instance_group = inventory_update.instance_group,
|
instance_group = inventory_update.instance_group,
|
||||||
@@ -2656,11 +2696,7 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
raise
|
raise
|
||||||
elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project:
|
elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project:
|
||||||
# This follows update, not sync, so make copy here
|
# This follows update, not sync, so make copy here
|
||||||
project_path = source_project.get_project_path(check_if_exists=False)
|
RunProjectUpdate.make_local_copy(source_project, private_data_dir)
|
||||||
RunProjectUpdate.make_local_copy(
|
|
||||||
project_path, os.path.join(private_data_dir, 'project'),
|
|
||||||
source_project.scm_type, source_project.scm_revision
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@task(queue=get_local_queuename)
|
@task(queue=get_local_queuename)
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ def test_no_changing_overwrite_behavior_if_used(post, patch, organization, admin
|
|||||||
data={
|
data={
|
||||||
'name': 'fooo',
|
'name': 'fooo',
|
||||||
'organization': organization.id,
|
'organization': organization.id,
|
||||||
'allow_override': True
|
'allow_override': True,
|
||||||
|
'scm_type': 'git',
|
||||||
|
'scm_url': 'https://github.com/ansible/test-playbooks.git'
|
||||||
},
|
},
|
||||||
user=admin_user,
|
user=admin_user,
|
||||||
expect=201
|
expect=201
|
||||||
@@ -83,7 +85,9 @@ def test_changing_overwrite_behavior_okay_if_not_used(post, patch, organization,
|
|||||||
data={
|
data={
|
||||||
'name': 'fooo',
|
'name': 'fooo',
|
||||||
'organization': organization.id,
|
'organization': organization.id,
|
||||||
'allow_override': True
|
'allow_override': True,
|
||||||
|
'scm_type': 'git',
|
||||||
|
'scm_url': 'https://github.com/ansible/test-playbooks.git'
|
||||||
},
|
},
|
||||||
user=admin_user,
|
user=admin_user,
|
||||||
expect=201
|
expect=201
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ def project(instance, organization):
|
|||||||
description="test-proj-desc",
|
description="test-proj-desc",
|
||||||
organization=organization,
|
organization=organization,
|
||||||
playbook_files=['helloworld.yml', 'alt-helloworld.yml'],
|
playbook_files=['helloworld.yml', 'alt-helloworld.yml'],
|
||||||
local_path='_92__test_proj',
|
|
||||||
scm_revision='1234567890123456789012345678901234567890',
|
scm_revision='1234567890123456789012345678901234567890',
|
||||||
scm_url='localhost',
|
scm_url='localhost',
|
||||||
scm_type='git'
|
scm_type='git'
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ class TestSCMUpdateFeatures:
|
|||||||
inventory_update = InventoryUpdate(
|
inventory_update = InventoryUpdate(
|
||||||
inventory_source=scm_inventory_source,
|
inventory_source=scm_inventory_source,
|
||||||
source_path=scm_inventory_source.source_path)
|
source_path=scm_inventory_source.source_path)
|
||||||
assert inventory_update.get_actual_source_path().endswith('_92__test_proj/inventory_file')
|
p = scm_inventory_source.source_project
|
||||||
|
assert inventory_update.get_actual_source_path().endswith(f'_{p.id}__test_proj/inventory_file')
|
||||||
|
|
||||||
def test_no_unwanted_updates(self, scm_inventory_source):
|
def test_no_unwanted_updates(self, scm_inventory_source):
|
||||||
# Changing the non-sensitive fields should not trigger update
|
# Changing the non-sensitive fields should not trigger update
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ def test_sensitive_change_triggers_update(project):
|
|||||||
mock_update.assert_called_once_with()
|
mock_update.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_local_path_autoset(organization):
|
||||||
|
with mock.patch.object(Project, "update"):
|
||||||
|
p = Project.objects.create(
|
||||||
|
name="test-proj",
|
||||||
|
organization=organization,
|
||||||
|
scm_url='localhost',
|
||||||
|
scm_type='git'
|
||||||
|
)
|
||||||
|
assert p.local_path == f'_{p.id}__test_proj'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_foreign_key_change_changes_modified_by(project, organization):
|
def test_foreign_key_change_changes_modified_by(project, organization):
|
||||||
assert project._get_fields_snapshot()['organization_id'] == organization.id
|
assert project._get_fields_snapshot()['organization_id'] == organization.id
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ def team_project_list(organization_factory):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_project_path(project):
|
def test_get_project_path(project):
|
||||||
# Test combining projects root with project local path
|
# Test combining projects root with project local path
|
||||||
with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/awx'):
|
with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/foo'):
|
||||||
assert project.get_project_path(check_if_exists=False) == '/var/lib/awx/_92__test_proj'
|
assert project.get_project_path(check_if_exists=False) == f'/var/lib/foo/_{project.id}__test_proj'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TestDependentInventoryUpdate:
|
|||||||
def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file):
|
def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file):
|
||||||
task = RunProjectUpdate()
|
task = RunProjectUpdate()
|
||||||
task.revision_path = scm_revision_file
|
task.revision_path = scm_revision_file
|
||||||
proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
|
proj_update = scm_inventory_source.source_project.create_project_update()
|
||||||
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
||||||
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
||||||
task.post_run_hook(proj_update, 'successful')
|
task.post_run_hook(proj_update, 'successful')
|
||||||
@@ -39,7 +39,7 @@ class TestDependentInventoryUpdate:
|
|||||||
def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file):
|
def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file):
|
||||||
task = RunProjectUpdate()
|
task = RunProjectUpdate()
|
||||||
task.revision_path = scm_revision_file
|
task.revision_path = scm_revision_file
|
||||||
proj_update = ProjectUpdate.objects.create(project=project)
|
proj_update = project.create_project_update()
|
||||||
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
||||||
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
||||||
task.post_run_hook(proj_update, 'successful')
|
task.post_run_hook(proj_update, 'successful')
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ def patch_Job():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def job():
|
def job():
|
||||||
return Job(pk=1, id=1, project=Project(), inventory=Inventory(), job_template=JobTemplate(id=1, name='foo'))
|
return Job(
|
||||||
|
pk=1, id=1,
|
||||||
|
project=Project(local_path='/projects/_23_foo'),
|
||||||
|
inventory=Inventory(), job_template=JobTemplate(id=1, name='foo'))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -406,7 +409,9 @@ class TestExtraVarSanitation(TestJobExecution):
|
|||||||
class TestGenericRun():
|
class TestGenericRun():
|
||||||
|
|
||||||
def test_generic_failure(self, patch_Job):
|
def test_generic_failure(self, patch_Job):
|
||||||
job = Job(status='running', inventory=Inventory(), project=Project())
|
job = Job(
|
||||||
|
status='running', inventory=Inventory(),
|
||||||
|
project=Project(local_path='/projects/_23_foo'))
|
||||||
job.websocket_emit_status = mock.Mock()
|
job.websocket_emit_status = mock.Mock()
|
||||||
|
|
||||||
task = tasks.RunJob()
|
task = tasks.RunJob()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
# The following variables will be set by the runner of this playbook:
|
# The following variables will be set by the runner of this playbook:
|
||||||
# project_path: PROJECTS_DIR/_local_path_
|
# projects_root: Global location for caching project checkouts and roles and collections
|
||||||
|
# should not have trailing slash on end
|
||||||
|
# local_path: Path within projects_root to use for this project
|
||||||
|
# project_path: A simple join of projects_root/local_path folders
|
||||||
# scm_url: https://server/repo
|
# scm_url: https://server/repo
|
||||||
# insights_url: Insights service URL (from configuration)
|
# insights_url: Insights service URL (from configuration)
|
||||||
# scm_branch: branch/tag/revision (HEAD if unset)
|
# scm_branch: branch/tag/revision (HEAD if unset)
|
||||||
@@ -11,8 +14,6 @@
|
|||||||
# scm_refspec: a refspec to fetch in addition to obtaining version
|
# scm_refspec: a refspec to fetch in addition to obtaining version
|
||||||
# roles_enabled: Value of the global setting to enable roles downloading
|
# roles_enabled: Value of the global setting to enable roles downloading
|
||||||
# collections_enabled: Value of the global setting to enable collections downloading
|
# collections_enabled: Value of the global setting to enable collections downloading
|
||||||
# roles_destination: Path to save roles from galaxy to
|
|
||||||
# collections_destination: Path to save collections from galaxy to
|
|
||||||
# awx_version: Current running version of the awx or tower as a string
|
# awx_version: Current running version of the awx or tower as a string
|
||||||
# awx_license_type: "open" for AWX; else presume Tower
|
# awx_license_type: "open" for AWX; else presume Tower
|
||||||
|
|
||||||
@@ -122,7 +123,10 @@
|
|||||||
register: doesRequirementsExist
|
register: doesRequirementsExist
|
||||||
|
|
||||||
- name: fetch galaxy roles from requirements.yml
|
- name: fetch galaxy roles from requirements.yml
|
||||||
command: ansible-galaxy role install -r roles/requirements.yml -p {{roles_destination|quote}}{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
command: >
|
||||||
|
ansible-galaxy role install -r roles/requirements.yml
|
||||||
|
--roles-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_roles
|
||||||
|
{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||||
args:
|
args:
|
||||||
chdir: "{{project_path|quote}}"
|
chdir: "{{project_path|quote}}"
|
||||||
register: galaxy_result
|
register: galaxy_result
|
||||||
@@ -143,7 +147,10 @@
|
|||||||
register: doesCollectionRequirementsExist
|
register: doesCollectionRequirementsExist
|
||||||
|
|
||||||
- name: fetch galaxy collections from collections/requirements.yml
|
- name: fetch galaxy collections from collections/requirements.yml
|
||||||
command: ansible-galaxy collection install -r collections/requirements.yml -p {{collections_destination|quote}}{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
command: >
|
||||||
|
ansible-galaxy collection install -r collections/requirements.yml
|
||||||
|
--collections-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections
|
||||||
|
{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||||
args:
|
args:
|
||||||
chdir: "{{project_path|quote}}"
|
chdir: "{{project_path|quote}}"
|
||||||
register: galaxy_collection_result
|
register: galaxy_collection_result
|
||||||
@@ -151,7 +158,7 @@
|
|||||||
changed_when: "'Installing ' in galaxy_collection_result.stdout"
|
changed_when: "'Installing ' in galaxy_collection_result.stdout"
|
||||||
environment:
|
environment:
|
||||||
ANSIBLE_FORCE_COLOR: false
|
ANSIBLE_FORCE_COLOR: false
|
||||||
ANSIBLE_COLLECTIONS_PATHS: "{{ collections_destination }}"
|
ANSIBLE_COLLECTIONS_PATHS: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections"
|
||||||
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no"
|
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no"
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ AWX supports the use of Ansible Collections. This section will give ways to use
|
|||||||
|
|
||||||
### Project Collections Requirements
|
### Project Collections Requirements
|
||||||
|
|
||||||
If you specify a Collections requirements file in SCM at `collections/requirements.yml`,
|
If you specify a collections requirements file in SCM at `collections/requirements.yml`,
|
||||||
then AWX will install Collections from that file in the implicit project sync
|
then AWX will install collections from that file to a special cache folder in project updates.
|
||||||
before a job run. The invocation looks like:
|
Before a job runs, the roles and/or collections will be copied from the special
|
||||||
|
cache folder to the job temporary folder.
|
||||||
|
|
||||||
|
The invocation looks like:
|
||||||
|
|
||||||
```
|
```
|
||||||
ansible-galaxy collection install -r requirements.yml -p <job tmp location>/requirements_collections
|
ansible-galaxy collection install -r requirements.yml -p <project cache location>/requirements_collections
|
||||||
```
|
```
|
||||||
|
|
||||||
Example of the resultant `tmp` directory where job is running:
|
Example of the resultant job `tmp` directory where job is running:
|
||||||
|
|
||||||
```
|
```
|
||||||
├── project
|
├── project
|
||||||
@@ -20,7 +23,7 @@ Example of the resultant `tmp` directory where job is running:
|
|||||||
│ └── debug.yml
|
│ └── debug.yml
|
||||||
├── requirements_collections
|
├── requirements_collections
|
||||||
│ └── ansible_collections
|
│ └── ansible_collections
|
||||||
│ └── username
|
│ └── collection_namespace
|
||||||
│ └── collection_name
|
│ └── collection_name
|
||||||
│ ├── FILES.json
|
│ ├── FILES.json
|
||||||
│ ├── MANIFEST.json
|
│ ├── MANIFEST.json
|
||||||
@@ -53,6 +56,33 @@ Example of the resultant `tmp` directory where job is running:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Cache Folder Mechanics
|
||||||
|
|
||||||
|
Every time a project is updated as a "check" job
|
||||||
|
(via `/api/v2/projects/N/update/` or by a schedule, workflow, etc.),
|
||||||
|
the roles and collections are downloaded and saved to the project's content cache.
|
||||||
|
In other words, the cache is invalidated every time a project is updated.
|
||||||
|
That means that the `ansible-galaxy` commands are ran to download content
|
||||||
|
even if the project revision does not change in the course of the update.
|
||||||
|
|
||||||
|
Project updates all initially target a staging directory at a path like:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/lib/awx/projects/.__awx_cache/_42__project_name/stage
|
||||||
|
```
|
||||||
|
|
||||||
|
After the update finishes, the task logic will decide what id to associate
|
||||||
|
with the content downloaded.
|
||||||
|
Then the folder will be renamed from "stage" to the cache id.
|
||||||
|
For instance, if the cache id is determined to be 63:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/lib/awx/projects/.__awx_cache/_42__project_name/63
|
||||||
|
```
|
||||||
|
|
||||||
|
The cache may be updated by project syncs (the "run" type) which happen before
|
||||||
|
job runs. It will populate the cache id set by the last "check" type update.
|
||||||
|
|
||||||
### Galaxy Server Selection
|
### Galaxy Server Selection
|
||||||
|
|
||||||
Ansible core default settings will download collections from the public
|
Ansible core default settings will download collections from the public
|
||||||
|
|||||||
Reference in New Issue
Block a user