mirror of
https://github.com/ansible/awx.git
synced 2026-02-05 03:24:50 -03:30
Compare commits
10 Commits
UI-Feature
...
thenets/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7486b024 | ||
|
|
d30c5ca9cd | ||
|
|
a3b21b261c | ||
|
|
d1d60c9ef1 | ||
|
|
925e055bb3 | ||
|
|
9f40d7a05c | ||
|
|
968c316c0c | ||
|
|
2fdce43f9e | ||
|
|
1106367962 | ||
|
|
b269ed48ee |
158
Makefile
158
Makefile
@@ -54,47 +54,6 @@ I18N_FLAG_FILE = .i18n_built
|
||||
VERSION PYTHON_VERSION docker-compose-sources \
|
||||
.git/hooks/pre-commit
|
||||
|
||||
clean-tmp:
|
||||
rm -rf tmp/
|
||||
|
||||
clean-venv:
|
||||
rm -rf venv/
|
||||
|
||||
clean-dist:
|
||||
rm -rf dist
|
||||
|
||||
clean-schema:
|
||||
rm -rf swagger.json
|
||||
rm -rf schema.json
|
||||
rm -rf reference-schema.json
|
||||
|
||||
clean-languages:
|
||||
rm -f $(I18N_FLAG_FILE)
|
||||
find ./awx/locale/ -type f -regex ".*\.mo$" -delete
|
||||
|
||||
## Remove temporary build files, compiled Python files.
|
||||
clean: clean-ui clean-api clean-awxkit clean-dist
|
||||
rm -rf awx/public
|
||||
rm -rf awx/lib/site-packages
|
||||
rm -rf awx/job_status
|
||||
rm -rf awx/job_output
|
||||
rm -rf reports
|
||||
rm -rf tmp
|
||||
rm -rf $(I18N_FLAG_FILE)
|
||||
mkdir tmp
|
||||
|
||||
clean-api:
|
||||
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
||||
rm -rf .tox
|
||||
find . -type f -regex ".*\.py[co]$$" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
rm -f awx/awx_test.sqlite3*
|
||||
rm -rf requirements/vendor
|
||||
rm -rf awx/projects
|
||||
|
||||
clean-awxkit:
|
||||
rm -rf awxkit/*.egg-info awxkit/.tox awxkit/build/*
|
||||
|
||||
## convenience target to assert environment variables are defined
|
||||
guard-%:
|
||||
@if [ "$${$*}" = "" ]; then \
|
||||
@@ -118,7 +77,7 @@ virtualenv_awx:
|
||||
fi; \
|
||||
fi
|
||||
|
||||
## Install third-party requirements needed for AWX's environment.
|
||||
## Install third-party requirements needed for AWX's environment.
|
||||
# this does not use system site packages intentionally
|
||||
requirements_awx: virtualenv_awx
|
||||
if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
|
||||
@@ -372,15 +331,6 @@ bulk_data:
|
||||
|
||||
UI_BUILD_FLAG_FILE = awx/ui/.ui-built
|
||||
|
||||
clean-ui:
|
||||
rm -rf node_modules
|
||||
rm -rf awx/ui/node_modules
|
||||
rm -rf awx/ui/build
|
||||
rm -rf awx/ui/src/locales/_build
|
||||
rm -rf $(UI_BUILD_FLAG_FILE)
|
||||
# the collectstatic command doesn't like it if this dir doesn't exist.
|
||||
mkdir -p awx/ui/build/static
|
||||
|
||||
awx/ui/node_modules:
|
||||
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
|
||||
|
||||
@@ -452,7 +402,7 @@ awx/projects:
|
||||
COMPOSE_UP_OPTS ?=
|
||||
COMPOSE_OPTS ?=
|
||||
CONTROL_PLANE_NODE_COUNT ?= 1
|
||||
EXECUTION_NODE_COUNT ?= 2
|
||||
EXECUTION_NODE_COUNT ?= 0
|
||||
MINIKUBE_CONTAINER_GROUP ?= false
|
||||
MINIKUBE_SETUP ?= false # if false, run minikube separately
|
||||
EXTRA_SOURCES_ANSIBLE_OPTS ?=
|
||||
@@ -503,15 +453,6 @@ detect-schema-change: genschema
|
||||
# Ignore differences in whitespace with -b
|
||||
diff -u -b reference-schema.json schema.json
|
||||
|
||||
docker-compose-clean: awx/projects
|
||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
||||
|
||||
docker-compose-container-group-clean:
|
||||
@if [ -f "tools/docker-compose-minikube/_sources/minikube" ]; then \
|
||||
tools/docker-compose-minikube/_sources/minikube delete; \
|
||||
fi
|
||||
rm -rf tools/docker-compose-minikube/_sources/
|
||||
|
||||
## Base development image build
|
||||
docker-compose-build:
|
||||
ansible-playbook tools/ansible/dockerfile.yml -e build_dev=True -e receptor_image=$(RECEPTOR_IMAGE)
|
||||
@@ -519,15 +460,6 @@ docker-compose-build:
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
|
||||
|
||||
docker-clean:
|
||||
$(foreach container_id,$(shell docker ps -f name=tools_awx -aq && docker ps -f name=tools_receptor -aq),docker stop $(container_id); docker rm -f $(container_id);)
|
||||
if [ "$(shell docker images | grep awx_devel)" ]; then \
|
||||
docker images | grep awx_devel | awk '{print $$3}' | xargs docker rmi --force; \
|
||||
fi
|
||||
|
||||
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
||||
docker volume rm -f tools_awx_db tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
||||
|
||||
docker-refresh: docker-clean docker-compose
|
||||
|
||||
## Docker Development Environment with Elastic Stack Connected
|
||||
@@ -540,14 +472,6 @@ docker-compose-cluster-elk: awx/projects docker-compose-sources
|
||||
docker-compose-container-group:
|
||||
MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
||||
|
||||
clean-elk:
|
||||
docker stop tools_kibana_1
|
||||
docker stop tools_logstash_1
|
||||
docker stop tools_elasticsearch_1
|
||||
docker rm tools_logstash_1
|
||||
docker rm tools_elasticsearch_1
|
||||
docker rm tools_kibana_1
|
||||
|
||||
psql-container:
|
||||
docker run -it --net tools_default --rm postgres:12 sh -c 'exec psql -h "postgres" -p "5432" -U postgres'
|
||||
|
||||
@@ -604,6 +528,84 @@ messages:
|
||||
print-%:
|
||||
@echo $($*)
|
||||
|
||||
# Cleaning
|
||||
# --------------------------------------
|
||||
## Remove temporary build files, compiled Python files.
|
||||
clean: clean-ui clean-api clean-awxkit clean-dist
|
||||
rm -rf awx/public
|
||||
rm -rf awx/lib/site-packages
|
||||
rm -rf awx/job_status
|
||||
rm -rf awx/job_output
|
||||
rm -rf reports
|
||||
rm -rf tmp
|
||||
rm -rf $(I18N_FLAG_FILE)
|
||||
mkdir tmp
|
||||
|
||||
clean-elk:
|
||||
docker stop tools_kibana_1
|
||||
docker stop tools_logstash_1
|
||||
docker stop tools_elasticsearch_1
|
||||
docker rm tools_logstash_1
|
||||
docker rm tools_elasticsearch_1
|
||||
docker rm tools_kibana_1
|
||||
|
||||
clean-ui:
|
||||
rm -rf node_modules
|
||||
rm -rf awx/ui/node_modules
|
||||
rm -rf awx/ui/build
|
||||
rm -rf awx/ui/src/locales/_build
|
||||
rm -rf $(UI_BUILD_FLAG_FILE)
|
||||
# the collectstatic command doesn't like it if this dir doesn't exist.
|
||||
mkdir -p awx/ui/build/static
|
||||
|
||||
clean-tmp:
|
||||
rm -rf tmp/
|
||||
|
||||
clean-venv:
|
||||
rm -rf venv/
|
||||
|
||||
clean-dist:
|
||||
rm -rf dist
|
||||
|
||||
clean-schema:
|
||||
rm -rf swagger.json
|
||||
rm -rf schema.json
|
||||
rm -rf reference-schema.json
|
||||
|
||||
clean-languages:
|
||||
rm -f $(I18N_FLAG_FILE)
|
||||
find ./awx/locale/ -type f -regex ".*\.mo$" -delete
|
||||
|
||||
clean-api:
|
||||
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
||||
rm -rf .tox
|
||||
find . -type f -regex ".*\.py[co]$$" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
rm -f awx/awx_test.sqlite3*
|
||||
rm -rf requirements/vendor
|
||||
rm -rf awx/projects
|
||||
|
||||
clean-awxkit:
|
||||
rm -rf awxkit/*.egg-info awxkit/.tox awxkit/build/*
|
||||
|
||||
docker-compose-clean: awx/projects
|
||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
||||
|
||||
docker-compose-container-group-clean:
|
||||
@if [ -f "tools/docker-compose-minikube/_sources/minikube" ]; then \
|
||||
tools/docker-compose-minikube/_sources/minikube delete; \
|
||||
fi
|
||||
rm -rf tools/docker-compose-minikube/_sources/
|
||||
|
||||
docker-clean:
|
||||
$(foreach container_id,$(shell docker ps -f name=tools_awx -aq && docker ps -f name=tools_receptor -aq),docker stop $(container_id); docker rm -f $(container_id);)
|
||||
if [ "$(shell docker images | grep awx_devel)" ]; then \
|
||||
docker images | grep awx_devel | awk '{print $$3}' | xargs docker rmi --force; \
|
||||
fi
|
||||
|
||||
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
||||
docker volume rm -f tools_awx_db tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
||||
|
||||
# HELP related targets
|
||||
# --------------------------------------
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ receptor_work_commands:
|
||||
command: ansible-runner
|
||||
params: worker
|
||||
allowruntimeparams: true
|
||||
verifysignature: true
|
||||
verifysignature: {{ sign_work }}
|
||||
custom_worksign_public_keyfile: receptor/work-public-key.pem
|
||||
custom_tls_certfile: receptor/tls/receptor.crt
|
||||
custom_tls_keyfile: receptor/tls/receptor.key
|
||||
|
||||
@@ -567,17 +567,6 @@ class Host(CommonModelNameNotUnique, RelatedJobsMixin):
|
||||
# Use .job_host_summaries.all() to get jobs affecting this host.
|
||||
# Use .job_events.all() to get events affecting this host.
|
||||
|
||||
'''
|
||||
We don't use timestamp, but we may in the future.
|
||||
'''
|
||||
|
||||
def update_ansible_facts(self, module, facts, timestamp=None):
|
||||
if module == "ansible":
|
||||
self.ansible_facts.update(facts)
|
||||
else:
|
||||
self.ansible_facts[module] = facts
|
||||
self.save()
|
||||
|
||||
def get_effective_host_name(self):
|
||||
"""
|
||||
Return the name of the host that will be used in actual ansible
|
||||
|
||||
@@ -44,7 +44,7 @@ from awx.main.models.notifications import (
|
||||
NotificationTemplate,
|
||||
JobNotificationMixin,
|
||||
)
|
||||
from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic
|
||||
from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic, log_excess_runtime
|
||||
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
|
||||
from awx.main.models.mixins import (
|
||||
ResourceMixin,
|
||||
@@ -857,8 +857,11 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
return host_queryset.iterator()
|
||||
return host_queryset
|
||||
|
||||
def start_job_fact_cache(self, destination, modification_times, timeout=None):
|
||||
@log_excess_runtime(logger, debug_cutoff=0.01, msg='Job {job_id} host facts prepared for {written_ct} hosts, took {delta:.3f} s', add_log_data=True)
|
||||
def start_job_fact_cache(self, destination, log_data, timeout=None):
|
||||
self.log_lifecycle("start_job_fact_cache")
|
||||
log_data['job_id'] = self.id
|
||||
log_data['written_ct'] = 0
|
||||
os.makedirs(destination, mode=0o700)
|
||||
|
||||
if timeout is None:
|
||||
@@ -869,6 +872,8 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
hosts = self._get_inventory_hosts(ansible_facts_modified__gte=timeout)
|
||||
else:
|
||||
hosts = self._get_inventory_hosts()
|
||||
|
||||
last_filepath_written = None
|
||||
for host in hosts:
|
||||
filepath = os.sep.join(map(str, [destination, host.name]))
|
||||
if not os.path.realpath(filepath).startswith(destination):
|
||||
@@ -878,23 +883,38 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
with codecs.open(filepath, 'w', encoding='utf-8') as f:
|
||||
os.chmod(f.name, 0o600)
|
||||
json.dump(host.ansible_facts, f)
|
||||
log_data['written_ct'] += 1
|
||||
last_filepath_written = filepath
|
||||
except IOError:
|
||||
system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name)))
|
||||
continue
|
||||
# make note of the time we wrote the file so we can check if it changed later
|
||||
modification_times[filepath] = os.path.getmtime(filepath)
|
||||
# make note of the time we wrote the last file so we can check if any file changed later
|
||||
if last_filepath_written:
|
||||
return os.path.getmtime(last_filepath_written)
|
||||
return None
|
||||
|
||||
def finish_job_fact_cache(self, destination, modification_times):
|
||||
@log_excess_runtime(
|
||||
logger,
|
||||
debug_cutoff=0.01,
|
||||
msg='Job {job_id} host facts: updated {updated_ct}, cleared {cleared_ct}, unchanged {unmodified_ct}, took {delta:.3f} s',
|
||||
add_log_data=True,
|
||||
)
|
||||
def finish_job_fact_cache(self, destination, facts_write_time, log_data):
|
||||
self.log_lifecycle("finish_job_fact_cache")
|
||||
log_data['job_id'] = self.id
|
||||
log_data['updated_ct'] = 0
|
||||
log_data['unmodified_ct'] = 0
|
||||
log_data['cleared_ct'] = 0
|
||||
hosts_to_update = []
|
||||
for host in self._get_inventory_hosts():
|
||||
filepath = os.sep.join(map(str, [destination, host.name]))
|
||||
if not os.path.realpath(filepath).startswith(destination):
|
||||
system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name)))
|
||||
continue
|
||||
if os.path.exists(filepath):
|
||||
# If the file changed since we wrote it pre-playbook run...
|
||||
# If the file changed since we wrote the last facts file, pre-playbook run...
|
||||
modified = os.path.getmtime(filepath)
|
||||
if modified > modification_times.get(filepath, 0):
|
||||
if (not facts_write_time) or modified > facts_write_time:
|
||||
with codecs.open(filepath, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
ansible_facts = json.load(f)
|
||||
@@ -902,7 +922,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
continue
|
||||
host.ansible_facts = ansible_facts
|
||||
host.ansible_facts_modified = now()
|
||||
host.save(update_fields=['ansible_facts', 'ansible_facts_modified'])
|
||||
hosts_to_update.append(host)
|
||||
system_tracking_logger.info(
|
||||
'New fact for inventory {} host {}'.format(smart_str(host.inventory.name), smart_str(host.name)),
|
||||
extra=dict(
|
||||
@@ -913,12 +933,21 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
job_id=self.id,
|
||||
),
|
||||
)
|
||||
log_data['updated_ct'] += 1
|
||||
else:
|
||||
log_data['unmodified_ct'] += 1
|
||||
else:
|
||||
# if the file goes missing, ansible removed it (likely via clear_facts)
|
||||
host.ansible_facts = {}
|
||||
host.ansible_facts_modified = now()
|
||||
hosts_to_update.append(host)
|
||||
system_tracking_logger.info('Facts cleared for inventory {} host {}'.format(smart_str(host.inventory.name), smart_str(host.name)))
|
||||
host.save()
|
||||
log_data['cleared_ct'] += 1
|
||||
if len(hosts_to_update) > 100:
|
||||
self.inventory.hosts.bulk_update(hosts_to_update, ['ansible_facts', 'ansible_facts_modified'])
|
||||
hosts_to_update = []
|
||||
if hosts_to_update:
|
||||
self.inventory.hosts.bulk_update(hosts_to_update, ['ansible_facts', 'ansible_facts_modified'])
|
||||
|
||||
|
||||
class LaunchTimeConfigBase(BaseModel):
|
||||
|
||||
@@ -426,7 +426,7 @@ class BaseTask(object):
|
||||
"""
|
||||
instance.log_lifecycle("post_run")
|
||||
|
||||
def final_run_hook(self, instance, status, private_data_dir, fact_modification_times):
|
||||
def final_run_hook(self, instance, status, private_data_dir):
|
||||
"""
|
||||
Hook for any steps to run after job/task is marked as complete.
|
||||
"""
|
||||
@@ -469,7 +469,6 @@ class BaseTask(object):
|
||||
self.instance = self.update_model(pk, status='running', start_args='') # blank field to remove encrypted passwords
|
||||
self.instance.websocket_emit_status("running")
|
||||
status, rc = 'error', None
|
||||
fact_modification_times = {}
|
||||
self.runner_callback.event_ct = 0
|
||||
|
||||
'''
|
||||
@@ -498,14 +497,6 @@ class BaseTask(object):
|
||||
if not os.path.exists(settings.AWX_ISOLATION_BASE_PATH):
|
||||
raise RuntimeError('AWX_ISOLATION_BASE_PATH=%s does not exist' % settings.AWX_ISOLATION_BASE_PATH)
|
||||
|
||||
# Fetch "cached" fact data from prior runs and put on the disk
|
||||
# where ansible expects to find it
|
||||
if getattr(self.instance, 'use_fact_cache', False):
|
||||
self.instance.start_job_fact_cache(
|
||||
os.path.join(private_data_dir, 'artifacts', str(self.instance.id), 'fact_cache'),
|
||||
fact_modification_times,
|
||||
)
|
||||
|
||||
# May have to serialize the value
|
||||
private_data_files, ssh_key_data = self.build_private_data_files(self.instance, private_data_dir)
|
||||
passwords = self.build_passwords(self.instance, kwargs)
|
||||
@@ -646,7 +637,7 @@ class BaseTask(object):
|
||||
self.instance.send_notification_templates('succeeded' if status == 'successful' else 'failed')
|
||||
|
||||
try:
|
||||
self.final_run_hook(self.instance, status, private_data_dir, fact_modification_times)
|
||||
self.final_run_hook(self.instance, status, private_data_dir)
|
||||
except Exception:
|
||||
logger.exception('{} Final run hook errored.'.format(self.instance.log_format))
|
||||
|
||||
@@ -1066,12 +1057,19 @@ class RunJob(SourceControlMixin, BaseTask):
|
||||
# ran inside of the event saving code
|
||||
update_smart_memberships_for_inventory(job.inventory)
|
||||
|
||||
# Fetch "cached" fact data from prior runs and put on the disk
|
||||
# where ansible expects to find it
|
||||
if job.use_fact_cache:
|
||||
self.facts_write_time = self.instance.start_job_fact_cache(os.path.join(private_data_dir, 'artifacts', str(job.id), 'fact_cache'))
|
||||
|
||||
def build_project_dir(self, job, private_data_dir):
|
||||
self.sync_and_copy(job.project, private_data_dir, scm_branch=job.scm_branch)
|
||||
|
||||
def final_run_hook(self, job, status, private_data_dir, fact_modification_times):
|
||||
super(RunJob, self).final_run_hook(job, status, private_data_dir, fact_modification_times)
|
||||
if not private_data_dir:
|
||||
def post_run_hook(self, job, status):
|
||||
super(RunJob, self).post_run_hook(job, status)
|
||||
job.refresh_from_db(fields=['job_env'])
|
||||
private_data_dir = job.job_env.get('AWX_PRIVATE_DATA_DIR')
|
||||
if (not private_data_dir) or (not hasattr(self, 'facts_write_time')):
|
||||
# If there's no private data dir, that means we didn't get into the
|
||||
# actual `run()` call; this _usually_ means something failed in
|
||||
# the pre_run_hook method
|
||||
@@ -1079,9 +1077,11 @@ class RunJob(SourceControlMixin, BaseTask):
|
||||
if job.use_fact_cache:
|
||||
job.finish_job_fact_cache(
|
||||
os.path.join(private_data_dir, 'artifacts', str(job.id), 'fact_cache'),
|
||||
fact_modification_times,
|
||||
self.facts_write_time,
|
||||
)
|
||||
|
||||
def final_run_hook(self, job, status, private_data_dir):
|
||||
super(RunJob, self).final_run_hook(job, status, private_data_dir)
|
||||
try:
|
||||
inventory = job.inventory
|
||||
except Inventory.DoesNotExist:
|
||||
|
||||
@@ -36,15 +36,14 @@ def job(mocker, hosts, inventory):
|
||||
|
||||
def test_start_job_fact_cache(hosts, job, inventory, tmpdir):
|
||||
fact_cache = os.path.join(tmpdir, 'facts')
|
||||
modified_times = {}
|
||||
job.start_job_fact_cache(fact_cache, modified_times, 0)
|
||||
last_modified = job.start_job_fact_cache(fact_cache, timeout=0)
|
||||
|
||||
for host in hosts:
|
||||
filepath = os.path.join(fact_cache, host.name)
|
||||
assert os.path.exists(filepath)
|
||||
with open(filepath, 'r') as f:
|
||||
assert f.read() == json.dumps(host.ansible_facts)
|
||||
assert filepath in modified_times
|
||||
assert os.path.getmtime(filepath) <= last_modified
|
||||
|
||||
|
||||
def test_fact_cache_with_invalid_path_traversal(job, inventory, tmpdir, mocker):
|
||||
@@ -58,18 +57,16 @@ def test_fact_cache_with_invalid_path_traversal(job, inventory, tmpdir, mocker):
|
||||
)
|
||||
|
||||
fact_cache = os.path.join(tmpdir, 'facts')
|
||||
job.start_job_fact_cache(fact_cache, {}, 0)
|
||||
job.start_job_fact_cache(fact_cache, timeout=0)
|
||||
# a file called "foo" should _not_ be written outside the facts dir
|
||||
assert os.listdir(os.path.join(fact_cache, '..')) == ['facts']
|
||||
|
||||
|
||||
def test_finish_job_fact_cache_with_existing_data(job, hosts, inventory, mocker, tmpdir):
|
||||
fact_cache = os.path.join(tmpdir, 'facts')
|
||||
modified_times = {}
|
||||
job.start_job_fact_cache(fact_cache, modified_times, 0)
|
||||
last_modified = job.start_job_fact_cache(fact_cache, timeout=0)
|
||||
|
||||
for h in hosts:
|
||||
h.save = mocker.Mock()
|
||||
bulk_update = mocker.patch('django.db.models.query.QuerySet.bulk_update')
|
||||
|
||||
ansible_facts_new = {"foo": "bar"}
|
||||
filepath = os.path.join(fact_cache, hosts[1].name)
|
||||
@@ -83,23 +80,20 @@ def test_finish_job_fact_cache_with_existing_data(job, hosts, inventory, mocker,
|
||||
new_modification_time = time.time() + 3600
|
||||
os.utime(filepath, (new_modification_time, new_modification_time))
|
||||
|
||||
job.finish_job_fact_cache(fact_cache, modified_times)
|
||||
job.finish_job_fact_cache(fact_cache, last_modified)
|
||||
|
||||
for host in (hosts[0], hosts[2], hosts[3]):
|
||||
host.save.assert_not_called()
|
||||
assert host.ansible_facts == {"a": 1, "b": 2}
|
||||
assert host.ansible_facts_modified is None
|
||||
assert hosts[1].ansible_facts == ansible_facts_new
|
||||
hosts[1].save.assert_called_once_with(update_fields=['ansible_facts', 'ansible_facts_modified'])
|
||||
bulk_update.assert_called_once_with([hosts[1]], ['ansible_facts', 'ansible_facts_modified'])
|
||||
|
||||
|
||||
def test_finish_job_fact_cache_with_bad_data(job, hosts, inventory, mocker, tmpdir):
|
||||
fact_cache = os.path.join(tmpdir, 'facts')
|
||||
modified_times = {}
|
||||
job.start_job_fact_cache(fact_cache, modified_times, 0)
|
||||
last_modified = job.start_job_fact_cache(fact_cache, timeout=0)
|
||||
|
||||
for h in hosts:
|
||||
h.save = mocker.Mock()
|
||||
bulk_update = mocker.patch('django.db.models.query.QuerySet.bulk_update')
|
||||
|
||||
for h in hosts:
|
||||
filepath = os.path.join(fact_cache, h.name)
|
||||
@@ -109,26 +103,22 @@ def test_finish_job_fact_cache_with_bad_data(job, hosts, inventory, mocker, tmpd
|
||||
new_modification_time = time.time() + 3600
|
||||
os.utime(filepath, (new_modification_time, new_modification_time))
|
||||
|
||||
job.finish_job_fact_cache(fact_cache, modified_times)
|
||||
job.finish_job_fact_cache(fact_cache, last_modified)
|
||||
|
||||
for h in hosts:
|
||||
h.save.assert_not_called()
|
||||
bulk_update.assert_not_called()
|
||||
|
||||
|
||||
def test_finish_job_fact_cache_clear(job, hosts, inventory, mocker, tmpdir):
|
||||
fact_cache = os.path.join(tmpdir, 'facts')
|
||||
modified_times = {}
|
||||
job.start_job_fact_cache(fact_cache, modified_times, 0)
|
||||
last_modified = job.start_job_fact_cache(fact_cache, timeout=0)
|
||||
|
||||
for h in hosts:
|
||||
h.save = mocker.Mock()
|
||||
bulk_update = mocker.patch('django.db.models.query.QuerySet.bulk_update')
|
||||
|
||||
os.remove(os.path.join(fact_cache, hosts[1].name))
|
||||
job.finish_job_fact_cache(fact_cache, modified_times)
|
||||
job.finish_job_fact_cache(fact_cache, last_modified)
|
||||
|
||||
for host in (hosts[0], hosts[2], hosts[3]):
|
||||
host.save.assert_not_called()
|
||||
assert host.ansible_facts == {"a": 1, "b": 2}
|
||||
assert host.ansible_facts_modified is None
|
||||
assert hosts[1].ansible_facts == {}
|
||||
hosts[1].save.assert_called_once_with()
|
||||
bulk_update.assert_called_once_with([hosts[1]], ['ansible_facts', 'ansible_facts_modified'])
|
||||
|
||||
@@ -90,6 +90,7 @@ __all__ = [
|
||||
'deepmerge',
|
||||
'get_event_partition_epoch',
|
||||
'cleanup_new_process',
|
||||
'log_excess_runtime',
|
||||
]
|
||||
|
||||
|
||||
@@ -1215,15 +1216,30 @@ def cleanup_new_process(func):
|
||||
return wrapper_cleanup_new_process
|
||||
|
||||
|
||||
def log_excess_runtime(func_logger, cutoff=5.0):
|
||||
def log_excess_runtime(func_logger, cutoff=5.0, debug_cutoff=5.0, msg=None, add_log_data=False):
|
||||
def log_excess_runtime_decorator(func):
|
||||
@functools.wraps(func)
|
||||
def _new_func(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
return_value = func(*args, **kwargs)
|
||||
delta = time.time() - start_time
|
||||
if delta > cutoff:
|
||||
logger.info(f'Running {func.__name__!r} took {delta:.2f}s')
|
||||
log_data = {'name': repr(func.__name__)}
|
||||
|
||||
if add_log_data:
|
||||
return_value = func(*args, log_data=log_data, **kwargs)
|
||||
else:
|
||||
return_value = func(*args, **kwargs)
|
||||
|
||||
log_data['delta'] = time.time() - start_time
|
||||
if isinstance(return_value, dict):
|
||||
log_data.update(return_value)
|
||||
|
||||
if msg is None:
|
||||
record_msg = 'Running {name} took {delta:.2f}s'
|
||||
else:
|
||||
record_msg = msg
|
||||
if log_data['delta'] > cutoff:
|
||||
func_logger.info(record_msg.format(**log_data))
|
||||
elif log_data['delta'] > debug_cutoff:
|
||||
func_logger.debug(record_msg.format(**log_data))
|
||||
return return_value
|
||||
|
||||
return _new_func
|
||||
|
||||
@@ -84,7 +84,7 @@ options:
|
||||
type: str
|
||||
execution_environment:
|
||||
description:
|
||||
- Execution Environment to use for the JT.
|
||||
- Execution Environment to use for the job template.
|
||||
type: str
|
||||
custom_virtualenv:
|
||||
description:
|
||||
|
||||
@@ -208,6 +208,29 @@ options:
|
||||
description:
|
||||
- Limit to act on, applied as a prompt, if job template prompts for limit
|
||||
type: str
|
||||
forks:
|
||||
description:
|
||||
- The number of parallel or simultaneous processes to use while executing the playbook, if job template prompts for forks
|
||||
type: int
|
||||
job_slice_count:
|
||||
description:
|
||||
- The number of jobs to slice into at runtime, if job template prompts for job slices. Will cause the Job Template to launch a workflow if value is greater than 1.
|
||||
type: int
|
||||
default: '1'
|
||||
timeout:
|
||||
description:
|
||||
- Maximum time in seconds to wait for a job to finish (server-side), if job template prompts for timeout.
|
||||
type: int
|
||||
execution_environment:
|
||||
description:
|
||||
- Name of Execution Environment to be applied to job as launch-time prompts.
|
||||
type: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of Execution Environment to be applied to job as launch-time prompts.
|
||||
- Uniqueness is not handled rigorously.
|
||||
type: str
|
||||
diff_mode:
|
||||
description:
|
||||
- Run diff mode, applied as a prompt, if job template prompts for diff mode
|
||||
@@ -298,7 +321,6 @@ options:
|
||||
related:
|
||||
description:
|
||||
- Related items to this workflow node.
|
||||
- Must include credentials, failure_nodes, always_nodes, success_nodes, even if empty.
|
||||
type: dict
|
||||
suboptions:
|
||||
always_nodes:
|
||||
@@ -342,6 +364,46 @@ options:
|
||||
description:
|
||||
- Name Credentials to be applied to job as launch-time prompts.
|
||||
elements: str
|
||||
organization:
|
||||
description:
|
||||
- Name of key for use in model for organizational reference
|
||||
type: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The organization of the credentials exists in.
|
||||
type: str
|
||||
labels:
|
||||
description:
|
||||
- Labels to be applied to job as launch-time prompts.
|
||||
- List of Label names.
|
||||
- Uniqueness is not handled rigorously.
|
||||
type: list
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name Labels to be applied to job as launch-time prompts.
|
||||
elements: str
|
||||
organization:
|
||||
description:
|
||||
- Name of key for use in model for organizational reference
|
||||
type: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The organization of the label node exists in.
|
||||
type: str
|
||||
instance_groups:
|
||||
description:
|
||||
- Instance groups to be applied to job as launch-time prompts.
|
||||
- List of Instance group names.
|
||||
- Uniqueness is not handled rigorously.
|
||||
type: list
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of Instance groups to be applied to job as launch-time prompts.
|
||||
elements: str
|
||||
destroy_current_nodes:
|
||||
description:
|
||||
- Set in order to destroy current workflow_nodes on the workflow.
|
||||
@@ -474,11 +536,21 @@ EXAMPLES = '''
|
||||
name: Default
|
||||
name: job template 2
|
||||
type: job_template
|
||||
execution_environment:
|
||||
name: My EE
|
||||
related:
|
||||
success_nodes: []
|
||||
failure_nodes: []
|
||||
always_nodes: []
|
||||
credentials: []
|
||||
credentials:
|
||||
- name: cyberark
|
||||
organization:
|
||||
name: Default
|
||||
instance_groups:
|
||||
- name: SunCavanaugh Cloud
|
||||
- name: default
|
||||
labels:
|
||||
- name: Custom Label
|
||||
- name: Another Custom Label
|
||||
organization:
|
||||
name: Default
|
||||
register: result
|
||||
|
||||
'''
|
||||
@@ -547,6 +619,9 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
|
||||
'limit',
|
||||
'diff_mode',
|
||||
'verbosity',
|
||||
'forks',
|
||||
'job_slice_count',
|
||||
'timeout',
|
||||
'all_parents_must_converge',
|
||||
'state',
|
||||
):
|
||||
@@ -555,6 +630,10 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
|
||||
workflow_node_fields[field_name] = field_val
|
||||
if workflow_node['identifier']:
|
||||
search_fields = {'identifier': workflow_node['identifier']}
|
||||
if 'execution_environment' in workflow_node:
|
||||
workflow_node_fields['execution_environment'] = module.get_one(
|
||||
'execution_environments', name_or_id=workflow_node['execution_environment']['name']
|
||||
)['id']
|
||||
|
||||
# Set Search fields
|
||||
search_fields['workflow_job_template'] = workflow_node_fields['workflow_job_template'] = workflow_id
|
||||
@@ -641,15 +720,26 @@ def create_workflow_nodes_association(module, response, workflow_nodes, workflow
|
||||
# Get id's for association fields
|
||||
association_fields = {}
|
||||
|
||||
for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'):
|
||||
for association in (
|
||||
'always_nodes',
|
||||
'success_nodes',
|
||||
'failure_nodes',
|
||||
'credentials',
|
||||
'labels',
|
||||
'instance_groups',
|
||||
):
|
||||
# Extract out information if it exists
|
||||
# Test if it is defined, else move to next association.
|
||||
prompt_lookup = ['credentials', 'labels', 'instance_groups']
|
||||
if association in workflow_node['related']:
|
||||
id_list = []
|
||||
lookup_data = {}
|
||||
for sub_name in workflow_node['related'][association]:
|
||||
if association == 'credentials':
|
||||
endpoint = 'credentials'
|
||||
lookup_data = {'name': sub_name['name']}
|
||||
if association in prompt_lookup:
|
||||
endpoint = association
|
||||
if 'organization' in sub_name:
|
||||
lookup_data['organization'] = module.resolve_name_to_id('organizations', sub_name['organization']['name'])
|
||||
lookup_data['name'] = sub_name['name']
|
||||
else:
|
||||
endpoint = 'workflow_job_template_nodes'
|
||||
lookup_data = {'identifier': sub_name['identifier']}
|
||||
|
||||
@@ -729,6 +729,24 @@
|
||||
organization:
|
||||
name: Default
|
||||
type: workflow_job_template
|
||||
forks: 12
|
||||
job_slice_count: 2
|
||||
timeout: 23
|
||||
execution_environment:
|
||||
name: "{{ ee1 }}"
|
||||
related:
|
||||
credentials:
|
||||
- name: "{{ scm_cred_name }}"
|
||||
organization:
|
||||
name: Default
|
||||
instance_groups:
|
||||
- name: "{{ ig1 }}"
|
||||
- name: "{{ ig2 }}"
|
||||
labels:
|
||||
- name: "{{ label1 }}"
|
||||
- name: "{{ label2 }}"
|
||||
organization:
|
||||
name: "{{ org_name }}"
|
||||
register: result
|
||||
|
||||
- name: Delete copied workflow job template
|
||||
|
||||
@@ -10,7 +10,7 @@ cryptography>=36.0.2,<37.0.0 # Until paramiko fixes https://github.com/paramiko/
|
||||
Cython<3 # Since the bump to PyYAML 5.4.1 this is now a mandatory dep
|
||||
daphne
|
||||
distro
|
||||
django==3.2.13 # see UPGRADE BLOCKERs
|
||||
django==3.2.16 # see UPGRADE BLOCKERs https://github.com/ansible/awx/security/dependabot/67
|
||||
django-auth-ldap
|
||||
django-cors-headers>=3.5.0
|
||||
django-crum
|
||||
|
||||
@@ -86,7 +86,7 @@ defusedxml==0.6.0
|
||||
# social-auth-core
|
||||
distro==1.5.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
django==3.2.13
|
||||
django==3.2.16
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# channels
|
||||
|
||||
@@ -13,6 +13,7 @@ receptor_image: quay.io/ansible/receptor:devel
|
||||
# Keys for signing work
|
||||
receptor_rsa_bits: 4096
|
||||
receptor_work_sign_reconfigure: false
|
||||
sign_work: no # currently defaults to no because openssl version mismatch causes "unknown block type PRIVATE KEY"
|
||||
work_sign_key_dir: '../_sources/receptor'
|
||||
work_sign_private_keyfile: "{{ work_sign_key_dir }}/work_private_key.pem"
|
||||
work_sign_public_keyfile: "{{ work_sign_key_dir }}/work_public_key.pem"
|
||||
|
||||
@@ -86,11 +86,13 @@
|
||||
command: openssl genrsa -out {{ work_sign_private_keyfile }} {{ receptor_rsa_bits }}
|
||||
args:
|
||||
creates: "{{ work_sign_private_keyfile }}"
|
||||
when: sign_work | bool
|
||||
|
||||
- name: Generate public RSA key for signing work
|
||||
command: openssl rsa -in {{ work_sign_private_keyfile }} -out {{ work_sign_public_keyfile }} -outform PEM -pubout
|
||||
args:
|
||||
creates: "{{ work_sign_public_keyfile }}"
|
||||
when: sign_work | bool
|
||||
|
||||
- name: Include LDAP tasks if enabled
|
||||
include_tasks: ldap.yml
|
||||
@@ -128,6 +130,8 @@
|
||||
src: "receptor-hop.conf.j2"
|
||||
dest: "{{ sources_dest }}/receptor/receptor-hop.conf"
|
||||
mode: '0600'
|
||||
when:
|
||||
- execution_node_count | int > 0
|
||||
|
||||
- name: Render Receptor Worker Config(s)
|
||||
template:
|
||||
|
||||
@@ -43,8 +43,10 @@ services:
|
||||
- "../../docker-compose/_sources/SECRET_KEY:/etc/tower/SECRET_KEY"
|
||||
- "../../docker-compose/_sources/receptor/receptor-awx-{{ loop.index }}.conf:/etc/receptor/receptor.conf"
|
||||
- "../../docker-compose/_sources/receptor/receptor-awx-{{ loop.index }}.conf.lock:/etc/receptor/receptor.conf.lock"
|
||||
{% if sign_work|bool %}
|
||||
- "../../docker-compose/_sources/receptor/work_public_key.pem:/etc/receptor/work_public_key.pem"
|
||||
- "../../docker-compose/_sources/receptor/work_private_key.pem:/etc/receptor/work_private_key.pem"
|
||||
{% endif %}
|
||||
# - "../../docker-compose/_sources/certs:/etc/receptor/certs" # TODO: optionally generate certs
|
||||
- "/sys/fs/cgroup:/sys/fs/cgroup"
|
||||
- "~/.kube/config:/var/lib/awx/.kube/config"
|
||||
|
||||
@@ -11,12 +11,16 @@
|
||||
- tcp-listener:
|
||||
port: 2222
|
||||
|
||||
{% if sign_work|bool %}
|
||||
- work-signing:
|
||||
privatekey: /etc/receptor/work_private_key.pem
|
||||
tokenexpiration: 1m
|
||||
{% endif %}
|
||||
|
||||
{% if sign_work|bool %}
|
||||
- work-verification:
|
||||
publickey: /etc/receptor/work_public_key.pem
|
||||
{% endif %}
|
||||
|
||||
{% for i in range(item | int + 1, control_plane_node_count | int + 1) %}
|
||||
- tcp-peer:
|
||||
@@ -40,7 +44,7 @@
|
||||
command: ansible-runner
|
||||
params: worker
|
||||
allowruntimeparams: true
|
||||
verifysignature: true
|
||||
verifysignature: {{ sign_work }}
|
||||
|
||||
- work-kubernetes:
|
||||
worktype: kubernetes-runtime-auth
|
||||
@@ -48,7 +52,7 @@
|
||||
allowruntimeauth: true
|
||||
allowruntimepod: true
|
||||
allowruntimeparams: true
|
||||
verifysignature: true
|
||||
verifysignature: {{ sign_work }}
|
||||
|
||||
- work-kubernetes:
|
||||
worktype: kubernetes-incluster-auth
|
||||
@@ -56,4 +60,4 @@
|
||||
allowruntimeauth: true
|
||||
allowruntimepod: true
|
||||
allowruntimeparams: true
|
||||
verifysignature: true
|
||||
verifysignature: {{ sign_work }}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
command: ansible-runner
|
||||
params: worker
|
||||
allowruntimeparams: true
|
||||
verifysignature: true
|
||||
verifysignature: {{ sign_work }}
|
||||
|
||||
- control-service:
|
||||
service: control
|
||||
|
||||
Reference in New Issue
Block a user