From cdfc9e05d41ebfc6f751bea73849872cc8558974 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 15 May 2020 17:52:08 -0400 Subject: [PATCH 01/53] Fix offline RHEL 8 builds This was causing our EL8 Brew builds to break, because it wasn't being vendored. This is in fact required for python3. It was being resolved as a dependency of other things (see list at end of line), so it was being downloaded on-the-fly since our normal builds have internet access. It only broke when it wasn't vendored for offline builds. --- requirements/requirements_ansible.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 5e8569e0f0..3ef3f1318d 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -26,7 +26,7 @@ azure-mgmt-loganalytics==0.2.0 # via -r /awx_devel/requirements/requirements_an azure-mgmt-marketplaceordering==0.1.0 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-monitor==0.5.2 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-network==2.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-nspkg==2.0.0; python_version < "3" # via -r /awx_devel/requirements/requirements_ansible.in, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web +azure-mgmt-nspkg==2.0.0 # via -r /awx_devel/requirements/requirements_ansible.in, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web azure-mgmt-rdbms==1.4.1 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-redis==5.0.0 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-resource==2.1.0 # via -r /awx_devel/requirements/requirements_ansible.in From c4d9b81c55d273309d290b8cb218d5a561e73624 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 18 May 2020 13:23:19 -0400 Subject: [PATCH 02/53] Fixes bug where all_parents_must_converge was not being set for new or existing approval nodes. --- .../workflow-maker/workflow-maker.controller.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 96cc1ad7e4..d701d220d6 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -148,11 +148,14 @@ export default ['$scope', 'TemplatesService', Object.keys(nodeRef).map((workflowMakerNodeId) => { const node = nodeRef[workflowMakerNodeId]; + const all_parents_must_converge = _.get(node, 'all_parents_must_converge', false); if (node.isNew) { if (node.unifiedJobTemplate && node.unifiedJobTemplate.unified_job_type === "workflow_approval") { addPromises.push(TemplatesService.addWorkflowNode({ url: $scope.workflowJobTemplateObj.related.workflow_nodes, - data: {} + data: { + all_parents_must_converge + } }).then(({data: newNodeData}) => { Rest.setUrl(newNodeData.related.create_approval_template); approvalTemplatePromises.push(Rest.post({ @@ -234,6 +237,14 @@ export default ['$scope', 'TemplatesService', }); })); } + if (node.originalNodeObject.all_parents_must_converge !== all_parents_must_converge) { + editPromises.push(TemplatesService.editWorkflowNode({ + id: node.originalNodeObject.id, + data: { + all_parents_must_converge + } + })); + } } else { editPromises.push(TemplatesService.editWorkflowNode({ id: node.originalNodeObject.id, From b83db0500fcc541cb4f627a1c8d7d395799e5752 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 19 May 2020 10:52:09 -0400 Subject: [PATCH 03/53] Enable management job notications for admins --- .../notifications/notification.controller.js | 6 ++++-- .../notifications/notification.route.js | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/management-jobs/notifications/notification.controller.js b/awx/ui/client/src/management-jobs/notifications/notification.controller.js index 62e8dff8a3..cc3b0b74be 100644 --- a/awx/ui/client/src/management-jobs/notifications/notification.controller.js +++ b/awx/ui/client/src/management-jobs/notifications/notification.controller.js @@ -6,10 +6,10 @@ export default [ 'NotificationsList', 'GetBasePath', 'ToggleNotification', 'NotificationsListInit', - '$stateParams', 'Dataset', '$scope', + '$stateParams', 'Dataset', '$scope', 'isAdmin', function( NotificationsList, GetBasePath, ToggleNotification, NotificationsListInit, - $stateParams, Dataset, $scope) { + $stateParams, Dataset, $scope, isAdmin) { var defaultUrl = GetBasePath('system_job_templates'), list = NotificationsList, id = $stateParams.management_id; @@ -19,6 +19,8 @@ export default $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.sufficientRoleForNotifToggle = isAdmin; + NotificationsListInit({ scope: $scope, url: defaultUrl, diff --git a/awx/ui/client/src/management-jobs/notifications/notification.route.js b/awx/ui/client/src/management-jobs/notifications/notification.route.js index a6b1f30d8a..7b6bf64585 100644 --- a/awx/ui/client/src/management-jobs/notifications/notification.route.js +++ b/awx/ui/client/src/management-jobs/notifications/notification.route.js @@ -39,6 +39,19 @@ export default { let path = `${GetBasePath('system_job_templates')}${$stateParams.management_id}`; Rest.setUrl(path); return Rest.get(path).then((res) => res.data); + }], + isAdmin: ['Rest', 'GetBasePath', function(Rest, GetBasePath) { + Rest.setUrl(GetBasePath('me')); + return Rest.get() + .then((res) => { + if (res.data && res.data.results && res.data.count && res.data.results[0] && res.data.results[0].is_superuser) { + return true; + } + return false; + }) + .catch(() => { + return false; + }); }] }, ncyBreadcrumb: { From b6d3c3c1a3f36c256a9bbff967701efb85039354 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 11 May 2020 16:58:13 -0400 Subject: [PATCH 04/53] drastically optimize job host summary creation see: https://github.com/ansible/awx/issues/6991 --- awx/main/models/events.py | 33 +++++++------ awx/main/tasks.py | 2 + .../tests/functional/models/test_events.py | 48 ++++++++++++++++++- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/awx/main/models/events.py b/awx/main/models/events.py index d5cd2d76e7..80d530a2c3 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -7,7 +7,7 @@ from collections import defaultdict from django.db import models, DatabaseError, connection from django.utils.dateparse import parse_datetime from django.utils.text import Truncator -from django.utils.timezone import utc +from django.utils.timezone import utc, now from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text @@ -407,11 +407,14 @@ class BasePlaybookEvent(CreatedModifiedModel): except (KeyError, ValueError): kwargs.pop('created', None) + host_map = kwargs.pop('host_map', {}) + sanitize_event_keys(kwargs, cls.VALID_KEYS) workflow_job_id = kwargs.pop('workflow_job_id', None) event = cls(**kwargs) if workflow_job_id: setattr(event, 'workflow_job_id', workflow_job_id) + setattr(event, 'host_map', host_map) event._update_from_event_data() return event @@ -484,8 +487,10 @@ class JobEvent(BasePlaybookEvent): if not self.job or not self.job.inventory: logger.info('Event {} missing job or inventory, host summaries not updated'.format(self.pk)) return - qs = self.job.inventory.hosts.filter(name__in=hostnames) job = self.job + + from awx.main.models.jobs import JobHostSummary # circular import + summaries = dict() for host in hostnames: host_stats = {} for stat in ('changed', 'dark', 'failures', 'ignored', 'ok', 'processed', 'rescued', 'skipped'): @@ -493,20 +498,18 @@ class JobEvent(BasePlaybookEvent): host_stats[stat] = self.event_data.get(stat, {}).get(host, 0) except AttributeError: # in case event_data[stat] isn't a dict. pass - if qs.filter(name=host).exists(): - host_actual = qs.get(name=host) - host_summary, created = job.job_host_summaries.get_or_create(host=host_actual, host_name=host_actual.name, defaults=host_stats) - else: - host_summary, created = job.job_host_summaries.get_or_create(host_name=host, defaults=host_stats) + host_id = self.host_map.get(host, None) + summaries.setdefault( + (host_id, host), + JobHostSummary(created=now(), modified=now(), job_id=job.id, host_id=host_id, host_name=host) + ) + host_summary = summaries[(host_id, host)] - if not created: - update_fields = [] - for stat, value in host_stats.items(): - if getattr(host_summary, stat) != value: - setattr(host_summary, stat, value) - update_fields.append(stat) - if update_fields: - host_summary.save(update_fields=update_fields) + for stat, value in host_stats.items(): + if getattr(host_summary, stat) != value: + setattr(host_summary, stat, value) + + JobHostSummary.objects.bulk_create(summaries.values()) @property def job_verbosity(self): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3a765364a3..38fd1f5207 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1215,6 +1215,8 @@ class BaseTask(object): else: event_data['host_name'] = '' event_data['host_id'] = '' + if event_data.get('event') == 'playbook_on_stats': + event_data['host_map'] = self.host_map if isinstance(self, RunProjectUpdate): # it's common for Ansible's SCM modules to print diff --git a/awx/main/tests/functional/models/test_events.py b/awx/main/tests/functional/models/test_events.py index a61c3fda25..0d2530b968 100644 --- a/awx/main/tests/functional/models/test_events.py +++ b/awx/main/tests/functional/models/test_events.py @@ -1,7 +1,9 @@ from unittest import mock import pytest -from awx.main.models import Job, JobEvent +from django.utils.timezone import now + +from awx.main.models import Job, JobEvent, Inventory, Host @pytest.mark.django_db @@ -61,3 +63,47 @@ def test_parent_failed(emit, event): assert events.count() == 2 for e in events.all(): assert e.failed is True + + +@pytest.mark.django_db +def test_host_summary_generation(): + hostnames = [f'Host {i}' for i in range(5000)] + inv = Inventory() + inv.save() + Host.objects.bulk_create([ + Host(created=now(), modified=now(), name=h, inventory_id=inv.id) + for h in hostnames + ]) + j = Job(inventory=inv) + j.save() + host_map = dict((host.name, host.id) for host in inv.hosts.all()) + JobEvent.create_from_data( + job_id=j.pk, + parent_uuid='abc123', + event='playbook_on_stats', + event_data={ + 'ok': dict((hostname, len(hostname)) for hostname in hostnames), + 'changed': {}, + 'dark': {}, + 'failures': {}, + 'ignored': {}, + 'processed': {}, + 'rescued': {}, + 'skipped': {}, + }, + host_map=host_map + ).save() + + assert j.job_host_summaries.count() == len(hostnames) + assert sorted([s.host_name for s in j.job_host_summaries.all()]) == sorted(hostnames) + + for s in j.job_host_summaries.all(): + assert host_map[s.host_name] == s.host_id + assert s.ok == len(s.host_name) + assert s.changed == 0 + assert s.dark == 0 + assert s.failures == 0 + assert s.ignored == 0 + assert s.processed == 0 + assert s.rescued == 0 + assert s.skipped == 0 From 3ea642f212f4a88438e0efba020185a60454c885 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 12 May 2020 09:38:59 -0400 Subject: [PATCH 05/53] properly handle host summary bulk updates if hosts go missing --- awx/main/models/events.py | 18 ++++---- .../tests/functional/models/test_events.py | 44 ++++++++++++++++++- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/awx/main/models/events.py b/awx/main/models/events.py index 80d530a2c3..57de629a8f 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -489,25 +489,23 @@ class JobEvent(BasePlaybookEvent): return job = self.job - from awx.main.models.jobs import JobHostSummary # circular import + from awx.main.models import Host, JobHostSummary # circular import + existing = Host.objects.filter(id__in=self.host_map.values()).values_list('id', flat=True) + summaries = dict() for host in hostnames: + host_id = self.host_map.get(host, None) + if host_id not in existing: + host_id = None host_stats = {} for stat in ('changed', 'dark', 'failures', 'ignored', 'ok', 'processed', 'rescued', 'skipped'): try: host_stats[stat] = self.event_data.get(stat, {}).get(host, 0) except AttributeError: # in case event_data[stat] isn't a dict. pass - host_id = self.host_map.get(host, None) - summaries.setdefault( - (host_id, host), - JobHostSummary(created=now(), modified=now(), job_id=job.id, host_id=host_id, host_name=host) + summaries[(host_id, host)] = JobHostSummary( + created=now(), modified=now(), job_id=job.id, host_id=host_id, host_name=host, **host_stats ) - host_summary = summaries[(host_id, host)] - - for stat, value in host_stats.items(): - if getattr(host_summary, stat) != value: - setattr(host_summary, stat, value) JobHostSummary.objects.bulk_create(summaries.values()) diff --git a/awx/main/tests/functional/models/test_events.py b/awx/main/tests/functional/models/test_events.py index 0d2530b968..6dd0cac06a 100644 --- a/awx/main/tests/functional/models/test_events.py +++ b/awx/main/tests/functional/models/test_events.py @@ -67,7 +67,7 @@ def test_parent_failed(emit, event): @pytest.mark.django_db def test_host_summary_generation(): - hostnames = [f'Host {i}' for i in range(5000)] + hostnames = [f'Host {i}' for i in range(500)] inv = Inventory() inv.save() Host.objects.bulk_create([ @@ -107,3 +107,45 @@ def test_host_summary_generation(): assert s.processed == 0 assert s.rescued == 0 assert s.skipped == 0 + + +@pytest.mark.django_db +def test_host_summary_generation_with_deleted_hosts(): + hostnames = [f'Host {i}' for i in range(10)] + inv = Inventory() + inv.save() + Host.objects.bulk_create([ + Host(created=now(), modified=now(), name=h, inventory_id=inv.id) + for h in hostnames + ]) + j = Job(inventory=inv) + j.save() + host_map = dict((host.name, host.id) for host in inv.hosts.all()) + + # delete half of the hosts during the playbook run + for h in inv.hosts.all()[:5]: + h.delete() + + JobEvent.create_from_data( + job_id=j.pk, + parent_uuid='abc123', + event='playbook_on_stats', + event_data={ + 'ok': dict((hostname, len(hostname)) for hostname in hostnames), + 'changed': {}, + 'dark': {}, + 'failures': {}, + 'ignored': {}, + 'processed': {}, + 'rescued': {}, + 'skipped': {}, + }, + host_map=host_map + ).save() + + + ids = sorted([s.host_id or -1 for s in j.job_host_summaries.order_by('id').all()]) + names = sorted([s.host_name for s in j.job_host_summaries.all()]) + assert ids == [-1, -1, -1, -1, -1, 6, 7, 8, 9, 10] + assert names == ['Host 0', 'Host 1', 'Host 2', 'Host 3', 'Host 4', 'Host 5', + 'Host 6', 'Host 7', 'Host 8', 'Host 9'] From 59d457207c8832e7c369a5112d6fb9b2c2b8c7be Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 12 May 2020 14:46:57 -0400 Subject: [PATCH 06/53] properly update .failed, .last_job_id, and last_job_host_summary --- awx/main/models/events.py | 20 ++++++++++++++++++- awx/main/models/jobs.py | 14 ------------- .../tests/functional/models/test_events.py | 6 +++++- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/awx/main/models/events.py b/awx/main/models/events.py index 57de629a8f..e8d87253eb 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -503,12 +503,30 @@ class JobEvent(BasePlaybookEvent): host_stats[stat] = self.event_data.get(stat, {}).get(host, 0) except AttributeError: # in case event_data[stat] isn't a dict. pass - summaries[(host_id, host)] = JobHostSummary( + summary = JobHostSummary( created=now(), modified=now(), job_id=job.id, host_id=host_id, host_name=host, **host_stats ) + summary.failed = bool(summary.dark or summary.failures) + summaries[(host_id, host)] = summary JobHostSummary.objects.bulk_create(summaries.values()) + # update the last_job_id and last_job_host_summary_id + # in single queries + host_mapping = dict( + (summary['host'], summary['id']) + for summary in JobHostSummary.objects.filter(job_id=job.id).values('id', 'host') + ) + all_hosts = Host.objects.filter( + pk__in=host_mapping.keys() + ).only('id') + for h in all_hosts: + h.last_job_id = job.id + if h.id in host_mapping: + h.last_job_host_summary_id = host_mapping[h.id] + Host.objects.bulk_update(all_hosts, ['last_job_id', 'last_job_host_summary_id']) + + @property def job_verbosity(self): return self.job.verbosity diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index e67478a8e8..45f89d4f9c 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1133,20 +1133,6 @@ class JobHostSummary(CreatedModifiedModel): self.failed = bool(self.dark or self.failures) update_fields.append('failed') super(JobHostSummary, self).save(*args, **kwargs) - self.update_host_last_job_summary() - - def update_host_last_job_summary(self): - update_fields = [] - if self.host is None: - return - if self.host.last_job_id != self.job_id: - self.host.last_job_id = self.job_id - update_fields.append('last_job_id') - if self.host.last_job_host_summary_id != self.id: - self.host.last_job_host_summary_id = self.id - update_fields.append('last_job_host_summary_id') - if update_fields: - self.host.save(update_fields=update_fields) class SystemJobOptions(BaseModel): diff --git a/awx/main/tests/functional/models/test_events.py b/awx/main/tests/functional/models/test_events.py index 6dd0cac06a..7f881a2fea 100644 --- a/awx/main/tests/functional/models/test_events.py +++ b/awx/main/tests/functional/models/test_events.py @@ -67,7 +67,7 @@ def test_parent_failed(emit, event): @pytest.mark.django_db def test_host_summary_generation(): - hostnames = [f'Host {i}' for i in range(500)] + hostnames = [f'Host {i}' for i in range(100)] inv = Inventory() inv.save() Host.objects.bulk_create([ @@ -108,6 +108,10 @@ def test_host_summary_generation(): assert s.rescued == 0 assert s.skipped == 0 + for host in Host.objects.all(): + assert host.last_job_id == j.id + assert host.last_job_host_summary.host == host + @pytest.mark.django_db def test_host_summary_generation_with_deleted_hosts(): From d85df2e4a1567a25ca5f783ca0f2407052577333 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 12 May 2020 16:20:22 -0400 Subject: [PATCH 07/53] further optimize job host summary queries --- awx/main/models/events.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/awx/main/models/events.py b/awx/main/models/events.py index e8d87253eb..ac33a311f4 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -490,12 +490,15 @@ class JobEvent(BasePlaybookEvent): job = self.job from awx.main.models import Host, JobHostSummary # circular import - existing = Host.objects.filter(id__in=self.host_map.values()).values_list('id', flat=True) + all_hosts = Host.objects.filter( + pk__in=self.host_map.values() + ).only('id') + existing_host_ids = set(h.id for h in all_hosts) summaries = dict() for host in hostnames: host_id = self.host_map.get(host, None) - if host_id not in existing: + if host_id not in existing_host_ids: host_id = None host_stats = {} for stat in ('changed', 'dark', 'failures', 'ignored', 'ok', 'processed', 'rescued', 'skipped'): @@ -514,12 +517,9 @@ class JobEvent(BasePlaybookEvent): # update the last_job_id and last_job_host_summary_id # in single queries host_mapping = dict( - (summary['host'], summary['id']) - for summary in JobHostSummary.objects.filter(job_id=job.id).values('id', 'host') + (summary['host_id'], summary['id']) + for summary in JobHostSummary.objects.filter(job_id=job.id).values('id', 'host_id') ) - all_hosts = Host.objects.filter( - pk__in=host_mapping.keys() - ).only('id') for h in all_hosts: h.last_job_id = job.id if h.id in host_mapping: From 71257c18c202a004b8bbc21ba9286a3f41ce5df5 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 20 May 2020 16:30:44 -0400 Subject: [PATCH 08/53] Revert "follow symlinks while discovering valid playbooks" This reverts commit 3dd21d720eb5351e63f9f31c4e601a67965dac56. --- awx/main/models/projects.py | 2 +- awx/main/utils/ansible.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 38ca20ab06..0207fec97b 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -199,7 +199,7 @@ class ProjectOptions(models.Model): results = [] project_path = self.get_project_path() if project_path: - for dirpath, dirnames, filenames in os.walk(smart_str(project_path), followlinks=True): + for dirpath, dirnames, filenames in os.walk(smart_str(project_path)): if skip_directory(dirpath): continue for filename in filenames: diff --git a/awx/main/utils/ansible.py b/awx/main/utils/ansible.py index eda6d6d214..18011504b9 100644 --- a/awx/main/utils/ansible.py +++ b/awx/main/utils/ansible.py @@ -64,7 +64,6 @@ def could_be_playbook(project_path, dir_path, filename): matched = True break except IOError: - logger.exception(f'failed to open {playbook_path}') return None if not matched: return None From 4c499b2d80e34251439640cb149ef4af96db6f7f Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Wed, 20 May 2020 11:03:13 -0400 Subject: [PATCH 09/53] Always check configuration before gathering data. We shouldn't perform expensive operations if we won't be able to send it. Only log at debug level, otherwise every node will log this every 5 minutes. --- awx/main/tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3a765364a3..a1c03df658 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -358,6 +358,9 @@ def gather_analytics(): from rest_framework.fields import DateTimeField if not settings.INSIGHTS_TRACKING_STATE: return + if not (settings.AUTOMATION_ANALYTICS_URL and settings.REDHAT_USERNAME and settings.REDHAT_PASSWORD): + logger.debug('Not gathering analytics, configuration is invalid') + return last_gather = Setting.objects.filter(key='AUTOMATION_ANALYTICS_LAST_GATHER').first() if last_gather: last_time = DateTimeField().to_internal_value(last_gather.value) From 492d01ff3b9be0eb48bd00bc7bd204fd5c1d95a6 Mon Sep 17 00:00:00 2001 From: Gabe Muniz Date: Mon, 11 May 2020 17:10:01 -0400 Subject: [PATCH 10/53] added try/except to virtual env --- awx/main/utils/common.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 0eb8741d9a..2a634b132e 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -1010,14 +1010,18 @@ def get_custom_venv_choices(custom_paths=None): custom_venv_choices = [] for custom_venv_path in all_venv_paths: - if os.path.exists(custom_venv_path): - custom_venv_choices.extend([ - os.path.join(custom_venv_path, x, '') - for x in os.listdir(custom_venv_path) - if x != 'awx' and - os.path.isdir(os.path.join(custom_venv_path, x)) and - os.path.exists(os.path.join(custom_venv_path, x, 'bin', 'activate')) - ]) + try: + if os.path.exists(custom_venv_path): + custom_venv_choices.extend([ + os.path.join(custom_venv_path, x, '') + for x in os.listdir(custom_venv_path) + if x != 'awx' and + os.path.isdir(os.path.join(custom_venv_path, x)) and + os.path.exists(os.path.join(custom_venv_path, x, 'bin', 'activate')) + ]) + except Exception: + logger.exception("Encountered an error while discovering custom virtual environments.") + pass return custom_venv_choices From 563d3944ed68cf66c3eb876d5ef1bb48e973c13b Mon Sep 17 00:00:00 2001 From: gamuniz Date: Mon, 11 May 2020 18:02:25 -0400 Subject: [PATCH 11/53] removed pass per feedback --- awx/main/utils/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 2a634b132e..d3e9d61f65 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -1021,7 +1021,6 @@ def get_custom_venv_choices(custom_paths=None): ]) except Exception: logger.exception("Encountered an error while discovering custom virtual environments.") - pass return custom_venv_choices From aec7d3cc939d0d8beb4eb65609bf8f7352fb89d2 Mon Sep 17 00:00:00 2001 From: Christian Adams Date: Tue, 26 May 2020 23:37:30 -0400 Subject: [PATCH 12/53] Correctly parse sumologic url paths - Sumologic includes a token with a '==' at the end of it's host path. This adds rsyslog conf parsing tests and does not escape equals signs. --- awx/main/tests/unit/api/test_logger.py | 17 ++++++++++++++--- awx/main/utils/external_logging.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/awx/main/tests/unit/api/test_logger.py b/awx/main/tests/unit/api/test_logger.py index 2a0bb9856d..95ee8f9a98 100644 --- a/awx/main/tests/unit/api/test_logger.py +++ b/awx/main/tests/unit/api/test_logger.py @@ -35,7 +35,7 @@ data_loggly = { # Test reconfigure logging settings function # name this whatever you want @pytest.mark.parametrize( - 'enabled, type, host, port, protocol, expected_config', [ + 'enabled, log_type, host, port, protocol, expected_config', [ ( True, 'loggly', @@ -135,9 +135,20 @@ data_loggly = { 'action(type="omhttp" server="yoursplunk.org" serverport="8088" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa ]) ), + ( + True, # valid sumologic config + 'sumologic', + 'https://endpoint5.collection.us2.sumologic.com/receiver/v1/http/ZaVnC4dhaV0qoiETY0MrM3wwLoDgO1jFgjOxE6-39qokkj3LGtOroZ8wNaN2M6DtgYrJZsmSi4-36_Up5TbbN_8hosYonLKHSSOSKY845LuLZBCBwStrHQ==', # noqa + None, + 'https', + '\n'.join([ + 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', + 'action(type="omhttp" server="endpoint5.collection.us2.sumologic.com" serverport="443" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="receiver/v1/http/ZaVnC4dhaV0qoiETY0MrM3wwLoDgO1jFgjOxE6-39qokkj3LGtOroZ8wNaN2M6DtgYrJZsmSi4-36_Up5TbbN_8hosYonLKHSSOSKY845LuLZBCBwStrHQ==")', # noqa + ]) + ), ] ) -def test_rsyslog_conf_template(enabled, type, host, port, protocol, expected_config): +def test_rsyslog_conf_template(enabled, log_type, host, port, protocol, expected_config): mock_settings, _ = _mock_logging_defaults() @@ -146,7 +157,7 @@ def test_rsyslog_conf_template(enabled, type, host, port, protocol, expected_con setattr(mock_settings, 'LOGGING', logging_defaults) setattr(mock_settings, 'LOGGING["handlers"]["external_logger"]["address"]', '/var/run/awx-rsyslog/rsyslog.sock') setattr(mock_settings, 'LOG_AGGREGATOR_ENABLED', enabled) - setattr(mock_settings, 'LOG_AGGREGATOR_TYPE', type) + setattr(mock_settings, 'LOG_AGGREGATOR_TYPE', log_type) setattr(mock_settings, 'LOG_AGGREGATOR_HOST', host) if port: setattr(mock_settings, 'LOG_AGGREGATOR_PORT', port) diff --git a/awx/main/utils/external_logging.py b/awx/main/utils/external_logging.py index 743e0f45ab..ac9d2834c8 100644 --- a/awx/main/utils/external_logging.py +++ b/awx/main/utils/external_logging.py @@ -78,7 +78,7 @@ def construct_rsyslog_conf_template(settings=settings): f'action.resumeInterval="{timeout}"' ] if parsed.path: - path = urlparse.quote(parsed.path[1:]) + path = urlparse.quote(parsed.path[1:], safe='/=') if parsed.query: path = f'{path}?{urlparse.quote(parsed.query)}' params.append(f'restpath="{path}"') From c48da1b3845b2a2c5367f2f03876394f827ab0ec Mon Sep 17 00:00:00 2001 From: Christian Adams Date: Tue, 26 May 2020 11:38:25 -0400 Subject: [PATCH 13/53] allow org admins to remove labels --- awx/main/access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index f1bbc42683..4705fb2cfc 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -495,7 +495,7 @@ class NotificationAttachMixin(BaseAccess): # due to this special case, we use symmetrical logic with attach permission return self._can_attach(notification_template=sub_obj, resource_obj=obj) return super(NotificationAttachMixin, self).can_unattach( - obj, sub_obj, relationship, relationship, data=data + obj, sub_obj, relationship, data=data ) From 85426f76a5a37885f331a794c9a000f3c944ccbc Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 14 May 2020 15:43:50 -0400 Subject: [PATCH 14/53] Fix misc. linter errors due to the flake8-3.8.1 release - [Ref] https://flake8.pycqa.org/en/latest/release-notes/ --- awx/asgi.py | 3 +-- awx/main/management/commands/callback_stats.py | 4 ++-- awx/main/models/inventory.py | 12 ++++++------ awx/main/scheduler/dag_simple.py | 8 ++++---- awx/main/signals.py | 8 ++++---- .../inventory/plugins/satellite6/files/foreman.yml | 10 +++++----- awx/main/tests/factories/tower.py | 8 ++++---- .../tests/functional/analytics/test_collectors.py | 6 +++--- .../commands/test_secret_key_regeneration.py | 1 + awx/main/tests/functional/test_dispatch.py | 2 ++ awx/settings/defaults.py | 2 +- 11 files changed, 33 insertions(+), 31 deletions(-) diff --git a/awx/asgi.py b/awx/asgi.py index 40640a4a19..698c5f7533 100644 --- a/awx/asgi.py +++ b/awx/asgi.py @@ -4,12 +4,11 @@ import os import logging import django from awx import __version__ as tower_version - # Prepare the AWX environment. from awx import prepare_env, MODE +from channels.routing import get_default_application # noqa prepare_env() # NOQA -from channels.routing import get_default_application """ diff --git a/awx/main/management/commands/callback_stats.py b/awx/main/management/commands/callback_stats.py index 74b7815b91..0a61089607 100644 --- a/awx/main/management/commands/callback_stats.py +++ b/awx/main/management/commands/callback_stats.py @@ -34,7 +34,7 @@ class Command(BaseCommand): if clear: for i in range(12): sys.stdout.write('\x1b[1A\x1b[2K') - for l in lines: - print(l) + for line in lines: + print(line) clear = True time.sleep(.25) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6e5ffce508..0bf553d385 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -2624,22 +2624,22 @@ class satellite6(PluginFileInjector): "environment": {"prefix": "foreman_environment_", "separator": "", "key": "foreman['environment_name'] | lower | regex_replace(' ', '') | " - "regex_replace('[^A-Za-z0-9\_]', '_') | regex_replace('none', '')"}, # NOQA: W605 + "regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '')"}, "location": {"prefix": "foreman_location_", "separator": "", - "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_')"}, + "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "organization": {"prefix": "foreman_organization_", "separator": "", - "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_')"}, + "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "lifecycle_environment": {"prefix": "foreman_lifecycle_environment_", "separator": "", "key": "foreman['content_facet_attributes']['lifecycle_environment_name'] | " - "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_')"}, + "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "content_view": {"prefix": "foreman_content_view_", "separator": "", "key": "foreman['content_facet_attributes']['content_view_name'] | " - "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_')"} - } + "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"} + } ret['legacy_hostvars'] = True # convert hostvar structure to the form used by the script ret['want_params'] = True diff --git a/awx/main/scheduler/dag_simple.py b/awx/main/scheduler/dag_simple.py index 065983577a..5a354edbba 100644 --- a/awx/main/scheduler/dag_simple.py +++ b/awx/main/scheduler/dag_simple.py @@ -152,8 +152,8 @@ class SimpleDAG(object): return self._get_children_by_label(this_ord, label) else: nodes = [] - for l in self.node_from_edges_by_label.keys(): - nodes.extend(self._get_children_by_label(this_ord, l)) + for label_obj in self.node_from_edges_by_label.keys(): + nodes.extend(self._get_children_by_label(this_ord, label_obj)) return nodes def _get_parents_by_label(self, node_index, label): @@ -168,8 +168,8 @@ class SimpleDAG(object): return self._get_parents_by_label(this_ord, label) else: nodes = [] - for l in self.node_to_edges_by_label.keys(): - nodes.extend(self._get_parents_by_label(this_ord, l)) + for label_obj in self.node_to_edges_by_label.keys(): + nodes.extend(self._get_parents_by_label(this_ord, label_obj)) return nodes def get_root_nodes(self): diff --git a/awx/main/signals.py b/awx/main/signals.py index dece9f49d6..adfbc65d01 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -150,9 +150,9 @@ def rbac_activity_stream(instance, sender, **kwargs): def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): - for l in instance.labels.all(): - if l.is_candidate_for_detach(): - l.delete() + for label in instance.labels.all(): + if label.is_candidate_for_detach(): + label.delete() def save_related_job_templates(sender, instance, **kwargs): @@ -393,7 +393,7 @@ def activity_stream_create(sender, instance, created, **kwargs): '{} ({})'.format(c.name, c.id) for c in instance.credentials.iterator() ] - changes['labels'] = [l.name for l in instance.labels.iterator()] + changes['labels'] = [label.name for label in instance.labels.iterator()] if 'extra_vars' in changes: changes['extra_vars'] = instance.display_extra_vars() if type(instance) == OAuth2AccessToken: diff --git a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml index a63520c2f0..11d6f67220 100644 --- a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml +++ b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml @@ -3,19 +3,19 @@ compose: ansible_ssh_host: foreman['ip6'] | default(foreman['ip'], true) group_prefix: foo_group_prefix keyed_groups: -- key: foreman['environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_') | regex_replace('none', '') +- key: foreman['environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '') prefix: foreman_environment_ separator: '' -- key: foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_') +- key: foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') prefix: foreman_location_ separator: '' -- key: foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_') +- key: foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') prefix: foreman_organization_ separator: '' -- key: foreman['content_facet_attributes']['lifecycle_environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_') +- key: foreman['content_facet_attributes']['lifecycle_environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') prefix: foreman_lifecycle_environment_ separator: '' -- key: foreman['content_facet_attributes']['content_view_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9\_]', '_') +- key: foreman['content_facet_attributes']['content_view_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') prefix: foreman_content_view_ separator: '' - key: '"%s-%s-%s" | format(app, tier, color)' diff --git a/awx/main/tests/factories/tower.py b/awx/main/tests/factories/tower.py index 1da5f126f9..87a7b436eb 100644 --- a/awx/main/tests/factories/tower.py +++ b/awx/main/tests/factories/tower.py @@ -319,11 +319,11 @@ def create_organization(name, roles=None, persisted=True, **kwargs): users = generate_users(org, teams, False, persisted, users=kwargs.get('users')) if 'labels' in kwargs: - for l in kwargs['labels']: - if type(l) is Label: - labels[l.name] = l + for label_obj in kwargs['labels']: + if type(label_obj) is Label: + labels[label_obj.name] = label_obj else: - labels[l] = mk_label(l, organization=org, persisted=persisted) + labels[label_obj] = mk_label(label_obj, organization=org, persisted=persisted) if 'notification_templates' in kwargs: for nt in kwargs['notification_templates']: diff --git a/awx/main/tests/functional/analytics/test_collectors.py b/awx/main/tests/functional/analytics/test_collectors.py index 500cd89053..ff53ac6bb4 100644 --- a/awx/main/tests/functional/analytics/test_collectors.py +++ b/awx/main/tests/functional/analytics/test_collectors.py @@ -88,7 +88,7 @@ def test_copy_tables_unified_job_query( with tempfile.TemporaryDirectory() as tmpdir: collectors.copy_tables(time_start, tmpdir, subset="unified_jobs") with open(os.path.join(tmpdir, "unified_jobs_table.csv")) as f: - lines = "".join([l for l in f]) + lines = "".join([line for line in f]) assert project_update_name in lines assert inventory_update_name in lines @@ -139,9 +139,9 @@ def test_copy_tables_workflow_job_node_query(sqlite_copy_expert, workflow_job): reader = csv.reader(f) # Pop the headers next(reader) - lines = [l for l in reader] + lines = [line for line in reader] - ids = [int(l[0]) for l in lines] + ids = [int(line[0]) for line in lines] assert ids == list( workflow_job.workflow_nodes.all().values_list("id", flat=True) diff --git a/awx/main/tests/functional/commands/test_secret_key_regeneration.py b/awx/main/tests/functional/commands/test_secret_key_regeneration.py index dffaacb866..d27b4329cd 100644 --- a/awx/main/tests/functional/commands/test_secret_key_regeneration.py +++ b/awx/main/tests/functional/commands/test_secret_key_regeneration.py @@ -65,6 +65,7 @@ class TestKeyRegeneration: assert nc['token'].startswith(PREFIX) Slack = nt.CLASS_FOR_NOTIFICATION_TYPE[nt.notification_type] + class TestBackend(Slack): def __init__(self, *args, **kw): diff --git a/awx/main/tests/functional/test_dispatch.py b/awx/main/tests/functional/test_dispatch.py index aa3c42ce26..caf54f0161 100644 --- a/awx/main/tests/functional/test_dispatch.py +++ b/awx/main/tests/functional/test_dispatch.py @@ -18,6 +18,8 @@ from awx.main.dispatch.worker import BaseWorker, TaskWorker ''' Prevent logger. calls from triggering database operations ''' + + @pytest.fixture(autouse=True) def _disable_database_settings(mocker): m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=mock.PropertyMock) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 031945dd6d..57d30bc23f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -247,7 +247,7 @@ TEMPLATES = [ 'loaders': [( 'django.template.loaders.cached.Loader', ('django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader',), + 'django.template.loaders.app_directories.Loader',), )], 'builtins': ['awx.main.templatetags.swagger'], }, From c53e5bdbcf16e8bd3cd844686998cdd3447aaf29 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 4 Jun 2020 15:10:14 -0400 Subject: [PATCH 15/53] properly write rsyslog configuration as 0640 see: https://github.com/ansible/tower/issues/4383 --- awx/main/utils/external_logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/utils/external_logging.py b/awx/main/utils/external_logging.py index 743e0f45ab..3714a94828 100644 --- a/awx/main/utils/external_logging.py +++ b/awx/main/utils/external_logging.py @@ -1,6 +1,6 @@ import os import shutil -import tempfile +import tempfile import urllib.parse as urlparse from django.conf import settings @@ -117,6 +117,7 @@ def reconfigure_rsyslog(): with tempfile.TemporaryDirectory(prefix='rsyslog-conf-') as temp_dir: path = temp_dir + '/rsyslog.conf.temp' with open(path, 'w') as f: + os.chmod(path, 0o640) f.write(tmpl + '\n') shutil.move(path, '/var/lib/awx/rsyslog/rsyslog.conf') supervisor_service_command(command='restart', service='awx-rsyslogd') From 4ce37ec849ca37fcc8c01839879cfdb9de1e013f Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Fri, 5 Jun 2020 11:13:01 -0700 Subject: [PATCH 16/53] Bump foreman collection to 0.8.1 * New release includes: 'add host_filters and want_ansible_ssh_host like script used to have' --- requirements/collections_requirements.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/collections_requirements.yml b/requirements/collections_requirements.yml index ea7abf5578..9e9634b7b9 100644 --- a/requirements/collections_requirements.yml +++ b/requirements/collections_requirements.yml @@ -7,7 +7,7 @@ collections: - name: amazon.aws version: 0.1.1 # version 0.1.0 seems to have gone missing - name: theforeman.foreman - version: 0.8.0 + version: 0.8.1 - name: google.cloud version: 0.0.9 # contains PR 167, should be good to go - name: openstack.cloud From 9ae344b772635122b926094699b3bb013762388f Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Tue, 19 May 2020 10:24:24 -0700 Subject: [PATCH 17/53] foreman: use group_prefix for all groups * awx's "compatibility layer" for the foreman plugin had the group_prefix hard-coded to 'foreman_' --- awx/main/models/inventory.py | 10 +++++----- .../inventory/plugins/satellite6/files/foreman.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 0bf553d385..c03cbcd987 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -2621,21 +2621,21 @@ class satellite6(PluginFileInjector): # Compatibility content group_by_hostvar = { - "environment": {"prefix": "foreman_environment_", + "environment": {"prefix": "{}environment_".format(group_prefix), "separator": "", "key": "foreman['environment_name'] | lower | regex_replace(' ', '') | " "regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '')"}, - "location": {"prefix": "foreman_location_", + "location": {"prefix": "{}location_".format(group_prefix), "separator": "", "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "organization": {"prefix": "foreman_organization_", + "organization": {"prefix": "{}organization_".format(group_prefix), "separator": "", "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "lifecycle_environment": {"prefix": "foreman_lifecycle_environment_", + "lifecycle_environment": {"prefix": "{}lifecycle_environment_".format(group_prefix), "separator": "", "key": "foreman['content_facet_attributes']['lifecycle_environment_name'] | " "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "content_view": {"prefix": "foreman_content_view_", + "content_view": {"prefix": "{}content_view_".format(group_prefix), "separator": "", "key": "foreman['content_facet_attributes']['content_view_name'] | " "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"} diff --git a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml index 11d6f67220..782bb89be7 100644 --- a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml +++ b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml @@ -4,19 +4,19 @@ compose: group_prefix: foo_group_prefix keyed_groups: - key: foreman['environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '') - prefix: foreman_environment_ + prefix: foo_group_prefixenvironment_ separator: '' - key: foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foreman_location_ + prefix: foo_group_prefixlocation_ separator: '' - key: foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foreman_organization_ + prefix: foo_group_prefixorganization_ separator: '' - key: foreman['content_facet_attributes']['lifecycle_environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foreman_lifecycle_environment_ + prefix: foo_group_prefixlifecycle_environment_ separator: '' - key: foreman['content_facet_attributes']['content_view_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foreman_content_view_ + prefix: foo_group_prefixcontent_view_ separator: '' - key: '"%s-%s-%s" | format(app, tier, color)' separator: '' From 9c20b9412af61094d37a6e2d0cb6c9fc7a5d2ad3 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Wed, 13 May 2020 08:30:39 -0400 Subject: [PATCH 18/53] delete and re-add host when ip address changes * The websocket backplane interconnect is done via ip address for Kubernetes and OpenShift. On init run_wsbroadcast reads all Instances from the DB and makes a decision to use the ip address or the hostname based, with preference given to the ip address if defined. For Kubernetes and OpenShift the nodes can load the Instance before the ip_address is set. This would cause the connection to be tried by hostname rather than ip address. This changeset ensures that an ip address set after an Instance record is created will be detected and used. --- awx/main/wsbroadcast.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/main/wsbroadcast.py b/awx/main/wsbroadcast.py index 243981a885..05ab30602f 100644 --- a/awx/main/wsbroadcast.py +++ b/awx/main/wsbroadcast.py @@ -162,6 +162,13 @@ class BroadcastWebsocketManager(object): deleted_remote_hosts = set(current_remote_hosts) - set(future_remote_hosts) new_remote_hosts = set(future_remote_hosts) - set(current_remote_hosts) + remote_addresses = {k: v.remote_host for k, v in self.broadcast_tasks.items()} + for hostname, address in known_hosts.items(): + if hostname in self.broadcast_tasks and \ + address != remote_addresses[hostname]: + deleted_remote_hosts.add(hostname) + new_remote_hosts.add(hostname) + if deleted_remote_hosts: logger.warn(f"Removing {deleted_remote_hosts} from websocket broadcast list") if new_remote_hosts: From 2f7ba75ae4c4a054dde1cae098fc609aae9b6946 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Tue, 12 May 2020 13:23:08 -0400 Subject: [PATCH 19/53] track stats by hostname not remote host/ip * broadcast websockets have stats tracked (i.e. connection status, number of messages total, messages per minute, etc). Previous to this change, stats were tracked by ip address, if it was defined on the instance, XOR hostname. This changeset tracks stats by hostname. --- awx/main/wsbroadcast.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/awx/main/wsbroadcast.py b/awx/main/wsbroadcast.py index 05ab30602f..a97baf45f4 100644 --- a/awx/main/wsbroadcast.py +++ b/awx/main/wsbroadcast.py @@ -37,7 +37,7 @@ def get_broadcast_hosts(): .order_by('hostname') \ .values('hostname', 'ip_address') \ .distinct() - return [i['ip_address'] or i['hostname'] for i in instances] + return {i['hostname']: i['ip_address'] or i['hostname'] for i in instances} def get_local_host(): @@ -149,15 +149,22 @@ class BroadcastWebsocketTask(WebsocketTask): class BroadcastWebsocketManager(object): def __init__(self): self.event_loop = asyncio.get_event_loop() + ''' + { + 'hostname1': BroadcastWebsocketTask(), + 'hostname2': BroadcastWebsocketTask(), + 'hostname3': BroadcastWebsocketTask(), + } + ''' self.broadcast_tasks = dict() - # parallel dict to broadcast_tasks that tracks stats self.local_hostname = get_local_host() self.stats_mgr = BroadcastWebsocketStatsManager(self.event_loop, self.local_hostname) async def run_per_host_websocket(self): while True: - future_remote_hosts = get_broadcast_hosts() + known_hosts = get_broadcast_hosts() + future_remote_hosts = known_hosts.keys() current_remote_hosts = self.broadcast_tasks.keys() deleted_remote_hosts = set(current_remote_hosts) - set(future_remote_hosts) new_remote_hosts = set(future_remote_hosts) - set(current_remote_hosts) @@ -184,7 +191,7 @@ class BroadcastWebsocketManager(object): broadcast_task = BroadcastWebsocketTask(name=self.local_hostname, event_loop=self.event_loop, stats=stats, - remote_host=h) + remote_host=known_hosts[h]) broadcast_task.start() self.broadcast_tasks[h] = broadcast_task From 71cc359ccf224733ad1d09f0f22269d7040b03ce Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 8 Jun 2020 18:07:33 -0400 Subject: [PATCH 20/53] don't block on log aggregator socket.send() calls see: https://github.com/ansible/tower/issues/4391 --- awx/main/utils/handlers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index ae0e83a9c5..c5e0014f8e 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -13,6 +13,10 @@ class RSysLogHandler(logging.handlers.SysLogHandler): append_nul = False + def _connect_unixsocket(self, address): + super(RSysLogHandler, self)._connect_unixsocket(address) + self.socket.setblocking(False) + def emit(self, msg): if not settings.LOG_AGGREGATOR_ENABLED: return @@ -26,6 +30,14 @@ class RSysLogHandler(logging.handlers.SysLogHandler): # unfortunately, we can't log that because...rsyslogd is down (and # would just us back ddown this code path) pass + except BlockingIOError: + # for , rsyslogd is no longer reading from the domain socket, and + # we're unable to write any more to it without blocking (we've seen this behavior + # from time to time when logging is totally misconfigured; + # in this scenario, it also makes more sense to just drop the messages, + # because the alternative is blocking the socket.send() in the + # Python process, which we definitely don't want to do) + pass ColorHandler = logging.StreamHandler From e9e410f4f89efe0d99d9520c37a556e69e946001 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Mon, 8 Jun 2020 16:30:52 +0200 Subject: [PATCH 21/53] Send content-type with mattermost notifications, fixes #7264 --- awx/main/notifications/mattermost_backend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/main/notifications/mattermost_backend.py b/awx/main/notifications/mattermost_backend.py index 7a759d41a3..78a23c72d1 100644 --- a/awx/main/notifications/mattermost_backend.py +++ b/awx/main/notifications/mattermost_backend.py @@ -3,7 +3,6 @@ import logging import requests -import json from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ @@ -45,7 +44,7 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase): payload['text'] = m.subject r = requests.post("{}".format(m.recipients()[0]), - data=json.dumps(payload), verify=(not self.mattermost_no_verify_ssl)) + json=payload, verify=(not self.mattermost_no_verify_ssl)) if r.status_code >= 400: logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text))) if not self.fail_silently: From 0e5f68ef53ea95c975e872683234948a783cbd7a Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 9 Jun 2020 11:07:22 -0400 Subject: [PATCH 22/53] Make all_parents_must_converge settable when creating node When targeting, ../workflow_job_templates/id#/workflow_nodes/ endpoint, user could not set all_parents_must_converge to true. 3.7.1 backport for awx issue #7063 --- awx/api/serializers.py | 2 +- awx/main/tests/functional/api/test_workflow_node.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d8152adb34..20239dd38a 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3612,7 +3612,7 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): ujt = self.instance.unified_job_template if ujt is None: ret = {} - for fd in ('workflow_job_template', 'identifier'): + for fd in ('workflow_job_template', 'identifier', 'all_parents_must_converge'): if fd in attrs: ret[fd] = attrs[fd] return ret diff --git a/awx/main/tests/functional/api/test_workflow_node.py b/awx/main/tests/functional/api/test_workflow_node.py index 64c22898df..ec70716f94 100644 --- a/awx/main/tests/functional/api/test_workflow_node.py +++ b/awx/main/tests/functional/api/test_workflow_node.py @@ -71,6 +71,18 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template, user=admin_user, expect=201) +@pytest.mark.django_db +@pytest.mark.parametrize("field_name, field_value", [ + ('all_parents_must_converge', True), + ('all_parents_must_converge', False), +]) +def test_create_node_with_field(field_name, field_value, workflow_job_template, post, admin_user): + url = reverse('api:workflow_job_template_workflow_nodes_list', + kwargs={'pk': workflow_job_template.pk}) + res = post(url, {field_name: field_value}, user=admin_user, expect=201) + assert res.data[field_name] == field_value + + @pytest.mark.django_db class TestApprovalNodes(): def test_approval_node_creation(self, post, approval_node, admin_user): From 18d09f892d6e014c4ddee47967519d5ac2aaecee Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Fri, 8 May 2020 12:25:14 -0700 Subject: [PATCH 23/53] disable reports option for foreman --- awx/plugins/inventory/foreman.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/awx/plugins/inventory/foreman.py b/awx/plugins/inventory/foreman.py index c3f97710d2..222084b5bd 100755 --- a/awx/plugins/inventory/foreman.py +++ b/awx/plugins/inventory/foreman.py @@ -176,8 +176,6 @@ class ForemanInventory(object): except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): self.want_facts = True - self.want_facts = self.want_facts and self.report_want_facts - try: self.want_hostcollections = config.getboolean('ansible', 'want_hostcollections') except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): @@ -272,12 +270,11 @@ class ForemanInventory(object): return results def _use_inventory_report(self): - if not self.foreman_use_reports_api: - return False - status_url = "%s/api/v2/status" % self.foreman_url - result = self._get_json(status_url) - foreman_version = (LooseVersion(result.get('version')) >= LooseVersion('1.24.0')) - return foreman_version + # The options required to enable the reports feature have never been fully + # surfaced by awx. Disabling reports in the foreman script altogether. + # Given that this script is being deprecated in favor of the foreman _plugin_, + # reports should be enabled for the foreman plugin in future version of awx. + return False def _fetch_params(self): options, params = ("no", "yes"), dict() From 1dd9772e4163c437b855a026676a52de31fbe7f9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 4 Apr 2019 12:20:35 -0400 Subject: [PATCH 24/53] Allow use of fallback instance_ids --- .../management/commands/inventory_import.py | 22 +++++++++++++------ awx/settings/defaults.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 9dad2c9a39..c5d1491365 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -271,7 +271,7 @@ class Command(BaseCommand): logging.DEBUG, 0])) logger.setLevel(log_levels.get(self.verbosity, 0)) - def _get_instance_id(self, from_dict, default=''): + def _get_instance_id(self, variables, default=''): ''' Retrieve the instance ID from the given dict of host variables. @@ -279,15 +279,23 @@ class Command(BaseCommand): the lookup will traverse into nested dicts, equivalent to: from_dict.get('foo', {}).get('bar', default) + + Multiple ID variables may be specified as 'foo.bar,foobar', so that + it will first try to find 'bar' inside of 'foo', and if unable, + will try to find 'foobar' as a fallback ''' instance_id = default if getattr(self, 'instance_id_var', None): - for key in self.instance_id_var.split('.'): - if not hasattr(from_dict, 'get'): - instance_id = default + for single_instance_id in self.instance_id_var.split(','): + from_dict = variables + for key in single_instance_id.split('.'): + if not hasattr(from_dict, 'get'): + instance_id = default + break + instance_id = from_dict.get(key, default) + from_dict = instance_id + if instance_id: break - instance_id = from_dict.get(key, default) - from_dict = instance_id return smart_text(instance_id) def _get_enabled(self, from_dict, default=None): @@ -422,7 +430,7 @@ class Command(BaseCommand): for mem_host in self.all_group.all_hosts.values(): instance_id = self._get_instance_id(mem_host.variables) if not instance_id: - logger.warning('Host "%s" has no "%s" variable', + logger.warning('Host "%s" has no "%s" variable(s)', mem_host.name, self.instance_id_var) continue mem_host.instance_id = instance_id diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 57d30bc23f..f35a8983bc 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -708,7 +708,7 @@ EC2_ENABLED_VAR = 'ec2_state' EC2_ENABLED_VALUE = 'running' # Inventory variable name containing unique instance ID. -EC2_INSTANCE_ID_VAR = 'ec2_id' +EC2_INSTANCE_ID_VAR = 'ec2_id,instance_id' # Filter for allowed group/host names when importing inventory from EC2. EC2_GROUP_FILTER = r'^.+$' From 17eaeb28a8e9028e192e6d9eccbbb19f864ba8bf Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Wed, 10 Jun 2020 13:30:05 -0700 Subject: [PATCH 25/53] update VMWARE_INSTANCE_ID_VAR * Favor instanceUuid * .. but fall back to instanceuuid if necessary --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index f35a8983bc..46b5e454c3 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -727,7 +727,7 @@ VMWARE_ENABLED_VAR = 'guest.gueststate' VMWARE_ENABLED_VALUE = 'running' # Inventory variable name containing the unique instance ID. -VMWARE_INSTANCE_ID_VAR = 'config.instanceuuid' +VMWARE_INSTANCE_ID_VAR = 'config.instanceUuid, config.instanceuuid' # Filter for allowed group and host names when importing inventory # from VMware. From 85deb8711c70da7f34ea67330a542259287ad298 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Thu, 4 Jun 2020 18:32:14 -0400 Subject: [PATCH 26/53] Add queue / instance group registration to heartbeat for k8s installs There is some history here. https://github.com/ansible/awx/pull/7190 <- This PR was an attempt at fixing a bug notting ran into where some jobs on k8s installs would get stuck in Waiting forever. The PR mentioned above introduced a bug where there are no instance groups on a fresh k8s-based install. This is because this process currently happens in the launch scripts, before the database is up. With this patch, queue / instance group registration happens in the heartbeat, right after auto-registering the instance. --- .../management/commands/register_queue.py | 103 ++++++++++-------- awx/main/managers.py | 5 +- .../templates/launch_awx_task.sh.j2 | 4 +- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/awx/main/management/commands/register_queue.py b/awx/main/management/commands/register_queue.py index 61761ec2aa..edd8068b89 100644 --- a/awx/main/management/commands/register_queue.py +++ b/awx/main/management/commands/register_queue.py @@ -16,31 +16,24 @@ class InstanceNotFound(Exception): super(InstanceNotFound, self).__init__(*args, **kwargs) -class Command(BaseCommand): +class RegisterQueue: + def __init__(self, queuename, controller, instance_percent, inst_min, hostname_list): + self.instance_not_found_err = None + self.queuename = queuename + self.controller = controller + self.instance_percent = instance_percent + self.instance_min = inst_min + self.hostname_list = hostname_list - def add_arguments(self, parser): - parser.add_argument('--queuename', dest='queuename', type=str, - help='Queue to create/update') - parser.add_argument('--hostnames', dest='hostnames', type=str, - help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)') - parser.add_argument('--controller', dest='controller', type=str, - default='', help='The controlling group (makes this an isolated group)') - parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0, - help='The percentage of active instances that will be assigned to this group'), - parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0, - help='The minimum number of instance that will be retained for this group from available instances') - - - def get_create_update_instance_group(self, queuename, instance_percent, instance_min): + def get_create_update_instance_group(self): created = False changed = False - - (ig, created) = InstanceGroup.objects.get_or_create(name=queuename) - if ig.policy_instance_percentage != instance_percent: - ig.policy_instance_percentage = instance_percent + (ig, created) = InstanceGroup.objects.get_or_create(name=self.queuename) + if ig.policy_instance_percentage != self.instance_percent: + ig.policy_instance_percentage = self.instance_percent changed = True - if ig.policy_instance_minimum != instance_min: - ig.policy_instance_minimum = instance_min + if ig.policy_instance_minimum != self.instance_min: + ig.policy_instance_minimum = self.instance_min changed = True if changed: @@ -48,12 +41,12 @@ class Command(BaseCommand): return (ig, created, changed) - def update_instance_group_controller(self, ig, controller): + def update_instance_group_controller(self, ig): changed = False control_ig = None - if controller: - control_ig = InstanceGroup.objects.filter(name=controller).first() + if self.controller: + control_ig = InstanceGroup.objects.filter(name=self.controller).first() if control_ig and ig.controller_id != control_ig.pk: ig.controller = control_ig @@ -62,10 +55,10 @@ class Command(BaseCommand): return (control_ig, changed) - def add_instances_to_group(self, ig, hostname_list): + def add_instances_to_group(self, ig): changed = False - instance_list_unique = set([x.strip() for x in hostname_list if x]) + instance_list_unique = set([x.strip() for x in self.hostname_list if x]) instances = [] for inst_name in instance_list_unique: instance = Instance.objects.filter(hostname=inst_name) @@ -86,43 +79,61 @@ class Command(BaseCommand): return (instances, changed) - def handle(self, **options): - instance_not_found_err = None - queuename = options.get('queuename') - if not queuename: - raise CommandError("Specify `--queuename` to use this command.") - ctrl = options.get('controller') - inst_per = options.get('instance_percent') - inst_min = options.get('instance_minimum') - hostname_list = [] - if options.get('hostnames'): - hostname_list = options.get('hostnames').split(",") - + def register(self): with advisory_lock('cluster_policy_lock'): with transaction.atomic(): changed2 = False changed3 = False - (ig, created, changed1) = self.get_create_update_instance_group(queuename, inst_per, inst_min) + (ig, created, changed1) = self.get_create_update_instance_group() if created: print("Creating instance group {}".format(ig.name)) elif not created: print("Instance Group already registered {}".format(ig.name)) - if ctrl: - (ig_ctrl, changed2) = self.update_instance_group_controller(ig, ctrl) + if self.controller: + (ig_ctrl, changed2) = self.update_instance_group_controller(ig) if changed2: - print("Set controller group {} on {}.".format(ctrl, queuename)) + print("Set controller group {} on {}.".format(self.controller, self.queuename)) try: - (instances, changed3) = self.add_instances_to_group(ig, hostname_list) + (instances, changed3) = self.add_instances_to_group(ig) for i in instances: print("Added instance {} to {}".format(i.hostname, ig.name)) except InstanceNotFound as e: - instance_not_found_err = e + self.instance_not_found_err = e if any([changed1, changed2, changed3]): print('(changed: True)') - if instance_not_found_err: - print(instance_not_found_err.message) + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument('--queuename', dest='queuename', type=str, + help='Queue to create/update') + parser.add_argument('--hostnames', dest='hostnames', type=str, + help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)') + parser.add_argument('--controller', dest='controller', type=str, + default='', help='The controlling group (makes this an isolated group)') + parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0, + help='The percentage of active instances that will be assigned to this group'), + parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0, + help='The minimum number of instance that will be retained for this group from available instances') + + + def handle(self, **options): + queuename = options.get('queuename') + if not queuename: + raise CommandError("Specify `--queuename` to use this command.") + ctrl = options.get('controller') + inst_per = options.get('instance_percent') + instance_min = options.get('instance_minimum') + hostname_list = [] + if options.get('hostnames'): + hostname_list = options.get('hostnames').split(",") + + rq = RegisterQueue(queuename, ctrl, inst_per, instance_min, hostname_list) + rq.register() + if rq.instance_not_found_err: + print(rq.instance_not_found_err.message) sys.exit(1) diff --git a/awx/main/managers.py b/awx/main/managers.py index 2076e7f0b0..f4b437d027 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -149,8 +149,11 @@ class InstanceManager(models.Manager): def get_or_register(self): if settings.AWX_AUTO_DEPROVISION_INSTANCES: + from awx.main.management.commands.register_queue import RegisterQueue pod_ip = os.environ.get('MY_POD_IP') - return self.register(ip_address=pod_ip) + registered = self.register(ip_address=pod_ip) + RegisterQueue('tower', None, 100, 0, []).register() + return registered else: return (False, self.me()) diff --git a/installer/roles/image_build/templates/launch_awx_task.sh.j2 b/installer/roles/image_build/templates/launch_awx_task.sh.j2 index 2e74dab678..c038059b03 100755 --- a/installer/roles/image_build/templates/launch_awx_task.sh.j2 +++ b/installer/roles/image_build/templates/launch_awx_task.sh.j2 @@ -12,6 +12,8 @@ ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c loca if [ -z "$AWX_SKIP_MIGRATIONS" ]; then awx-manage migrate --noinput + awx-manage provision_instance --hostname=$(hostname) + awx-manage register_queue --queuename=tower --instance_percent=100 fi if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then @@ -21,8 +23,6 @@ if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then {% endif %} fi echo 'from django.conf import settings; x = settings.AWX_TASK_ENV; x["HOME"] = "/var/lib/awx"; settings.AWX_TASK_ENV = x' | awx-manage shell -awx-manage provision_instance --hostname=$(hostname) -awx-manage register_queue --queuename=tower --instance_percent=100 unset $(cut -d = -f -1 /etc/tower/conf.d/environment.sh) From 9514adaf3ae3396104a54f78e6c5ec983a90f3c9 Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Wed, 10 Jun 2020 13:55:54 -0700 Subject: [PATCH 27/53] wrap --instance-id-var in quotes --- awx/main/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 5a287a1444..09014d9da6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2554,7 +2554,7 @@ class RunInventoryUpdate(BaseTask): args.append('--exclude-empty-groups') if getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper(), False): args.extend(['--instance-id-var', - getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper()),]) + "'{}'".format(getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper())),]) # Add arguments for the source inventory script args.append('--source') args.append(self.pseudo_build_inventory(inventory_update, private_data_dir)) From 8eee0d40dd0d673ab99f7839cd5b7f0baa1a331f Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Thu, 11 Jun 2020 09:52:54 -0700 Subject: [PATCH 28/53] revert EC2_INSTANCE_ID_VAR --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 46b5e454c3..ba7c481df0 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -708,7 +708,7 @@ EC2_ENABLED_VAR = 'ec2_state' EC2_ENABLED_VALUE = 'running' # Inventory variable name containing unique instance ID. -EC2_INSTANCE_ID_VAR = 'ec2_id,instance_id' +EC2_INSTANCE_ID_VAR = 'ec2_id' # Filter for allowed group/host names when importing inventory from EC2. EC2_GROUP_FILTER = r'^.+$' From 40eb3e43f8d0c3343ebc5fd6aafb15b63e8525c6 Mon Sep 17 00:00:00 2001 From: ansible-translation-bot Date: Fri, 12 Jun 2020 17:03:37 +0000 Subject: [PATCH 29/53] UI translation strings for release_3.7.1 branch --- awx/locale/fr/LC_MESSAGES/django.po | 364 ++++++++++++++-------------- awx/locale/ja/LC_MESSAGES/django.po | 364 ++++++++++++++-------------- awx/locale/zh/LC_MESSAGES/django.po | 364 ++++++++++++++-------------- awx/ui/po/fr.po | 25 +- awx/ui/po/ja.po | 24 +- awx/ui/po/zh.po | 27 +-- 6 files changed, 582 insertions(+), 586 deletions(-) diff --git a/awx/locale/fr/LC_MESSAGES/django.po b/awx/locale/fr/LC_MESSAGES/django.po index 2e07a6232a..62c2ba7292 100644 --- a/awx/locale/fr/LC_MESSAGES/django.po +++ b/awx/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-27 13:55+0000\n" +"POT-Creation-Date: 2020-05-28 21:45+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -528,7 +528,7 @@ msgstr "Le projet de modèle d'inventaire est manquant ou non défini." msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "Inconnu, il se peut que le job ait été exécuté avant que les configurations de lancement ne soient sauvegardées." -#: awx/api/serializers.py:3252 awx/main/tasks.py:2795 awx/main/tasks.py:2813 +#: awx/api/serializers.py:3252 awx/main/tasks.py:2800 awx/main/tasks.py:2818 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} ne sont pas autorisés à utiliser les commandes ad hoc." @@ -547,324 +547,324 @@ msgstr "La variable fournie {} n'a pas de valeur de base de données de remplaç msgid "\"$encrypted$ is a reserved keyword, may not be used for {}.\"" msgstr "\"$encrypted$ est un mot clé réservé et ne peut pas être utilisé comme {}.\"" -#: awx/api/serializers.py:4070 +#: awx/api/serializers.py:4078 msgid "A project is required to run a job." msgstr "Un projet est nécessaire pour exécuter une tâche." -#: awx/api/serializers.py:4072 +#: awx/api/serializers.py:4080 msgid "Missing a revision to run due to failed project update." msgstr "Une révision n'a pas été exécutée en raison de l'échec de la mise à jour du projet." -#: awx/api/serializers.py:4076 +#: awx/api/serializers.py:4084 msgid "The inventory associated with this Job Template is being deleted." msgstr "L'inventaire associé à ce modèle de tâche est en cours de suppression." -#: awx/api/serializers.py:4078 awx/api/serializers.py:4194 +#: awx/api/serializers.py:4086 awx/api/serializers.py:4202 msgid "The provided inventory is being deleted." msgstr "L'inventaire fourni est en cours de suppression." -#: awx/api/serializers.py:4086 +#: awx/api/serializers.py:4094 msgid "Cannot assign multiple {} credentials." msgstr "Ne peut pas attribuer plusieurs identifiants {}." -#: awx/api/serializers.py:4090 +#: awx/api/serializers.py:4098 msgid "Cannot assign a Credential of kind `{}`" msgstr "Ne peut pas attribuer d'information d'identification de type `{}`" -#: awx/api/serializers.py:4103 +#: awx/api/serializers.py:4111 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "Le retrait des identifiants {} au moment du lancement sans procurer de valeurs de remplacement n'est pas pris en charge. La liste fournie manquait d'identifiant(s): {}." -#: awx/api/serializers.py:4192 +#: awx/api/serializers.py:4200 msgid "The inventory associated with this Workflow is being deleted." msgstr "L'inventaire associé à ce flux de travail est en cours de suppression." -#: awx/api/serializers.py:4263 +#: awx/api/serializers.py:4271 msgid "Message type '{}' invalid, must be either 'message' or 'body'" msgstr "Type de message '{}' invalide, doit être soit 'message' soit 'body'" -#: awx/api/serializers.py:4269 +#: awx/api/serializers.py:4277 msgid "Expected string for '{}', found {}, " msgstr "Chaîne attendue pour '{}', trouvé {}, " -#: awx/api/serializers.py:4273 +#: awx/api/serializers.py:4281 msgid "Messages cannot contain newlines (found newline in {} event)" msgstr "Les messages ne peuvent pas contenir de nouvelles lignes (trouvé nouvelle ligne dans l'événement {})" -#: awx/api/serializers.py:4279 +#: awx/api/serializers.py:4287 msgid "Expected dict for 'messages' field, found {}" msgstr "Dict attendu pour le champ 'messages', trouvé {}" -#: awx/api/serializers.py:4283 +#: awx/api/serializers.py:4291 msgid "" "Event '{}' invalid, must be one of 'started', 'success', 'error', or " "'workflow_approval'" msgstr "L'événement '{}' est invalide, il doit être de type 'started', 'success', 'error' ou 'workflow_approval'" -#: awx/api/serializers.py:4289 +#: awx/api/serializers.py:4297 msgid "Expected dict for event '{}', found {}" msgstr "Dict attendu pour l'événement '{}', trouvé {}" -#: awx/api/serializers.py:4294 +#: awx/api/serializers.py:4302 msgid "" "Workflow Approval event '{}' invalid, must be one of 'running', 'approved', " "'timed_out', or 'denied'" msgstr "L'événement d'approbation de workflow '{}' n'est pas valide, il doit être 'running', 'approved', 'timed_out' ou 'denied'" -#: awx/api/serializers.py:4301 +#: awx/api/serializers.py:4309 msgid "Expected dict for workflow approval event '{}', found {}" msgstr "Dict attendu pour l'événement d'approbation du workflow '{}', trouvé {}" -#: awx/api/serializers.py:4328 +#: awx/api/serializers.py:4336 msgid "Unable to render message '{}': {}" msgstr "Impossible de rendre le message '{}' : {}" -#: awx/api/serializers.py:4330 +#: awx/api/serializers.py:4338 msgid "Field '{}' unavailable" msgstr "Champ '{}' non disponible" -#: awx/api/serializers.py:4332 +#: awx/api/serializers.py:4340 msgid "Security error due to field '{}'" msgstr "Erreur de sécurité due au champ '{}'" -#: awx/api/serializers.py:4352 +#: awx/api/serializers.py:4360 msgid "Webhook body for '{}' should be a json dictionary. Found type '{}'." msgstr "Le corps du webhook pour '{}' doit être un dictionnaire json. Trouvé le type '{}'." -#: awx/api/serializers.py:4355 +#: awx/api/serializers.py:4363 msgid "Webhook body for '{}' is not a valid json dictionary ({})." msgstr "Le corps du webhook pour '{}' n'est pas un dictionnaire json valide ({})." -#: awx/api/serializers.py:4373 +#: awx/api/serializers.py:4381 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "Champs obligatoires manquants pour la configuration des notifications : notification_type" -#: awx/api/serializers.py:4400 +#: awx/api/serializers.py:4408 msgid "No values specified for field '{}'" msgstr "Aucune valeur spécifiée pour le champ '{}'" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4413 msgid "HTTP method must be either 'POST' or 'PUT'." msgstr "La méthode HTTP doit être soit 'POST' soit 'PUT'." -#: awx/api/serializers.py:4407 +#: awx/api/serializers.py:4415 msgid "Missing required fields for Notification Configuration: {}." msgstr "Champs obligatoires manquants pour la configuration des notifications : {}." -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4418 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "Type de champ de configuration '{}' incorrect, {} attendu." -#: awx/api/serializers.py:4427 +#: awx/api/serializers.py:4435 msgid "Notification body" msgstr "Corps de notification" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4515 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "DTSTART valide obligatoire dans rrule. La valeur doit commencer par : DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:4509 +#: awx/api/serializers.py:4517 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "DTSTART ne peut correspondre à une DateHeure naïve. Spécifier ;TZINFO= ou YYYYMMDDTHHMMSSZZ." -#: awx/api/serializers.py:4511 +#: awx/api/serializers.py:4519 msgid "Multiple DTSTART is not supported." msgstr "Une seule valeur DTSTART est prise en charge." -#: awx/api/serializers.py:4513 +#: awx/api/serializers.py:4521 msgid "RRULE required in rrule." msgstr "RRULE obligatoire dans rrule." -#: awx/api/serializers.py:4515 +#: awx/api/serializers.py:4523 msgid "Multiple RRULE is not supported." msgstr "Une seule valeur RRULE est prise en charge." -#: awx/api/serializers.py:4517 +#: awx/api/serializers.py:4525 msgid "INTERVAL required in rrule." msgstr "INTERVAL obligatoire dans rrule." -#: awx/api/serializers.py:4519 +#: awx/api/serializers.py:4527 msgid "SECONDLY is not supported." msgstr "SECONDLY n'est pas pris en charge." -#: awx/api/serializers.py:4521 +#: awx/api/serializers.py:4529 msgid "Multiple BYMONTHDAYs not supported." msgstr "Une seule valeur BYMONTHDAY est prise en charge." -#: awx/api/serializers.py:4523 +#: awx/api/serializers.py:4531 msgid "Multiple BYMONTHs not supported." msgstr "Une seule valeur BYMONTH est prise en charge." -#: awx/api/serializers.py:4525 +#: awx/api/serializers.py:4533 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY avec un préfixe numérique non pris en charge." -#: awx/api/serializers.py:4527 +#: awx/api/serializers.py:4535 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY non pris en charge." -#: awx/api/serializers.py:4529 +#: awx/api/serializers.py:4537 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO non pris en charge." -#: awx/api/serializers.py:4531 +#: awx/api/serializers.py:4539 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "RRULE peut contenir à la fois COUNT et UNTIL" -#: awx/api/serializers.py:4535 +#: awx/api/serializers.py:4543 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 non pris en charge." -#: awx/api/serializers.py:4541 +#: awx/api/serializers.py:4549 msgid "rrule parsing failed validation: {}" msgstr "L'analyse rrule n'a pas pu être validée : {}" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4611 msgid "Inventory Source must be a cloud resource." msgstr "La source d'inventaire doit être une ressource cloud." -#: awx/api/serializers.py:4605 +#: awx/api/serializers.py:4613 msgid "Manual Project cannot have a schedule set." msgstr "Le projet manuel ne peut pas avoir de calendrier défini." -#: awx/api/serializers.py:4608 +#: awx/api/serializers.py:4616 msgid "" "Inventory sources with `update_on_project_update` cannot be scheduled. " "Schedule its source project `{}` instead." msgstr "Impossible de planifier les sources d'inventaire avec `update_on_project_update`. Planifiez plutôt son projet source`{}`." -#: awx/api/serializers.py:4618 +#: awx/api/serializers.py:4626 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "Le nombre de jobs en cours d'exécution ou en attente qui sont ciblés pour cette instance." -#: awx/api/serializers.py:4623 +#: awx/api/serializers.py:4631 msgid "Count of all jobs that target this instance" msgstr "Le nombre de jobs qui ciblent cette instance." -#: awx/api/serializers.py:4656 +#: awx/api/serializers.py:4664 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance group" msgstr "Le nombre de jobs en cours d'exécution ou en attente qui sont ciblés pour ce groupe d'instances." -#: awx/api/serializers.py:4661 +#: awx/api/serializers.py:4669 msgid "Count of all jobs that target this instance group" msgstr "Le nombre de jobs qui ciblent ce groupe d'instances" -#: awx/api/serializers.py:4666 +#: awx/api/serializers.py:4674 msgid "Indicates whether instance group controls any other group" msgstr "Indique si le groupe d'instances contrôle un autre groupe" -#: awx/api/serializers.py:4670 +#: awx/api/serializers.py:4678 msgid "" "Indicates whether instances in this group are isolated.Isolated groups have " "a designated controller group." msgstr "Indique si les instances de ce groupe sont isolées. Les groupes isolés ont un groupe de contrôleurs désigné." -#: awx/api/serializers.py:4675 +#: awx/api/serializers.py:4683 msgid "" "Indicates whether instances in this group are containerized.Containerized " "groups have a designated Openshift or Kubernetes cluster." msgstr "Indique si les instances de ce groupe sont conteneurisées. Les groupes conteneurisés ont un groupe Openshift ou Kubernetes désigné." -#: awx/api/serializers.py:4683 +#: awx/api/serializers.py:4691 msgid "Policy Instance Percentage" msgstr "Pourcentage d'instances de stratégie" -#: awx/api/serializers.py:4684 +#: awx/api/serializers.py:4692 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "Le pourcentage minimum de toutes les instances qui seront automatiquement assignées à ce groupe lorsque de nouvelles instances seront mises en ligne." -#: awx/api/serializers.py:4689 +#: awx/api/serializers.py:4697 msgid "Policy Instance Minimum" msgstr "Instances de stratégies minimum" -#: awx/api/serializers.py:4690 +#: awx/api/serializers.py:4698 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "Nombre minimum statique d'instances qui seront automatiquement assignées à ce groupe lors de la mise en ligne de nouvelles instances." -#: awx/api/serializers.py:4695 +#: awx/api/serializers.py:4703 msgid "Policy Instance List" msgstr "Listes d'instances de stratégie" -#: awx/api/serializers.py:4696 +#: awx/api/serializers.py:4704 msgid "List of exact-match Instances that will be assigned to this group" msgstr "Liste des cas de concordance exacte qui seront assignés à ce groupe." -#: awx/api/serializers.py:4722 +#: awx/api/serializers.py:4730 msgid "Duplicate entry {}." msgstr "Entrée dupliquée {}." -#: awx/api/serializers.py:4724 +#: awx/api/serializers.py:4732 msgid "{} is not a valid hostname of an existing instance." msgstr "{} n'est pas un nom d'hôte valide d'instance existante." -#: awx/api/serializers.py:4726 awx/api/views/mixin.py:98 +#: awx/api/serializers.py:4734 awx/api/views/mixin.py:98 msgid "" "Isolated instances may not be added or removed from instances groups via the " "API." msgstr "Des instances isolées ne peuvent pas être ajoutées ou supprimées de groupes d'instances via l'API." -#: awx/api/serializers.py:4728 awx/api/views/mixin.py:102 +#: awx/api/serializers.py:4736 awx/api/views/mixin.py:102 msgid "Isolated instance group membership may not be managed via the API." msgstr "L'appartenance à un groupe d'instances isolées n'est sans doute pas gérée par l'API." -#: awx/api/serializers.py:4730 awx/api/serializers.py:4735 -#: awx/api/serializers.py:4740 +#: awx/api/serializers.py:4738 awx/api/serializers.py:4743 +#: awx/api/serializers.py:4748 msgid "Containerized instances may not be managed via the API" msgstr "Les instances conteneurisées ne peuvent pas être gérées via l'API" -#: awx/api/serializers.py:4745 +#: awx/api/serializers.py:4753 msgid "tower instance group name may not be changed." msgstr "Le nom de groupe de l'instance Tower ne peut pas être modifié." -#: awx/api/serializers.py:4750 +#: awx/api/serializers.py:4758 msgid "Only Kubernetes credentials can be associated with an Instance Group" msgstr "Seuls les identifiants Kubernetes peuvent être associés à un groupe d'instances" -#: awx/api/serializers.py:4789 +#: awx/api/serializers.py:4797 msgid "" "When present, shows the field name of the role or relationship that changed." msgstr "Le cas échéant, affiche le nom de champ du rôle ou de la relation qui a changé." -#: awx/api/serializers.py:4791 +#: awx/api/serializers.py:4799 msgid "" "When present, shows the model on which the role or relationship was defined." msgstr "Le cas échéant, affiche le modèle sur lequel le rôle ou la relation a été défini." -#: awx/api/serializers.py:4824 +#: awx/api/serializers.py:4832 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "Un récapitulatif des valeurs nouvelles et modifiées lorsqu'un objet est créé, mis à jour ou supprimé" -#: awx/api/serializers.py:4826 +#: awx/api/serializers.py:4834 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "Pour créer, mettre à jour et supprimer des événements, il s'agit du type d'objet qui a été affecté. Pour associer et dissocier des événements, il s'agit du type d'objet associé à ou dissocié de object2." -#: awx/api/serializers.py:4829 +#: awx/api/serializers.py:4837 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "Laisser vide pour créer, mettre à jour et supprimer des événements. Pour associer et dissocier des événements, il s'agit du type d'objet auquel object1 est associé." -#: awx/api/serializers.py:4832 +#: awx/api/serializers.py:4840 msgid "The action taken with respect to the given object(s)." msgstr "Action appliquée par rapport à l'objet ou aux objets donnés." @@ -1638,7 +1638,7 @@ msgstr "Exemple de paramètre" msgid "Example setting which can be different for each user." msgstr "Exemple de paramètre qui peut être différent pour chaque utilisateur." -#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:55 +#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:56 msgid "User" msgstr "Utilisateur" @@ -1741,15 +1741,15 @@ msgstr "Système" msgid "OtherSystem" msgstr "Autre Système" -#: awx/conf/views.py:47 +#: awx/conf/views.py:48 msgid "Setting Categories" msgstr "Catégories de paramètre" -#: awx/conf/views.py:69 +#: awx/conf/views.py:70 msgid "Setting Detail" msgstr "Détails du paramètre" -#: awx/conf/views.py:160 +#: awx/conf/views.py:162 msgid "Logging Connectivity Test" msgstr "Journalisation du test de connectivité" @@ -2795,7 +2795,7 @@ msgstr "URL Conjur" msgid "API Key" msgstr "Clé API" -#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1017 +#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1018 msgid "Account" msgstr "Compte" @@ -2882,7 +2882,7 @@ msgid "" msgstr "Nom du backend secret (s'il est laissé vide, le premier segment du chemin secret sera utilisé)." #: awx/main/credential_plugins/hashivault.py:60 -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1023 msgid "Key Name" msgstr "Nom de la clé" @@ -3259,7 +3259,7 @@ msgid "" "Management (IAM) users." msgstr "Le service de jeton de sécurité (STS) est un service Web qui permet de demander des informations d’identification provisoires avec des privilèges limités pour les utilisateurs d’AWS Identity and Access Management (IAM)." -#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:832 +#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:833 msgid "OpenStack" msgstr "OpenStack" @@ -3300,7 +3300,7 @@ msgstr "Les domaines OpenStack définissent les limites administratives. Ils son msgid "Verify SSL" msgstr "Vérifier SSL" -#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:829 +#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:830 msgid "VMware vCenter" msgstr "VMware vCenter" @@ -3313,7 +3313,7 @@ msgid "" "Enter the hostname or IP address that corresponds to your VMware vCenter." msgstr "Saisir le nom d’hôte ou l’adresse IP qui correspond à votre VMware vCenter." -#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:830 +#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:831 msgid "Red Hat Satellite 6" msgstr "Red Hat Satellite 6" @@ -3327,7 +3327,7 @@ msgid "" "example, https://satellite.example.org" msgstr "Veuillez saisir l’URL qui correspond à votre serveur Red Hat Satellite 6. Par exemple, https://satellite.example.org" -#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:831 +#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:832 msgid "Red Hat CloudForms" msgstr "Red Hat CloudForms" @@ -3341,7 +3341,7 @@ msgid "" "instance. For example, https://cloudforms.example.org" msgstr "Veuillez saisir l’URL de la machine virtuelle qui correspond à votre instance de CloudForm. Par exemple, https://cloudforms.example.org" -#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:827 +#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:828 msgid "Google Compute Engine" msgstr "Google Compute Engine" @@ -3370,7 +3370,7 @@ msgid "" "Paste the contents of the PEM file associated with the service account email." msgstr "Collez le contenu du fichier PEM associé à l’adresse électronique du compte de service." -#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:828 +#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:829 msgid "Microsoft Azure Resource Manager" msgstr "Microsoft Azure Resource Manager" @@ -3408,7 +3408,7 @@ msgstr "Jeton d'accès personnel GitLab" msgid "This token needs to come from your profile settings in GitLab" msgstr "Ce jeton doit provenir de vos paramètres de profil dans GitLab" -#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:833 +#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:834 msgid "Red Hat Virtualization" msgstr "Red Hat Virtualization" @@ -3424,7 +3424,7 @@ msgstr "Fichier CA" msgid "Absolute file path to the CA file to use (optional)" msgstr "Chemin d'accès absolu vers le fichier CA à utiliser (en option)" -#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:834 +#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:835 msgid "Ansible Tower" msgstr "Ansible Tower" @@ -3468,7 +3468,7 @@ msgstr "La source doit être une information d'identification externe" msgid "Input field must be defined on target credential (options are {})." msgstr "Le champ de saisie doit être défini sur des informations d'identification externes (les options sont {})." -#: awx/main/models/events.py:152 awx/main/models/events.py:655 +#: awx/main/models/events.py:152 awx/main/models/events.py:674 msgid "Host Failed" msgstr "Échec de l'hôte" @@ -3476,7 +3476,7 @@ msgstr "Échec de l'hôte" msgid "Host Started" msgstr "Hôte démarré" -#: awx/main/models/events.py:154 awx/main/models/events.py:656 +#: awx/main/models/events.py:154 awx/main/models/events.py:675 msgid "Host OK" msgstr "Hôte OK" @@ -3484,11 +3484,11 @@ msgstr "Hôte OK" msgid "Host Failure" msgstr "Échec de l'hôte" -#: awx/main/models/events.py:156 awx/main/models/events.py:662 +#: awx/main/models/events.py:156 awx/main/models/events.py:681 msgid "Host Skipped" msgstr "Hôte ignoré" -#: awx/main/models/events.py:157 awx/main/models/events.py:657 +#: awx/main/models/events.py:157 awx/main/models/events.py:676 msgid "Host Unreachable" msgstr "Hôte inaccessible" @@ -3572,27 +3572,27 @@ msgstr "Scène démarrée" msgid "Playbook Complete" msgstr "Playbook terminé" -#: awx/main/models/events.py:184 awx/main/models/events.py:672 +#: awx/main/models/events.py:184 awx/main/models/events.py:691 msgid "Debug" msgstr "Déboguer" -#: awx/main/models/events.py:185 awx/main/models/events.py:673 +#: awx/main/models/events.py:185 awx/main/models/events.py:692 msgid "Verbose" msgstr "Verbeux" -#: awx/main/models/events.py:186 awx/main/models/events.py:674 +#: awx/main/models/events.py:186 awx/main/models/events.py:693 msgid "Deprecated" msgstr "Obsolète" -#: awx/main/models/events.py:187 awx/main/models/events.py:675 +#: awx/main/models/events.py:187 awx/main/models/events.py:694 msgid "Warning" msgstr "Avertissement" -#: awx/main/models/events.py:188 awx/main/models/events.py:676 +#: awx/main/models/events.py:188 awx/main/models/events.py:695 msgid "System Warning" msgstr "Avertissement système" -#: awx/main/models/events.py:189 awx/main/models/events.py:677 +#: awx/main/models/events.py:189 awx/main/models/events.py:696 #: awx/main/models/unified_jobs.py:75 msgid "Error" msgstr "Erreur" @@ -3620,300 +3620,300 @@ msgid "" "this group" msgstr "Liste des cas de concordance exacte qui seront toujours assignés automatiquement à ce groupe." -#: awx/main/models/inventory.py:79 +#: awx/main/models/inventory.py:80 msgid "Hosts have a direct link to this inventory." msgstr "Les hôtes ont un lien direct vers cet inventaire." -#: awx/main/models/inventory.py:80 +#: awx/main/models/inventory.py:81 msgid "Hosts for inventory generated using the host_filter property." msgstr "Hôtes pour inventaire générés avec la propriété host_filter." -#: awx/main/models/inventory.py:85 +#: awx/main/models/inventory.py:86 msgid "inventories" msgstr "inventaires" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:93 msgid "Organization containing this inventory." msgstr "Organisation contenant cet inventaire." -#: awx/main/models/inventory.py:99 +#: awx/main/models/inventory.py:100 msgid "Inventory variables in JSON or YAML format." msgstr "Variables d'inventaire au format JSON ou YAML." -#: awx/main/models/inventory.py:104 +#: awx/main/models/inventory.py:105 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether any hosts in this inventory have failed." msgstr "Ce champ est obsolète et sera supprimé dans une prochaine version. Indicateur signalant si des hôtes de cet inventaire ont échoué." -#: awx/main/models/inventory.py:110 +#: awx/main/models/inventory.py:111 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of hosts in this inventory." msgstr "Ce champ est obsolète et sera supprimé dans une prochaine version. Nombre total d'hôtes dans cet inventaire." -#: awx/main/models/inventory.py:116 +#: awx/main/models/inventory.py:117 msgid "" "This field is deprecated and will be removed in a future release. Number of " "hosts in this inventory with active failures." msgstr "Ce champ est obsolète et sera supprimé dans une prochaine version. Nombre d'hôtes dans cet inventaire avec des échecs actifs." -#: awx/main/models/inventory.py:122 +#: awx/main/models/inventory.py:123 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of groups in this inventory." msgstr "Ce champ est obsolète et sera supprimé dans une prochaine version. Nombre total de groupes dans cet inventaire." -#: awx/main/models/inventory.py:128 +#: awx/main/models/inventory.py:129 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether this inventory has any external inventory sources." msgstr "Ce champ est obsolète et sera supprimé dans une prochaine version. Indicateur signalant si cet inventaire a des sources d’inventaire externes." -#: awx/main/models/inventory.py:134 +#: awx/main/models/inventory.py:135 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "Nombre total de sources d'inventaire externes configurées dans cet inventaire." -#: awx/main/models/inventory.py:139 +#: awx/main/models/inventory.py:140 msgid "Number of external inventory sources in this inventory with failures." msgstr "Nombre total de sources d'inventaire externes en échec dans cet inventaire." -#: awx/main/models/inventory.py:146 +#: awx/main/models/inventory.py:147 msgid "Kind of inventory being represented." msgstr "Genre d'inventaire représenté." -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:153 msgid "Filter that will be applied to the hosts of this inventory." msgstr "Filtre appliqué aux hôtes de cet inventaire." -#: awx/main/models/inventory.py:180 +#: awx/main/models/inventory.py:181 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "Informations d'identification à utiliser par les hôtes appartenant à cet inventaire lors de l'accès à l'API Red Hat Insights ." -#: awx/main/models/inventory.py:189 +#: awx/main/models/inventory.py:190 msgid "Flag indicating the inventory is being deleted." msgstr "Marqueur indiquant que cet inventaire est en cours de suppression." -#: awx/main/models/inventory.py:244 +#: awx/main/models/inventory.py:245 msgid "Could not parse subset as slice specification." msgstr "N'a pas pu traiter les sous-ensembles en tant que spécification de découpage." -#: awx/main/models/inventory.py:248 +#: awx/main/models/inventory.py:249 msgid "Slice number must be less than total number of slices." msgstr "Le nombre de tranches doit être inférieur au nombre total de tranches." -#: awx/main/models/inventory.py:250 +#: awx/main/models/inventory.py:251 msgid "Slice number must be 1 or higher." msgstr "Le nombre de tranches doit être 1 ou valeur supérieure." -#: awx/main/models/inventory.py:387 +#: awx/main/models/inventory.py:388 msgid "Assignment not allowed for Smart Inventory" msgstr "Attribution non autorisée pour un inventaire Smart" -#: awx/main/models/inventory.py:389 awx/main/models/projects.py:166 +#: awx/main/models/inventory.py:390 awx/main/models/projects.py:166 msgid "Credential kind must be 'insights'." msgstr "Le genre d'informations d'identification doit être 'insights'." -#: awx/main/models/inventory.py:474 +#: awx/main/models/inventory.py:475 msgid "Is this host online and available for running jobs?" msgstr "Cet hôte est-il en ligne et disponible pour exécuter des tâches ?" -#: awx/main/models/inventory.py:480 +#: awx/main/models/inventory.py:481 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "Valeur utilisée par la source d'inventaire distante pour identifier l'hôte de façon unique" -#: awx/main/models/inventory.py:485 +#: awx/main/models/inventory.py:486 msgid "Host variables in JSON or YAML format." msgstr "Variables d'hôte au format JSON ou YAML." -#: awx/main/models/inventory.py:508 +#: awx/main/models/inventory.py:509 msgid "Inventory source(s) that created or modified this host." msgstr "Sources d'inventaire qui ont créé ou modifié cet hôte." -#: awx/main/models/inventory.py:513 +#: awx/main/models/inventory.py:514 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "Structure JSON arbitraire des faits ansible les plus récents, par hôte." -#: awx/main/models/inventory.py:519 +#: awx/main/models/inventory.py:520 msgid "The date and time ansible_facts was last modified." msgstr "Date et heure de la dernière modification apportée à ansible_facts." -#: awx/main/models/inventory.py:526 +#: awx/main/models/inventory.py:527 msgid "Red Hat Insights host unique identifier." msgstr "Identifiant unique de l'hôte de Red Hat Insights." -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:641 msgid "Group variables in JSON or YAML format." msgstr "Variables de groupe au format JSON ou YAML." -#: awx/main/models/inventory.py:646 +#: awx/main/models/inventory.py:647 msgid "Hosts associated directly with this group." msgstr "Hôtes associés directement à ce groupe." -#: awx/main/models/inventory.py:652 +#: awx/main/models/inventory.py:653 msgid "Inventory source(s) that created or modified this group." msgstr "Sources d'inventaire qui ont créé ou modifié ce groupe." -#: awx/main/models/inventory.py:824 +#: awx/main/models/inventory.py:825 msgid "File, Directory or Script" msgstr "Fichier, répertoire ou script" -#: awx/main/models/inventory.py:825 +#: awx/main/models/inventory.py:826 msgid "Sourced from a Project" msgstr "Provenance d'un projet" -#: awx/main/models/inventory.py:826 +#: awx/main/models/inventory.py:827 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:835 +#: awx/main/models/inventory.py:836 msgid "Custom Script" msgstr "Script personnalisé" -#: awx/main/models/inventory.py:952 +#: awx/main/models/inventory.py:953 msgid "Inventory source variables in YAML or JSON format." msgstr "Variables de source d'inventaire au format JSON ou YAML." -#: awx/main/models/inventory.py:963 +#: awx/main/models/inventory.py:964 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "Liste d'expressions de filtre séparées par des virgules (EC2 uniquement). Les hôtes sont importés lorsque l'UN des filtres correspondent." -#: awx/main/models/inventory.py:969 +#: awx/main/models/inventory.py:970 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "Limiter automatiquement les groupes créés à partir de la source d'inventaire (EC2 uniquement)." -#: awx/main/models/inventory.py:973 +#: awx/main/models/inventory.py:974 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "Écraser les groupes locaux et les hôtes de la source d'inventaire distante." -#: awx/main/models/inventory.py:977 +#: awx/main/models/inventory.py:978 msgid "Overwrite local variables from remote inventory source." msgstr "Écraser les variables locales de la source d'inventaire distante." -#: awx/main/models/inventory.py:982 awx/main/models/jobs.py:154 +#: awx/main/models/inventory.py:983 awx/main/models/jobs.py:154 #: awx/main/models/projects.py:135 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "Délai écoulé (en secondes) avant que la tâche ne soit annulée." -#: awx/main/models/inventory.py:1015 +#: awx/main/models/inventory.py:1016 msgid "Image ID" msgstr "ID d'image" -#: awx/main/models/inventory.py:1016 +#: awx/main/models/inventory.py:1017 msgid "Availability Zone" msgstr "Zone de disponibilité" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1019 msgid "Instance ID" msgstr "ID d'instance" -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1020 msgid "Instance State" msgstr "État de l'instance" -#: awx/main/models/inventory.py:1020 +#: awx/main/models/inventory.py:1021 msgid "Platform" msgstr "Plateforme " -#: awx/main/models/inventory.py:1021 +#: awx/main/models/inventory.py:1022 msgid "Instance Type" msgstr "Type d'instance" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1024 msgid "Region" msgstr "Région" -#: awx/main/models/inventory.py:1024 +#: awx/main/models/inventory.py:1025 msgid "Security Group" msgstr "Groupe de sécurité" -#: awx/main/models/inventory.py:1025 +#: awx/main/models/inventory.py:1026 msgid "Tags" msgstr "Balises" -#: awx/main/models/inventory.py:1026 +#: awx/main/models/inventory.py:1027 msgid "Tag None" msgstr "Ne rien baliser" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1028 msgid "VPC ID" msgstr "ID VPC" -#: awx/main/models/inventory.py:1095 +#: awx/main/models/inventory.py:1096 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "Les sources d'inventaire cloud (telles que %s) requièrent des informations d'identification pour le service cloud correspondant." -#: awx/main/models/inventory.py:1101 +#: awx/main/models/inventory.py:1102 msgid "Credential is required for a cloud source." msgstr "Les informations d'identification sont requises pour une source cloud." -#: awx/main/models/inventory.py:1104 +#: awx/main/models/inventory.py:1105 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "Les identifiants de type machine, contrôle de la source, insights ou archivage sécurisé ne sont pas autorisés par les sources d'inventaire personnalisées." -#: awx/main/models/inventory.py:1109 +#: awx/main/models/inventory.py:1110 msgid "" "Credentials of type insights and vault are disallowed for scm inventory " "sources." msgstr "Les identifiants de type insights ou archivage sécurisé ne sont pas autorisés pour les sources d'inventaire scm." -#: awx/main/models/inventory.py:1169 +#: awx/main/models/inventory.py:1170 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "Région %(source)s non valide : %(region)s" -#: awx/main/models/inventory.py:1193 +#: awx/main/models/inventory.py:1194 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "Expression de filtre non valide : %(filter)s" -#: awx/main/models/inventory.py:1214 +#: awx/main/models/inventory.py:1215 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "Choix de regroupement non valide : %(choice)s" -#: awx/main/models/inventory.py:1242 +#: awx/main/models/inventory.py:1243 msgid "Project containing inventory file used as source." msgstr "Projet contenant le fichier d'inventaire utilisé comme source." -#: awx/main/models/inventory.py:1415 +#: awx/main/models/inventory.py:1416 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "On n'autorise pas plus d'une source d'inventaire basé SCM avec mise à jour pré-inventaire ou mise à jour projet." -#: awx/main/models/inventory.py:1422 +#: awx/main/models/inventory.py:1423 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " "update on launch." msgstr "Impossible de mettre à jour une source d'inventaire SCM lors du lancement si elle est définie pour se mettre à jour lors de l'actualisation du projet. À la place, configurez le projet source correspondant pour qu'il se mette à jour au moment du lancement." -#: awx/main/models/inventory.py:1428 +#: awx/main/models/inventory.py:1429 msgid "Cannot set source_path if not SCM type." msgstr "Impossible de définir chemin_source si pas du type SCM." -#: awx/main/models/inventory.py:1471 +#: awx/main/models/inventory.py:1472 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "Les fichiers d'inventaire de cette mise à jour de projet ont été utilisés pour la mise à jour de l'inventaire." -#: awx/main/models/inventory.py:1582 +#: awx/main/models/inventory.py:1583 msgid "Inventory script contents" msgstr "Contenus des scripts d'inventaire" -#: awx/main/models/inventory.py:1587 +#: awx/main/models/inventory.py:1588 msgid "Organization owning this inventory script" msgstr "Organisation propriétaire de ce script d'inventaire." @@ -4012,28 +4012,28 @@ msgstr "Inventaire appliqué en tant qu'invite, en supposant que le modèle de t msgid "job host summaries" msgstr "récapitulatifs des hôtes pour la tâche" -#: awx/main/models/jobs.py:1158 +#: awx/main/models/jobs.py:1144 msgid "Remove jobs older than a certain number of days" msgstr "Supprimer les tâches plus anciennes qu'un certain nombre de jours" -#: awx/main/models/jobs.py:1159 +#: awx/main/models/jobs.py:1145 msgid "Remove activity stream entries older than a certain number of days" msgstr "Supprimer les entrées du flux d'activité plus anciennes qu'un certain nombre de jours" -#: awx/main/models/jobs.py:1160 +#: awx/main/models/jobs.py:1146 msgid "Removes expired browser sessions from the database" msgstr "Supprime les sessions de navigateur expirées dans la base de données" -#: awx/main/models/jobs.py:1161 +#: awx/main/models/jobs.py:1147 msgid "Removes expired OAuth 2 access tokens and refresh tokens" msgstr "Supprime les jetons d'accès OAuth 2 et les jetons d’actualisation arrivés à expiration" -#: awx/main/models/jobs.py:1231 +#: awx/main/models/jobs.py:1217 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "Les variables {list_of_keys} ne sont pas autorisées pour les tâches système." -#: awx/main/models/jobs.py:1247 +#: awx/main/models/jobs.py:1233 msgid "days must be a positive integer." msgstr "jours doit être un entier positif." @@ -4777,7 +4777,7 @@ msgstr "Aucun chemin de traitement des erreurs trouvé, flux de travail marqué msgid "The approval node {name} ({pk}) has expired after {timeout} seconds." msgstr "Le nœud d'approbation {name} ({pk}) a expiré après {timeout} secondes." -#: awx/main/tasks.py:1053 +#: awx/main/tasks.py:1049 msgid "Invalid virtual environment selected: {}" msgstr "Environnement virtuel non valide sélectionné : {}" @@ -4814,53 +4814,53 @@ msgstr "Aucun chemin de traitement des erreurs pour le ou les nœuds de tâche d msgid "Unable to convert \"%s\" to boolean" msgstr "Impossible de convertir \"%s\" en booléen" -#: awx/main/utils/common.py:275 +#: awx/main/utils/common.py:261 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Type de SCM \"%s\" non pris en charge" -#: awx/main/utils/common.py:282 awx/main/utils/common.py:294 -#: awx/main/utils/common.py:313 +#: awx/main/utils/common.py:268 awx/main/utils/common.py:280 +#: awx/main/utils/common.py:299 #, python-format msgid "Invalid %s URL" msgstr "URL %s non valide" -#: awx/main/utils/common.py:284 awx/main/utils/common.py:323 +#: awx/main/utils/common.py:270 awx/main/utils/common.py:309 #, python-format msgid "Unsupported %s URL" msgstr "URL %s non prise en charge" -#: awx/main/utils/common.py:325 +#: awx/main/utils/common.py:311 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "Hôte \"%s\" non pris en charge pour le fichier ://URL" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:313 #, python-format msgid "Host is required for %s URL" msgstr "L'hôte est requis pour l'URL %s" -#: awx/main/utils/common.py:345 +#: awx/main/utils/common.py:331 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "Le nom d'utilisateur doit être \"git\" pour l'accès SSH à %s." -#: awx/main/utils/common.py:351 +#: awx/main/utils/common.py:337 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "Le nom d'utilisateur doit être \"hg\" pour l'accès SSH à %s." -#: awx/main/utils/common.py:682 +#: awx/main/utils/common.py:668 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "Le type d'entrée ’{data_type}’ n'est pas un dictionnaire" -#: awx/main/utils/common.py:715 +#: awx/main/utils/common.py:701 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "Variables non compatibles avec la norme JSON (error : {json_error})" -#: awx/main/utils/common.py:721 +#: awx/main/utils/common.py:707 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." diff --git a/awx/locale/ja/LC_MESSAGES/django.po b/awx/locale/ja/LC_MESSAGES/django.po index db72a7ec8f..a878a3d1ad 100644 --- a/awx/locale/ja/LC_MESSAGES/django.po +++ b/awx/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-27 13:55+0000\n" +"POT-Creation-Date: 2020-05-28 21:45+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -528,7 +528,7 @@ msgstr "ジョブテンプレートインベントリーが見つからないか msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "不明です。ジョブは起動設定が保存される前に実行された可能性があります。" -#: awx/api/serializers.py:3252 awx/main/tasks.py:2795 awx/main/tasks.py:2813 +#: awx/api/serializers.py:3252 awx/main/tasks.py:2800 awx/main/tasks.py:2818 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} の使用はアドホックコマンドで禁止されています。" @@ -547,324 +547,324 @@ msgstr "指定された変数 {} には置き換えるデータベースの値 msgid "\"$encrypted$ is a reserved keyword, may not be used for {}.\"" msgstr "\"$encrypted$ は予約されたキーワードで、{} には使用できません。\"" -#: awx/api/serializers.py:4070 +#: awx/api/serializers.py:4078 msgid "A project is required to run a job." msgstr "ジョブを実行するにはプロジェクトが必要です。" -#: awx/api/serializers.py:4072 +#: awx/api/serializers.py:4080 msgid "Missing a revision to run due to failed project update." msgstr "プロジェクトの更新に失敗したため、実行するリビジョンがありません。" -#: awx/api/serializers.py:4076 +#: awx/api/serializers.py:4084 msgid "The inventory associated with this Job Template is being deleted." msgstr "このジョブテンプレートに関連付けられているインベントリーが削除されています。" -#: awx/api/serializers.py:4078 awx/api/serializers.py:4194 +#: awx/api/serializers.py:4086 awx/api/serializers.py:4202 msgid "The provided inventory is being deleted." msgstr "指定されたインベントリーが削除されています。" -#: awx/api/serializers.py:4086 +#: awx/api/serializers.py:4094 msgid "Cannot assign multiple {} credentials." msgstr "複数の {} 認証情報を割り当てることができません。" -#: awx/api/serializers.py:4090 +#: awx/api/serializers.py:4098 msgid "Cannot assign a Credential of kind `{}`" msgstr "`{}`の種類の認証情報を割り当てることができません。" -#: awx/api/serializers.py:4103 +#: awx/api/serializers.py:4111 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "置き換えなしで起動時に {} 認証情報を削除することはサポートされていません。指定された一覧には認証情報がありません: {}" -#: awx/api/serializers.py:4192 +#: awx/api/serializers.py:4200 msgid "The inventory associated with this Workflow is being deleted." msgstr "このワークフローに関連付けられているインベントリーが削除されています。" -#: awx/api/serializers.py:4263 +#: awx/api/serializers.py:4271 msgid "Message type '{}' invalid, must be either 'message' or 'body'" msgstr "メッセージタイプ '{}' が無効です。'メッセージ' または 'ボディー' のいずれかに指定する必要があります。" -#: awx/api/serializers.py:4269 +#: awx/api/serializers.py:4277 msgid "Expected string for '{}', found {}, " msgstr "'{}' の文字列が必要ですが、{} が見つかりました。 " -#: awx/api/serializers.py:4273 +#: awx/api/serializers.py:4281 msgid "Messages cannot contain newlines (found newline in {} event)" msgstr "メッセージでは改行を追加できません ({} イベントに改行が含まれます)" -#: awx/api/serializers.py:4279 +#: awx/api/serializers.py:4287 msgid "Expected dict for 'messages' field, found {}" msgstr "'messages' フィールドには辞書が必要ですが、{} が見つかりました。" -#: awx/api/serializers.py:4283 +#: awx/api/serializers.py:4291 msgid "" "Event '{}' invalid, must be one of 'started', 'success', 'error', or " "'workflow_approval'" msgstr "イベント '{}' は無効です。'started'、'success'、'error' または 'workflow_approval' のいずれかでなければなりません。" -#: awx/api/serializers.py:4289 +#: awx/api/serializers.py:4297 msgid "Expected dict for event '{}', found {}" msgstr "イベント '{}' には辞書が必要ですが、{} が見つかりました。" -#: awx/api/serializers.py:4294 +#: awx/api/serializers.py:4302 msgid "" "Workflow Approval event '{}' invalid, must be one of 'running', 'approved', " "'timed_out', or 'denied'" msgstr "ワークフロー承認イベント '{}' が無効です。'running'、'approved'、'timed_out' または 'denied' のいずれかでなければなりません。" -#: awx/api/serializers.py:4301 +#: awx/api/serializers.py:4309 msgid "Expected dict for workflow approval event '{}', found {}" msgstr "ワークフロー承認イベント '{}' には辞書が必要ですが、{} が見つかりました。" -#: awx/api/serializers.py:4328 +#: awx/api/serializers.py:4336 msgid "Unable to render message '{}': {}" msgstr "メッセージ '{}' のレンダリングができません: {}" -#: awx/api/serializers.py:4330 +#: awx/api/serializers.py:4338 msgid "Field '{}' unavailable" msgstr "フィールド '{}' が利用できません" -#: awx/api/serializers.py:4332 +#: awx/api/serializers.py:4340 msgid "Security error due to field '{}'" msgstr "フィールド '{}' が原因のセキュリティーエラー" -#: awx/api/serializers.py:4352 +#: awx/api/serializers.py:4360 msgid "Webhook body for '{}' should be a json dictionary. Found type '{}'." msgstr "'{}' の Webhook のボディーは json 辞書でなければなりません。'{}' のタイプが見つかりました。" -#: awx/api/serializers.py:4355 +#: awx/api/serializers.py:4363 msgid "Webhook body for '{}' is not a valid json dictionary ({})." msgstr "'{}' の Webhook ボディーは有効な json 辞書ではありません ({})。" -#: awx/api/serializers.py:4373 +#: awx/api/serializers.py:4381 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "通知設定の必須フィールドがありません: notification_type" -#: awx/api/serializers.py:4400 +#: awx/api/serializers.py:4408 msgid "No values specified for field '{}'" msgstr "フィールド '{}' に値が指定されていません" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4413 msgid "HTTP method must be either 'POST' or 'PUT'." msgstr "HTTP メソッドは 'POST' または 'PUT' のいずれかでなければなりません。" -#: awx/api/serializers.py:4407 +#: awx/api/serializers.py:4415 msgid "Missing required fields for Notification Configuration: {}." msgstr "通知設定の必須フィールドがありません: {}。" -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4418 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "設定フィールド '{}' のタイプが正しくありません。{} が予期されました。" -#: awx/api/serializers.py:4427 +#: awx/api/serializers.py:4435 msgid "Notification body" msgstr "通知ボディー" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4515 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "有効な DTSTART が rrule で必要です。値は DTSTART:YYYYMMDDTHHMMSSZ で開始する必要があります。" -#: awx/api/serializers.py:4509 +#: awx/api/serializers.py:4517 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "DTSTART をネイティブの日時にすることができません。;TZINFO= or YYYYMMDDTHHMMSSZZ を指定します。" -#: awx/api/serializers.py:4511 +#: awx/api/serializers.py:4519 msgid "Multiple DTSTART is not supported." msgstr "複数の DTSTART はサポートされません。" -#: awx/api/serializers.py:4513 +#: awx/api/serializers.py:4521 msgid "RRULE required in rrule." msgstr "RRULE が rrule で必要です。" -#: awx/api/serializers.py:4515 +#: awx/api/serializers.py:4523 msgid "Multiple RRULE is not supported." msgstr "複数の RRULE はサポートされません。" -#: awx/api/serializers.py:4517 +#: awx/api/serializers.py:4525 msgid "INTERVAL required in rrule." msgstr "INTERVAL が rrule で必要です。" -#: awx/api/serializers.py:4519 +#: awx/api/serializers.py:4527 msgid "SECONDLY is not supported." msgstr "SECONDLY はサポートされません。" -#: awx/api/serializers.py:4521 +#: awx/api/serializers.py:4529 msgid "Multiple BYMONTHDAYs not supported." msgstr "複数の BYMONTHDAY はサポートされません。" -#: awx/api/serializers.py:4523 +#: awx/api/serializers.py:4531 msgid "Multiple BYMONTHs not supported." msgstr "複数の BYMONTH はサポートされません。" -#: awx/api/serializers.py:4525 +#: awx/api/serializers.py:4533 msgid "BYDAY with numeric prefix not supported." msgstr "数字の接頭辞のある BYDAY はサポートされません。" -#: awx/api/serializers.py:4527 +#: awx/api/serializers.py:4535 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY はサポートされません。" -#: awx/api/serializers.py:4529 +#: awx/api/serializers.py:4537 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO はサポートされません。" -#: awx/api/serializers.py:4531 +#: awx/api/serializers.py:4539 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "RRULE には COUNT と UNTIL の両方を含めることができません" -#: awx/api/serializers.py:4535 +#: awx/api/serializers.py:4543 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 はサポートされません。" -#: awx/api/serializers.py:4541 +#: awx/api/serializers.py:4549 msgid "rrule parsing failed validation: {}" msgstr "rrule の構文解析で検証に失敗しました: {}" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4611 msgid "Inventory Source must be a cloud resource." msgstr "インベントリーソースはクラウドリソースでなければなりません。" -#: awx/api/serializers.py:4605 +#: awx/api/serializers.py:4613 msgid "Manual Project cannot have a schedule set." msgstr "手動プロジェクトにはスケジュールを設定できません。" -#: awx/api/serializers.py:4608 +#: awx/api/serializers.py:4616 msgid "" "Inventory sources with `update_on_project_update` cannot be scheduled. " "Schedule its source project `{}` instead." msgstr "「update_on_project_update」が設定されたインベントリーソースはスケジュールできません。代わりのそのソースプロジェクト「{}」 をスケジュールします。" -#: awx/api/serializers.py:4618 +#: awx/api/serializers.py:4626 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "このインスタンスにターゲット設定されている実行中または待機状態のジョブの数" -#: awx/api/serializers.py:4623 +#: awx/api/serializers.py:4631 msgid "Count of all jobs that target this instance" msgstr "このインスタンスをターゲットに設定するすべてのジョブの数" -#: awx/api/serializers.py:4656 +#: awx/api/serializers.py:4664 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance group" msgstr "このインスタンスグループにターゲット設定されている実行中または待機状態のジョブの数" -#: awx/api/serializers.py:4661 +#: awx/api/serializers.py:4669 msgid "Count of all jobs that target this instance group" msgstr "このインスタンスグループをターゲットに設定するすべてのジョブの数" -#: awx/api/serializers.py:4666 +#: awx/api/serializers.py:4674 msgid "Indicates whether instance group controls any other group" msgstr "インスタンスグループが他のグループを制御するかどうかを指定します。" -#: awx/api/serializers.py:4670 +#: awx/api/serializers.py:4678 msgid "" "Indicates whether instances in this group are isolated.Isolated groups have " "a designated controller group." msgstr "このグループ内でインスタンスを分離させるかを指定します。分離されたグループには指定したコントローラーグループがあります。" -#: awx/api/serializers.py:4675 +#: awx/api/serializers.py:4683 msgid "" "Indicates whether instances in this group are containerized.Containerized " "groups have a designated Openshift or Kubernetes cluster." msgstr "このグループ内でインスタンスをコンテナー化するかを指定します。コンテナー化したグループには、指定の OpenShift または Kubernetes クラスターが含まれます。" -#: awx/api/serializers.py:4683 +#: awx/api/serializers.py:4691 msgid "Policy Instance Percentage" msgstr "ポリシーインスタンスの割合" -#: awx/api/serializers.py:4684 +#: awx/api/serializers.py:4692 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "新規インスタンスがオンラインになると、このグループに自動的に最小限割り当てられるインスタンスの割合を選択します。" -#: awx/api/serializers.py:4689 +#: awx/api/serializers.py:4697 msgid "Policy Instance Minimum" msgstr "ポリシーインスタンスの最小値" -#: awx/api/serializers.py:4690 +#: awx/api/serializers.py:4698 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "新規インスタンスがオンラインになると、このグループに自動的に最小限割り当てられるインスタンス数を入力します。" -#: awx/api/serializers.py:4695 +#: awx/api/serializers.py:4703 msgid "Policy Instance List" msgstr "ポリシーインスタンスの一覧" -#: awx/api/serializers.py:4696 +#: awx/api/serializers.py:4704 msgid "List of exact-match Instances that will be assigned to this group" msgstr "このグループに割り当てられる完全一致のインスタンスの一覧" -#: awx/api/serializers.py:4722 +#: awx/api/serializers.py:4730 msgid "Duplicate entry {}." msgstr "重複するエントリー {}。" -#: awx/api/serializers.py:4724 +#: awx/api/serializers.py:4732 msgid "{} is not a valid hostname of an existing instance." msgstr "{} は既存インスタンスの有効なホスト名ではありません。" -#: awx/api/serializers.py:4726 awx/api/views/mixin.py:98 +#: awx/api/serializers.py:4734 awx/api/views/mixin.py:98 msgid "" "Isolated instances may not be added or removed from instances groups via the " "API." msgstr "分離されたインスタンスは、API 経由でインスタンスグループから追加したり、削除したりすることができません。" -#: awx/api/serializers.py:4728 awx/api/views/mixin.py:102 +#: awx/api/serializers.py:4736 awx/api/views/mixin.py:102 msgid "Isolated instance group membership may not be managed via the API." msgstr "分離されたインスタンスグループのメンバーシップは API で管理できません。" -#: awx/api/serializers.py:4730 awx/api/serializers.py:4735 -#: awx/api/serializers.py:4740 +#: awx/api/serializers.py:4738 awx/api/serializers.py:4743 +#: awx/api/serializers.py:4748 msgid "Containerized instances may not be managed via the API" msgstr "コンテナー化されたインスタンスは API で管理されないことがあります" -#: awx/api/serializers.py:4745 +#: awx/api/serializers.py:4753 msgid "tower instance group name may not be changed." msgstr "Tower のインスタンスグループ名は変更できません。" -#: awx/api/serializers.py:4750 +#: awx/api/serializers.py:4758 msgid "Only Kubernetes credentials can be associated with an Instance Group" msgstr "インスタンスグループに関連付けることができる Kubernetes 認証情報のみです" -#: awx/api/serializers.py:4789 +#: awx/api/serializers.py:4797 msgid "" "When present, shows the field name of the role or relationship that changed." msgstr "これがある場合には、変更された関係またはロールのフィールド名を表示します。" -#: awx/api/serializers.py:4791 +#: awx/api/serializers.py:4799 msgid "" "When present, shows the model on which the role or relationship was defined." msgstr "これがある場合には、ロールまたは関係が定義されているモデルを表示します。" -#: awx/api/serializers.py:4824 +#: awx/api/serializers.py:4832 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "オブジェクトの作成、更新または削除時の新規値および変更された値の概要" -#: awx/api/serializers.py:4826 +#: awx/api/serializers.py:4834 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "作成、更新、および削除イベントの場合、これは影響を受けたオブジェクトタイプになります。関連付けおよび関連付け解除イベントの場合、これは object2 に関連付けられたか、またはその関連付けが解除されたオブジェクトタイプになります。" -#: awx/api/serializers.py:4829 +#: awx/api/serializers.py:4837 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "作成、更新、および削除イベントの場合は設定されません。関連付けおよび関連付け解除イベントの場合、これは object1 が関連付けられるオブジェクトタイプになります。" -#: awx/api/serializers.py:4832 +#: awx/api/serializers.py:4840 msgid "The action taken with respect to the given object(s)." msgstr "指定されたオブジェクトについて実行されたアクション。" @@ -1638,7 +1638,7 @@ msgstr "設定例" msgid "Example setting which can be different for each user." msgstr "ユーザーごとに異なる設定例" -#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:55 +#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:56 msgid "User" msgstr "ユーザー" @@ -1741,15 +1741,15 @@ msgstr "システム" msgid "OtherSystem" msgstr "他のシステム" -#: awx/conf/views.py:47 +#: awx/conf/views.py:48 msgid "Setting Categories" msgstr "設定カテゴリー" -#: awx/conf/views.py:69 +#: awx/conf/views.py:70 msgid "Setting Detail" msgstr "設定の詳細" -#: awx/conf/views.py:160 +#: awx/conf/views.py:162 msgid "Logging Connectivity Test" msgstr "ロギング接続テスト" @@ -2794,7 +2794,7 @@ msgstr "Conjur URL" msgid "API Key" msgstr "API キー" -#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1017 +#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1018 msgid "Account" msgstr "アカウント" @@ -2881,7 +2881,7 @@ msgid "" msgstr "KV シークレットバックエンド名 (空白の場合は、シークレットパスの最初のセグメントが使用されます)。" #: awx/main/credential_plugins/hashivault.py:60 -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1023 msgid "Key Name" msgstr "キー名" @@ -3258,7 +3258,7 @@ msgid "" "Management (IAM) users." msgstr "セキュリティートークンサービス (STS) は、AWS Identity and Access Management (IAM) ユーザーの一時的な、権限の制限された認証情報を要求できる web サービスです。" -#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:832 +#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:833 msgid "OpenStack" msgstr "OpenStack" @@ -3298,7 +3298,7 @@ msgstr "OpenStack ドメインは管理上の境界を定義します。これ msgid "Verify SSL" msgstr "SSL の検証" -#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:829 +#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:830 msgid "VMware vCenter" msgstr "VMware vCenter" @@ -3311,7 +3311,7 @@ msgid "" "Enter the hostname or IP address that corresponds to your VMware vCenter." msgstr "VMware vCenter に対応するホスト名または IP アドレスを入力します。" -#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:830 +#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:831 msgid "Red Hat Satellite 6" msgstr "Red Hat Satellite 6" @@ -3325,7 +3325,7 @@ msgid "" "example, https://satellite.example.org" msgstr "Red Hat Satellite 6 Server に対応する URL を入力します (例: https://satellite.example.org)。" -#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:831 +#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:832 msgid "Red Hat CloudForms" msgstr "Red Hat CloudForms" @@ -3339,7 +3339,7 @@ msgid "" "instance. For example, https://cloudforms.example.org" msgstr "CloudForms インスタンスに対応する仮想マシンの URL を入力します (例: https://cloudforms.example.org)。" -#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:827 +#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:828 msgid "Google Compute Engine" msgstr "Google Compute Engine" @@ -3368,7 +3368,7 @@ msgid "" "Paste the contents of the PEM file associated with the service account email." msgstr "サービスアカウントメールに関連付けられた PEM ファイルの内容を貼り付けます。" -#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:828 +#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:829 msgid "Microsoft Azure Resource Manager" msgstr "Microsoft Azure Resource Manager" @@ -3406,7 +3406,7 @@ msgstr "GitLab パーソナルアクセストークン" msgid "This token needs to come from your profile settings in GitLab" msgstr "このトークンは GitLab のプロファイル設定から取得する必要があります。" -#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:833 +#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:834 msgid "Red Hat Virtualization" msgstr "Red Hat Virtualization" @@ -3422,7 +3422,7 @@ msgstr "CA ファイル" msgid "Absolute file path to the CA file to use (optional)" msgstr "使用する CA ファイルへの絶対ファイルパス (オプション)" -#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:834 +#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:835 msgid "Ansible Tower" msgstr "Ansible Tower" @@ -3466,7 +3466,7 @@ msgstr "ソースは、外部の認証情報でなければなりません。" msgid "Input field must be defined on target credential (options are {})." msgstr "入力フィールドは、ターゲットの認証情報 (オプションは {}) で定義する必要があります。" -#: awx/main/models/events.py:152 awx/main/models/events.py:655 +#: awx/main/models/events.py:152 awx/main/models/events.py:674 msgid "Host Failed" msgstr "ホストの失敗" @@ -3474,7 +3474,7 @@ msgstr "ホストの失敗" msgid "Host Started" msgstr "ホストの開始" -#: awx/main/models/events.py:154 awx/main/models/events.py:656 +#: awx/main/models/events.py:154 awx/main/models/events.py:675 msgid "Host OK" msgstr "ホスト OK" @@ -3482,11 +3482,11 @@ msgstr "ホスト OK" msgid "Host Failure" msgstr "ホストの失敗" -#: awx/main/models/events.py:156 awx/main/models/events.py:662 +#: awx/main/models/events.py:156 awx/main/models/events.py:681 msgid "Host Skipped" msgstr "ホストがスキップされました" -#: awx/main/models/events.py:157 awx/main/models/events.py:657 +#: awx/main/models/events.py:157 awx/main/models/events.py:676 msgid "Host Unreachable" msgstr "ホストに到達できません" @@ -3570,27 +3570,27 @@ msgstr "プレイの開始" msgid "Playbook Complete" msgstr "Playbook の完了" -#: awx/main/models/events.py:184 awx/main/models/events.py:672 +#: awx/main/models/events.py:184 awx/main/models/events.py:691 msgid "Debug" msgstr "デバッグ" -#: awx/main/models/events.py:185 awx/main/models/events.py:673 +#: awx/main/models/events.py:185 awx/main/models/events.py:692 msgid "Verbose" msgstr "詳細" -#: awx/main/models/events.py:186 awx/main/models/events.py:674 +#: awx/main/models/events.py:186 awx/main/models/events.py:693 msgid "Deprecated" msgstr "非推奨" -#: awx/main/models/events.py:187 awx/main/models/events.py:675 +#: awx/main/models/events.py:187 awx/main/models/events.py:694 msgid "Warning" msgstr "警告" -#: awx/main/models/events.py:188 awx/main/models/events.py:676 +#: awx/main/models/events.py:188 awx/main/models/events.py:695 msgid "System Warning" msgstr "システム警告" -#: awx/main/models/events.py:189 awx/main/models/events.py:677 +#: awx/main/models/events.py:189 awx/main/models/events.py:696 #: awx/main/models/unified_jobs.py:75 msgid "Error" msgstr "エラー" @@ -3618,300 +3618,300 @@ msgid "" "this group" msgstr "このグループに常に自動的に割り当てられる完全一致のインスタンスの一覧" -#: awx/main/models/inventory.py:79 +#: awx/main/models/inventory.py:80 msgid "Hosts have a direct link to this inventory." msgstr "ホストにはこのインベントリーへの直接のリンクがあります。" -#: awx/main/models/inventory.py:80 +#: awx/main/models/inventory.py:81 msgid "Hosts for inventory generated using the host_filter property." msgstr "host_filter プロパティーを使用して生成されたインベントリーのホスト。" -#: awx/main/models/inventory.py:85 +#: awx/main/models/inventory.py:86 msgid "inventories" msgstr "インベントリー" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:93 msgid "Organization containing this inventory." msgstr "このインベントリーを含む組織。" -#: awx/main/models/inventory.py:99 +#: awx/main/models/inventory.py:100 msgid "Inventory variables in JSON or YAML format." msgstr "JSON または YAML 形式のインベントリー変数。" -#: awx/main/models/inventory.py:104 +#: awx/main/models/inventory.py:105 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether any hosts in this inventory have failed." msgstr "このフィールドは非推奨で、今後のリリースで削除予定です。このインベントリーのホストが失敗したかどうかを示すフラグ。" -#: awx/main/models/inventory.py:110 +#: awx/main/models/inventory.py:111 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of hosts in this inventory." msgstr "このフィールドは非推奨で、今後のリリースで削除予定です。このインベントリーでの合計ホスト数。" -#: awx/main/models/inventory.py:116 +#: awx/main/models/inventory.py:117 msgid "" "This field is deprecated and will be removed in a future release. Number of " "hosts in this inventory with active failures." msgstr "このフィールドは非推奨で、今後のリリースで削除予定です。このインベントリーで障害が発生中のホスト数。" -#: awx/main/models/inventory.py:122 +#: awx/main/models/inventory.py:123 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of groups in this inventory." msgstr "このフィールドは非推奨で、今後のリリースで削除予定です。このインベントリーでの合計グループ数。" -#: awx/main/models/inventory.py:128 +#: awx/main/models/inventory.py:129 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether this inventory has any external inventory sources." msgstr "このフィールドは非推奨で、今後のリリースで削除予定です。このインベントリーに外部のインベントリーソースがあるかどうかを示すフラグ。" -#: awx/main/models/inventory.py:134 +#: awx/main/models/inventory.py:135 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "このインベントリー内で設定される外部インベントリーソースの合計数。" -#: awx/main/models/inventory.py:139 +#: awx/main/models/inventory.py:140 msgid "Number of external inventory sources in this inventory with failures." msgstr "エラーのあるこのインベントリー内の外部インベントリーソースの数。" -#: awx/main/models/inventory.py:146 +#: awx/main/models/inventory.py:147 msgid "Kind of inventory being represented." msgstr "表示されているインベントリーの種類。" -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:153 msgid "Filter that will be applied to the hosts of this inventory." msgstr "このインべントリーのホストに適用されるフィルター。" -#: awx/main/models/inventory.py:180 +#: awx/main/models/inventory.py:181 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "Red Hat Insights API へのアクセス時にこのインベントリーに属するホストによって使用される認証情報。" -#: awx/main/models/inventory.py:189 +#: awx/main/models/inventory.py:190 msgid "Flag indicating the inventory is being deleted." msgstr "このインベントリーが削除されていることを示すフラグ。" -#: awx/main/models/inventory.py:244 +#: awx/main/models/inventory.py:245 msgid "Could not parse subset as slice specification." msgstr "サブセットをスライスの詳細として解析できませんでした。" -#: awx/main/models/inventory.py:248 +#: awx/main/models/inventory.py:249 msgid "Slice number must be less than total number of slices." msgstr "スライス番号はスライスの合計数より小さくなければなりません。" -#: awx/main/models/inventory.py:250 +#: awx/main/models/inventory.py:251 msgid "Slice number must be 1 or higher." msgstr "スライス番号は 1 以上でなければなりません。" -#: awx/main/models/inventory.py:387 +#: awx/main/models/inventory.py:388 msgid "Assignment not allowed for Smart Inventory" msgstr "割り当てはスマートインベントリーでは許可されません" -#: awx/main/models/inventory.py:389 awx/main/models/projects.py:166 +#: awx/main/models/inventory.py:390 awx/main/models/projects.py:166 msgid "Credential kind must be 'insights'." msgstr "認証情報の種類は「insights」である必要があります。" -#: awx/main/models/inventory.py:474 +#: awx/main/models/inventory.py:475 msgid "Is this host online and available for running jobs?" msgstr "このホストはオンラインで、ジョブを実行するために利用できますか?" -#: awx/main/models/inventory.py:480 +#: awx/main/models/inventory.py:481 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "ホストを一意に識別するためにリモートインベントリーソースで使用される値" -#: awx/main/models/inventory.py:485 +#: awx/main/models/inventory.py:486 msgid "Host variables in JSON or YAML format." msgstr "JSON または YAML 形式のホスト変数。" -#: awx/main/models/inventory.py:508 +#: awx/main/models/inventory.py:509 msgid "Inventory source(s) that created or modified this host." msgstr "このホストを作成または変更したインベントリーソース。" -#: awx/main/models/inventory.py:513 +#: awx/main/models/inventory.py:514 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "ホスト別の最新 ansible_facts の任意の JSON 構造。" -#: awx/main/models/inventory.py:519 +#: awx/main/models/inventory.py:520 msgid "The date and time ansible_facts was last modified." msgstr "ansible_facts の最終変更日時。" -#: awx/main/models/inventory.py:526 +#: awx/main/models/inventory.py:527 msgid "Red Hat Insights host unique identifier." msgstr "Red Hat Insights ホスト固有 ID。" -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:641 msgid "Group variables in JSON or YAML format." msgstr "JSON または YAML 形式のグループ変数。" -#: awx/main/models/inventory.py:646 +#: awx/main/models/inventory.py:647 msgid "Hosts associated directly with this group." msgstr "このグループに直接関連付けられたホスト。" -#: awx/main/models/inventory.py:652 +#: awx/main/models/inventory.py:653 msgid "Inventory source(s) that created or modified this group." msgstr "このグループを作成または変更したインベントリーソース。" -#: awx/main/models/inventory.py:824 +#: awx/main/models/inventory.py:825 msgid "File, Directory or Script" msgstr "ファイル、ディレクトリーまたはスクリプト" -#: awx/main/models/inventory.py:825 +#: awx/main/models/inventory.py:826 msgid "Sourced from a Project" msgstr "ソース: プロジェクト" -#: awx/main/models/inventory.py:826 +#: awx/main/models/inventory.py:827 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:835 +#: awx/main/models/inventory.py:836 msgid "Custom Script" msgstr "カスタムスクリプト" -#: awx/main/models/inventory.py:952 +#: awx/main/models/inventory.py:953 msgid "Inventory source variables in YAML or JSON format." msgstr "YAML または JSON 形式のインベントリーソース変数。" -#: awx/main/models/inventory.py:963 +#: awx/main/models/inventory.py:964 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "カンマ区切りのフィルター式の一覧 (EC2 のみ) です。ホストは、フィルターのいずれかが一致する場合にインポートされます。" -#: awx/main/models/inventory.py:969 +#: awx/main/models/inventory.py:970 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "インベントリーソースから自動的に作成されるグループを制限します (EC2 のみ)。" -#: awx/main/models/inventory.py:973 +#: awx/main/models/inventory.py:974 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "リモートインベントリーソースからのローカルグループおよびホストを上書きします。" -#: awx/main/models/inventory.py:977 +#: awx/main/models/inventory.py:978 msgid "Overwrite local variables from remote inventory source." msgstr "リモートインベントリーソースからのローカル変数を上書きします。" -#: awx/main/models/inventory.py:982 awx/main/models/jobs.py:154 +#: awx/main/models/inventory.py:983 awx/main/models/jobs.py:154 #: awx/main/models/projects.py:135 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "タスクが取り消される前の実行時間 (秒数)。" -#: awx/main/models/inventory.py:1015 +#: awx/main/models/inventory.py:1016 msgid "Image ID" msgstr "イメージ ID" -#: awx/main/models/inventory.py:1016 +#: awx/main/models/inventory.py:1017 msgid "Availability Zone" msgstr "アベイラビリティーゾーン" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1019 msgid "Instance ID" msgstr "インスタンス ID" -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1020 msgid "Instance State" msgstr "インスタンスの状態" -#: awx/main/models/inventory.py:1020 +#: awx/main/models/inventory.py:1021 msgid "Platform" msgstr "プラットフォーム" -#: awx/main/models/inventory.py:1021 +#: awx/main/models/inventory.py:1022 msgid "Instance Type" msgstr "インスタンスタイプ" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1024 msgid "Region" msgstr "リージョン" -#: awx/main/models/inventory.py:1024 +#: awx/main/models/inventory.py:1025 msgid "Security Group" msgstr "セキュリティーグループ" -#: awx/main/models/inventory.py:1025 +#: awx/main/models/inventory.py:1026 msgid "Tags" msgstr "タグ" -#: awx/main/models/inventory.py:1026 +#: awx/main/models/inventory.py:1027 msgid "Tag None" msgstr "タグ None" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1028 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1095 +#: awx/main/models/inventory.py:1096 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "クラウドベースのインベントリーソース (%s など) には一致するクラウドサービスの認証情報が必要です。" -#: awx/main/models/inventory.py:1101 +#: awx/main/models/inventory.py:1102 msgid "Credential is required for a cloud source." msgstr "認証情報がクラウドソースに必要です。" -#: awx/main/models/inventory.py:1104 +#: awx/main/models/inventory.py:1105 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "タイプがマシン、ソースコントロール、Insights および Vault の認証情報はカスタムインベントリーソースには許可されません。" -#: awx/main/models/inventory.py:1109 +#: awx/main/models/inventory.py:1110 msgid "" "Credentials of type insights and vault are disallowed for scm inventory " "sources." msgstr "タイプが Insights および Vault の認証情報は SCM のインベントリーソースには許可されません。" -#: awx/main/models/inventory.py:1169 +#: awx/main/models/inventory.py:1170 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "無効な %(source)s リージョン: %(region)s" -#: awx/main/models/inventory.py:1193 +#: awx/main/models/inventory.py:1194 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "無効なフィルター式: %(filter)s" -#: awx/main/models/inventory.py:1214 +#: awx/main/models/inventory.py:1215 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "無効なグループ (選択による): %(choice)s" -#: awx/main/models/inventory.py:1242 +#: awx/main/models/inventory.py:1243 msgid "Project containing inventory file used as source." msgstr "ソースとして使用されるインベントリーファイルが含まれるプロジェクト。" -#: awx/main/models/inventory.py:1415 +#: awx/main/models/inventory.py:1416 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "複数の SCM ベースのインベントリーソースについて、インベントリー別のプロジェクト更新時の更新は許可されません。" -#: awx/main/models/inventory.py:1422 +#: awx/main/models/inventory.py:1423 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " "update on launch." msgstr "プロジェクト更新時の更新に設定している場合、SCM ベースのインベントリーソースを更新できません。その代わりに起動時に更新するように対応するソースプロジェクトを設定します。" -#: awx/main/models/inventory.py:1428 +#: awx/main/models/inventory.py:1429 msgid "Cannot set source_path if not SCM type." msgstr "SCM タイプでない場合 source_path を設定できません。" -#: awx/main/models/inventory.py:1471 +#: awx/main/models/inventory.py:1472 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "このプロジェクト更新のインベントリーファイルがインベントリー更新に使用されました。" -#: awx/main/models/inventory.py:1582 +#: awx/main/models/inventory.py:1583 msgid "Inventory script contents" msgstr "インベントリースクリプトの内容" -#: awx/main/models/inventory.py:1587 +#: awx/main/models/inventory.py:1588 msgid "Organization owning this inventory script" msgstr "このインベントリースクリプトを所有する組織" @@ -4010,28 +4010,28 @@ msgstr "インベントリーがプロンプトとして適用されると、ジ msgid "job host summaries" msgstr "ジョブホストの概要" -#: awx/main/models/jobs.py:1158 +#: awx/main/models/jobs.py:1144 msgid "Remove jobs older than a certain number of days" msgstr "特定の日数より前のジョブを削除" -#: awx/main/models/jobs.py:1159 +#: awx/main/models/jobs.py:1145 msgid "Remove activity stream entries older than a certain number of days" msgstr "特定の日数より前のアクティビティーストリームのエントリーを削除" -#: awx/main/models/jobs.py:1160 +#: awx/main/models/jobs.py:1146 msgid "Removes expired browser sessions from the database" msgstr "期限切れブラウザーセッションをデータベースから削除" -#: awx/main/models/jobs.py:1161 +#: awx/main/models/jobs.py:1147 msgid "Removes expired OAuth 2 access tokens and refresh tokens" msgstr "期限切れの OAuth 2 アクセストークンを削除し、トークンを更新" -#: awx/main/models/jobs.py:1231 +#: awx/main/models/jobs.py:1217 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "システムジョブでは変数 {list_of_keys} を使用できません。" -#: awx/main/models/jobs.py:1247 +#: awx/main/models/jobs.py:1233 msgid "days must be a positive integer." msgstr "日数は正の整数である必要があります。" @@ -4775,7 +4775,7 @@ msgstr "エラーの処理パスが見つかりません。ワークフローを msgid "The approval node {name} ({pk}) has expired after {timeout} seconds." msgstr "承認ノード {name} ({pk}) は {timeout} 秒後に失効しました。" -#: awx/main/tasks.py:1053 +#: awx/main/tasks.py:1049 msgid "Invalid virtual environment selected: {}" msgstr "無効な仮想環境が選択されました: {}" @@ -4812,53 +4812,53 @@ msgstr "ワークフロージョブのノードにエラーハンドルパスが msgid "Unable to convert \"%s\" to boolean" msgstr "\"%s\" をブール値に変換できません" -#: awx/main/utils/common.py:275 +#: awx/main/utils/common.py:261 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "サポートされない SCM タイプ \"%s\"" -#: awx/main/utils/common.py:282 awx/main/utils/common.py:294 -#: awx/main/utils/common.py:313 +#: awx/main/utils/common.py:268 awx/main/utils/common.py:280 +#: awx/main/utils/common.py:299 #, python-format msgid "Invalid %s URL" msgstr "無効な %s URL" -#: awx/main/utils/common.py:284 awx/main/utils/common.py:323 +#: awx/main/utils/common.py:270 awx/main/utils/common.py:309 #, python-format msgid "Unsupported %s URL" msgstr "サポートされない %s URL" -#: awx/main/utils/common.py:325 +#: awx/main/utils/common.py:311 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "file:// URL でサポートされないホスト \"%s\"" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:313 #, python-format msgid "Host is required for %s URL" msgstr "%s URL にはホストが必要です" -#: awx/main/utils/common.py:345 +#: awx/main/utils/common.py:331 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "%s への SSH アクセスではユーザー名を \"git\" にする必要があります。" -#: awx/main/utils/common.py:351 +#: awx/main/utils/common.py:337 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "%s への SSH アクセスではユーザー名を \"hg\" にする必要があります。" -#: awx/main/utils/common.py:682 +#: awx/main/utils/common.py:668 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "入力タイプ `{data_type}` は辞書ではありません" -#: awx/main/utils/common.py:715 +#: awx/main/utils/common.py:701 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "変数には JSON 標準との互換性がありません (エラー: {json_error})" -#: awx/main/utils/common.py:721 +#: awx/main/utils/common.py:707 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." diff --git a/awx/locale/zh/LC_MESSAGES/django.po b/awx/locale/zh/LC_MESSAGES/django.po index b615e5d02b..7827966e11 100644 --- a/awx/locale/zh/LC_MESSAGES/django.po +++ b/awx/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-27 13:55+0000\n" +"POT-Creation-Date: 2020-05-28 21:45+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -527,7 +527,7 @@ msgstr "作业模板清单缺失或未定义。" msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "未知,在保存启动配置前作业可能已经运行。" -#: awx/api/serializers.py:3252 awx/main/tasks.py:2795 awx/main/tasks.py:2813 +#: awx/api/serializers.py:3252 awx/main/tasks.py:2800 awx/main/tasks.py:2818 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} 被禁止在临时命令中使用。" @@ -546,324 +546,324 @@ msgstr "提供的变量 {} 没有要替换的数据库值。" msgid "\"$encrypted$ is a reserved keyword, may not be used for {}.\"" msgstr "\"$encrypted$ 是一个保留的关键字,可能不能用于 {}\"" -#: awx/api/serializers.py:4070 +#: awx/api/serializers.py:4078 msgid "A project is required to run a job." msgstr "运行一个作业时需要一个项目。" -#: awx/api/serializers.py:4072 +#: awx/api/serializers.py:4080 msgid "Missing a revision to run due to failed project update." msgstr "由于项目更新失败,缺少运行的修订版本。" -#: awx/api/serializers.py:4076 +#: awx/api/serializers.py:4084 msgid "The inventory associated with this Job Template is being deleted." msgstr "与此作业模板关联的清单将被删除。" -#: awx/api/serializers.py:4078 awx/api/serializers.py:4194 +#: awx/api/serializers.py:4086 awx/api/serializers.py:4202 msgid "The provided inventory is being deleted." msgstr "提供的清单将被删除。" -#: awx/api/serializers.py:4086 +#: awx/api/serializers.py:4094 msgid "Cannot assign multiple {} credentials." msgstr "无法分配多个 {} 凭证。" -#: awx/api/serializers.py:4090 +#: awx/api/serializers.py:4098 msgid "Cannot assign a Credential of kind `{}`" msgstr "无法分配类型为 `{}` 的凭证" -#: awx/api/serializers.py:4103 +#: awx/api/serializers.py:4111 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "不支持在不替换的情况下在启动时删除 {} 凭证。提供的列表缺少凭证:{}。" -#: awx/api/serializers.py:4192 +#: awx/api/serializers.py:4200 msgid "The inventory associated with this Workflow is being deleted." msgstr "与此 Workflow 关联的清单将被删除。" -#: awx/api/serializers.py:4263 +#: awx/api/serializers.py:4271 msgid "Message type '{}' invalid, must be either 'message' or 'body'" msgstr "消息类型 '{}' 无效,必须是 'message' 或 'body'" -#: awx/api/serializers.py:4269 +#: awx/api/serializers.py:4277 msgid "Expected string for '{}', found {}, " msgstr "'{}' 的预期字符串,找到 {}," -#: awx/api/serializers.py:4273 +#: awx/api/serializers.py:4281 msgid "Messages cannot contain newlines (found newline in {} event)" msgstr "消息不能包含新行(在 {} 事件中找到新行)" -#: awx/api/serializers.py:4279 +#: awx/api/serializers.py:4287 msgid "Expected dict for 'messages' field, found {}" msgstr "'messages' 字段的预期字典,找到 {}" -#: awx/api/serializers.py:4283 +#: awx/api/serializers.py:4291 msgid "" "Event '{}' invalid, must be one of 'started', 'success', 'error', or " "'workflow_approval'" msgstr "事件 '{}' 无效,必须是 'started'、'success'、'error' 或 'workflow_approval' 之一" -#: awx/api/serializers.py:4289 +#: awx/api/serializers.py:4297 msgid "Expected dict for event '{}', found {}" msgstr "事件 '{}' 的预期字典,找到 {}" -#: awx/api/serializers.py:4294 +#: awx/api/serializers.py:4302 msgid "" "Workflow Approval event '{}' invalid, must be one of 'running', 'approved', " "'timed_out', or 'denied'" msgstr "工作流批准事件 '{}' 无效,必须是 'running'、'approved'、'timed_out' 或 'denied' 之一。" -#: awx/api/serializers.py:4301 +#: awx/api/serializers.py:4309 msgid "Expected dict for workflow approval event '{}', found {}" msgstr "工作流批准事件 '{}' 的预期字典,找到 {}" -#: awx/api/serializers.py:4328 +#: awx/api/serializers.py:4336 msgid "Unable to render message '{}': {}" msgstr "无法呈现消息 '{}':{}" -#: awx/api/serializers.py:4330 +#: awx/api/serializers.py:4338 msgid "Field '{}' unavailable" msgstr "字段 '{}' 不可用" -#: awx/api/serializers.py:4332 +#: awx/api/serializers.py:4340 msgid "Security error due to field '{}'" msgstr "因为字段 '{}' 导致安全错误" -#: awx/api/serializers.py:4352 +#: awx/api/serializers.py:4360 msgid "Webhook body for '{}' should be a json dictionary. Found type '{}'." msgstr "'{}' 的 Webhook 正文应该是 json 字典。找到类型 '{}'。" -#: awx/api/serializers.py:4355 +#: awx/api/serializers.py:4363 msgid "Webhook body for '{}' is not a valid json dictionary ({})." msgstr "'{}' 的 Webhook 正文不是有效的 json 字典 ({})。" -#: awx/api/serializers.py:4373 +#: awx/api/serializers.py:4381 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "通知配置缺少所需字段:notification_type" -#: awx/api/serializers.py:4400 +#: awx/api/serializers.py:4408 msgid "No values specified for field '{}'" msgstr "没有为字段 '{}' 指定值" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4413 msgid "HTTP method must be either 'POST' or 'PUT'." msgstr "HTTP 方法必须是 'POST' 或 'PUT'。" -#: awx/api/serializers.py:4407 +#: awx/api/serializers.py:4415 msgid "Missing required fields for Notification Configuration: {}." msgstr "通知配置缺少所需字段:{}。" -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4418 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "配置字段 '{}' 类型错误,预期为 {}。" -#: awx/api/serializers.py:4427 +#: awx/api/serializers.py:4435 msgid "Notification body" msgstr "通知正文" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4515 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "rrule 中需要有效的 DTSTART。值应该以 DTSTART:YYYMMDDTHHMMSSZ 开头" -#: awx/api/serializers.py:4509 +#: awx/api/serializers.py:4517 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "DTSTART 不能是一个不带时区的日期时间。指定 ;TZINFO= 或 YYYMMDDTHHMMSSZ。" -#: awx/api/serializers.py:4511 +#: awx/api/serializers.py:4519 msgid "Multiple DTSTART is not supported." msgstr "不支持多个 DTSTART。" -#: awx/api/serializers.py:4513 +#: awx/api/serializers.py:4521 msgid "RRULE required in rrule." msgstr "rrule 中需要 RRULE。" -#: awx/api/serializers.py:4515 +#: awx/api/serializers.py:4523 msgid "Multiple RRULE is not supported." msgstr "不支持多个 RRULE。" -#: awx/api/serializers.py:4517 +#: awx/api/serializers.py:4525 msgid "INTERVAL required in rrule." msgstr "rrule 需要 INTERVAL。" -#: awx/api/serializers.py:4519 +#: awx/api/serializers.py:4527 msgid "SECONDLY is not supported." msgstr "不支持 SECONDLY。" -#: awx/api/serializers.py:4521 +#: awx/api/serializers.py:4529 msgid "Multiple BYMONTHDAYs not supported." msgstr "不支持多个 BYMONTHDAY。" -#: awx/api/serializers.py:4523 +#: awx/api/serializers.py:4531 msgid "Multiple BYMONTHs not supported." msgstr "不支持多个 BYMONTH。" -#: awx/api/serializers.py:4525 +#: awx/api/serializers.py:4533 msgid "BYDAY with numeric prefix not supported." msgstr "不支持带有数字前缀的 BYDAY。" -#: awx/api/serializers.py:4527 +#: awx/api/serializers.py:4535 msgid "BYYEARDAY not supported." msgstr "不支持 BYYEARDAY。" -#: awx/api/serializers.py:4529 +#: awx/api/serializers.py:4537 msgid "BYWEEKNO not supported." msgstr "不支持 BYWEEKNO。" -#: awx/api/serializers.py:4531 +#: awx/api/serializers.py:4539 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "RRULE 可能不包含 COUNT 和 UNTIL" -#: awx/api/serializers.py:4535 +#: awx/api/serializers.py:4543 msgid "COUNT > 999 is unsupported." msgstr "不支持 COUNT > 999。" -#: awx/api/serializers.py:4541 +#: awx/api/serializers.py:4549 msgid "rrule parsing failed validation: {}" msgstr "rrule 解析失败验证:{}" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4611 msgid "Inventory Source must be a cloud resource." msgstr "清单源必须是云资源。" -#: awx/api/serializers.py:4605 +#: awx/api/serializers.py:4613 msgid "Manual Project cannot have a schedule set." msgstr "手动项目不能有计划集。" -#: awx/api/serializers.py:4608 +#: awx/api/serializers.py:4616 msgid "" "Inventory sources with `update_on_project_update` cannot be scheduled. " "Schedule its source project `{}` instead." msgstr "无法调度带有 `update_on_project_update` 的清单源。改为调度其源项目 `{}`。" -#: awx/api/serializers.py:4618 +#: awx/api/serializers.py:4626 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "处于运行状态或等待状态的针对此实例的作业计数" -#: awx/api/serializers.py:4623 +#: awx/api/serializers.py:4631 msgid "Count of all jobs that target this instance" msgstr "所有针对此实例的作业计数" -#: awx/api/serializers.py:4656 +#: awx/api/serializers.py:4664 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance group" msgstr "处于运行状态或等待状态的针对此实例组的作业计数" -#: awx/api/serializers.py:4661 +#: awx/api/serializers.py:4669 msgid "Count of all jobs that target this instance group" msgstr "所有针对此实例组的作业计数" -#: awx/api/serializers.py:4666 +#: awx/api/serializers.py:4674 msgid "Indicates whether instance group controls any other group" msgstr "指明实例组是否控制任何其他组" -#: awx/api/serializers.py:4670 +#: awx/api/serializers.py:4678 msgid "" "Indicates whether instances in this group are isolated.Isolated groups have " "a designated controller group." msgstr "指明此组中的实例是否被隔离。隔离的组具有指定的控制器组。" -#: awx/api/serializers.py:4675 +#: awx/api/serializers.py:4683 msgid "" "Indicates whether instances in this group are containerized.Containerized " "groups have a designated Openshift or Kubernetes cluster." msgstr "指明此组中的实例是否容器化。容器化的组具有指定的 Openshift 或 Kubernetes 集群。" -#: awx/api/serializers.py:4683 +#: awx/api/serializers.py:4691 msgid "Policy Instance Percentage" msgstr "策略实例百分比" -#: awx/api/serializers.py:4684 +#: awx/api/serializers.py:4692 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "新实例上线时将自动分配给此组的所有实例的最小百分比。" -#: awx/api/serializers.py:4689 +#: awx/api/serializers.py:4697 msgid "Policy Instance Minimum" msgstr "策略实例最小值" -#: awx/api/serializers.py:4690 +#: awx/api/serializers.py:4698 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "新实例上线时自动分配给此组的静态最小实例数量。" -#: awx/api/serializers.py:4695 +#: awx/api/serializers.py:4703 msgid "Policy Instance List" msgstr "策略实例列表" -#: awx/api/serializers.py:4696 +#: awx/api/serializers.py:4704 msgid "List of exact-match Instances that will be assigned to this group" msgstr "将分配给此组的完全匹配实例的列表" -#: awx/api/serializers.py:4722 +#: awx/api/serializers.py:4730 msgid "Duplicate entry {}." msgstr "重复条目 {}。" -#: awx/api/serializers.py:4724 +#: awx/api/serializers.py:4732 msgid "{} is not a valid hostname of an existing instance." msgstr "{} 不是现有实例的有效主机名。" -#: awx/api/serializers.py:4726 awx/api/views/mixin.py:98 +#: awx/api/serializers.py:4734 awx/api/views/mixin.py:98 msgid "" "Isolated instances may not be added or removed from instances groups via the " "API." msgstr "可能无法通过 API 为实例组添加或删除隔离的实例。" -#: awx/api/serializers.py:4728 awx/api/views/mixin.py:102 +#: awx/api/serializers.py:4736 awx/api/views/mixin.py:102 msgid "Isolated instance group membership may not be managed via the API." msgstr "可能无法通过 API 管理隔离的实例组成员资格。" -#: awx/api/serializers.py:4730 awx/api/serializers.py:4735 -#: awx/api/serializers.py:4740 +#: awx/api/serializers.py:4738 awx/api/serializers.py:4743 +#: awx/api/serializers.py:4748 msgid "Containerized instances may not be managed via the API" msgstr "可能无法通过 API 管理容器化实例" -#: awx/api/serializers.py:4745 +#: awx/api/serializers.py:4753 msgid "tower instance group name may not be changed." msgstr "可能不会更改 tower 实例组名称。" -#: awx/api/serializers.py:4750 +#: awx/api/serializers.py:4758 msgid "Only Kubernetes credentials can be associated with an Instance Group" msgstr "只有 Kubernetes 凭证可以与实例组关联" -#: awx/api/serializers.py:4789 +#: awx/api/serializers.py:4797 msgid "" "When present, shows the field name of the role or relationship that changed." msgstr "存在时,显示更改的角色或关系的字段名称。" -#: awx/api/serializers.py:4791 +#: awx/api/serializers.py:4799 msgid "" "When present, shows the model on which the role or relationship was defined." msgstr "存在时,显示定义角色或关系的模型。" -#: awx/api/serializers.py:4824 +#: awx/api/serializers.py:4832 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "创建、更新或删除对象时新值和更改值的概述" -#: awx/api/serializers.py:4826 +#: awx/api/serializers.py:4834 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "对于创建、更新和删除事件,这是受影响的对象类型。对于关联和解除关联事件,这是与对象 2 关联或解除关联的对象类型。" -#: awx/api/serializers.py:4829 +#: awx/api/serializers.py:4837 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "创建、更新和删除事件未填充。对于关联和解除关联事件,这是对象 1 要关联的对象类型。" -#: awx/api/serializers.py:4832 +#: awx/api/serializers.py:4840 msgid "The action taken with respect to the given object(s)." msgstr "对给定对象执行的操作。" @@ -1637,7 +1637,7 @@ msgstr "设置示例" msgid "Example setting which can be different for each user." msgstr "每个用户之间可以各不相同的设置示例。" -#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:55 +#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:56 msgid "User" msgstr "用户" @@ -1740,15 +1740,15 @@ msgstr "系统" msgid "OtherSystem" msgstr "OtherSystem" -#: awx/conf/views.py:47 +#: awx/conf/views.py:48 msgid "Setting Categories" msgstr "设置类别" -#: awx/conf/views.py:69 +#: awx/conf/views.py:70 msgid "Setting Detail" msgstr "设置详情" -#: awx/conf/views.py:160 +#: awx/conf/views.py:162 msgid "Logging Connectivity Test" msgstr "日志记录连接测试" @@ -2793,7 +2793,7 @@ msgstr "Conjur URL" msgid "API Key" msgstr "API 密钥" -#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1017 +#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1018 msgid "Account" msgstr "帐户" @@ -2880,7 +2880,7 @@ msgid "" msgstr "kv 机密后端的名称(如果留空,将使用机密路径的第一个分段)。" #: awx/main/credential_plugins/hashivault.py:60 -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1023 msgid "Key Name" msgstr "密钥名称" @@ -3257,7 +3257,7 @@ msgid "" "Management (IAM) users." msgstr "安全令牌服务 (STS) 是一个 Web 服务,让您可以为 AWS 身份和访问管理 (IAM) 用户请求临时的有限权限凭证。" -#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:832 +#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:833 msgid "OpenStack" msgstr "OpenStack" @@ -3297,7 +3297,7 @@ msgstr "OpenStack 域定义了管理边界。只有 Keystone v3 身份验证 URL msgid "Verify SSL" msgstr "验证 SSL" -#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:829 +#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:830 msgid "VMware vCenter" msgstr "VMware vCenter" @@ -3310,7 +3310,7 @@ msgid "" "Enter the hostname or IP address that corresponds to your VMware vCenter." msgstr "输入与 VMware vCenter 对应的主机名或 IP 地址。" -#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:830 +#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:831 msgid "Red Hat Satellite 6" msgstr "红帽卫星 6" @@ -3324,7 +3324,7 @@ msgid "" "example, https://satellite.example.org" msgstr "输入与您的红帽卫星 6 服务器对应的 URL。例如:https://satellite.example.org" -#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:831 +#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:832 msgid "Red Hat CloudForms" msgstr "Red Hat CloudForms" @@ -3338,7 +3338,7 @@ msgid "" "instance. For example, https://cloudforms.example.org" msgstr "输入与您的 CloudForms 实例对应的虚拟机的 URL。例如:https://cloudforms.example.org" -#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:827 +#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:828 msgid "Google Compute Engine" msgstr "Google Compute Engine" @@ -3367,7 +3367,7 @@ msgid "" "Paste the contents of the PEM file associated with the service account email." msgstr "粘贴与服务账户电子邮件关联的 PEM 文件的内容。" -#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:828 +#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:829 msgid "Microsoft Azure Resource Manager" msgstr "Microsoft Azure Resource Manager" @@ -3405,7 +3405,7 @@ msgstr "GitLab 个人访问令牌" msgid "This token needs to come from your profile settings in GitLab" msgstr "此令牌需要来自您在 GitLab 中的配置文件设置" -#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:833 +#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:834 msgid "Red Hat Virtualization" msgstr "红帽虚拟化" @@ -3421,7 +3421,7 @@ msgstr "CA 文件" msgid "Absolute file path to the CA file to use (optional)" msgstr "要使用的 CA 文件的绝对文件路径(可选)" -#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:834 +#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:835 msgid "Ansible Tower" msgstr "Ansible Tower" @@ -3465,7 +3465,7 @@ msgstr "源必须是外部凭证" msgid "Input field must be defined on target credential (options are {})." msgstr "输入字段必须在目标凭证上定义(选项为 {})。" -#: awx/main/models/events.py:152 awx/main/models/events.py:655 +#: awx/main/models/events.py:152 awx/main/models/events.py:674 msgid "Host Failed" msgstr "主机故障" @@ -3473,7 +3473,7 @@ msgstr "主机故障" msgid "Host Started" msgstr "主机已启动" -#: awx/main/models/events.py:154 awx/main/models/events.py:656 +#: awx/main/models/events.py:154 awx/main/models/events.py:675 msgid "Host OK" msgstr "主机正常" @@ -3481,11 +3481,11 @@ msgstr "主机正常" msgid "Host Failure" msgstr "主机故障" -#: awx/main/models/events.py:156 awx/main/models/events.py:662 +#: awx/main/models/events.py:156 awx/main/models/events.py:681 msgid "Host Skipped" msgstr "主机已跳过" -#: awx/main/models/events.py:157 awx/main/models/events.py:657 +#: awx/main/models/events.py:157 awx/main/models/events.py:676 msgid "Host Unreachable" msgstr "主机无法访问" @@ -3569,27 +3569,27 @@ msgstr "Play 已启动" msgid "Playbook Complete" msgstr "Playbook 完成" -#: awx/main/models/events.py:184 awx/main/models/events.py:672 +#: awx/main/models/events.py:184 awx/main/models/events.py:691 msgid "Debug" msgstr "调试" -#: awx/main/models/events.py:185 awx/main/models/events.py:673 +#: awx/main/models/events.py:185 awx/main/models/events.py:692 msgid "Verbose" msgstr "详细" -#: awx/main/models/events.py:186 awx/main/models/events.py:674 +#: awx/main/models/events.py:186 awx/main/models/events.py:693 msgid "Deprecated" msgstr "已弃用" -#: awx/main/models/events.py:187 awx/main/models/events.py:675 +#: awx/main/models/events.py:187 awx/main/models/events.py:694 msgid "Warning" msgstr "警告" -#: awx/main/models/events.py:188 awx/main/models/events.py:676 +#: awx/main/models/events.py:188 awx/main/models/events.py:695 msgid "System Warning" msgstr "系统警告" -#: awx/main/models/events.py:189 awx/main/models/events.py:677 +#: awx/main/models/events.py:189 awx/main/models/events.py:696 #: awx/main/models/unified_jobs.py:75 msgid "Error" msgstr "错误" @@ -3617,300 +3617,300 @@ msgid "" "this group" msgstr "将始终自动分配给此组的完全匹配实例的列表" -#: awx/main/models/inventory.py:79 +#: awx/main/models/inventory.py:80 msgid "Hosts have a direct link to this inventory." msgstr "主机具有指向此清单的直接链接。" -#: awx/main/models/inventory.py:80 +#: awx/main/models/inventory.py:81 msgid "Hosts for inventory generated using the host_filter property." msgstr "使用 host_filter 属性生成的清单的主机。" -#: awx/main/models/inventory.py:85 +#: awx/main/models/inventory.py:86 msgid "inventories" msgstr "清单" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:93 msgid "Organization containing this inventory." msgstr "包含此清单的机构。" -#: awx/main/models/inventory.py:99 +#: awx/main/models/inventory.py:100 msgid "Inventory variables in JSON or YAML format." msgstr "JSON 或 YAML 格式的清单变量。" -#: awx/main/models/inventory.py:104 +#: awx/main/models/inventory.py:105 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether any hosts in this inventory have failed." msgstr "此字段已弃用,并将在以后的发行版本中删除。指示此清单中是否有任何主机故障的标记。" -#: awx/main/models/inventory.py:110 +#: awx/main/models/inventory.py:111 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of hosts in this inventory." msgstr "此字段已弃用,并将在以后的发行版本中删除。此清单中的主机总数。" -#: awx/main/models/inventory.py:116 +#: awx/main/models/inventory.py:117 msgid "" "This field is deprecated and will be removed in a future release. Number of " "hosts in this inventory with active failures." msgstr "此字段已弃用,并将在以后的发行版本中删除。此清单中有活跃故障的主机数量。" -#: awx/main/models/inventory.py:122 +#: awx/main/models/inventory.py:123 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of groups in this inventory." msgstr "此字段已弃用,并将在以后的发行版本中删除。此清单中的总组数。" -#: awx/main/models/inventory.py:128 +#: awx/main/models/inventory.py:129 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether this inventory has any external inventory sources." msgstr "此字段已弃用,并将在以后的发行版本中删除。表示此清单是否有任何外部清单源的标记。" -#: awx/main/models/inventory.py:134 +#: awx/main/models/inventory.py:135 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "在此清单中配置的外部清单源总数。" -#: awx/main/models/inventory.py:139 +#: awx/main/models/inventory.py:140 msgid "Number of external inventory sources in this inventory with failures." msgstr "此清单中有故障的外部清单源数量。" -#: awx/main/models/inventory.py:146 +#: awx/main/models/inventory.py:147 msgid "Kind of inventory being represented." msgstr "所代表的清单种类。" -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:153 msgid "Filter that will be applied to the hosts of this inventory." msgstr "将应用到此清单的主机的过滤器。" -#: awx/main/models/inventory.py:180 +#: awx/main/models/inventory.py:181 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "访问红帽 Insights API 时供属于此清单的主机使用的凭证。" -#: awx/main/models/inventory.py:189 +#: awx/main/models/inventory.py:190 msgid "Flag indicating the inventory is being deleted." msgstr "指示正在删除清单的标记。" -#: awx/main/models/inventory.py:244 +#: awx/main/models/inventory.py:245 msgid "Could not parse subset as slice specification." msgstr "无法将子集作为分片规格来解析。" -#: awx/main/models/inventory.py:248 +#: awx/main/models/inventory.py:249 msgid "Slice number must be less than total number of slices." msgstr "分片数量必须小于分片总数。" -#: awx/main/models/inventory.py:250 +#: awx/main/models/inventory.py:251 msgid "Slice number must be 1 or higher." msgstr "分片数量必须为 1 或更高。" -#: awx/main/models/inventory.py:387 +#: awx/main/models/inventory.py:388 msgid "Assignment not allowed for Smart Inventory" msgstr "智能清单不允许分配" -#: awx/main/models/inventory.py:389 awx/main/models/projects.py:166 +#: awx/main/models/inventory.py:390 awx/main/models/projects.py:166 msgid "Credential kind must be 'insights'." msgstr "凭证种类必须是 'inights'。" -#: awx/main/models/inventory.py:474 +#: awx/main/models/inventory.py:475 msgid "Is this host online and available for running jobs?" msgstr "此主机是否在线,并可用于运行作业?" -#: awx/main/models/inventory.py:480 +#: awx/main/models/inventory.py:481 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "远程清单源用来唯一标识主机的值" -#: awx/main/models/inventory.py:485 +#: awx/main/models/inventory.py:486 msgid "Host variables in JSON or YAML format." msgstr "JSON 或 YAML 格式的主机变量。" -#: awx/main/models/inventory.py:508 +#: awx/main/models/inventory.py:509 msgid "Inventory source(s) that created or modified this host." msgstr "创建或修改此主机的清单源。" -#: awx/main/models/inventory.py:513 +#: awx/main/models/inventory.py:514 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "每个主机最近的 ansible_facts 的任意 JSON 结构。" -#: awx/main/models/inventory.py:519 +#: awx/main/models/inventory.py:520 msgid "The date and time ansible_facts was last modified." msgstr "最后修改 ansible_facts 的日期和时间。" -#: awx/main/models/inventory.py:526 +#: awx/main/models/inventory.py:527 msgid "Red Hat Insights host unique identifier." msgstr "红帽 Insights 主机唯一标识符。" -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:641 msgid "Group variables in JSON or YAML format." msgstr "JSON 或 YAML 格式的组变量。" -#: awx/main/models/inventory.py:646 +#: awx/main/models/inventory.py:647 msgid "Hosts associated directly with this group." msgstr "与此组直接关联的主机。" -#: awx/main/models/inventory.py:652 +#: awx/main/models/inventory.py:653 msgid "Inventory source(s) that created or modified this group." msgstr "创建或修改此组的清单源。" -#: awx/main/models/inventory.py:824 +#: awx/main/models/inventory.py:825 msgid "File, Directory or Script" msgstr "文件、目录或脚本" -#: awx/main/models/inventory.py:825 +#: awx/main/models/inventory.py:826 msgid "Sourced from a Project" msgstr "源于项目" -#: awx/main/models/inventory.py:826 +#: awx/main/models/inventory.py:827 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:835 +#: awx/main/models/inventory.py:836 msgid "Custom Script" msgstr "自定义脚本" -#: awx/main/models/inventory.py:952 +#: awx/main/models/inventory.py:953 msgid "Inventory source variables in YAML or JSON format." msgstr "YAML 或 JSON 格式的清单源变量。" -#: awx/main/models/inventory.py:963 +#: awx/main/models/inventory.py:964 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "以逗号分隔的过滤器表达式列表(仅限 EC2)。当任何过滤器匹配时会导入主机。" -#: awx/main/models/inventory.py:969 +#: awx/main/models/inventory.py:970 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "限制从清单源自动创建的组(仅限 EC2)。" -#: awx/main/models/inventory.py:973 +#: awx/main/models/inventory.py:974 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "从远程清单源覆盖本地组和主机。" -#: awx/main/models/inventory.py:977 +#: awx/main/models/inventory.py:978 msgid "Overwrite local variables from remote inventory source." msgstr "从远程清单源覆盖本地变量。" -#: awx/main/models/inventory.py:982 awx/main/models/jobs.py:154 +#: awx/main/models/inventory.py:983 awx/main/models/jobs.py:154 #: awx/main/models/projects.py:135 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "取消任务前运行的时间(以秒为单位)。" -#: awx/main/models/inventory.py:1015 +#: awx/main/models/inventory.py:1016 msgid "Image ID" msgstr "镜像 ID" -#: awx/main/models/inventory.py:1016 +#: awx/main/models/inventory.py:1017 msgid "Availability Zone" msgstr "可用性区域" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1019 msgid "Instance ID" msgstr "实例 ID" -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1020 msgid "Instance State" msgstr "实例状态" -#: awx/main/models/inventory.py:1020 +#: awx/main/models/inventory.py:1021 msgid "Platform" msgstr "平台" -#: awx/main/models/inventory.py:1021 +#: awx/main/models/inventory.py:1022 msgid "Instance Type" msgstr "实例类型" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1024 msgid "Region" msgstr "区域" -#: awx/main/models/inventory.py:1024 +#: awx/main/models/inventory.py:1025 msgid "Security Group" msgstr "安全组" -#: awx/main/models/inventory.py:1025 +#: awx/main/models/inventory.py:1026 msgid "Tags" msgstr "标签" -#: awx/main/models/inventory.py:1026 +#: awx/main/models/inventory.py:1027 msgid "Tag None" msgstr "标签 None" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1028 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1095 +#: awx/main/models/inventory.py:1096 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "基于云的清单源(如 %s)需要匹配的云服务的凭证。" -#: awx/main/models/inventory.py:1101 +#: awx/main/models/inventory.py:1102 msgid "Credential is required for a cloud source." msgstr "云源需要凭证。" -#: awx/main/models/inventory.py:1104 +#: awx/main/models/inventory.py:1105 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "对于自定义清单源,不允许使用机器、源控制、insights 和 vault 类型的凭证。" -#: awx/main/models/inventory.py:1109 +#: awx/main/models/inventory.py:1110 msgid "" "Credentials of type insights and vault are disallowed for scm inventory " "sources." msgstr "对于 scm 清单源,不允许使用 insights 和 vault 类型的凭证。" -#: awx/main/models/inventory.py:1169 +#: awx/main/models/inventory.py:1170 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "无效的 %(source)s 区域:%(region)s" -#: awx/main/models/inventory.py:1193 +#: awx/main/models/inventory.py:1194 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "无效的过滤器表达式:%(filter)s" -#: awx/main/models/inventory.py:1214 +#: awx/main/models/inventory.py:1215 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "选择的组无效:%(choice)s" -#: awx/main/models/inventory.py:1242 +#: awx/main/models/inventory.py:1243 msgid "Project containing inventory file used as source." msgstr "包含用作源的清单文件的项目。" -#: awx/main/models/inventory.py:1415 +#: awx/main/models/inventory.py:1416 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "不允许多个基于 SCM 的清单源按清单在项目更新时更新。" -#: awx/main/models/inventory.py:1422 +#: awx/main/models/inventory.py:1423 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " "update on launch." msgstr "如果设置为在项目更新时更新,则无法在启动时更新基于 SCM 的清单源。应将对应的源项目配置为在启动时更新。" -#: awx/main/models/inventory.py:1428 +#: awx/main/models/inventory.py:1429 msgid "Cannot set source_path if not SCM type." msgstr "如果不是 SCM 类型,则无法设置 source_path。" -#: awx/main/models/inventory.py:1471 +#: awx/main/models/inventory.py:1472 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "此项目更新中的清单文件用于清单更新。" -#: awx/main/models/inventory.py:1582 +#: awx/main/models/inventory.py:1583 msgid "Inventory script contents" msgstr "清单脚本内容" -#: awx/main/models/inventory.py:1587 +#: awx/main/models/inventory.py:1588 msgid "Organization owning this inventory script" msgstr "拥有此清单脚本的机构" @@ -4009,28 +4009,28 @@ msgstr "作为提示而应用的清单,假定作业模板提示提供清单" msgid "job host summaries" msgstr "作业主机摘要" -#: awx/main/models/jobs.py:1158 +#: awx/main/models/jobs.py:1144 msgid "Remove jobs older than a certain number of days" msgstr "删除超过特定天数的作业" -#: awx/main/models/jobs.py:1159 +#: awx/main/models/jobs.py:1145 msgid "Remove activity stream entries older than a certain number of days" msgstr "删除比特定天数旧的活动流条目" -#: awx/main/models/jobs.py:1160 +#: awx/main/models/jobs.py:1146 msgid "Removes expired browser sessions from the database" msgstr "从数据库中删除已过期的浏览器会话" -#: awx/main/models/jobs.py:1161 +#: awx/main/models/jobs.py:1147 msgid "Removes expired OAuth 2 access tokens and refresh tokens" msgstr "删除已过期的 OAuth 2 访问令牌并刷新令牌" -#: awx/main/models/jobs.py:1231 +#: awx/main/models/jobs.py:1217 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "系统作业不允许使用变量 {list_of_keys}。" -#: awx/main/models/jobs.py:1247 +#: awx/main/models/jobs.py:1233 msgid "days must be a positive integer." msgstr "天必须为正整数。" @@ -4774,7 +4774,7 @@ msgstr "未找到错误处理路径,将工作流标记为失败" msgid "The approval node {name} ({pk}) has expired after {timeout} seconds." msgstr "批准节点 {name} ({pk}) 已在 {timeout} 秒后过期。" -#: awx/main/tasks.py:1053 +#: awx/main/tasks.py:1049 msgid "Invalid virtual environment selected: {}" msgstr "选择了无效的虚拟环境:{}" @@ -4811,53 +4811,53 @@ msgstr "工作流作业节点没有错误处理路径 []。工作流作业节点 msgid "Unable to convert \"%s\" to boolean" msgstr "无法将 \"%s\" 转换为布尔值" -#: awx/main/utils/common.py:275 +#: awx/main/utils/common.py:261 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "不受支持的 SCM 类型 \"%s\"" -#: awx/main/utils/common.py:282 awx/main/utils/common.py:294 -#: awx/main/utils/common.py:313 +#: awx/main/utils/common.py:268 awx/main/utils/common.py:280 +#: awx/main/utils/common.py:299 #, python-format msgid "Invalid %s URL" msgstr "无效的 %s URL" -#: awx/main/utils/common.py:284 awx/main/utils/common.py:323 +#: awx/main/utils/common.py:270 awx/main/utils/common.py:309 #, python-format msgid "Unsupported %s URL" msgstr "不受支持的 %s URL" -#: awx/main/utils/common.py:325 +#: awx/main/utils/common.py:311 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "用于 file:// URL的主机 \"%s\" 不受支持" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:313 #, python-format msgid "Host is required for %s URL" msgstr "%s URL 需要主机" -#: awx/main/utils/common.py:345 +#: awx/main/utils/common.py:331 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "用户名必须是 \"git\" 以供 SSH 访问 %s。" -#: awx/main/utils/common.py:351 +#: awx/main/utils/common.py:337 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "用户名必须是 \"hg\" 以供 SSH 访问 %s。" -#: awx/main/utils/common.py:682 +#: awx/main/utils/common.py:668 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "输入类型 `{data_type}` 不是字典" -#: awx/main/utils/common.py:715 +#: awx/main/utils/common.py:701 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "与 JSON 标准不兼容的变量(错误:{json_error})" -#: awx/main/utils/common.py:721 +#: awx/main/utils/common.py:707 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." diff --git a/awx/ui/po/fr.po b/awx/ui/po/fr.po index 71d684fb90..59ed681256 100644 --- a/awx/ui/po/fr.po +++ b/awx/ui/po/fr.po @@ -4429,11 +4429,12 @@ msgstr "Remplacer les variables qui se trouvent dans azure_rm.ini et qui sont ut #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:282 msgid "" "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Remplacez les variables qui se trouvent dans cloudforms.ini et qui sont utilisées par le script de mise à jour de l'inventaire. Voici un exemple de configuration de variable\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." +" \n" +" voir cloudforms.ini dans Ansible Collections github repo.\n" +" Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:217 msgid "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables" @@ -4442,20 +4443,20 @@ msgstr "Remplacer les variables qui se trouvent dans ec2.ini et qui sont utilis #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:299 msgid "" "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Remplacez les variables qui se trouvent dans foreman.ini et qui sont utilisées par le script de mise à jour de l'inventaire. Voici un exemple de configuration de variable\n" -" \n" -" view foreman.ini in the Ansible github repo. Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." +" \n" +" voir foreman.ini dans Ansible le référentiel github repo. Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:265 msgid "" "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Remplacez les variables qui se trouvent dans openstack.yml et qui sont utilisées par le script de mise à jour de l'inventaire. Voici un exemple de configuration de variable\n" -" \n" -" view openstack.yml in the Ansible github repo. Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." +" \n" +" voir openstack.yml dans le référentiel Ansible github repo. Entrez les variables d’inventaire avec la syntaxe JSON ou YAML. Utilisez le bouton radio pour basculer entre les deux. Consultez la documentation d’Ansible Tower pour avoir un exemple de syntaxe." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:241 msgid "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables" diff --git a/awx/ui/po/ja.po b/awx/ui/po/ja.po index 7e44693fb0..e292156170 100644 --- a/awx/ui/po/ja.po +++ b/awx/ui/po/ja.po @@ -4427,11 +4427,11 @@ msgstr "azure_rm.ini にあり、インベントリー更新スクリプトで #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:282 msgid "" "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "cloudforms.ini にあり、インベントリー更新スクリプトで使用される変数を上書きします。たとえば、変数の設定\n" -" \n" -" は Ansible github リポジトリーで cloudforms.ini を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用してこの 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" +" \n" +" は Ansible Collections github リポジトリーで cloudforms.ini を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用してこの 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:217 msgid "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables" @@ -4440,20 +4440,16 @@ msgstr "ec2.ini にあり、インベントリー更新スクリプトで使用 #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:299 msgid "" "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "foreman.ini にあり、インベントリー更新スクリプトで使用される変数を上書きします。たとえば、変数の設定\n" -" \n" -" は Ansible github リポジトリーで foreman.ini を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用してこの 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" +" \n" +" view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "foreman.ini にあり、インベントリー更新スクリプトで使用される変数を上書きします。たとえば、変数の設定 は Ansible Collections github リポジトリーで foreman.ini を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用してこの 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:265 msgid "" "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "openstack.yml にあり、インベントリー更新スクリプトで使用される変数を上書きします。たとえば、変数の設定\n" -" \n" -" は Ansible github リポジトリーで openstack.yml を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用してこの 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" +" \n" +" view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "openstack.yml にあり、インベントリー更新スクリプトで使用される変数を上書きします。たとえば、変数の設定 は Openstack github リポジトリーで openstack.yml を表示します。 JSON または YAML 構文のいずれかを使用してインベントリー変数を入力します。ラジオボタンを使用して 2 つの間の切り替えを行います。構文のサンプルについては、Ansible Tower ドキュメントを参照してください。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:241 msgid "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables" diff --git a/awx/ui/po/zh.po b/awx/ui/po/zh.po index 23e2bb39c6..92348c9c6e 100644 --- a/awx/ui/po/zh.po +++ b/awx/ui/po/zh.po @@ -4424,16 +4424,16 @@ msgstr "其他(云提供商)" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:316 msgid "Override variables found in azure_rm.ini and used by the inventory update script. For a detailed description of these variables" -msgstr "覆写 azure_rm.ini 中由清单更新脚本使用的变量。有关这些变量的详细描述" +msgstr "覆盖 azure_rm.ini 中由清单更新脚本使用的变量。有关这些变量的详细描述" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:282 msgid "" "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" +" \n" +" view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "覆盖 cloudforms.ini 中由清单更新脚本使用的变量。一个变量配置示例包括在\n" " \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" Ansible github repo 的 cloudforms.ini 中。 使用 JSON 或 YAML 格式输入清单变量。通过单选按钮可以切换这两个格式。详情请参阅 Ansible Tower 的相关文档。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:217 msgid "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables" @@ -4442,20 +4442,19 @@ msgstr "覆写 ec2.ini 中由清单更新脚本使用的变量。有关这些变 #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:299 msgid "" "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" +" \n" +" view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "覆盖 foreman.ini 中由清单脚本使用的变量。一个变量配置示例包括在\n" " \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" Ansible github repo 的 foreman.ini 中。 使用 JSON 或 YAML 格式输入清单变量。通过单选按旧可以切换这两个格式。详情请参阅 Ansible Tower 的相关文档。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:265 msgid "" "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "覆盖 openstack.yml 中由清单脚本使用的变量。一个变量配置示例包括在 \n" +" Openstack github repo 的 openstack.yml 中。 使用 JSON 或 YAML 格式输入清单变量。通过单选按旧可以切换这两个格式。详情请参阅 Ansible Tower 的相关文档。" #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:241 msgid "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables" From eaadbe9730ec37d11c15c76ec7be6645b8a2da53 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 16 Jun 2020 12:36:51 -0400 Subject: [PATCH 30/53] fix a regression in how job host summaries are generated this change fixes a bug introduced in the optimization at https://github.com/ansible/awx/pull/7352 1. Create inventory with multiple hosts 2. Run a playbook with a limit to match only one host 3. Run job, verify that it only acts on the one host 4. Go to inventory host list and see that all the hosts have last_job updated to point to the job that only acted on one host. --- awx/main/models/events.py | 6 +- .../tests/functional/models/test_events.py | 57 ++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/awx/main/models/events.py b/awx/main/models/events.py index ac33a311f4..1f79b0e24b 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -338,7 +338,7 @@ class BasePlaybookEvent(CreatedModifiedModel): if isinstance(self, JobEvent): hostnames = self._hostnames() - self._update_host_summary_from_stats(hostnames) + self._update_host_summary_from_stats(set(hostnames)) if self.job.inventory: try: self.job.inventory.update_computed_fields() @@ -521,7 +521,9 @@ class JobEvent(BasePlaybookEvent): for summary in JobHostSummary.objects.filter(job_id=job.id).values('id', 'host_id') ) for h in all_hosts: - h.last_job_id = job.id + # if the hostname *shows up* in the playbook_on_stats event + if h.name in hostnames: + h.last_job_id = job.id if h.id in host_mapping: h.last_job_host_summary_id = host_mapping[h.id] Host.objects.bulk_update(all_hosts, ['last_job_id', 'last_job_host_summary_id']) diff --git a/awx/main/tests/functional/models/test_events.py b/awx/main/tests/functional/models/test_events.py index 7f881a2fea..943bd34654 100644 --- a/awx/main/tests/functional/models/test_events.py +++ b/awx/main/tests/functional/models/test_events.py @@ -3,7 +3,7 @@ import pytest from django.utils.timezone import now -from awx.main.models import Job, JobEvent, Inventory, Host +from awx.main.models import Job, JobEvent, Inventory, Host, JobHostSummary @pytest.mark.django_db @@ -153,3 +153,58 @@ def test_host_summary_generation_with_deleted_hosts(): assert ids == [-1, -1, -1, -1, -1, 6, 7, 8, 9, 10] assert names == ['Host 0', 'Host 1', 'Host 2', 'Host 3', 'Host 4', 'Host 5', 'Host 6', 'Host 7', 'Host 8', 'Host 9'] + + +@pytest.mark.django_db +def test_host_summary_generation_with_limit(): + # Make an inventory with 10 hosts, run a playbook with a --limit + # pointed at *one* host, + # Verify that *only* that host has an associated JobHostSummary and that + # *only* that host has an updated value for .last_job. + hostnames = [f'Host {i}' for i in range(10)] + inv = Inventory() + inv.save() + Host.objects.bulk_create([ + Host(created=now(), modified=now(), name=h, inventory_id=inv.id) + for h in hostnames + ]) + j = Job(inventory=inv) + j.save() + + # host map is a data structure that tracks a mapping of host name --> ID + # for the inventory, _regardless_ of whether or not there's a limit + # applied to the actual playbook run + host_map = dict((host.name, host.id) for host in inv.hosts.all()) + + # by making the playbook_on_stats *only* include Host 1, we're emulating + # the behavior of a `--limit=Host 1` + matching_host = Host.objects.get(name='Host 1') + JobEvent.create_from_data( + job_id=j.pk, + parent_uuid='abc123', + event='playbook_on_stats', + event_data={ + 'ok': {matching_host.name: len(matching_host.name)}, # effectively, limit=Host 1 + 'changed': {}, + 'dark': {}, + 'failures': {}, + 'ignored': {}, + 'processed': {}, + 'rescued': {}, + 'skipped': {}, + }, + host_map=host_map + ).save() + + # since the playbook_on_stats only references one host, + # there should *only* be on JobHostSummary record (and it should + # be related to the appropriate Host) + assert JobHostSummary.objects.count() == 1 + for h in Host.objects.all(): + if h.name == 'Host 1': + assert h.last_job_id == j.id + assert h.last_job_host_summary_id == JobHostSummary.objects.first().id + else: + # all other hosts in the inventory should remain untouched + assert h.last_job_id is None + assert h.last_job_host_summary_id is None From 3cf4f4729d075a88bd413951e8cc2cc37462b986 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 18 Jun 2020 09:22:04 -0400 Subject: [PATCH 31/53] [DO NOT PORT to AWX] Pin dev requirements (#4413) --- requirements/requirements_dev.txt | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index fa73f78c78..8a2e1cee66 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,21 +1,21 @@ django-debug-toolbar==1.11 -django-rest-swagger -pprofile +django-rest-swagger==2.2.0 +pprofile==2.0.4 ipython==5.2.1 -unittest2 -pep8 -flake8 +unittest2==1.1.0 +pep8==1.7.1 +flake8==3.7.9 pluggy==0.6.0 -pyflakes +pyflakes==2.2.0 pytest==3.6.0 -pytest-cov -pytest-django +pytest-cov==2.8.1 +pytest-django==3.9.0 pytest-pythonpath pytest-mock==1.11.1 -pytest-timeout -pytest-xdist<1.28.0 -tox # for awxkit -logutils +pytest-timeout==1.3.4 +pytest-xdist==1.27.0 +tox==3.15.0 # for awxkit +logutils==0.3.5 jupyter matplotlib backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirectory From 35fe2554559fd8ee347d632c6d8e2f300bd59c67 Mon Sep 17 00:00:00 2001 From: Jim Ladd Date: Wed, 17 Jun 2020 12:27:49 -0700 Subject: [PATCH 32/53] add backwards support for ssl_verify in foreman * plugin changed option name from ssl_verify to validate_cert --- awx/main/models/inventory.py | 5 +++++ .../plugins/satellite6/files/foreman.yml | 1 + awx/main/tests/unit/models/test_inventory.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index c03cbcd987..3f72da41d3 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -2597,6 +2597,7 @@ class satellite6(PluginFileInjector): def inventory_as_dict(self, inventory_update, private_data_dir): ret = super(satellite6, self).inventory_as_dict(inventory_update, private_data_dir) + ret['validate_certs'] = False group_patterns = '[]' group_prefix = 'foreman_' @@ -2616,6 +2617,10 @@ class satellite6(PluginFileInjector): want_ansible_ssh_host = v elif k == 'satellite6_want_facts' and isinstance(v, bool): want_facts = v + # add backwards support for ssl_verify + # plugin uses new option, validate_certs, instead + elif k == 'ssl_verify' and isinstance(v, bool): + ret['validate_certs'] = v else: ret[k] = str(v) diff --git a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml index 782bb89be7..fcad2586f6 100644 --- a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml +++ b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml @@ -24,6 +24,7 @@ keyed_groups: separator: '' legacy_hostvars: true plugin: theforeman.foreman.foreman +validate_certs: false want_facts: true want_hostcollections: true want_params: true diff --git a/awx/main/tests/unit/models/test_inventory.py b/awx/main/tests/unit/models/test_inventory.py index 26ef5e1fa9..dc6af0e828 100644 --- a/awx/main/tests/unit/models/test_inventory.py +++ b/awx/main/tests/unit/models/test_inventory.py @@ -72,6 +72,23 @@ def test_invalid_kind_clean_insights_credential(): assert json.dumps(str(e.value)) == json.dumps(str([u'Assignment not allowed for Smart Inventory'])) +@pytest.mark.parametrize('source_vars,validate_certs', [ + ({'ssl_verify': True}, True), + ({'ssl_verify': False}, False), + ({'validate_certs': True}, True), + ({'validate_certs': False}, False)]) +def test_satellite_plugin_backwards_support_for_ssl_verify(source_vars, validate_certs): + injector = InventorySource.injectors['satellite6']('2.9') + inv_src = InventorySource( + name='satellite source', source='satellite6', + source_vars=source_vars + ) + + ret = injector.inventory_as_dict(inv_src, '/tmp/foo') + assert 'validate_certs' in ret + assert ret['validate_certs'] in (validate_certs, str(validate_certs)) + + class TestControlledBySCM(): def test_clean_source_path_valid(self): inv_src = InventorySource(source_path='/not_real/', From af199dff7ae7533a0f94bb0ab9dd3f88906ef5ac Mon Sep 17 00:00:00 2001 From: ansible-translation-bot Date: Fri, 19 Jun 2020 14:19:23 +0000 Subject: [PATCH 33/53] UI translation strings for release_3.7.1 branch for es and nl --- awx/locale/es/LC_MESSAGES/django.po | 364 ++++++++++++++-------------- awx/locale/nl/LC_MESSAGES/django.po | 364 ++++++++++++++-------------- awx/ui/po/es.po | 26 +- awx/ui/po/nl.po | 24 +- 4 files changed, 387 insertions(+), 391 deletions(-) diff --git a/awx/locale/es/LC_MESSAGES/django.po b/awx/locale/es/LC_MESSAGES/django.po index 4074866547..4cba6a5d55 100644 --- a/awx/locale/es/LC_MESSAGES/django.po +++ b/awx/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-27 13:55+0000\n" +"POT-Creation-Date: 2020-05-28 21:45+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -528,7 +528,7 @@ msgstr "Inventario en la plantilla de trabajo no encontrado o no definido." msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "Desconocido; este trabajo pudo haberse ejecutado antes de guardar la configuración de lanzamiento." -#: awx/api/serializers.py:3252 awx/main/tasks.py:2795 awx/main/tasks.py:2813 +#: awx/api/serializers.py:3252 awx/main/tasks.py:2800 awx/main/tasks.py:2818 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} tienen uso prohibido en comandos ad hoc." @@ -547,324 +547,324 @@ msgstr "La variable {} provista no tiene un valor de base de datos con qué reem msgid "\"$encrypted$ is a reserved keyword, may not be used for {}.\"" msgstr "\"$encrypted$ es una palabra clave reservada y no puede usarse para {}\"." -#: awx/api/serializers.py:4070 +#: awx/api/serializers.py:4078 msgid "A project is required to run a job." msgstr "Se requiere un proyecto para ejecutar una tarea." -#: awx/api/serializers.py:4072 +#: awx/api/serializers.py:4080 msgid "Missing a revision to run due to failed project update." msgstr "Falta una revisión para ejecutar debido a un error en la actualización del proyecto." -#: awx/api/serializers.py:4076 +#: awx/api/serializers.py:4084 msgid "The inventory associated with this Job Template is being deleted." msgstr "Se está eliminando el inventario asociado con esta plantilla de trabajo." -#: awx/api/serializers.py:4078 awx/api/serializers.py:4194 +#: awx/api/serializers.py:4086 awx/api/serializers.py:4202 msgid "The provided inventory is being deleted." msgstr "El inventario provisto se está eliminando." -#: awx/api/serializers.py:4086 +#: awx/api/serializers.py:4094 msgid "Cannot assign multiple {} credentials." msgstr "No se pueden asignar múltiples credenciales {}." -#: awx/api/serializers.py:4090 +#: awx/api/serializers.py:4098 msgid "Cannot assign a Credential of kind `{}`" msgstr "No puede asignar una credencial del tipo `{}`" -#: awx/api/serializers.py:4103 +#: awx/api/serializers.py:4111 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "No se admite quitar la credencial {} en el momento de lanzamiento sin reemplazo. La lista provista no contaba con credencial(es): {}." -#: awx/api/serializers.py:4192 +#: awx/api/serializers.py:4200 msgid "The inventory associated with this Workflow is being deleted." msgstr "Se está eliminando el inventario asociado con este flujo de trabajo." -#: awx/api/serializers.py:4263 +#: awx/api/serializers.py:4271 msgid "Message type '{}' invalid, must be either 'message' or 'body'" msgstr "El tipo de mensaje '{}' no es válido, debe ser 'mensaje' o 'cuerpo'." -#: awx/api/serializers.py:4269 +#: awx/api/serializers.py:4277 msgid "Expected string for '{}', found {}, " msgstr "Cadena esperada para '{}', se encontró {}," -#: awx/api/serializers.py:4273 +#: awx/api/serializers.py:4281 msgid "Messages cannot contain newlines (found newline in {} event)" msgstr "Los mensajes no pueden contener nuevas líneas (se encontró una nueva línea en el evento {})" -#: awx/api/serializers.py:4279 +#: awx/api/serializers.py:4287 msgid "Expected dict for 'messages' field, found {}" msgstr "Dict esperado para el campo 'mensajes', se encontró {}" -#: awx/api/serializers.py:4283 +#: awx/api/serializers.py:4291 msgid "" "Event '{}' invalid, must be one of 'started', 'success', 'error', or " "'workflow_approval'" msgstr "El evento '{}' no es válido, debe ser uno de 'iniciado', 'éxito', 'error' o 'aprobación_de_flujo de trabajo'" -#: awx/api/serializers.py:4289 +#: awx/api/serializers.py:4297 msgid "Expected dict for event '{}', found {}" msgstr "Dict esperado para el evento '{}', se encontró {}" -#: awx/api/serializers.py:4294 +#: awx/api/serializers.py:4302 msgid "" "Workflow Approval event '{}' invalid, must be one of 'running', 'approved', " "'timed_out', or 'denied'" msgstr "El evento de aprobación del flujo de trabajo '{}' no es válido, debe ser uno de 'en ejecución', 'aprobado', 'tiempo de espera agotado' o 'denegado'" -#: awx/api/serializers.py:4301 +#: awx/api/serializers.py:4309 msgid "Expected dict for workflow approval event '{}', found {}" msgstr "Dict esperado para el evento de aprobación del flujo de trabajo '{}', se encontró {}" -#: awx/api/serializers.py:4328 +#: awx/api/serializers.py:4336 msgid "Unable to render message '{}': {}" msgstr "No se puede procesar el mensaje '{}': {}" -#: awx/api/serializers.py:4330 +#: awx/api/serializers.py:4338 msgid "Field '{}' unavailable" msgstr "Campo '{}' no disponible" -#: awx/api/serializers.py:4332 +#: awx/api/serializers.py:4340 msgid "Security error due to field '{}'" msgstr "Error de seguridad debido al campo '{}'" -#: awx/api/serializers.py:4352 +#: awx/api/serializers.py:4360 msgid "Webhook body for '{}' should be a json dictionary. Found type '{}'." msgstr "El cuerpo de Webhook para '{}' debería ser un diccionario json. Se encontró el tipo '{}'." -#: awx/api/serializers.py:4355 +#: awx/api/serializers.py:4363 msgid "Webhook body for '{}' is not a valid json dictionary ({})." msgstr "El cuerpo de Webhook para '{}' no es un diccionario json válido ({})." -#: awx/api/serializers.py:4373 +#: awx/api/serializers.py:4381 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "Campos obligatorios no definidos para la configuración de notificación: notification_type" -#: awx/api/serializers.py:4400 +#: awx/api/serializers.py:4408 msgid "No values specified for field '{}'" msgstr "Ningún valor especificado para el campo '{}'" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4413 msgid "HTTP method must be either 'POST' or 'PUT'." msgstr "El método HTTP debe ser 'POST' o 'PUT'." -#: awx/api/serializers.py:4407 +#: awx/api/serializers.py:4415 msgid "Missing required fields for Notification Configuration: {}." msgstr "Campos no definidos para la configuración de notificación: {}." -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4418 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "Tipo incorrecto en la configuración del campo '{} ', esperado {}." -#: awx/api/serializers.py:4427 +#: awx/api/serializers.py:4435 msgid "Notification body" msgstr "Cuerpo de la notificación" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4515 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "DTSTART válido necesario en rrule. El valor debe empezar con: DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:4509 +#: awx/api/serializers.py:4517 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "DTSTART no puede ser una fecha/hora ingenua. Especifique ;TZINFO= o YYYYMMDDTHHMMSSZZ." -#: awx/api/serializers.py:4511 +#: awx/api/serializers.py:4519 msgid "Multiple DTSTART is not supported." msgstr "Múltiple DTSTART no está soportado." -#: awx/api/serializers.py:4513 +#: awx/api/serializers.py:4521 msgid "RRULE required in rrule." msgstr "RRULE requerido en rrule." -#: awx/api/serializers.py:4515 +#: awx/api/serializers.py:4523 msgid "Multiple RRULE is not supported." msgstr "Múltiple RRULE no está soportado." -#: awx/api/serializers.py:4517 +#: awx/api/serializers.py:4525 msgid "INTERVAL required in rrule." msgstr "INTERVAL requerido en 'rrule'." -#: awx/api/serializers.py:4519 +#: awx/api/serializers.py:4527 msgid "SECONDLY is not supported." msgstr "SECONDLY no está soportado." -#: awx/api/serializers.py:4521 +#: awx/api/serializers.py:4529 msgid "Multiple BYMONTHDAYs not supported." msgstr "Multiple BYMONTHDAYs no soportado." -#: awx/api/serializers.py:4523 +#: awx/api/serializers.py:4531 msgid "Multiple BYMONTHs not supported." msgstr "Multiple BYMONTHs no soportado." -#: awx/api/serializers.py:4525 +#: awx/api/serializers.py:4533 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY con prefijo numérico no soportado." -#: awx/api/serializers.py:4527 +#: awx/api/serializers.py:4535 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY no soportado." -#: awx/api/serializers.py:4529 +#: awx/api/serializers.py:4537 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO no soportado." -#: awx/api/serializers.py:4531 +#: awx/api/serializers.py:4539 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "RRULE no puede contener ambos COUNT y UNTIL" -#: awx/api/serializers.py:4535 +#: awx/api/serializers.py:4543 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 no está soportada." -#: awx/api/serializers.py:4541 +#: awx/api/serializers.py:4549 msgid "rrule parsing failed validation: {}" msgstr "validación fallida analizando rrule: {}" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4611 msgid "Inventory Source must be a cloud resource." msgstr "Fuente del inventario debe ser un recurso cloud." -#: awx/api/serializers.py:4605 +#: awx/api/serializers.py:4613 msgid "Manual Project cannot have a schedule set." msgstr "El proyecto manual no puede tener una programación establecida." -#: awx/api/serializers.py:4608 +#: awx/api/serializers.py:4616 msgid "" "Inventory sources with `update_on_project_update` cannot be scheduled. " "Schedule its source project `{}` instead." msgstr "No se pueden programar las fuentes de inventario con `update_on_project_update. En su lugar, programe su proyecto fuente `{}`." -#: awx/api/serializers.py:4618 +#: awx/api/serializers.py:4626 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "Cantidad de tareas en estado de ejecución o espera que están destinadas para esta instancia" -#: awx/api/serializers.py:4623 +#: awx/api/serializers.py:4631 msgid "Count of all jobs that target this instance" msgstr "Todos los trabajos que abordan esta instancia" -#: awx/api/serializers.py:4656 +#: awx/api/serializers.py:4664 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance group" msgstr "Cantidad de tareas en estado de ejecución o espera que están destinadas para este grupo de instancia" -#: awx/api/serializers.py:4661 +#: awx/api/serializers.py:4669 msgid "Count of all jobs that target this instance group" msgstr "Todos los trabajos que abordan este grupo de instancias" -#: awx/api/serializers.py:4666 +#: awx/api/serializers.py:4674 msgid "Indicates whether instance group controls any other group" msgstr "Indica si el grupo de instancias controla algún otro grupo" -#: awx/api/serializers.py:4670 +#: awx/api/serializers.py:4678 msgid "" "Indicates whether instances in this group are isolated.Isolated groups have " "a designated controller group." msgstr "Indica si las instancias de este grupo están aisladas. Los grupos aislados tienen un grupo controlador designado." -#: awx/api/serializers.py:4675 +#: awx/api/serializers.py:4683 msgid "" "Indicates whether instances in this group are containerized.Containerized " "groups have a designated Openshift or Kubernetes cluster." msgstr "Indica si las instancias de este grupo son contenedorizadas. Los grupos contenedorizados tienen un clúster Openshift o Kubernetes designado." -#: awx/api/serializers.py:4683 +#: awx/api/serializers.py:4691 msgid "Policy Instance Percentage" msgstr "Porcentaje de instancias de políticas" -#: awx/api/serializers.py:4684 +#: awx/api/serializers.py:4692 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "Porcentaje mínimo de todas las instancias que se asignarán automáticamente a este grupo cuando nuevas instancias aparezcan en línea." -#: awx/api/serializers.py:4689 +#: awx/api/serializers.py:4697 msgid "Policy Instance Minimum" msgstr "Mínimo de instancias de políticas" -#: awx/api/serializers.py:4690 +#: awx/api/serializers.py:4698 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "Número mínimo estático de instancias que se asignarán automáticamente a este grupo cuando aparezcan nuevas instancias en línea." -#: awx/api/serializers.py:4695 +#: awx/api/serializers.py:4703 msgid "Policy Instance List" msgstr "Lista de instancias de políticas" -#: awx/api/serializers.py:4696 +#: awx/api/serializers.py:4704 msgid "List of exact-match Instances that will be assigned to this group" msgstr "Lista de instancias con coincidencia exacta que se asignarán a este grupo" -#: awx/api/serializers.py:4722 +#: awx/api/serializers.py:4730 msgid "Duplicate entry {}." msgstr "Entrada por duplicado {}." -#: awx/api/serializers.py:4724 +#: awx/api/serializers.py:4732 msgid "{} is not a valid hostname of an existing instance." msgstr "{} no es un nombre de host válido de una instancia existente." -#: awx/api/serializers.py:4726 awx/api/views/mixin.py:98 +#: awx/api/serializers.py:4734 awx/api/views/mixin.py:98 msgid "" "Isolated instances may not be added or removed from instances groups via the " "API." msgstr "No se pueden agregar ni eliminar instancias aisladas de los grupos de instancias a través de la API." -#: awx/api/serializers.py:4728 awx/api/views/mixin.py:102 +#: awx/api/serializers.py:4736 awx/api/views/mixin.py:102 msgid "Isolated instance group membership may not be managed via the API." msgstr "La membresía del grupo de instancias aisladas no puede administrarse a través de la API." -#: awx/api/serializers.py:4730 awx/api/serializers.py:4735 -#: awx/api/serializers.py:4740 +#: awx/api/serializers.py:4738 awx/api/serializers.py:4743 +#: awx/api/serializers.py:4748 msgid "Containerized instances may not be managed via the API" msgstr "Las instancias contenedorizadas no pueden ser gestionadas a través de la API." -#: awx/api/serializers.py:4745 +#: awx/api/serializers.py:4753 msgid "tower instance group name may not be changed." msgstr "No se puede cambiar el nombre del grupo de la instancia de tower." -#: awx/api/serializers.py:4750 +#: awx/api/serializers.py:4758 msgid "Only Kubernetes credentials can be associated with an Instance Group" msgstr "Solo las credenciales de Kubernetes pueden asociarse a un grupo de instancias." -#: awx/api/serializers.py:4789 +#: awx/api/serializers.py:4797 msgid "" "When present, shows the field name of the role or relationship that changed." msgstr "Cuando está presente, muestra el nombre de campo de la función o relación que cambió." -#: awx/api/serializers.py:4791 +#: awx/api/serializers.py:4799 msgid "" "When present, shows the model on which the role or relationship was defined." msgstr "Cuando está presente, muestra el modelo sobre el cual se definió el rol o la relación." -#: awx/api/serializers.py:4824 +#: awx/api/serializers.py:4832 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "Un resumen de los valores nuevos y cambiados cuando un objeto es creado, actualizado o eliminado." -#: awx/api/serializers.py:4826 +#: awx/api/serializers.py:4834 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "Para crear, actualizar y eliminar eventos éste es el tipo de objeto que fue afectado. Para asociar o desasociar eventos éste es el tipo de objeto asociado o desasociado con object2." -#: awx/api/serializers.py:4829 +#: awx/api/serializers.py:4837 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "Vacío para crear, actualizar y eliminar eventos. Para asociar y desasociar eventos éste es el tipo de objetos que object1 con el está asociado." -#: awx/api/serializers.py:4832 +#: awx/api/serializers.py:4840 msgid "The action taken with respect to the given object(s)." msgstr "La acción tomada al respeto al/los especificado(s) objeto(s)." @@ -1638,7 +1638,7 @@ msgstr "Ejemplo de ajuste" msgid "Example setting which can be different for each user." msgstr "Ejemplo de configuración que puede ser diferente para cada usuario." -#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:55 +#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:56 msgid "User" msgstr "Usuario" @@ -1741,15 +1741,15 @@ msgstr "Sistema" msgid "OtherSystem" msgstr "Otro sistema" -#: awx/conf/views.py:47 +#: awx/conf/views.py:48 msgid "Setting Categories" msgstr "Categorías de ajustes" -#: awx/conf/views.py:69 +#: awx/conf/views.py:70 msgid "Setting Detail" msgstr "Detalles del ajuste" -#: awx/conf/views.py:160 +#: awx/conf/views.py:162 msgid "Logging Connectivity Test" msgstr "Registrando prueba de conectividad" @@ -2794,7 +2794,7 @@ msgstr "URL de Conjur" msgid "API Key" msgstr "Clave API" -#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1017 +#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1018 msgid "Account" msgstr "Cuenta" @@ -2881,7 +2881,7 @@ msgid "" msgstr "El nombre del backend de secretos kv (si deja vacío, se utilizará el primer segmento de la ruta del secreto)." #: awx/main/credential_plugins/hashivault.py:60 -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1023 msgid "Key Name" msgstr "Nombre clave" @@ -3258,7 +3258,7 @@ msgid "" "Management (IAM) users." msgstr "El Security Token Service (STS) es un servicio web que habilita su solicitud temporalmente y con credenciales con privilegio limitado para usuarios de AWS Identity y Access Management (IAM)." -#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:832 +#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:833 msgid "OpenStack" msgstr "OpenStack" @@ -3298,7 +3298,7 @@ msgstr "Los dominios OpenStack definen los límites administrativos. Solo es nec msgid "Verify SSL" msgstr "Verificar SSL" -#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:829 +#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:830 msgid "VMware vCenter" msgstr "VMware vCenter" @@ -3311,7 +3311,7 @@ msgid "" "Enter the hostname or IP address that corresponds to your VMware vCenter." msgstr "Introduzca el nombre de host o la dirección IP que corresponda a su VMWare vCenter." -#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:830 +#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:831 msgid "Red Hat Satellite 6" msgstr "Red Hat Satellite 6" @@ -3325,7 +3325,7 @@ msgid "" "example, https://satellite.example.org" msgstr "Introduzca la URL que corresponda a su servidor Red Hat Satellite 6. Por ejemplo, https://satellite.example.org" -#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:831 +#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:832 msgid "Red Hat CloudForms" msgstr "Red Hat CloudForms" @@ -3339,7 +3339,7 @@ msgid "" "instance. For example, https://cloudforms.example.org" msgstr "Introduzca la URL para la máquina virtual que corresponda a su instancia de CloudForm. Por ejemplo, https://cloudforms.example.org" -#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:827 +#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:828 msgid "Google Compute Engine" msgstr "Google Compute Engine" @@ -3368,7 +3368,7 @@ msgid "" "Paste the contents of the PEM file associated with the service account email." msgstr "Pegue el contenido del fichero PEM asociado al correo de la cuenta de servicio." -#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:828 +#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:829 msgid "Microsoft Azure Resource Manager" msgstr "Microsoft Azure Resource Manager" @@ -3406,7 +3406,7 @@ msgstr "Token de acceso personal de GitLab" msgid "This token needs to come from your profile settings in GitLab" msgstr "Este token debe provenir de la configuración de su perfil en GitLab" -#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:833 +#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:834 msgid "Red Hat Virtualization" msgstr "Virtualización de Red Hat" @@ -3422,7 +3422,7 @@ msgstr "Archivo CA" msgid "Absolute file path to the CA file to use (optional)" msgstr "Ruta de archivo absoluta al archivo CA por usar (opcional)" -#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:834 +#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:835 msgid "Ansible Tower" msgstr "Ansible Tower" @@ -3466,7 +3466,7 @@ msgstr "El oriden debe ser una credencial externa" msgid "Input field must be defined on target credential (options are {})." msgstr "El campo de entrada debe definirse en la credencial de destino (las opciones son {})." -#: awx/main/models/events.py:152 awx/main/models/events.py:655 +#: awx/main/models/events.py:152 awx/main/models/events.py:674 msgid "Host Failed" msgstr "Servidor fallido" @@ -3474,7 +3474,7 @@ msgstr "Servidor fallido" msgid "Host Started" msgstr "Host iniciado" -#: awx/main/models/events.py:154 awx/main/models/events.py:656 +#: awx/main/models/events.py:154 awx/main/models/events.py:675 msgid "Host OK" msgstr "Servidor OK" @@ -3482,11 +3482,11 @@ msgstr "Servidor OK" msgid "Host Failure" msgstr "Fallo del servidor" -#: awx/main/models/events.py:156 awx/main/models/events.py:662 +#: awx/main/models/events.py:156 awx/main/models/events.py:681 msgid "Host Skipped" msgstr "Servidor omitido" -#: awx/main/models/events.py:157 awx/main/models/events.py:657 +#: awx/main/models/events.py:157 awx/main/models/events.py:676 msgid "Host Unreachable" msgstr "Servidor no alcanzable" @@ -3570,27 +3570,27 @@ msgstr "Jugada iniciada" msgid "Playbook Complete" msgstr "Playbook terminado" -#: awx/main/models/events.py:184 awx/main/models/events.py:672 +#: awx/main/models/events.py:184 awx/main/models/events.py:691 msgid "Debug" msgstr "Debug" -#: awx/main/models/events.py:185 awx/main/models/events.py:673 +#: awx/main/models/events.py:185 awx/main/models/events.py:692 msgid "Verbose" msgstr "Nivel de detalle" -#: awx/main/models/events.py:186 awx/main/models/events.py:674 +#: awx/main/models/events.py:186 awx/main/models/events.py:693 msgid "Deprecated" msgstr "Obsoleto" -#: awx/main/models/events.py:187 awx/main/models/events.py:675 +#: awx/main/models/events.py:187 awx/main/models/events.py:694 msgid "Warning" msgstr "Advertencia" -#: awx/main/models/events.py:188 awx/main/models/events.py:676 +#: awx/main/models/events.py:188 awx/main/models/events.py:695 msgid "System Warning" msgstr "Advertencia del sistema" -#: awx/main/models/events.py:189 awx/main/models/events.py:677 +#: awx/main/models/events.py:189 awx/main/models/events.py:696 #: awx/main/models/unified_jobs.py:75 msgid "Error" msgstr "Error" @@ -3618,300 +3618,300 @@ msgid "" "this group" msgstr "Lista de instancias con coincidencia exacta que se asignarán siempre automáticamente a este grupo" -#: awx/main/models/inventory.py:79 +#: awx/main/models/inventory.py:80 msgid "Hosts have a direct link to this inventory." msgstr "Los hosts tienen un enlace directo a este inventario." -#: awx/main/models/inventory.py:80 +#: awx/main/models/inventory.py:81 msgid "Hosts for inventory generated using the host_filter property." msgstr "Hosts para inventario generados a través de la propiedad host_filter." -#: awx/main/models/inventory.py:85 +#: awx/main/models/inventory.py:86 msgid "inventories" msgstr "inventarios" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:93 msgid "Organization containing this inventory." msgstr "Organización que contiene este inventario." -#: awx/main/models/inventory.py:99 +#: awx/main/models/inventory.py:100 msgid "Inventory variables in JSON or YAML format." msgstr "Variables de inventario en formato JSON o YAML." -#: awx/main/models/inventory.py:104 +#: awx/main/models/inventory.py:105 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether any hosts in this inventory have failed." msgstr "Este campo es obsoleto y se eliminará en un lanzamiento futuro. Indicador que establece si algún host en este inventario ha fallado." -#: awx/main/models/inventory.py:110 +#: awx/main/models/inventory.py:111 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of hosts in this inventory." msgstr "Este campo es obsoleto y se eliminará en un lanzamiento futuro. Cantidad total de hosts en este inventario." -#: awx/main/models/inventory.py:116 +#: awx/main/models/inventory.py:117 msgid "" "This field is deprecated and will be removed in a future release. Number of " "hosts in this inventory with active failures." msgstr "Este campo es obsoleto y se eliminará en un lanzamiento futuro. Cantidad de hosts en este inventario con fallas activas." -#: awx/main/models/inventory.py:122 +#: awx/main/models/inventory.py:123 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of groups in this inventory." msgstr "Este campo es obsoleto y se eliminará en un lanzamiento futuro. Cantidad total de grupos en este inventario." -#: awx/main/models/inventory.py:128 +#: awx/main/models/inventory.py:129 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether this inventory has any external inventory sources." msgstr "Este campo es obsoleto y se eliminará en un lanzamiento futuro. Indicador que establece si este inventario tiene algúna fuente de inventario externa." -#: awx/main/models/inventory.py:134 +#: awx/main/models/inventory.py:135 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "Número total de inventarios de origen externo configurado dentro de este inventario." -#: awx/main/models/inventory.py:139 +#: awx/main/models/inventory.py:140 msgid "Number of external inventory sources in this inventory with failures." msgstr "Número de inventarios de origen externo en este inventario con errores." -#: awx/main/models/inventory.py:146 +#: awx/main/models/inventory.py:147 msgid "Kind of inventory being represented." msgstr "Tipo de inventario que se representa." -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:153 msgid "Filter that will be applied to the hosts of this inventory." msgstr "Filtro que se aplicará a los hosts de este inventario." -#: awx/main/models/inventory.py:180 +#: awx/main/models/inventory.py:181 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "Credenciales que utilizarán los hosts que pertenecen a este inventario cuando accedan a la API de Red Hat Insights." -#: awx/main/models/inventory.py:189 +#: awx/main/models/inventory.py:190 msgid "Flag indicating the inventory is being deleted." msgstr "Indicador que muestra que el inventario se eliminará." -#: awx/main/models/inventory.py:244 +#: awx/main/models/inventory.py:245 msgid "Could not parse subset as slice specification." msgstr "No se pudo analizar el subconjunto según las especificaciones de fraccionamiento." -#: awx/main/models/inventory.py:248 +#: awx/main/models/inventory.py:249 msgid "Slice number must be less than total number of slices." msgstr "El número de fraccionamiento debe ser inferior al número total de fraccionamientos." -#: awx/main/models/inventory.py:250 +#: awx/main/models/inventory.py:251 msgid "Slice number must be 1 or higher." msgstr "El número de fraccionamiento debe ser 1 o superior." -#: awx/main/models/inventory.py:387 +#: awx/main/models/inventory.py:388 msgid "Assignment not allowed for Smart Inventory" msgstr "Tarea no permitida para el inventario inteligente" -#: awx/main/models/inventory.py:389 awx/main/models/projects.py:166 +#: awx/main/models/inventory.py:390 awx/main/models/projects.py:166 msgid "Credential kind must be 'insights'." msgstr "Tipo de credencial debe ser 'insights'." -#: awx/main/models/inventory.py:474 +#: awx/main/models/inventory.py:475 msgid "Is this host online and available for running jobs?" msgstr "¿Está este servidor funcionando y disponible para ejecutar trabajos?" -#: awx/main/models/inventory.py:480 +#: awx/main/models/inventory.py:481 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "El valor usado por el inventario de fuente remota para identificar de forma única el servidor" -#: awx/main/models/inventory.py:485 +#: awx/main/models/inventory.py:486 msgid "Host variables in JSON or YAML format." msgstr "Variables del servidor en formato JSON o YAML." -#: awx/main/models/inventory.py:508 +#: awx/main/models/inventory.py:509 msgid "Inventory source(s) that created or modified this host." msgstr "Fuente(s) del inventario que crearon o modificaron este servidor." -#: awx/main/models/inventory.py:513 +#: awx/main/models/inventory.py:514 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "Estructura de JSON arbitraria de ansible_facts más reciente por host." -#: awx/main/models/inventory.py:519 +#: awx/main/models/inventory.py:520 msgid "The date and time ansible_facts was last modified." msgstr "La fecha y hora en las que se modificó ansible_facts por última vez." -#: awx/main/models/inventory.py:526 +#: awx/main/models/inventory.py:527 msgid "Red Hat Insights host unique identifier." msgstr "Identificador único de host de Red Hat Insights." -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:641 msgid "Group variables in JSON or YAML format." msgstr "Grupo de variables en formato JSON o YAML." -#: awx/main/models/inventory.py:646 +#: awx/main/models/inventory.py:647 msgid "Hosts associated directly with this group." msgstr "Hosts associated directly with this group." -#: awx/main/models/inventory.py:652 +#: awx/main/models/inventory.py:653 msgid "Inventory source(s) that created or modified this group." msgstr "Fuente(s) de inventario que crearon o modificaron este grupo." -#: awx/main/models/inventory.py:824 +#: awx/main/models/inventory.py:825 msgid "File, Directory or Script" msgstr "Archivo, directorio o script" -#: awx/main/models/inventory.py:825 +#: awx/main/models/inventory.py:826 msgid "Sourced from a Project" msgstr "Extraído de un proyecto" -#: awx/main/models/inventory.py:826 +#: awx/main/models/inventory.py:827 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:835 +#: awx/main/models/inventory.py:836 msgid "Custom Script" msgstr "Script personalizado" -#: awx/main/models/inventory.py:952 +#: awx/main/models/inventory.py:953 msgid "Inventory source variables in YAML or JSON format." msgstr "Variables para la fuente del inventario en formato YAML o JSON." -#: awx/main/models/inventory.py:963 +#: awx/main/models/inventory.py:964 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "Lista de expresiones de filtrado separadas por coma (sólo EC2). Servidores son importados cuando ALGÚN filtro coincide." -#: awx/main/models/inventory.py:969 +#: awx/main/models/inventory.py:970 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "Limitar grupos creados automáticamente desde la fuente del inventario (sólo EC2)" -#: awx/main/models/inventory.py:973 +#: awx/main/models/inventory.py:974 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "Sobrescribir grupos locales y servidores desde una fuente remota del inventario." -#: awx/main/models/inventory.py:977 +#: awx/main/models/inventory.py:978 msgid "Overwrite local variables from remote inventory source." msgstr "Sobrescribir las variables locales desde una fuente remota del inventario." -#: awx/main/models/inventory.py:982 awx/main/models/jobs.py:154 +#: awx/main/models/inventory.py:983 awx/main/models/jobs.py:154 #: awx/main/models/projects.py:135 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "La cantidad de tiempo (en segundos) para ejecutar antes de que se cancele la tarea." -#: awx/main/models/inventory.py:1015 +#: awx/main/models/inventory.py:1016 msgid "Image ID" msgstr "Id de imagen" -#: awx/main/models/inventory.py:1016 +#: awx/main/models/inventory.py:1017 msgid "Availability Zone" msgstr "Zona de disponibilidad" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1019 msgid "Instance ID" msgstr "ID de instancia" -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1020 msgid "Instance State" msgstr "Estado de instancia" -#: awx/main/models/inventory.py:1020 +#: awx/main/models/inventory.py:1021 msgid "Platform" msgstr "Plataforma" -#: awx/main/models/inventory.py:1021 +#: awx/main/models/inventory.py:1022 msgid "Instance Type" msgstr "Tipo de instancia" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1024 msgid "Region" msgstr "Región" -#: awx/main/models/inventory.py:1024 +#: awx/main/models/inventory.py:1025 msgid "Security Group" msgstr "Grupo de seguridad" -#: awx/main/models/inventory.py:1025 +#: awx/main/models/inventory.py:1026 msgid "Tags" msgstr "Etiquetas" -#: awx/main/models/inventory.py:1026 +#: awx/main/models/inventory.py:1027 msgid "Tag None" msgstr "Etiqueta ninguna" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1028 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1095 +#: awx/main/models/inventory.py:1096 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "Las fuentes de inventario basados en la nube (como %s) requieren credenciales para el servicio en la nube coincidente." -#: awx/main/models/inventory.py:1101 +#: awx/main/models/inventory.py:1102 msgid "Credential is required for a cloud source." msgstr "Un credencial es necesario para una fuente cloud." -#: awx/main/models/inventory.py:1104 +#: awx/main/models/inventory.py:1105 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "Credenciales de tipo de máquina, control de fuentes, conocimientos y vault no están permitidas para las fuentes de inventario personalizado." -#: awx/main/models/inventory.py:1109 +#: awx/main/models/inventory.py:1110 msgid "" "Credentials of type insights and vault are disallowed for scm inventory " "sources." msgstr "Las credenciales de tipo de Insights y Vault no están permitidas para fuentes de inventario de SCM." -#: awx/main/models/inventory.py:1169 +#: awx/main/models/inventory.py:1170 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "Región %(source)s no válida: %(region)s" -#: awx/main/models/inventory.py:1193 +#: awx/main/models/inventory.py:1194 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "Expresión de filtro no válida: %(filter)s" -#: awx/main/models/inventory.py:1214 +#: awx/main/models/inventory.py:1215 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "Grupo por elección no válido: %(choice)s" -#: awx/main/models/inventory.py:1242 +#: awx/main/models/inventory.py:1243 msgid "Project containing inventory file used as source." msgstr "Proyecto que contiene el archivo de inventario usado como fuente." -#: awx/main/models/inventory.py:1415 +#: awx/main/models/inventory.py:1416 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "No se permite más de una fuente de inventario basada en SCM con actualización en la actualización del proyecto por inventario." -#: awx/main/models/inventory.py:1422 +#: awx/main/models/inventory.py:1423 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " "update on launch." msgstr "No se puede actualizar la fuente de inventario basada en SCM en la ejecución si está configurada para actualizarse en la actualización del proyecto. En su lugar, configure el proyecto de fuente correspondiente para actualizar en la ejecución." -#: awx/main/models/inventory.py:1428 +#: awx/main/models/inventory.py:1429 msgid "Cannot set source_path if not SCM type." msgstr "No se puede configurar source_path si no es de tipo SCM." -#: awx/main/models/inventory.py:1471 +#: awx/main/models/inventory.py:1472 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "Los archivos de inventario de esta actualización de proyecto se utilizaron para la actualización del inventario." -#: awx/main/models/inventory.py:1582 +#: awx/main/models/inventory.py:1583 msgid "Inventory script contents" msgstr "Contenido del script de inventario" -#: awx/main/models/inventory.py:1587 +#: awx/main/models/inventory.py:1588 msgid "Organization owning this inventory script" msgstr "Organización propietario de este script de inventario" @@ -4010,28 +4010,28 @@ msgstr "Inventario aplicado como un aviso, asumiendo que la plantilla de trabajo msgid "job host summaries" msgstr "Resumen de trabajos de servidor" -#: awx/main/models/jobs.py:1158 +#: awx/main/models/jobs.py:1144 msgid "Remove jobs older than a certain number of days" msgstr "Eliminar trabajos más antiguos que el ńumero de días especificado" -#: awx/main/models/jobs.py:1159 +#: awx/main/models/jobs.py:1145 msgid "Remove activity stream entries older than a certain number of days" msgstr "Eliminar entradas del flujo de actividad más antiguos que el número de días especificado" -#: awx/main/models/jobs.py:1160 +#: awx/main/models/jobs.py:1146 msgid "Removes expired browser sessions from the database" msgstr "Elimina las sesiones de navegador expiradas de la base de datos" -#: awx/main/models/jobs.py:1161 +#: awx/main/models/jobs.py:1147 msgid "Removes expired OAuth 2 access tokens and refresh tokens" msgstr "Elimina los tokens de acceso OAuth2 expirados y los tokens de actualización" -#: awx/main/models/jobs.py:1231 +#: awx/main/models/jobs.py:1217 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "Las variables {list_of_keys} no están permitidas para los trabajos del sistema." -#: awx/main/models/jobs.py:1247 +#: awx/main/models/jobs.py:1233 msgid "days must be a positive integer." msgstr "días debe ser un número entero." @@ -4775,7 +4775,7 @@ msgstr "No se encontraron errores al manejar las rutas, el flujo de trabajo se m msgid "The approval node {name} ({pk}) has expired after {timeout} seconds." msgstr "El nodo de autorización {name} ({pk}) ha expirado después de {timeout} segundos." -#: awx/main/tasks.py:1053 +#: awx/main/tasks.py:1049 msgid "Invalid virtual environment selected: {}" msgstr "Entorno virtual seleccionado no válido: {}" @@ -4812,53 +4812,53 @@ msgstr "No hay una ruta de acceso de control de errores para los nodos de tarea msgid "Unable to convert \"%s\" to boolean" msgstr "No puede convertir \"%s\" a booleano" -#: awx/main/utils/common.py:275 +#: awx/main/utils/common.py:261 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Tipo de SCM \"%s\" no admitido" -#: awx/main/utils/common.py:282 awx/main/utils/common.py:294 -#: awx/main/utils/common.py:313 +#: awx/main/utils/common.py:268 awx/main/utils/common.py:280 +#: awx/main/utils/common.py:299 #, python-format msgid "Invalid %s URL" msgstr "URL %s no válida" -#: awx/main/utils/common.py:284 awx/main/utils/common.py:323 +#: awx/main/utils/common.py:270 awx/main/utils/common.py:309 #, python-format msgid "Unsupported %s URL" msgstr "URL %s no admitida" -#: awx/main/utils/common.py:325 +#: awx/main/utils/common.py:311 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "Host \"%s\" no admitido para URL de file://" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:313 #, python-format msgid "Host is required for %s URL" msgstr "El host es obligatorio para URL %s" -#: awx/main/utils/common.py:345 +#: awx/main/utils/common.py:331 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "El nombre de usuario debe ser \"git\" para el acceso de SSH a %s." -#: awx/main/utils/common.py:351 +#: awx/main/utils/common.py:337 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "El nombre de usuario debe ser \"hg\" para el acceso de SSH a %s." -#: awx/main/utils/common.py:682 +#: awx/main/utils/common.py:668 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "El tipo de entrada `{data_type}` no está en el diccionario" -#: awx/main/utils/common.py:715 +#: awx/main/utils/common.py:701 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "Variables no compatibles con el estándar de JSON (error: {json_error})" -#: awx/main/utils/common.py:721 +#: awx/main/utils/common.py:707 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." diff --git a/awx/locale/nl/LC_MESSAGES/django.po b/awx/locale/nl/LC_MESSAGES/django.po index 43c4d25526..7bc60ae7f6 100644 --- a/awx/locale/nl/LC_MESSAGES/django.po +++ b/awx/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-27 13:55+0000\n" +"POT-Creation-Date: 2020-05-28 21:45+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -528,7 +528,7 @@ msgstr "De taaksjablooninventaris ontbreekt of is niet gedefinieerd." msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "Onbekend, taak is mogelijk al uitgevoerd voordat opstartinstellingen opgeslagen waren." -#: awx/api/serializers.py:3252 awx/main/tasks.py:2795 awx/main/tasks.py:2813 +#: awx/api/serializers.py:3252 awx/main/tasks.py:2800 awx/main/tasks.py:2818 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} kunnen niet worden gebruikt in ad-hocopdrachten." @@ -547,324 +547,324 @@ msgstr "Opgegeven variabele {} heeft geen databasewaarde om mee te vervangen." msgid "\"$encrypted$ is a reserved keyword, may not be used for {}.\"" msgstr "'$encrypted$ is een gereserveerd sleutelwoord en mag niet gebruikt worden voor {}.'" -#: awx/api/serializers.py:4070 +#: awx/api/serializers.py:4078 msgid "A project is required to run a job." msgstr "Een project is nodig om een taak kunnen uitvoeren." -#: awx/api/serializers.py:4072 +#: awx/api/serializers.py:4080 msgid "Missing a revision to run due to failed project update." msgstr "Een revisie om uit te voeren ontbreekt vanwege een projectupdate." -#: awx/api/serializers.py:4076 +#: awx/api/serializers.py:4084 msgid "The inventory associated with this Job Template is being deleted." msgstr "De aan deze taaksjabloon gekoppelde inventaris wordt verwijderd." -#: awx/api/serializers.py:4078 awx/api/serializers.py:4194 +#: awx/api/serializers.py:4086 awx/api/serializers.py:4202 msgid "The provided inventory is being deleted." msgstr "Opgegeven inventaris wordt verwijderd." -#: awx/api/serializers.py:4086 +#: awx/api/serializers.py:4094 msgid "Cannot assign multiple {} credentials." msgstr "Kan niet meerdere toegangsgegevens voor {} toewijzen." -#: awx/api/serializers.py:4090 +#: awx/api/serializers.py:4098 msgid "Cannot assign a Credential of kind `{}`" msgstr "Kan geen toegangsgegevens van het type '{}' toewijzen" -#: awx/api/serializers.py:4103 +#: awx/api/serializers.py:4111 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "Toegangsgegevens voor {} verwijderen bij opstarten zonder vervanging wordt niet ondersteund. De volgende toegangsgegevens ontbraken uit de opgegeven lijst: {}." -#: awx/api/serializers.py:4192 +#: awx/api/serializers.py:4200 msgid "The inventory associated with this Workflow is being deleted." msgstr "De inventaris die gerelateerd is aan deze workflow wordt verwijderd." -#: awx/api/serializers.py:4263 +#: awx/api/serializers.py:4271 msgid "Message type '{}' invalid, must be either 'message' or 'body'" msgstr "Berichttype ‘{}‘ ongeldig, moet ‘bericht‘ ofwel ‘body‘ zijn" -#: awx/api/serializers.py:4269 +#: awx/api/serializers.py:4277 msgid "Expected string for '{}', found {}, " msgstr "Verwachte string voor '{}', {} gevonden, " -#: awx/api/serializers.py:4273 +#: awx/api/serializers.py:4281 msgid "Messages cannot contain newlines (found newline in {} event)" msgstr "Berichten kunnen geen newlines bevatten (newline gevonden in {} gebeurtenis)" -#: awx/api/serializers.py:4279 +#: awx/api/serializers.py:4287 msgid "Expected dict for 'messages' field, found {}" msgstr "Verwacht dictaat voor veld 'berichten', {} gevonden" -#: awx/api/serializers.py:4283 +#: awx/api/serializers.py:4291 msgid "" "Event '{}' invalid, must be one of 'started', 'success', 'error', or " "'workflow_approval'" msgstr "Gebeurtenis ‘{}‘ ongeldig, moet ‘gestart‘, ‘geslaagd‘, ‘fout‘ of ‘workflow_goedkeuring‘ zijn" -#: awx/api/serializers.py:4289 +#: awx/api/serializers.py:4297 msgid "Expected dict for event '{}', found {}" msgstr "Verwacht dictaat voor gebeurtenis ‘{}‘, {} gevonden" -#: awx/api/serializers.py:4294 +#: awx/api/serializers.py:4302 msgid "" "Workflow Approval event '{}' invalid, must be one of 'running', 'approved', " "'timed_out', or 'denied'" msgstr "Workflow Goedkeuringsgebeurtenis ‘{}‘ ongeldig, moet ‘uitvoerend‘, ‘goedgekeurd‘, ‘onderbroken‘ of ‘geweigerd‘ zijn" -#: awx/api/serializers.py:4301 +#: awx/api/serializers.py:4309 msgid "Expected dict for workflow approval event '{}', found {}" msgstr "Verwacht dictaat voor goedkeuring van de workflow ‘{}‘, {} gevonden" -#: awx/api/serializers.py:4328 +#: awx/api/serializers.py:4336 msgid "Unable to render message '{}': {}" msgstr "Niet in staat om bericht '{}' weer te geven: {}" -#: awx/api/serializers.py:4330 +#: awx/api/serializers.py:4338 msgid "Field '{}' unavailable" msgstr "Veld ‘{}‘ niet beschikbaar" -#: awx/api/serializers.py:4332 +#: awx/api/serializers.py:4340 msgid "Security error due to field '{}'" msgstr "Veiligheidsfout als gevolg van veld ‘{}‘" -#: awx/api/serializers.py:4352 +#: awx/api/serializers.py:4360 msgid "Webhook body for '{}' should be a json dictionary. Found type '{}'." msgstr "Webhook-body voor '{}' zou een json-woordenboek moeten zijn. Gevonden type '{}'." -#: awx/api/serializers.py:4355 +#: awx/api/serializers.py:4363 msgid "Webhook body for '{}' is not a valid json dictionary ({})." msgstr "Webhook-body voor ‘{}‘ is geen geldig json-woordenboek ({})." -#: awx/api/serializers.py:4373 +#: awx/api/serializers.py:4381 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "Ontbrekende vereiste velden voor kennisgevingsconfiguratie: notification_type" -#: awx/api/serializers.py:4400 +#: awx/api/serializers.py:4408 msgid "No values specified for field '{}'" msgstr "Geen waarden opgegeven voor veld '{}'" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4413 msgid "HTTP method must be either 'POST' or 'PUT'." msgstr "De HTTP-methode moet 'POSTEN' of 'PLAATSEN' zijn." -#: awx/api/serializers.py:4407 +#: awx/api/serializers.py:4415 msgid "Missing required fields for Notification Configuration: {}." msgstr "Ontbrekende vereiste velden voor kennisgevingsconfiguratie: {}." -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4418 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "Configuratieveld '{}' onjuist type, {} verwacht." -#: awx/api/serializers.py:4427 +#: awx/api/serializers.py:4435 msgid "Notification body" msgstr "Meldingsbody" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4515 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "Geldige DTSTART vereist in rrule. De waarde moet beginnen met: DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:4509 +#: awx/api/serializers.py:4517 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "DTSTART kan geen eenvoudige datum/tijd zijn. Geef ;TZINFO= of YYYYMMDDTHHMMSSZZ op." -#: awx/api/serializers.py:4511 +#: awx/api/serializers.py:4519 msgid "Multiple DTSTART is not supported." msgstr "Meervoudige DTSTART wordt niet ondersteund." -#: awx/api/serializers.py:4513 +#: awx/api/serializers.py:4521 msgid "RRULE required in rrule." msgstr "RRULE vereist in rrule." -#: awx/api/serializers.py:4515 +#: awx/api/serializers.py:4523 msgid "Multiple RRULE is not supported." msgstr "Meervoudige RRULE wordt niet ondersteund." -#: awx/api/serializers.py:4517 +#: awx/api/serializers.py:4525 msgid "INTERVAL required in rrule." msgstr "INTERVAL is vereist in rrule." -#: awx/api/serializers.py:4519 +#: awx/api/serializers.py:4527 msgid "SECONDLY is not supported." msgstr "SECONDLY wordt niet ondersteund." -#: awx/api/serializers.py:4521 +#: awx/api/serializers.py:4529 msgid "Multiple BYMONTHDAYs not supported." msgstr "Meerdere BYMONTHDAY's worden niet ondersteund." -#: awx/api/serializers.py:4523 +#: awx/api/serializers.py:4531 msgid "Multiple BYMONTHs not supported." msgstr "Meerdere BYMONTH's worden niet ondersteund." -#: awx/api/serializers.py:4525 +#: awx/api/serializers.py:4533 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY met numeriek voorvoegsel wordt niet ondersteund." -#: awx/api/serializers.py:4527 +#: awx/api/serializers.py:4535 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY wordt niet ondersteund." -#: awx/api/serializers.py:4529 +#: awx/api/serializers.py:4537 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO wordt niet ondersteund." -#: awx/api/serializers.py:4531 +#: awx/api/serializers.py:4539 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "RRULE mag niet zowel COUNT als UNTIL bevatten" -#: awx/api/serializers.py:4535 +#: awx/api/serializers.py:4543 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 wordt niet ondersteund." -#: awx/api/serializers.py:4541 +#: awx/api/serializers.py:4549 msgid "rrule parsing failed validation: {}" msgstr "de validering van rrule-parsering is mislukt: {}" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4611 msgid "Inventory Source must be a cloud resource." msgstr "Inventarisbron moet een cloudresource zijn." -#: awx/api/serializers.py:4605 +#: awx/api/serializers.py:4613 msgid "Manual Project cannot have a schedule set." msgstr "Handmatig project kan geen ingesteld schema hebben." -#: awx/api/serializers.py:4608 +#: awx/api/serializers.py:4616 msgid "" "Inventory sources with `update_on_project_update` cannot be scheduled. " "Schedule its source project `{}` instead." msgstr "Inventarisbronnen met `update_on_project_update` kan niet worden ingepland. Plan in plaats daarvan het bronproject `{}` in." -#: awx/api/serializers.py:4618 +#: awx/api/serializers.py:4626 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "Aantal taken met status 'in uitvoering' of 'wachten' die in aanmerking komen voor deze instantie" -#: awx/api/serializers.py:4623 +#: awx/api/serializers.py:4631 msgid "Count of all jobs that target this instance" msgstr "Aantal taken die deze instantie als doel hebben" -#: awx/api/serializers.py:4656 +#: awx/api/serializers.py:4664 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance group" msgstr "Aantal taken met status 'in uitvoering' of 'wachten' die in aanmerking komen voor deze instantiegroep" -#: awx/api/serializers.py:4661 +#: awx/api/serializers.py:4669 msgid "Count of all jobs that target this instance group" msgstr "Aantal van alle taken die deze instantiegroep als doel hebben" -#: awx/api/serializers.py:4666 +#: awx/api/serializers.py:4674 msgid "Indicates whether instance group controls any other group" msgstr "Geeft aan of de instantiegroep een andere groep regelt" -#: awx/api/serializers.py:4670 +#: awx/api/serializers.py:4678 msgid "" "Indicates whether instances in this group are isolated.Isolated groups have " "a designated controller group." msgstr "Geeft aan of instanties in deze groep geïsoleerd zijn. Geïsoleerde groepen hebben een aangewezen regelaarsgroep." -#: awx/api/serializers.py:4675 +#: awx/api/serializers.py:4683 msgid "" "Indicates whether instances in this group are containerized.Containerized " "groups have a designated Openshift or Kubernetes cluster." msgstr "Geeft aan of instanties in deze groep geclusterd zijn. Geclusterde groepen hebben een aangewezen Openshift of Kubernetes-cluster." -#: awx/api/serializers.py:4683 +#: awx/api/serializers.py:4691 msgid "Policy Instance Percentage" msgstr "Beleid instantiepercentage" -#: awx/api/serializers.py:4684 +#: awx/api/serializers.py:4692 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "Minimumpercentage van alle instanties die automatisch toegewezen worden aan deze groep wanneer nieuwe instanties online komen." -#: awx/api/serializers.py:4689 +#: awx/api/serializers.py:4697 msgid "Policy Instance Minimum" msgstr "Beleid instantieminimum" -#: awx/api/serializers.py:4690 +#: awx/api/serializers.py:4698 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "Statistisch minimumaantal instanties dat automatisch toegewezen wordt aan deze groep wanneer nieuwe instanties online komen." -#: awx/api/serializers.py:4695 +#: awx/api/serializers.py:4703 msgid "Policy Instance List" msgstr "Beleid instantielijst" -#: awx/api/serializers.py:4696 +#: awx/api/serializers.py:4704 msgid "List of exact-match Instances that will be assigned to this group" msgstr "Lijst van exact overeenkomende instanties die worden toegewezen aan deze groep" -#: awx/api/serializers.py:4722 +#: awx/api/serializers.py:4730 msgid "Duplicate entry {}." msgstr "Dubbele invoer {}." -#: awx/api/serializers.py:4724 +#: awx/api/serializers.py:4732 msgid "{} is not a valid hostname of an existing instance." msgstr "{} is geen geldige hostnaam voor een bestaande instantie." -#: awx/api/serializers.py:4726 awx/api/views/mixin.py:98 +#: awx/api/serializers.py:4734 awx/api/views/mixin.py:98 msgid "" "Isolated instances may not be added or removed from instances groups via the " "API." msgstr "Geïsoleerde instanties mogen niet toegevoegd worden aan of verwijderd worden uit instantiegroepen via de API." -#: awx/api/serializers.py:4728 awx/api/views/mixin.py:102 +#: awx/api/serializers.py:4736 awx/api/views/mixin.py:102 msgid "Isolated instance group membership may not be managed via the API." msgstr "Lidmaatschap van geïsoleerde instantiegroep mag niet beheerd worden via de API." -#: awx/api/serializers.py:4730 awx/api/serializers.py:4735 -#: awx/api/serializers.py:4740 +#: awx/api/serializers.py:4738 awx/api/serializers.py:4743 +#: awx/api/serializers.py:4748 msgid "Containerized instances may not be managed via the API" msgstr "Geclusterde instanties worden mogelijk niet beheerd via de API" -#: awx/api/serializers.py:4745 +#: awx/api/serializers.py:4753 msgid "tower instance group name may not be changed." msgstr "Naam van de tower-instantiegroep mag niet gewijzigd worden." -#: awx/api/serializers.py:4750 +#: awx/api/serializers.py:4758 msgid "Only Kubernetes credentials can be associated with an Instance Group" msgstr "Alleen de toegangsgegevens van Kubernetes kunnen worden geassocieerd met een Instantiegroep" -#: awx/api/serializers.py:4789 +#: awx/api/serializers.py:4797 msgid "" "When present, shows the field name of the role or relationship that changed." msgstr "Geeft, indien aanwezig, de veldnaam aan van de rol of relatie die veranderd is." -#: awx/api/serializers.py:4791 +#: awx/api/serializers.py:4799 msgid "" "When present, shows the model on which the role or relationship was defined." msgstr "Laat, indien aanwezig, het model zien waarvoor de rol of de relatie is gedefinieerd." -#: awx/api/serializers.py:4824 +#: awx/api/serializers.py:4832 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "Een overzicht van de nieuwe en gewijzigde waarden wanneer een object wordt gemaakt, bijgewerkt of verwijderd" -#: awx/api/serializers.py:4826 +#: awx/api/serializers.py:4834 msgid "" "For create, update, and delete events this is the object type that was " "affected. For associate and disassociate events this is the object type " "associated or disassociated with object2." msgstr "Voor maak-, update- en verwijder-gebeurtenissen is dit het betreffende objecttype. Voor koppel- en ontkoppel-gebeurtenissen is dit het objecttype dat wordt gekoppeld aan of ontkoppeld van object2." -#: awx/api/serializers.py:4829 +#: awx/api/serializers.py:4837 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated " "with." msgstr "Niet-ingevuld voor maak-, update- en verwijder-gebeurtenissen. Voor koppel- en ontkoppel-gebeurtenissen is dit het objecttype waaraan object1 wordt gekoppeld." -#: awx/api/serializers.py:4832 +#: awx/api/serializers.py:4840 msgid "The action taken with respect to the given object(s)." msgstr "De actie ondernomen met betrekking tot de gegeven objecten." @@ -1638,7 +1638,7 @@ msgstr "Voorbeeld van instelling" msgid "Example setting which can be different for each user." msgstr "Voorbeeld van instelling die anders kan zijn voor elke gebruiker." -#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:55 +#: awx/conf/conf.py:92 awx/conf/registry.py:81 awx/conf/views.py:56 msgid "User" msgstr "Gebruiker" @@ -1741,15 +1741,15 @@ msgstr "Systeem" msgid "OtherSystem" msgstr "OtherSystem" -#: awx/conf/views.py:47 +#: awx/conf/views.py:48 msgid "Setting Categories" msgstr "Instellingscategorieën" -#: awx/conf/views.py:69 +#: awx/conf/views.py:70 msgid "Setting Detail" msgstr "Instellingsdetail" -#: awx/conf/views.py:160 +#: awx/conf/views.py:162 msgid "Logging Connectivity Test" msgstr "Connectiviteitstest logboekregistratie" @@ -2794,7 +2794,7 @@ msgstr "Conjur-URL" msgid "API Key" msgstr "API-sleutel" -#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1017 +#: awx/main/credential_plugins/conjur.py:28 awx/main/models/inventory.py:1018 msgid "Account" msgstr "Account" @@ -2881,7 +2881,7 @@ msgid "" msgstr "De naam van de geheime back-end (indien dit veld leeg wordt gelaten, wordt het eerste segment van het geheime pad gebruikt)." #: awx/main/credential_plugins/hashivault.py:60 -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1023 msgid "Key Name" msgstr "Sleutelnaam" @@ -3258,7 +3258,7 @@ msgid "" "Management (IAM) users." msgstr "Security Token Service (STS) is een webdienst waarmee u tijdelijke toegangsgegevens met beperkte rechten aan kunt vragen voor gebruikers van AWS Identity en Access Management (IAM)" -#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:832 +#: awx/main/models/credential/__init__.py:780 awx/main/models/inventory.py:833 msgid "OpenStack" msgstr "OpenStack" @@ -3298,7 +3298,7 @@ msgstr "Domeinen van OpenStack bepalen administratieve grenzen. Het is alleen no msgid "Verify SSL" msgstr "SSL verifiëren" -#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:829 +#: awx/main/models/credential/__init__.py:823 awx/main/models/inventory.py:830 msgid "VMware vCenter" msgstr "VMware vCenter" @@ -3311,7 +3311,7 @@ msgid "" "Enter the hostname or IP address that corresponds to your VMware vCenter." msgstr "Voer de hostnaam of het IP-adres in dat overeenkomt met uw VMware vCenter." -#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:830 +#: awx/main/models/credential/__init__.py:849 awx/main/models/inventory.py:831 msgid "Red Hat Satellite 6" msgstr "Red Hat Satellite 6" @@ -3325,7 +3325,7 @@ msgid "" "example, https://satellite.example.org" msgstr "Voer de URL in die overeenkomt met uw sRed Hat Satellite 6-server. Bijvoorbeeld https://satellite.example.org" -#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:831 +#: awx/main/models/credential/__init__.py:875 awx/main/models/inventory.py:832 msgid "Red Hat CloudForms" msgstr "Red Hat CloudForms" @@ -3339,7 +3339,7 @@ msgid "" "instance. For example, https://cloudforms.example.org" msgstr "Voer de URL in voor de virtuele machine die overeenkomt met uw CloudForms-instantie. Bijvoorbeeld https://cloudforms.example.org" -#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:827 +#: awx/main/models/credential/__init__.py:902 awx/main/models/inventory.py:828 msgid "Google Compute Engine" msgstr "Google Compute Engine" @@ -3368,7 +3368,7 @@ msgid "" "Paste the contents of the PEM file associated with the service account email." msgstr "Plak hier de inhoud van het PEM-bestand dat bij de e-mail van het serviceaccount hoort." -#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:828 +#: awx/main/models/credential/__init__.py:936 awx/main/models/inventory.py:829 msgid "Microsoft Azure Resource Manager" msgstr "Microsoft Azure Resource Manager" @@ -3406,7 +3406,7 @@ msgstr "Persoonlijke toegangstoken van GitLab" msgid "This token needs to come from your profile settings in GitLab" msgstr "Deze token moet afkomstig zijn van uw profielinstellingen in GitLab" -#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:833 +#: awx/main/models/credential/__init__.py:1041 awx/main/models/inventory.py:834 msgid "Red Hat Virtualization" msgstr "Red Hat-virtualizering" @@ -3422,7 +3422,7 @@ msgstr "CA-bestand" msgid "Absolute file path to the CA file to use (optional)" msgstr "Absoluut bestandspad naar het CA-bestand om te gebruiken (optioneel)" -#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:834 +#: awx/main/models/credential/__init__.py:1091 awx/main/models/inventory.py:835 msgid "Ansible Tower" msgstr "Ansible Tower" @@ -3466,7 +3466,7 @@ msgstr "Bron moet een extern toegangsgegeven zijn" msgid "Input field must be defined on target credential (options are {})." msgstr "Inputveld moet gedefinieerd worden op doel-toegangsgegeven (opties zijn {})." -#: awx/main/models/events.py:152 awx/main/models/events.py:655 +#: awx/main/models/events.py:152 awx/main/models/events.py:674 msgid "Host Failed" msgstr "Host is mislukt" @@ -3474,7 +3474,7 @@ msgstr "Host is mislukt" msgid "Host Started" msgstr "Host gestart" -#: awx/main/models/events.py:154 awx/main/models/events.py:656 +#: awx/main/models/events.py:154 awx/main/models/events.py:675 msgid "Host OK" msgstr "Host OK" @@ -3482,11 +3482,11 @@ msgstr "Host OK" msgid "Host Failure" msgstr "Hostmislukking" -#: awx/main/models/events.py:156 awx/main/models/events.py:662 +#: awx/main/models/events.py:156 awx/main/models/events.py:681 msgid "Host Skipped" msgstr "Host overgeslagen" -#: awx/main/models/events.py:157 awx/main/models/events.py:657 +#: awx/main/models/events.py:157 awx/main/models/events.py:676 msgid "Host Unreachable" msgstr "Host onbereikbaar" @@ -3570,27 +3570,27 @@ msgstr "Afspelen gestart" msgid "Playbook Complete" msgstr "Draaiboek voltooid" -#: awx/main/models/events.py:184 awx/main/models/events.py:672 +#: awx/main/models/events.py:184 awx/main/models/events.py:691 msgid "Debug" msgstr "Foutopsporing" -#: awx/main/models/events.py:185 awx/main/models/events.py:673 +#: awx/main/models/events.py:185 awx/main/models/events.py:692 msgid "Verbose" msgstr "Uitgebreid" -#: awx/main/models/events.py:186 awx/main/models/events.py:674 +#: awx/main/models/events.py:186 awx/main/models/events.py:693 msgid "Deprecated" msgstr "Afgeschaft" -#: awx/main/models/events.py:187 awx/main/models/events.py:675 +#: awx/main/models/events.py:187 awx/main/models/events.py:694 msgid "Warning" msgstr "Waarschuwing" -#: awx/main/models/events.py:188 awx/main/models/events.py:676 +#: awx/main/models/events.py:188 awx/main/models/events.py:695 msgid "System Warning" msgstr "Systeemwaarschuwing" -#: awx/main/models/events.py:189 awx/main/models/events.py:677 +#: awx/main/models/events.py:189 awx/main/models/events.py:696 #: awx/main/models/unified_jobs.py:75 msgid "Error" msgstr "Fout" @@ -3618,300 +3618,300 @@ msgid "" "this group" msgstr "Lijst van exact overeenkomende instanties die altijd automatisch worden toegewezen aan deze groep" -#: awx/main/models/inventory.py:79 +#: awx/main/models/inventory.py:80 msgid "Hosts have a direct link to this inventory." msgstr "Hosts hebben een directe koppeling naar deze inventaris." -#: awx/main/models/inventory.py:80 +#: awx/main/models/inventory.py:81 msgid "Hosts for inventory generated using the host_filter property." msgstr "Hosts voor inventaris gegenereerd met de eigenschap host_filter." -#: awx/main/models/inventory.py:85 +#: awx/main/models/inventory.py:86 msgid "inventories" msgstr "inventarissen" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:93 msgid "Organization containing this inventory." msgstr "Organisatie die deze inventaris bevat." -#: awx/main/models/inventory.py:99 +#: awx/main/models/inventory.py:100 msgid "Inventory variables in JSON or YAML format." msgstr "Inventarisvariabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:104 +#: awx/main/models/inventory.py:105 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether any hosts in this inventory have failed." msgstr "Dit veld is verouderd en wordt verwijderd uit toekomstige uitgaven. De vlag geeft aan of hosts in deze inventaris storingen ondervinden." -#: awx/main/models/inventory.py:110 +#: awx/main/models/inventory.py:111 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of hosts in this inventory." msgstr "Dit veld is verouderd en wordt verwijderd uit toekomstige uitgaven. Totaalaantal hosts in deze inventaris." -#: awx/main/models/inventory.py:116 +#: awx/main/models/inventory.py:117 msgid "" "This field is deprecated and will be removed in a future release. Number of " "hosts in this inventory with active failures." msgstr "Dit veld is verouderd en wordt verwijderd uit toekomstige uitgaven. Aantal hosts in deze inventaris met actieve storingen." -#: awx/main/models/inventory.py:122 +#: awx/main/models/inventory.py:123 msgid "" "This field is deprecated and will be removed in a future release. Total " "number of groups in this inventory." msgstr "Dit veld is verouderd en wordt verwijderd uit toekomstige uitgaven. Totaalaantal groepen in deze inventaris." -#: awx/main/models/inventory.py:128 +#: awx/main/models/inventory.py:129 msgid "" "This field is deprecated and will be removed in a future release. Flag " "indicating whether this inventory has any external inventory sources." msgstr "Dit veld is verouderd en wordt verwijderd uit toekomstige uitgaven. Vlag die aangeeft of deze inventaris externe inventarisbronnen bevat." -#: awx/main/models/inventory.py:134 +#: awx/main/models/inventory.py:135 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "Totaal aantal externe inventarisbronnen dat binnen deze inventaris is geconfigureerd." -#: awx/main/models/inventory.py:139 +#: awx/main/models/inventory.py:140 msgid "Number of external inventory sources in this inventory with failures." msgstr "Aantal externe inventarisbronnen in deze inventaris met mislukkingen." -#: awx/main/models/inventory.py:146 +#: awx/main/models/inventory.py:147 msgid "Kind of inventory being represented." msgstr "Soort inventaris dat wordt voorgesteld." -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:153 msgid "Filter that will be applied to the hosts of this inventory." msgstr "Filter dat wordt toegepast op de hosts van deze inventaris." -#: awx/main/models/inventory.py:180 +#: awx/main/models/inventory.py:181 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "Referenties die worden gebruikt door hosts die behoren tot deze inventaris bij toegang tot de Red Hat Insights API." -#: awx/main/models/inventory.py:189 +#: awx/main/models/inventory.py:190 msgid "Flag indicating the inventory is being deleted." msgstr "Vlag die aangeeft dat de inventaris wordt verwijderd." -#: awx/main/models/inventory.py:244 +#: awx/main/models/inventory.py:245 msgid "Could not parse subset as slice specification." msgstr "Kan subset niet als deelspecificatie parseren." -#: awx/main/models/inventory.py:248 +#: awx/main/models/inventory.py:249 msgid "Slice number must be less than total number of slices." msgstr "Deelaantal moet lager zijn dan het totale aantal delen." -#: awx/main/models/inventory.py:250 +#: awx/main/models/inventory.py:251 msgid "Slice number must be 1 or higher." msgstr "Deelaantal moet 1 of hoger zijn." -#: awx/main/models/inventory.py:387 +#: awx/main/models/inventory.py:388 msgid "Assignment not allowed for Smart Inventory" msgstr "Toewijzing niet toegestaan voor Smart-inventaris" -#: awx/main/models/inventory.py:389 awx/main/models/projects.py:166 +#: awx/main/models/inventory.py:390 awx/main/models/projects.py:166 msgid "Credential kind must be 'insights'." msgstr "Referentiesoort moet 'insights' zijn." -#: awx/main/models/inventory.py:474 +#: awx/main/models/inventory.py:475 msgid "Is this host online and available for running jobs?" msgstr "Is deze host online en beschikbaar om taken uit te voeren?" -#: awx/main/models/inventory.py:480 +#: awx/main/models/inventory.py:481 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "De waarde die de externe inventarisbron gebruikt om de host uniek te identificeren" -#: awx/main/models/inventory.py:485 +#: awx/main/models/inventory.py:486 msgid "Host variables in JSON or YAML format." msgstr "Hostvariabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:508 +#: awx/main/models/inventory.py:509 msgid "Inventory source(s) that created or modified this host." msgstr "Inventarisbronnen die deze host hebben gemaakt of gewijzigd." -#: awx/main/models/inventory.py:513 +#: awx/main/models/inventory.py:514 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "Willekeurige JSON-structuur van meest recente ansible_facts, per host/" -#: awx/main/models/inventory.py:519 +#: awx/main/models/inventory.py:520 msgid "The date and time ansible_facts was last modified." msgstr "De datum en tijd waarop ansible_facts voor het laatst is gewijzigd." -#: awx/main/models/inventory.py:526 +#: awx/main/models/inventory.py:527 msgid "Red Hat Insights host unique identifier." msgstr "Unieke id van Red Hat Insights-host." -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:641 msgid "Group variables in JSON or YAML format." msgstr "Groepeer variabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:646 +#: awx/main/models/inventory.py:647 msgid "Hosts associated directly with this group." msgstr "Hosts direct gekoppeld aan deze groep." -#: awx/main/models/inventory.py:652 +#: awx/main/models/inventory.py:653 msgid "Inventory source(s) that created or modified this group." msgstr "Inventarisbronnen die deze groep hebben gemaakt of gewijzigd." -#: awx/main/models/inventory.py:824 +#: awx/main/models/inventory.py:825 msgid "File, Directory or Script" msgstr "Bestand, map of script" -#: awx/main/models/inventory.py:825 +#: awx/main/models/inventory.py:826 msgid "Sourced from a Project" msgstr "Afkomstig uit een project" -#: awx/main/models/inventory.py:826 +#: awx/main/models/inventory.py:827 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:835 +#: awx/main/models/inventory.py:836 msgid "Custom Script" msgstr "Aangepast script" -#: awx/main/models/inventory.py:952 +#: awx/main/models/inventory.py:953 msgid "Inventory source variables in YAML or JSON format." msgstr "Bronvariabelen inventaris in YAML- of JSON-indeling." -#: awx/main/models/inventory.py:963 +#: awx/main/models/inventory.py:964 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "Door komma's gescheiden lijst met filterexpressies (alleen EC2). Hosts worden geïmporteerd wanneer WILLEKEURIG WELK van de filters overeenkomt." -#: awx/main/models/inventory.py:969 +#: awx/main/models/inventory.py:970 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "Overschrijf groepen die automatisch gemaakt worden op grond van inventarisbron (alleen EC2)." -#: awx/main/models/inventory.py:973 +#: awx/main/models/inventory.py:974 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "Overschrijf lokale groepen en hosts op grond van externe inventarisbron." -#: awx/main/models/inventory.py:977 +#: awx/main/models/inventory.py:978 msgid "Overwrite local variables from remote inventory source." msgstr "Overschrijf lokale variabelen op grond van externe inventarisbron." -#: awx/main/models/inventory.py:982 awx/main/models/jobs.py:154 +#: awx/main/models/inventory.py:983 awx/main/models/jobs.py:154 #: awx/main/models/projects.py:135 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "De hoeveelheid tijd (in seconden) voor uitvoering voordat de taak wordt geannuleerd." -#: awx/main/models/inventory.py:1015 +#: awx/main/models/inventory.py:1016 msgid "Image ID" msgstr "Image-id" -#: awx/main/models/inventory.py:1016 +#: awx/main/models/inventory.py:1017 msgid "Availability Zone" msgstr "Beschikbaarheidszone" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1019 msgid "Instance ID" msgstr "Instantie-id" -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1020 msgid "Instance State" msgstr "Instantiestaat" -#: awx/main/models/inventory.py:1020 +#: awx/main/models/inventory.py:1021 msgid "Platform" msgstr "Platform" -#: awx/main/models/inventory.py:1021 +#: awx/main/models/inventory.py:1022 msgid "Instance Type" msgstr "Instantietype" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1024 msgid "Region" msgstr "Regio" -#: awx/main/models/inventory.py:1024 +#: awx/main/models/inventory.py:1025 msgid "Security Group" msgstr "Beveiligingsgroep" -#: awx/main/models/inventory.py:1025 +#: awx/main/models/inventory.py:1026 msgid "Tags" msgstr "Tags" -#: awx/main/models/inventory.py:1026 +#: awx/main/models/inventory.py:1027 msgid "Tag None" msgstr "Tag geen" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1028 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1095 +#: awx/main/models/inventory.py:1096 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "Cloudgebaseerde inventarisbronnen (zoals %s) vereisen toegangsgegevens voor de overeenkomende cloudservice." -#: awx/main/models/inventory.py:1101 +#: awx/main/models/inventory.py:1102 msgid "Credential is required for a cloud source." msgstr "Referentie is vereist voor een cloudbron." -#: awx/main/models/inventory.py:1104 +#: awx/main/models/inventory.py:1105 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "Toegangsgegevens van soort machine, bronbeheer, inzichten en kluis zijn niet toegestaan voor aangepaste inventarisbronnen." -#: awx/main/models/inventory.py:1109 +#: awx/main/models/inventory.py:1110 msgid "" "Credentials of type insights and vault are disallowed for scm inventory " "sources." msgstr "Toegangsgegevens van het soort inzichten en kluis zijn niet toegestaan voor scm-inventarisbronnen." -#: awx/main/models/inventory.py:1169 +#: awx/main/models/inventory.py:1170 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "Ongeldige %(source)s regio: %(region)s" -#: awx/main/models/inventory.py:1193 +#: awx/main/models/inventory.py:1194 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "Ongeldige filterexpressie: %(filter)s" -#: awx/main/models/inventory.py:1214 +#: awx/main/models/inventory.py:1215 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "Ongeldige groep op keuze: %(choice)s" -#: awx/main/models/inventory.py:1242 +#: awx/main/models/inventory.py:1243 msgid "Project containing inventory file used as source." msgstr "Project met inventarisbestand dat wordt gebruikt als bron." -#: awx/main/models/inventory.py:1415 +#: awx/main/models/inventory.py:1416 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "Het is niet toegestaan meer dan één SCM-gebaseerde inventarisbron met een update bovenop een projectupdate per inventaris te hebben." -#: awx/main/models/inventory.py:1422 +#: awx/main/models/inventory.py:1423 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " "update on launch." msgstr "Kan SCM-gebaseerde inventarisbron niet bijwerken bij opstarten indien ingesteld op bijwerken bij projectupdate. Configureer in plaats daarvan het overeenkomstige bronproject om bij te werken bij opstarten." -#: awx/main/models/inventory.py:1428 +#: awx/main/models/inventory.py:1429 msgid "Cannot set source_path if not SCM type." msgstr "Kan source_path niet instellen als het geen SCM-type is." -#: awx/main/models/inventory.py:1471 +#: awx/main/models/inventory.py:1472 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "Inventarisbestanden uit deze projectupdate zijn gebruikt voor de inventarisupdate." -#: awx/main/models/inventory.py:1582 +#: awx/main/models/inventory.py:1583 msgid "Inventory script contents" msgstr "Inhoud inventarisscript" -#: awx/main/models/inventory.py:1587 +#: awx/main/models/inventory.py:1588 msgid "Organization owning this inventory script" msgstr "Organisatie die eigenaar is van deze inventarisscript" @@ -4010,28 +4010,28 @@ msgstr "Inventarisatie toegepast als een melding, neemt de vorm aan van taaksjab msgid "job host summaries" msgstr "taakhostoverzichten" -#: awx/main/models/jobs.py:1158 +#: awx/main/models/jobs.py:1144 msgid "Remove jobs older than a certain number of days" msgstr "Taken ouder dan een bepaald aantal dagen verwijderen" -#: awx/main/models/jobs.py:1159 +#: awx/main/models/jobs.py:1145 msgid "Remove activity stream entries older than a certain number of days" msgstr "Vermeldingen activiteitenstroom ouder dan een bepaald aantal dagen verwijderen" -#: awx/main/models/jobs.py:1160 +#: awx/main/models/jobs.py:1146 msgid "Removes expired browser sessions from the database" msgstr "Verwijdert verlopen browsersessies uit de database" -#: awx/main/models/jobs.py:1161 +#: awx/main/models/jobs.py:1147 msgid "Removes expired OAuth 2 access tokens and refresh tokens" msgstr "Verwijdert vervallen OAuth 2-toegangstokens en verversingstokens" -#: awx/main/models/jobs.py:1231 +#: awx/main/models/jobs.py:1217 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "Variabelen {list_of_keys} zijn niet toegestaan voor systeemtaken." -#: awx/main/models/jobs.py:1247 +#: awx/main/models/jobs.py:1233 msgid "days must be a positive integer." msgstr "dagen moet een positief geheel getal zijn." @@ -4775,7 +4775,7 @@ msgstr "Geen foutafhandelingspaden gevonden, workflow gemarkeerd als mislukt" msgid "The approval node {name} ({pk}) has expired after {timeout} seconds." msgstr "Goedkeuringsknooppunt {name} ({pk}) is na {timeout} seconden verlopen." -#: awx/main/tasks.py:1053 +#: awx/main/tasks.py:1049 msgid "Invalid virtual environment selected: {}" msgstr "Ongeldige virtuele omgeving geselecteerd: {}" @@ -4812,53 +4812,53 @@ msgstr "Geen foutverwerkingspad voor workflowtaakknooppunt(en) []. Voor workflow msgid "Unable to convert \"%s\" to boolean" msgstr "Kan ‘%s‘ niet omzetten naar boolean" -#: awx/main/utils/common.py:275 +#: awx/main/utils/common.py:261 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Niet-ondersteund SCM-type ‘%s‘" -#: awx/main/utils/common.py:282 awx/main/utils/common.py:294 -#: awx/main/utils/common.py:313 +#: awx/main/utils/common.py:268 awx/main/utils/common.py:280 +#: awx/main/utils/common.py:299 #, python-format msgid "Invalid %s URL" msgstr "Ongeldige %s URL" -#: awx/main/utils/common.py:284 awx/main/utils/common.py:323 +#: awx/main/utils/common.py:270 awx/main/utils/common.py:309 #, python-format msgid "Unsupported %s URL" msgstr "Niet-ondersteunde %s URL" -#: awx/main/utils/common.py:325 +#: awx/main/utils/common.py:311 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "Niet-ondersteunde host ‘%s‘ voor bestand:// URL" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:313 #, python-format msgid "Host is required for %s URL" msgstr "Host is vereist voor %s URL" -#: awx/main/utils/common.py:345 +#: awx/main/utils/common.py:331 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "Gebruikersnaam moet ‘git‘ zijn voor SSH-toegang tot %s." -#: awx/main/utils/common.py:351 +#: awx/main/utils/common.py:337 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "Gebruikersnaam moet ‘hg‘ zijn voor SSH-toegang tot %s." -#: awx/main/utils/common.py:682 +#: awx/main/utils/common.py:668 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "Soort input `{data_type}` is geen woordenlijst" -#: awx/main/utils/common.py:715 +#: awx/main/utils/common.py:701 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "Variabelen niet compatibel met JSON-norm (fout: {json_error})" -#: awx/main/utils/common.py:721 +#: awx/main/utils/common.py:707 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." diff --git a/awx/ui/po/es.po b/awx/ui/po/es.po index 624eddae3a..c70c66ee02 100644 --- a/awx/ui/po/es.po +++ b/awx/ui/po/es.po @@ -4428,11 +4428,11 @@ msgstr "Variables de invalidación halladas en azure_rm.ini y utilizadas por el #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:282 msgid "" "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Variables de invalidación halladas en cloudforms.ini y utilizadas en el script de actualización del inventario. Para ver un ejemplo de configuración variable, \n" -" \n" -" vea cloudforms.ini en el repositorio github de Ansible. Ingrese variables de inventario con sintaxis de JSON o YAML. Utilice el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." +" \n" +" view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "Variables de invalidación halladas en cloudforms.ini y usadas en el script de actualización del inventario. Para ver un ejemplo de configuración variable, \n" +" \n" +" vea cloudforms.ini en el repositorio github de Ansible Collections. Ingrese variables de inventario con sintaxis de JSON o YAML. Use el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:217 msgid "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables" @@ -4441,20 +4441,16 @@ msgstr "Variables de invalidación halladas en ec2.ini y utilizadas en el script #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:299 msgid "" "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Variables de invalidación halladas en foreman.ini y utilizadas en el script de actualización del inventario. Para ver un ejemplo de configuración de variable\n" -" \n" -" vea foreman.ini en el repositorio github de Ansible. Ingrese variables de inventario con sintaxis de JSON o YAML. Utilice el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." +" \n" +" view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "Variables de invalidación halladas en foreman.ini y usadas en el script de actualización del inventario. Para ver un ejemplo de configuración variable, vea foreman.ini en el repositorio github de Ansible Collections. Ingrese variables de inventario con sintaxis de JSON o YAML. Use el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:265 msgid "" "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." -msgstr "Variables de invalidación halladas en openstack.yml y utilizadas en el script de actualización del inventario. Para ver un ejemplo de configuración de variable,\n" -" \n" -" vea openstack.yml en el repositorio github de Ansible. Ingrese variables de inventario con la sintaxis de JSON o YAML. Utilice el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." +" \n" +" view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +msgstr "Variables de invalidación halladas en openstack.yml y usadas en el script de actualización del inventario. Para ver un ejemplo de configuración de variable, vea openstack.yml en el repositorio github de Openstack. Ingrese variables de inventario con la sintaxis de JSON o YAML. Use el botón de opción para alternar entre los dos. Consulte la documentación de Ansible Tower para ver ejemplos de sintaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:241 msgid "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables" diff --git a/awx/ui/po/nl.po b/awx/ui/po/nl.po index 44d146d52f..721a28368c 100644 --- a/awx/ui/po/nl.po +++ b/awx/ui/po/nl.po @@ -4428,11 +4428,11 @@ msgstr "Variabelen overschrijven die aangetroffen zijn in azure_rm.ini en die ge #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:282 msgid "" "Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view cloudforms.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Variabelen overschrijven die zijn aangetroffen in cloudforms.ini en die gebruikt worden door het script van de inventarisupdate. Zie voor een voorbeeld van de configuratie van de variabele \n" -" \n" -" cloudfroms.ini in de Ansible github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radio-knop om tussen de twee de wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." +" a href=\\\"https://github.com/ansible-collections/community.general/blob/master/scripts/inventory/cloudforms.ini\\\" target=\\\"_blank\\\">\n" +" cloudfroms.ini in de Ansible Collections github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radio-knop om tussen de twee de wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:217 msgid "Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables" @@ -4441,20 +4441,20 @@ msgstr "Variabelen overschrijven die aangetroffen zijn in ec2.ini en die gebruik #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:299 msgid "" "Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration\n" -" \n" -" view foreman.ini in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Variabelen overschrijven die aangetroffen zijn in foreman.ini en die gebruikt worden door het script van de inventarisupdate. Zie voor een voorbeeld van de configuratie van de variabelen\n" -" \n" -" foreman.ini in de Ansible github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radioknop om tussen de twee te wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." +" \n" +" foreman.ini in de Ansible Collections github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radioknop om tussen de twee te wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:265 msgid "" "Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration\n" -" \n" -" view openstack.yml in the Ansible github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." +" \n" +" view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax." msgstr "Variabelen overschrijven die aangetroffen zijn in openstack.yml en die gebruikt worden door het script van de inventarisupdate. Zie voor een voorbeeld van de configuratie van de variabelen\n" -" \n" -" openstack.yml in de Ansible github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radioknop om tussen de twee te wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." +" \n" +" openstack.yml in de Openstack github repo.Voer de variabelen van de inventaris in met JSON- of YAML-syntaxis. Gebruik de radioknop om tussen de twee te wisselen. Raadpleeg de documentatie van Ansible Tower voor voorbeeldsyntaxis." #: client/src/inventories-hosts/inventories/related/sources/sources.form.js:241 msgid "Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables" From a88f03b372c478529b1debc53565f7cd244c4113 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Mon, 6 Jul 2020 13:48:58 -0400 Subject: [PATCH 34/53] Reintroduce label filtering Labels are visible if you have a role on the org they are in, or on a job template they're attached to. --- awx/main/access.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 4705fb2cfc..d0f3bd6c96 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2480,13 +2480,16 @@ class NotificationAccess(BaseAccess): class LabelAccess(BaseAccess): ''' - I can see/use a Label if I have permission to associated organization + I can see/use a Label if I have permission to associated organization, or to a JT that the label is on ''' model = Label prefetch_related = ('modified_by', 'created_by', 'organization',) def filtered_queryset(self): - return self.model.objects.all() + return self.model.objects.filter( + Q(organization__in=Organization.accessible_pk_qs(self.user, 'read_role')) | + Q(unifiedjobtemplate_labels__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) + ) @check_superuser def can_add(self, data): From 2f1b4d81e1758e76d4240fe39bfe38f418c316fd Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 6 Jul 2020 13:50:33 -0400 Subject: [PATCH 35/53] use jinja2.sandbox for credential type injectors --- awx/main/models/credential/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 1701c1fb24..b16896be1f 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -11,7 +11,7 @@ import tempfile from types import SimpleNamespace # Jinja2 -from jinja2 import Template +from jinja2 import sandbox # Django from django.db import models @@ -514,8 +514,11 @@ class CredentialType(CommonModelNameNotUnique): # If any file templates are provided, render the files and update the # special `tower` template namespace so the filename can be # referenced in other injectors + + sandbox_env = sandbox.ImmutableSandboxedEnvironment() + for file_label, file_tmpl in file_tmpls.items(): - data = Template(file_tmpl).render(**namespace) + data = sandbox_env.from_string(file_tmpl).render(**namespace) _, path = tempfile.mkstemp(dir=private_data_dir) with open(path, 'w') as f: f.write(data) @@ -537,14 +540,14 @@ class CredentialType(CommonModelNameNotUnique): except ValidationError as e: logger.error('Ignoring prohibited env var {}, reason: {}'.format(env_var, e)) continue - env[env_var] = Template(tmpl).render(**namespace) - safe_env[env_var] = Template(tmpl).render(**safe_namespace) + env[env_var] = sandbox_env.from_string(tmpl).render(**namespace) + safe_env[env_var] = sandbox_env.from_string(tmpl).render(**safe_namespace) if 'INVENTORY_UPDATE_ID' not in env: # awx-manage inventory_update does not support extra_vars via -e extra_vars = {} for var_name, tmpl in self.injectors.get('extra_vars', {}).items(): - extra_vars[var_name] = Template(tmpl).render(**namespace) + extra_vars[var_name] = sandbox_env.from_string(tmpl).render(**namespace) def build_extra_vars_file(vars, private_dir): handle, path = tempfile.mkstemp(dir = private_dir) From d14aee70a1ebafc5076f93eac148e2bed41f6484 Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Mon, 6 Jul 2020 15:26:39 -0400 Subject: [PATCH 36/53] Don't follow redirects in credential plugins --- awx/main/credential_plugins/aim.py | 1 + awx/main/credential_plugins/conjur.py | 4 +++- awx/main/credential_plugins/hashivault.py | 10 ++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index c75d4d85aa..c63181ed46 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -95,6 +95,7 @@ def aim_backend(**kwargs): timeout=30, cert=cert, verify=verify, + allow_redirects=False, ) res.raise_for_status() return res.json()['Content'] diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index 55fd2e60f2..a851277134 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -63,7 +63,8 @@ def conjur_backend(**kwargs): auth_kwargs = { 'headers': {'Content-Type': 'text/plain'}, - 'data': api_key + 'data': api_key, + 'allow_redirects': False, } if cacert: auth_kwargs['verify'] = create_temporary_fifo(cacert.encode()) @@ -78,6 +79,7 @@ def conjur_backend(**kwargs): lookup_kwargs = { 'headers': {'Authorization': 'Token token="{}"'.format(token)}, + 'allow_redirects': False, } if cacert: lookup_kwargs['verify'] = create_temporary_fifo(cacert.encode()) diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index c9caafba6b..c094f747d9 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -97,7 +97,10 @@ def kv_backend(**kwargs): cacert = kwargs.get('cacert', None) api_version = kwargs['api_version'] - request_kwargs = {'timeout': 30} + request_kwargs = { + 'timeout': 30, + 'allow_redirects': False, + } if cacert: request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) @@ -150,7 +153,10 @@ def ssh_backend(**kwargs): role = kwargs['role'] cacert = kwargs.get('cacert', None) - request_kwargs = {'timeout': 30} + request_kwargs = { + 'timeout': 30, + 'allow_redirects': False, + } if cacert: request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) From b7f37d5e26e16f682c4d1486a54f22a03aff74b7 Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Mon, 6 Jul 2020 21:17:21 -0400 Subject: [PATCH 37/53] Reduce error detail in webhook notification --- awx/main/notifications/grafana_backend.py | 4 ++-- awx/main/notifications/hipchat_backend.py | 4 ++-- awx/main/notifications/mattermost_backend.py | 4 ++-- awx/main/notifications/rocketchat_backend.py | 4 ++-- awx/main/notifications/webhook_backend.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/awx/main/notifications/grafana_backend.py b/awx/main/notifications/grafana_backend.py index 23816c8d06..01699b24dc 100644 --- a/awx/main/notifications/grafana_backend.py +++ b/awx/main/notifications/grafana_backend.py @@ -99,8 +99,8 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase): headers=grafana_headers, verify=(not self.grafana_no_verify_ssl)) if r.status_code >= 400: - logger.error(smart_text(_("Error sending notification grafana: {}").format(r.text))) + logger.error(smart_text(_("Error sending notification grafana: {}").format(r.status_code))) if not self.fail_silently: - raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.text))) + raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.status_code))) sent_messages += 1 return sent_messages diff --git a/awx/main/notifications/hipchat_backend.py b/awx/main/notifications/hipchat_backend.py index 16790644a3..1dfde08546 100644 --- a/awx/main/notifications/hipchat_backend.py +++ b/awx/main/notifications/hipchat_backend.py @@ -47,8 +47,8 @@ class HipChatBackend(AWXBaseEmailBackend, CustomNotificationBase): "from": m.from_email, "message_format": "text"}) if r.status_code != 204: - logger.error(smart_text(_("Error sending messages: {}").format(r.text))) + logger.error(smart_text(_("Error sending messages: {}").format(r.status_code))) if not self.fail_silently: - raise Exception(smart_text(_("Error sending message to hipchat: {}").format(r.text))) + raise Exception(smart_text(_("Error sending message to hipchat: {}").format(r.status_code))) sent_messages += 1 return sent_messages diff --git a/awx/main/notifications/mattermost_backend.py b/awx/main/notifications/mattermost_backend.py index 78a23c72d1..59a1c6f5e1 100644 --- a/awx/main/notifications/mattermost_backend.py +++ b/awx/main/notifications/mattermost_backend.py @@ -46,8 +46,8 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase): r = requests.post("{}".format(m.recipients()[0]), json=payload, verify=(not self.mattermost_no_verify_ssl)) if r.status_code >= 400: - logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text))) + logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.status_code))) if not self.fail_silently: - raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.text))) + raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.status_code))) sent_messages += 1 return sent_messages diff --git a/awx/main/notifications/rocketchat_backend.py b/awx/main/notifications/rocketchat_backend.py index 1ad367fb57..df271bf80d 100644 --- a/awx/main/notifications/rocketchat_backend.py +++ b/awx/main/notifications/rocketchat_backend.py @@ -46,9 +46,9 @@ class RocketChatBackend(AWXBaseEmailBackend, CustomNotificationBase): if r.status_code >= 400: logger.error(smart_text( - _("Error sending notification rocket.chat: {}").format(r.text))) + _("Error sending notification rocket.chat: {}").format(r.status_code))) if not self.fail_silently: raise Exception(smart_text( - _("Error sending notification rocket.chat: {}").format(r.text))) + _("Error sending notification rocket.chat: {}").format(r.status_code))) sent_messages += 1 return sent_messages diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index b9c2c35d22..a33cf026f8 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -72,8 +72,8 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase): headers=self.headers, verify=(not self.disable_ssl_verification)) if r.status_code >= 400: - logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text))) + logger.error(smart_text(_("Error sending notification webhook: {}").format(r.status_code))) if not self.fail_silently: - raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text))) + raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.status_code))) sent_messages += 1 return sent_messages From 5a96af79d43df982e02f3fa25120fb588f9f3bcb Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Mon, 6 Jul 2020 21:41:28 -0400 Subject: [PATCH 38/53] Reduce error detail in credential lookups --- awx/api/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index aca8d892a0..a0a39fa1e1 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -1397,7 +1397,7 @@ class CredentialExternalTest(SubDetailAPIView): obj.credential_type.plugin.backend(**backend_kwargs) return Response({}, status=status.HTTP_202_ACCEPTED) except requests.exceptions.HTTPError as exc: - message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text) + message = 'HTTP {}'.format(exc.response.status_code) return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST) except Exception as exc: return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST) From 61d3a765eea93964d2992acb656c18309213e1ca Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 7 Jul 2020 10:59:14 -0400 Subject: [PATCH 39/53] prevent unsafe jinja from being saved in the first place for cred types see: https://github.com/ansible/tower-security/issues/21 --- awx/main/fields.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 4e854bbb8b..57d2d9e505 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -7,8 +7,8 @@ import json import re import urllib.parse -from jinja2 import Environment, StrictUndefined -from jinja2.exceptions import UndefinedError, TemplateSyntaxError +from jinja2 import sandbox, StrictUndefined +from jinja2.exceptions import UndefinedError, TemplateSyntaxError, SecurityError # Django from django.contrib.postgres.fields import JSONField as upstream_JSONBField @@ -932,7 +932,7 @@ class CredentialTypeInjectorField(JSONSchemaField): self.validate_env_var_allowed(key) for key, tmpl in injector.items(): try: - Environment( + sandbox.ImmutableSandboxedEnvironment( undefined=StrictUndefined ).from_string(tmpl).render(valid_namespace) except UndefinedError as e: @@ -942,6 +942,10 @@ class CredentialTypeInjectorField(JSONSchemaField): code='invalid', params={'value': value}, ) + except SecurityError as e: + raise django_exceptions.ValidationError( + _('Encountered unsafe code execution: {}').format(e) + ) except TemplateSyntaxError as e: raise django_exceptions.ValidationError( _('Syntax error rendering template for {sub_key} inside of {type} ({error_msg})').format( From 7322e134360e0f7b803f39ece1a727c426f427db Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 8 Jul 2020 16:53:05 -0400 Subject: [PATCH 40/53] add tests for clarified label permissions --- awx/main/tests/functional/test_rbac_label.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/functional/test_rbac_label.py b/awx/main/tests/functional/test_rbac_label.py index 955894c06f..ed819df9f0 100644 --- a/awx/main/tests/functional/test_rbac_label.py +++ b/awx/main/tests/functional/test_rbac_label.py @@ -20,8 +20,19 @@ def test_label_get_queryset_su(label, user): @pytest.mark.django_db -def test_label_access(label, user): +def test_label_read_access(label, user): access = LabelAccess(user('user', False)) + assert not access.can_read(label) + label.organization.member_role.members.add(user('user', False)) + assert access.can_read(label) + + +@pytest.mark.django_db +def test_label_jt_read_access(label, user, job_template): + access = LabelAccess(user('user', False)) + assert not access.can_read(label) + job_template.read_role.members.add(user('user', False)) + job_template.labels.add(label) assert access.can_read(label) From 4f2ce901374dd0a3b9a6226ce12b97fc557bfaa4 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 9 Jul 2020 11:13:37 -0400 Subject: [PATCH 41/53] Include instance_id in host edit request --- .../inventories/related/hosts/edit/host-edit.controller.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js index 098a6113a2..e54127f176 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js @@ -26,6 +26,9 @@ description: $scope.description, enabled: $scope.host.enabled }; + if (typeof $scope.host.instance_id !== 'undefined') { + host.instance_id = $scope.host.instance_id; + } HostsService.put(host).then(function(){ $state.go('.', null, {reload: true}); }); From 1edae24644ad40009d47a859df89e4d0d3e4a9d6 Mon Sep 17 00:00:00 2001 From: Gabe Muniz Date: Mon, 13 Jul 2020 14:53:34 -0400 Subject: [PATCH 42/53] fixed broken UI links --- .../inventories/related/sources/sources.form.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index 2dda6bf73d..bc939e3365 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -215,7 +215,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ dataTitle: i18n._("Source Variables"), dataPlacement: 'right', awPopOver: "

" + i18n._("Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables ") + - "" + + "" + i18n._("view ec2.ini in the community.aws repo.") + "

" + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + i18n._("JSON:") + "
\n" + @@ -239,7 +239,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ dataTitle: i18n._("Source Variables"), dataPlacement: 'right', awPopOver: "

" + i18n._("Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables ") + - "" + + "" + i18n._("view vmware_inventory.ini in the vmware community repo.") + "

" + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + i18n._("JSON:") + "
\n" + @@ -280,7 +280,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ dataTitle: i18n._("Source Variables"), dataPlacement: 'right', awPopOver: i18n._(`Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration - + view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`), dataContainer: 'body', subForm: 'sourceSubForm' @@ -297,7 +297,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ dataTitle: i18n._("Source Variables"), dataPlacement: 'right', awPopOver: i18n._(`Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration - + view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`), dataContainer: 'body', subForm: 'sourceSubForm' @@ -314,7 +314,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ dataTitle: i18n._("Source Variables"), dataPlacement: 'right', awPopOver: "

" + i18n._("Override variables found in azure_rm.ini and used by the inventory update script. For a detailed description of these variables ") + - "" + + "" + i18n._("view azure_rm.ini in the Ansible community.general github repo.") + "

" + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + i18n._("JSON:") + "
\n" + From 310a0f88e54796793886f8a47f6a7146c93fa92f Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 27 May 2020 16:03:05 -0400 Subject: [PATCH 43/53] remove the usage of create_temporary_fifo from credential plugins this resolves an issue that causes an endless hang on with Cyberark AIM lookups when a certificate *and* key are specified the underlying issue here is that we can't rely on the underyling Python ssl implementation to *only* read from the fifo that stores the pem data *only once*; in reality, we need to just use *actual* tempfiles for stability purposes see: https://github.com/ansible/awx/issues/6986 see: https://github.com/urllib3/urllib3/issues/1880 --- awx/main/credential_plugins/aim.py | 29 +++++----------- awx/main/credential_plugins/conjur.py | 27 ++++++--------- awx/main/credential_plugins/hashivault.py | 20 +++++------ awx/main/credential_plugins/plugin.py | 42 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index c63181ed46..5853f8305f 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -1,15 +1,10 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles from urllib.parse import quote, urlencode, urljoin from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - aim_inputs = { 'fields': [{ 'id': 'url', @@ -81,22 +76,14 @@ def aim_backend(**kwargs): request_qs = '?' + urlencode(query_params, quote_via=quote) request_url = urljoin(url, '/'.join(['AIMWebService', 'api', 'Accounts'])) - cert = None - if client_cert and client_key: - cert = ( - create_temporary_fifo(client_cert.encode()), - create_temporary_fifo(client_key.encode()) + with CertFiles(client_cert, client_key) as cert: + res = requests.get( + request_url + request_qs, + timeout=30, + cert=cert, + verify=verify, + allow_redirects=False, ) - elif client_cert: - cert = create_temporary_fifo(client_cert.encode()) - - res = requests.get( - request_url + request_qs, - timeout=30, - cert=cert, - verify=verify, - allow_redirects=False, - ) res.raise_for_status() return res.json()['Content'] diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index a851277134..286600dd1d 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -1,4 +1,4 @@ -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import base64 from urllib.parse import urljoin, quote_plus @@ -6,11 +6,6 @@ from urllib.parse import urljoin, quote_plus from django.utils.translation import ugettext_lazy as _ import requests -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - conjur_inputs = { 'fields': [{ @@ -66,14 +61,14 @@ def conjur_backend(**kwargs): 'data': api_key, 'allow_redirects': False, } - if cacert: - auth_kwargs['verify'] = create_temporary_fifo(cacert.encode()) - # https://www.conjur.org/api.html#authentication-authenticate-post - resp = requests.post( - urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), - **auth_kwargs - ) + with CertFiles(cacert) as cert: + # https://www.conjur.org/api.html#authentication-authenticate-post + auth_kwargs['verify'] = cert + resp = requests.post( + urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), + **auth_kwargs + ) resp.raise_for_status() token = base64.b64encode(resp.content).decode('utf-8') @@ -81,8 +76,6 @@ def conjur_backend(**kwargs): 'headers': {'Authorization': 'Token token="{}"'.format(token)}, 'allow_redirects': False, } - if cacert: - lookup_kwargs['verify'] = create_temporary_fifo(cacert.encode()) # https://www.conjur.org/api.html#secrets-retrieve-a-secret-get path = urljoin(url, '/'.join([ @@ -94,7 +87,9 @@ def conjur_backend(**kwargs): if version: path = '?'.join([path, version]) - resp = requests.get(path, timeout=30, **lookup_kwargs) + with CertFiles(cacert) as cert: + lookup_kwargs['verify'] = cert + resp = requests.get(path, timeout=30, **lookup_kwargs) resp.raise_for_status() return resp.text diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index c094f747d9..47ee73e6ad 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -3,16 +3,11 @@ import os import pathlib from urllib.parse import urljoin -from .plugin import CredentialPlugin +from .plugin import CredentialPlugin, CertFiles import requests from django.utils.translation import ugettext_lazy as _ -# AWX -from awx.main.utils import ( - create_temporary_fifo, -) - base_inputs = { 'fields': [{ 'id': 'url', @@ -101,8 +96,6 @@ def kv_backend(**kwargs): 'timeout': 30, 'allow_redirects': False, } - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) sess = requests.Session() sess.headers['Authorization'] = 'Bearer {}'.format(token) @@ -129,7 +122,9 @@ def kv_backend(**kwargs): path_segments = [secret_path] request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/') - response = sess.get(request_url, **request_kwargs) + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + response = sess.get(request_url, **request_kwargs) response.raise_for_status() json = response.json() @@ -157,8 +152,6 @@ def ssh_backend(**kwargs): 'timeout': 30, 'allow_redirects': False, } - if cacert: - request_kwargs['verify'] = create_temporary_fifo(cacert.encode()) request_kwargs['json'] = {'public_key': kwargs['public_key']} if kwargs.get('valid_principals'): @@ -170,7 +163,10 @@ def ssh_backend(**kwargs): sess.headers['X-Vault-Token'] = token # https://www.vaultproject.io/api/secret/ssh/index.html#sign-ssh-key request_url = '/'.join([url, secret_path, 'sign', role]).rstrip('/') - resp = sess.post(request_url, **request_kwargs) + + with CertFiles(cacert) as cert: + request_kwargs['verify'] = cert + resp = sess.post(request_url, **request_kwargs) resp.raise_for_status() return resp.json()['data']['signed_key'] diff --git a/awx/main/credential_plugins/plugin.py b/awx/main/credential_plugins/plugin.py index c5edde7bc1..def2676a02 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/awx/main/credential_plugins/plugin.py @@ -1,3 +1,45 @@ +import os +import tempfile + from collections import namedtuple CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend']) + + +class CertFiles(): + """ + A context manager used for writing a certificate and (optional) key + to $TMPDIR, and cleaning up afterwards. + + This is particularly useful as a shared resource for credential plugins + that want to pull cert/key data out of the database and persist it + temporarily to the file system so that it can loaded into the openssl + certificate chain (generally, for HTTPS requests plugins make via the + Python requests library) + + with CertFiles(cert_data, key_data) as cert: + # cert is string representing a path to the cert or pemfile + # temporarily written to disk + requests.post(..., cert=cert) + """ + + certfile = None + + def __init__(self, cert, key=None): + self.cert = cert + self.key = key + + def __enter__(self): + if not self.cert: + return None + self.certfile = tempfile.NamedTemporaryFile('wb', delete=False) + self.certfile.write(self.cert.encode()) + if self.key: + self.certfile.write(b'\n') + self.certfile.write(self.key.encode()) + self.certfile.flush() + return str(self.certfile.name) + + def __exit__(self, *args): + if self.certfile and os.path.exists(self.certfile.name): + os.remove(self.certfile.name) From 0e8f30a4a2705b80ab60f52b79055bb8439e7414 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 21 Jul 2020 13:03:24 -0400 Subject: [PATCH 44/53] Upgrade community.vmware for better error surfacing --- requirements/collections_requirements.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/collections_requirements.yml b/requirements/collections_requirements.yml index 9e9634b7b9..2eb5263d80 100644 --- a/requirements/collections_requirements.yml +++ b/requirements/collections_requirements.yml @@ -13,6 +13,6 @@ collections: - name: openstack.cloud version: 0.0.1-dev85 # earlier had checksum mismatch - name: community.vmware - version: 0.4.0 # first to contain necessary grouping and filtering features + version: 1.0.0 - name: ovirt.ovirt version: 1.0.0 # contains naming fix, was originally named ovirt.ovirt_collection From 860183f178be99e3041ffbdd9fc72bea09bcddfb Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 17 Jul 2020 14:34:53 -0400 Subject: [PATCH 45/53] update the named URL code to properly return 404 vs 403 --- awx/api/generics.py | 24 +++++++++++++++++++++ awx/api/views/__init__.py | 9 ++++++++ awx/main/middleware.py | 19 ++++++++++++++-- awx/main/tests/functional/test_named_url.py | 24 +++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 83780ad03c..1476de1043 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -51,6 +51,7 @@ from awx.main.utils import ( StubLicense ) from awx.main.utils.db import get_all_field_names +from awx.main.views import ApiErrorView from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer from awx.api.versioning import URLPathVersioning from awx.api.metadata import SublistAttachDetatchMetadata, Metadata @@ -188,6 +189,29 @@ class APIView(views.APIView): ''' Log warning for 400 requests. Add header with elapsed time. ''' + + # + # If the URL was rewritten, and we get a 404, we should entirely + # replace the view in the request context with an ApiErrorView() + # Without this change, there will be subtle differences in the BrowseableAPIRenderer + # + # These differences could provide contextual clues which would allow + # anonymous users to determine if usernames were valid or not + # (e.g., if an anonymous user visited `/api/v2/users/valid/`, and got a 404, + # but also saw that the page heading said "User Detail", they might notice + # that's a difference in behavior from a request to `/api/v2/users/not-valid/`, which + # would show a page header of "Not Found"). Changing the view here + # guarantees that the rendered response will look exactly like the response + # when you visit a URL that has no matching URL paths in `awx.api.urls`. + # + if response.status_code == 404 and 'awx.named_url_rewritten' in request.environ: + self.headers.pop('Allow', None) + response = super(APIView, self).finalize_response(request, response, *args, **kwargs) + view = ApiErrorView() + setattr(view, 'request', request) + response.renderer_context['view'] = view + return response + if response.status_code >= 400: status_msg = "status %s received by user %s attempting to access %s from %s" % \ (response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None)) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index a0a39fa1e1..6d864c408b 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -171,6 +171,15 @@ def api_exception_handler(exc, context): exc = ParseError(exc.args[0]) if isinstance(context['view'], UnifiedJobStdout): context['view'].renderer_classes = [renderers.BrowsableAPIRenderer, JSONRenderer] + if isinstance(exc, APIException): + req = context['request']._request + if 'awx.named_url_rewritten' in req.environ and not str(getattr(exc, 'status_code', 0)).startswith('2'): + # if the URL was rewritten, and it's not a 2xx level status code, + # revert the request.path to its original value to avoid leaking + # any context about the existance of resources + req.path = req.environ['awx.named_url_rewritten'] + if exc.status_code == 403: + exc = NotFound(detail=_('Not found.')) return exception_handler(exc, context) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 112ae17aa5..781266e8dd 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -14,7 +14,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.db.migrations.executor import MigrationExecutor from django.db import connection -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import redirect from django.apps import apps from django.utils.deprecation import MiddlewareMixin from django.utils.translation import ugettext_lazy as _ @@ -148,7 +148,21 @@ class URLModificationMiddleware(MiddlewareMixin): def _named_url_to_pk(cls, node, resource, named_url): kwargs = {} if node.populate_named_url_query_kwargs(kwargs, named_url): - return str(get_object_or_404(node.model, **kwargs).pk) + match = node.model.objects.filter(**kwargs).first() + if match: + return str(match.pk) + else: + # if the name does *not* resolve to any actual resource, + # we should still attempt to route it through so that 401s are + # respected + # using "zero" here will cause the URL regex to match e.g., + # /api/v2/users//, but it also means that anonymous + # users will go down the path of having their credentials + # verified; in this way, *anonymous* users will that visit + # /api/v2/users/invalid-username/ *won't* see a 404, they'll + # see a 401 as if they'd gone to /api/v2/users/0/ + # + return '0' if resource == 'job_templates' and '++' not in named_url: # special case for deprecated job template case # will not raise a 404 on its own @@ -178,6 +192,7 @@ class URLModificationMiddleware(MiddlewareMixin): old_path = request.path_info new_path = self._convert_named_url(old_path) if request.path_info != new_path: + request.environ['awx.named_url_rewritten'] = request.path request.path = request.path.replace(request.path_info, new_path) request.path_info = new_path diff --git a/awx/main/tests/functional/test_named_url.py b/awx/main/tests/functional/test_named_url.py index dcf2111992..6482dac3a8 100644 --- a/awx/main/tests/functional/test_named_url.py +++ b/awx/main/tests/functional/test_named_url.py @@ -219,3 +219,27 @@ def test_credential(get, admin_user, credentialtype_ssh): url = reverse('api:credential_detail', kwargs={'pk': test_cred.pk}) response = get(url, user=admin_user, expect=200) assert response.data['related']['named_url'].endswith('/test_cred++Machine+ssh++/') + + +@pytest.mark.django_db +def test_403_vs_404(get): + cindy = User.objects.create( + username='cindy', + password='test_user', + is_superuser=False + ) + bob = User.objects.create( + username='bob', + password='test_user', + is_superuser=False + ) + + # bob cannot see cindy, pk lookup should be a 403 + url = reverse('api:user_detail', kwargs={'pk': cindy.pk}) + get(url, user=bob, expect=403) + + # bob cannot see cindy, username lookup should be a 404 + get('/api/v2/users/cindy/', user=bob, expect=404) + + get(f'/api/v2/users/{cindy.pk}/', expect=401) + get('/api/v2/users/cindy/', expect=404) From 6b82ae46bce33f0912822cf27950ec4370bafed3 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 4 Jun 2020 14:19:46 -0400 Subject: [PATCH 46/53] Force worker processes to have a different signal handler from the parent Situations have come up where the 5+ minute kill signal for run_task_manager is emitted to the worker process running it, but since the worker improperly inherited the AWXConsumerBase().stop() handler a deadlock ultimately was triggered on the database connection. --- awx/main/dispatch/worker/base.py | 1 + awx/main/scheduler/task_manager.py | 1 + 2 files changed, 2 insertions(+) diff --git a/awx/main/dispatch/worker/base.py b/awx/main/dispatch/worker/base.py index b0611676fa..7001cd9bb9 100644 --- a/awx/main/dispatch/worker/base.py +++ b/awx/main/dispatch/worker/base.py @@ -35,6 +35,7 @@ class WorkerSignalHandler: def __init__(self): self.kill_now = False + signal.signal(signal.SIGTERM, signal.SIG_DFL) signal.signal(signal.SIGINT, self.exit_gracefully) def exit_gracefully(self, *args, **kwargs): diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 97a429a415..bc6de6bd8f 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -581,3 +581,4 @@ class TaskManager(): logger.debug("Starting Scheduler") with task_manager_bulk_reschedule(): self._schedule() + logger.debug("Finishing Scheduler") From f29e7b9c810829c93bd1e067de3273c8c53d0e24 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 23 Jul 2020 14:50:35 -0400 Subject: [PATCH 47/53] properly report 30x errors on credential plugin tests --- awx/main/credential_plugins/aim.py | 4 ++-- awx/main/credential_plugins/conjur.py | 6 +++--- awx/main/credential_plugins/hashivault.py | 6 +++--- awx/main/credential_plugins/plugin.py | 10 ++++++++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/awx/main/credential_plugins/aim.py b/awx/main/credential_plugins/aim.py index 5853f8305f..7c99665bf0 100644 --- a/awx/main/credential_plugins/aim.py +++ b/awx/main/credential_plugins/aim.py @@ -1,4 +1,4 @@ -from .plugin import CredentialPlugin, CertFiles +from .plugin import CredentialPlugin, CertFiles, raise_for_status from urllib.parse import quote, urlencode, urljoin @@ -84,7 +84,7 @@ def aim_backend(**kwargs): verify=verify, allow_redirects=False, ) - res.raise_for_status() + raise_for_status(res) return res.json()['Content'] diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index 286600dd1d..59bc0fbaef 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -1,4 +1,4 @@ -from .plugin import CredentialPlugin, CertFiles +from .plugin import CredentialPlugin, CertFiles, raise_for_status import base64 from urllib.parse import urljoin, quote_plus @@ -69,7 +69,7 @@ def conjur_backend(**kwargs): urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), **auth_kwargs ) - resp.raise_for_status() + raise_for_status(resp) token = base64.b64encode(resp.content).decode('utf-8') lookup_kwargs = { @@ -90,7 +90,7 @@ def conjur_backend(**kwargs): with CertFiles(cacert) as cert: lookup_kwargs['verify'] = cert resp = requests.get(path, timeout=30, **lookup_kwargs) - resp.raise_for_status() + raise_for_status(resp) return resp.text diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index 47ee73e6ad..2658607a8e 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -3,7 +3,7 @@ import os import pathlib from urllib.parse import urljoin -from .plugin import CredentialPlugin, CertFiles +from .plugin import CredentialPlugin, CertFiles, raise_for_status import requests from django.utils.translation import ugettext_lazy as _ @@ -125,7 +125,7 @@ def kv_backend(**kwargs): with CertFiles(cacert) as cert: request_kwargs['verify'] = cert response = sess.get(request_url, **request_kwargs) - response.raise_for_status() + raise_for_status(response) json = response.json() if api_version == 'v2': @@ -168,7 +168,7 @@ def ssh_backend(**kwargs): request_kwargs['verify'] = cert resp = sess.post(request_url, **request_kwargs) - resp.raise_for_status() + raise_for_status(resp) return resp.json()['data']['signed_key'] diff --git a/awx/main/credential_plugins/plugin.py b/awx/main/credential_plugins/plugin.py index def2676a02..fa5c770fd1 100644 --- a/awx/main/credential_plugins/plugin.py +++ b/awx/main/credential_plugins/plugin.py @@ -3,9 +3,19 @@ import tempfile from collections import namedtuple +from requests.exceptions import HTTPError + CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend']) +def raise_for_status(resp): + resp.raise_for_status() + if resp.status_code >= 300: + exc = HTTPError() + setattr(exc, 'response', resp) + raise exc + + class CertFiles(): """ A context manager used for writing a certificate and (optional) key From 1c08206792d57ca4136206856a0ecd08e16f42e4 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 28 Jul 2020 11:16:52 -0400 Subject: [PATCH 48/53] pin pytest-forked to fix broken unit tests --- requirements/requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 8a2e1cee66..745cf546e5 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -10,6 +10,7 @@ pyflakes==2.2.0 pytest==3.6.0 pytest-cov==2.8.1 pytest-django==3.9.0 +pytest-forked==1.2.0 # https://github.com/pytest-dev/pytest-forked/commit/664645adb28cb74c6b4caea6869842e60df82667 pytest-pythonpath pytest-mock==1.11.1 pytest-timeout==1.3.4 From 91594a1ae8c7a4fd074826c2a1b1e3f9279ceb03 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 28 Jul 2020 10:21:24 -0400 Subject: [PATCH 49/53] properly obfuscate connection errors for credential lookup failure --- awx/api/views/__init__.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 6d864c408b..4023f5f89b 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -14,6 +14,8 @@ import time from base64 import b64encode from collections import OrderedDict, Iterable +from urllib3.exceptions import ConnectTimeoutError + # Django from django.conf import settings @@ -1409,7 +1411,15 @@ class CredentialExternalTest(SubDetailAPIView): message = 'HTTP {}'.format(exc.response.status_code) return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST) except Exception as exc: - return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + message = exc.__class__.__name__ + args = getattr(exc, 'args', []) + for a in args: + if isinstance( + getattr(a, 'reason', None), + ConnectTimeoutError + ): + message = str(a.reason) + return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST) class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): @@ -1458,10 +1468,18 @@ class CredentialTypeExternalTest(SubDetailAPIView): obj.plugin.backend(**backend_kwargs) return Response({}, status=status.HTTP_202_ACCEPTED) except requests.exceptions.HTTPError as exc: - message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text) + message = 'HTTP {}'.format(exc.response.status_code) return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST) except Exception as exc: - return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + message = exc.__class__.__name__ + args = getattr(exc, 'args', []) + for a in args: + if isinstance( + getattr(a, 'reason', None), + ConnectTimeoutError + ): + message = str(a.reason) + return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST) class HostRelatedSearchMixin(object): From c6eb8cf59be420740d561e823ab3210b209c3d53 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Fri, 29 May 2020 15:56:47 -0400 Subject: [PATCH 50/53] Cache downloaded roles & collections Populate the cache the first time the job is run for a revision that needs them, and for future runs for that revision just copy it into the private directory. Delete the cache on project deletion. Invalidate the cache on a new project revision Also download roles/collections during the sync job Since we're writing into a per-revision cache, we can do this easily now. Don't try and install content if there aren't any requirements expecting it Adjust pathing to the proper location. Force install if doing a manual sync. Requirements may be unversioned. Remove the cache when delete-on-update is set Integrate content caching with existing task logic Revert the --force flags use the update id as metric for role caching Shift the movement of cache to job folder from rsync task to python Only install roles and collections if needed Deal with roles and collections for jobs without sync Skip local copy if roles or collections turned off update docs for content caching Design pivot - use empty cache dir to indicate lack of content Do not cache content if we did not install content Test changes to allay concerns about reliability of local_path Do not blow away cache for SCM inventory updates Remove project update vars no longer used Remove job pre-creation of content folders code style edit, always use cache_id as property in tasks Fix log message --- awx/main/models/projects.py | 36 +++- awx/main/tasks.py | 171 +++++++++++------- awx/main/tests/functional/api/test_project.py | 8 +- awx/main/tests/functional/conftest.py | 1 - .../tests/functional/models/test_inventory.py | 3 +- .../tests/functional/models/test_project.py | 12 ++ awx/main/tests/functional/test_projects.py | 4 +- awx/main/tests/functional/test_tasks.py | 4 +- awx/main/tests/unit/test_tasks.py | 9 +- awx/playbooks/project_update.yml | 19 +- docs/collections.md | 42 ++++- 11 files changed, 213 insertions(+), 96 deletions(-) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 0207fec97b..81bfa92fae 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -194,6 +194,11 @@ class ProjectOptions(models.Model): if not check_if_exists or os.path.exists(smart_str(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 def playbooks(self): results = [] @@ -418,6 +423,10 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn return True return False + @property + def cache_id(self): + return str(self.last_job_id) + @property def notification_templates(self): base_notification_templates = NotificationTemplate.objects @@ -455,11 +464,12 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn ) 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) - if self.scm_type and path_to_delete: # non-manual, concrete path - from awx.main.tasks import delete_project_files - delete_project_files.delay(path_to_delete) + for path_to_delete in paths_to_delete: + if self.scm_type and path_to_delete: # non-manual, concrete path + from awx.main.tasks import delete_project_files + delete_project_files.delay(path_to_delete) return r @@ -554,6 +564,19 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage def result_stdout_raw(self): 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): 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): added_update_fields = [] if not self.job_tags: - job_tags = ['update_{}'.format(self.scm_type)] - if self.job_type == 'run': - job_tags.append('install_roles') - job_tags.append('install_collections') + job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections'] self.job_tags = ','.join(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': diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 09014d9da6..1aee0b2bdd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1867,44 +1867,31 @@ class RunJob(BaseTask): project_path = job.project.get_project_path(check_if_exists=False) job_revision = job.project.scm_revision 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: pass # manual projects are not synced, user has responsibility for that elif not os.path.exists(project_path): logger.debug('Performing fresh clone of {} on this instance.'.format(job.project)) - sync_needs = all_sync_needs - elif not job.project.scm_revision: - logger.debug('Revision not known for {}, will sync with remote'.format(job.project)) - sync_needs = all_sync_needs - elif job.project.scm_type == 'git': + sync_needs.append(source_update_tag) + elif job.project.scm_type == 'git' and job.project.scm_revision and (not branch_override): git_repo = git.Repo(project_path) try: - desired_revision = job.project.scm_revision - 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 + if job_revision == git_repo.head.commit.hexsha: logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format)) else: - sync_needs = all_sync_needs + sync_needs.append(source_update_tag) except (ValueError, BadGitName): 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: - sync_needs = all_sync_needs - # Galaxy requirements are not supported for manual projects - 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') + logger.debug('Project not available locally, {} will sync with remote'.format(job.log_format)) + sync_needs.append(source_update_tag) - galaxy_collections_req_path = os.path.join(project_path, 'collections', 'requirements.yml') - if os.path.exists(galaxy_collections_req_path): - logger.debug('Running project sync for {} because of galaxy collections requirements.'.format(job.log_format)) - sync_needs.append('install_collections') + has_cache = os.path.exists(os.path.join(job.project.get_cache_path(), job.project.cache_id)) + # Galaxy requirements are not supported for manual projects + if job.project.scm_type and ((not has_cache) or branch_override): + sync_needs.extend(['install_roles', 'install_collections']) if sync_needs: pu_ig = job.instance_group @@ -1922,7 +1909,7 @@ class RunJob(BaseTask): execution_node=pu_en, 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 if 'update_' not in sync_metafields['job_tags']: sync_metafields['scm_revision'] = job_revision @@ -1954,10 +1941,7 @@ class RunJob(BaseTask): if job_revision: job = self.update_model(job.pk, scm_revision=job_revision) # Project update does not copy the folder, so copy here - RunProjectUpdate.make_local_copy( - project_path, os.path.join(private_data_dir, 'project'), - job.project.scm_type, job_revision - ) + RunProjectUpdate.make_local_copy(job.project, private_data_dir, scm_revision=job_revision) if job.inventory.kind == 'smart': # cache smart inventory memberships so that the host_filter query is not @@ -1997,10 +1981,7 @@ class RunProjectUpdate(BaseTask): @property def proot_show_paths(self): - show_paths = [settings.PROJECTS_ROOT] - if self.job_private_data_dir: - show_paths.append(self.job_private_data_dir) - return show_paths + return [settings.PROJECTS_ROOT] def __init__(self, *args, job_private_data_dir=None, **kwargs): super(RunProjectUpdate, self).__init__(*args, **kwargs) @@ -2034,12 +2015,6 @@ class RunProjectUpdate(BaseTask): credential = project_update.credential if credential.has_input('ssh_key_data'): 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 def build_passwords(self, project_update, runtime_passwords): @@ -2167,13 +2142,17 @@ class RunProjectUpdate(BaseTask): extra_vars.update(extra_vars_new) 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 branch_override): - scm_branch = project_update.project.scm_revision + if project_update.job_type == 'run' and (not project_update.branch_override): + if project_update.project.scm_revision: + scm_branch = project_update.project.scm_revision + elif not scm_branch: + raise RuntimeError('Could not determine a revision to run from project.') elif not scm_branch: scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD') 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, 'awx_license_type': get_license(show_key=False).get('license_type', 'UNLICENSED'), 'awx_version': get_awx_version(), @@ -2183,9 +2162,6 @@ class RunProjectUpdate(BaseTask): 'roles_enabled': settings.AWX_ROLES_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 if project_update.scm_refspec: extra_vars['scm_refspec'] = project_update.scm_refspec @@ -2317,8 +2293,7 @@ class RunProjectUpdate(BaseTask): os.mkdir(settings.PROJECTS_ROOT) self.acquire_lock(instance) self.original_branch = None - if (instance.scm_type == 'git' and instance.job_type == 'run' and instance.project and - instance.scm_branch != instance.project.scm_branch): + if instance.scm_type == 'git' and instance.branch_override: project_path = instance.project.get_project_path(check_if_exists=False) if os.path.exists(project_path): git_repo = git.Repo(project_path) @@ -2327,17 +2302,48 @@ class RunProjectUpdate(BaseTask): else: 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 - def make_local_copy(project_path, destination_folder, scm_type, scm_revision): - if scm_type == 'git': + def clear_project_cache(cache_dir, keep_value): + 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) if not os.path.exists(destination_folder): os.mkdir(destination_folder, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) tmp_branch_name = 'awx_internal/{}'.format(uuid4()) # 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.') - 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 source_as_uri = Path(project_path).as_uri() git.Repo.clone_from( @@ -2356,19 +2362,48 @@ class RunProjectUpdate(BaseTask): else: 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): # To avoid hangs, very important to release lock even if errors happen here try: if self.playbook_new_revision: instance.scm_revision = self.playbook_new_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: # copy project folder before resetting to default branch # because some git-tree-specific resources (like submodules) might matter - self.make_local_copy( - instance.get_project_path(check_if_exists=False), os.path.join(self.job_private_data_dir, 'project'), - instance.scm_type, instance.scm_revision - ) + self.make_local_copy(instance, self.job_private_data_dir) if self.original_branch: # for git project syncs, non-default branches can be problems # restore to branch the repo was on before this run @@ -2634,13 +2669,21 @@ class RunInventoryUpdate(BaseTask): source_project = None if inventory_update.inventory_source: source_project = inventory_update.inventory_source.source_project - if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and source_project): - # In project sync, pulling galaxy roles is not needed + if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and + 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( _eager_fields=dict( launch_type="sync", 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', execution_node=inventory_update.execution_node, instance_group = inventory_update.instance_group, @@ -2664,11 +2707,7 @@ class RunInventoryUpdate(BaseTask): raise elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project: # This follows update, not sync, so make copy here - project_path = source_project.get_project_path(check_if_exists=False) - RunProjectUpdate.make_local_copy( - project_path, os.path.join(private_data_dir, 'project'), - source_project.scm_type, source_project.scm_revision - ) + RunProjectUpdate.make_local_copy(source_project, private_data_dir) @task(queue=get_local_queuename) diff --git a/awx/main/tests/functional/api/test_project.py b/awx/main/tests/functional/api/test_project.py index af46363557..09fed17c67 100644 --- a/awx/main/tests/functional/api/test_project.py +++ b/awx/main/tests/functional/api/test_project.py @@ -54,7 +54,9 @@ def test_no_changing_overwrite_behavior_if_used(post, patch, organization, admin data={ 'name': 'fooo', 'organization': organization.id, - 'allow_override': True + 'allow_override': True, + 'scm_type': 'git', + 'scm_url': 'https://github.com/ansible/test-playbooks.git' }, user=admin_user, expect=201 @@ -83,7 +85,9 @@ def test_changing_overwrite_behavior_okay_if_not_used(post, patch, organization, data={ 'name': 'fooo', 'organization': organization.id, - 'allow_override': True + 'allow_override': True, + 'scm_type': 'git', + 'scm_url': 'https://github.com/ansible/test-playbooks.git' }, user=admin_user, expect=201 diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index f6accff877..7111950003 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -145,7 +145,6 @@ def project(instance, organization): description="test-proj-desc", organization=organization, playbook_files=['helloworld.yml', 'alt-helloworld.yml'], - local_path='_92__test_proj', scm_revision='1234567890123456789012345678901234567890', scm_url='localhost', scm_type='git' diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index dc9d2bf164..b0d7f7533f 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -170,7 +170,8 @@ class TestSCMUpdateFeatures: inventory_update = InventoryUpdate( inventory_source=scm_inventory_source, 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): # Changing the non-sensitive fields should not trigger update diff --git a/awx/main/tests/functional/models/test_project.py b/awx/main/tests/functional/models/test_project.py index 3f57691ac3..2cf43c5690 100644 --- a/awx/main/tests/functional/models/test_project.py +++ b/awx/main/tests/functional/models/test_project.py @@ -34,6 +34,18 @@ def test_sensitive_change_triggers_update(project): 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 def test_foreign_key_change_changes_modified_by(project, organization): assert project._get_fields_snapshot()['organization_id'] == organization.id diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index ef4b59630d..ccfbd06627 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -29,8 +29,8 @@ def team_project_list(organization_factory): @pytest.mark.django_db def test_get_project_path(project): # Test combining projects root with project local path - with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/awx'): - assert project.get_project_path(check_if_exists=False) == '/var/lib/awx/_92__test_proj' + with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/foo'): + assert project.get_project_path(check_if_exists=False) == f'/var/lib/foo/_{project.id}__test_proj' @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_tasks.py b/awx/main/tests/functional/test_tasks.py index f1cf382a7c..c7bc50c8d2 100644 --- a/awx/main/tests/functional/test_tasks.py +++ b/awx/main/tests/functional/test_tasks.py @@ -30,7 +30,7 @@ class TestDependentInventoryUpdate: def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file): task = RunProjectUpdate() 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, 'release_lock'): 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): task = RunProjectUpdate() 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, 'release_lock'): task.post_run_hook(proj_update, 'successful') diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index e7e208d9a3..6b0c1a6d82 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -61,7 +61,10 @@ def patch_Job(): @pytest.fixture 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 @@ -361,7 +364,9 @@ class TestExtraVarSanitation(TestJobExecution): class TestGenericRun(): 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() task = tasks.RunJob() diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index fc791069a9..d9a11ca0ac 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -1,6 +1,9 @@ --- # 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 # insights_url: Insights service URL (from configuration) # scm_branch: branch/tag/revision (HEAD if unset) @@ -11,8 +14,6 @@ # scm_refspec: a refspec to fetch in addition to obtaining version # roles_enabled: Value of the global setting to enable roles 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_license_type: "open" for AWX; else presume Tower @@ -136,7 +137,10 @@ register: doesRequirementsExist - name: fetch galaxy roles from requirements.yml - command: ansible-galaxy install -r roles/requirements.yml -p {{roles_destination|quote}}{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }} + command: > + ansible-galaxy install -r roles/requirements.yml + --roles-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_roles + {{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }} args: chdir: "{{project_path|quote}}" register: galaxy_result @@ -157,7 +161,10 @@ register: doesCollectionRequirementsExist - 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: chdir: "{{project_path|quote}}" register: galaxy_collection_result @@ -165,7 +172,7 @@ changed_when: "'Installing ' in galaxy_collection_result.stdout" environment: 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" when: diff --git a/docs/collections.md b/docs/collections.md index 5cbf3eab7a..68c11950f8 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -4,15 +4,18 @@ AWX supports the use of Ansible Collections. This section will give ways to use ### Project Collections Requirements -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 -before a job run. The invocation looks like: +If you specify a collections requirements file in SCM at `collections/requirements.yml`, +then AWX will install collections from that file to a special cache folder in project updates. +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 /requirements_collections +ansible-galaxy collection install -r requirements.yml -p /requirements_collections ``` -Example of the resultant `tmp` directory where job is running: +Example of the resultant job `tmp` directory where job is running: ``` ├── project @@ -20,7 +23,7 @@ Example of the resultant `tmp` directory where job is running: │   └── debug.yml ├── requirements_collections │   └── ansible_collections -│   └── username +│   └── collection_namespace │   └── collection_name │   ├── FILES.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 Ansible core default settings will download collections from the public From daaa0c8efedd10d98815bea7bc52798ef09b938a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 27 Jul 2020 13:37:09 -0400 Subject: [PATCH 51/53] Avoid using long name of option not in 2.8 --- awx/playbooks/project_update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index d9a11ca0ac..621966c23e 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -163,7 +163,7 @@ - name: fetch galaxy collections from collections/requirements.yml command: > ansible-galaxy collection install -r collections/requirements.yml - --collections-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections + -p {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections {{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }} args: chdir: "{{project_path|quote}}" From b1481ec6e3658bd3f0edc5cdd4c692e6aca8647b Mon Sep 17 00:00:00 2001 From: Christian Adams Date: Tue, 28 Jul 2020 21:23:14 -0400 Subject: [PATCH 52/53] Use quotations when marking strings for translation --- .../related/sources/sources.form.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index bc939e3365..3c76dd2e61 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -262,9 +262,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ parseTypeName: 'envParseType', dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: i18n._(`Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration - - view openstack.yml in the Openstack github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`), + awPopOver: i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration") + + '' + + i18n._("view openstack.yml in the Openstack github repo.") + "" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."), dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -279,9 +279,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ parseTypeName: 'envParseType', dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: i18n._(`Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration - - view cloudforms.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`), + awPopOver: i18n._("Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration") + + '' + + i18n._("view cloudforms.ini in the Ansible Collections github repo.") + "" + i18n._(" Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."), dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -296,9 +296,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ parseTypeName: 'envParseType', dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: i18n._(`Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration - - view foreman.ini in the Ansible Collections github repo. Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`), + awPopOver: i18n._("Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration") + + '' + + i18n._("view foreman.ini in the Ansible Collections github repo.") + "" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."), dataContainer: 'body', subForm: 'sourceSubForm' }, From 6067fc36f65b267aa7751fb9c1832dd793e0cd58 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 5 Aug 2020 14:53:01 -0400 Subject: [PATCH 53/53] begin a 14.0.0 changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaabcfd2e9..33878802aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. +## 14.0.0 (Aug TBD, 2020) +- Fixed https://access.redhat.com/security/cve/cve-2020-14327 - Server-side request forgery on credentials +- Fixed https://access.redhat.com/security/cve/cve-2020-14328 - Server-side request forgery on webhooks +- Fixed https://access.redhat.com/security/cve/cve-2020-14329 - Sensitive data exposure on labels +- Fixed https://access.redhat.com/security/cve/cve-2020-14337 - Named URLs allow for testing the presence or absence of objects + ## 13.0.0 (Jun 23, 2020) - Added import and export commands to the official AWX CLI, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125). - Removed scripts as a means of running inventory updates of built-in types (https://github.com/ansible/awx/pull/6911)