diff --git a/.gitignore b/.gitignore index b1674af811..137c233c19 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ pep8.txt scratch testem.log awx/awx_test.sqlite3-journal +.pytest_cache/ # Mac OS X *.DS_Store diff --git a/Makefile b/Makefile index bc5d29b729..54d0c49be3 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,8 @@ DEV_DOCKER_TAG_BASE ?= gcr.io/ansible-tower-engineering # Comma separated list SRC_ONLY_PKGS ?= cffi,pycparser,psycopg2,twilio +CURWD = $(shell pwd) + # Determine appropriate shasum command UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) @@ -219,7 +221,7 @@ init: if [ "$(AWX_GROUP_QUEUES)" == "tower,thepentagon" ]; then \ $(MANAGEMENT_COMMAND) provision_instance --hostname=isolated; \ $(MANAGEMENT_COMMAND) register_queue --queuename='thepentagon' --hostnames=isolated --controller=tower; \ - $(MANAGEMENT_COMMAND) generate_isolated_key | ssh -o "StrictHostKeyChecking no" root@isolated 'cat > /root/.ssh/authorized_keys'; \ + $(MANAGEMENT_COMMAND) generate_isolated_key | ssh -o "StrictHostKeyChecking no" root@isolated 'cat >> /root/.ssh/authorized_keys'; \ fi; # Refresh development environment after pulling new code. @@ -372,7 +374,7 @@ awx-link: sed -i "s/placeholder/$(shell git describe --long | sed 's/\./\\./g')/" /awx_devel/awx.egg-info/PKG-INFO cp /tmp/awx.egg-link /venv/awx/lib/python2.7/site-packages/awx.egg-link -TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests awx/network_ui/tests/unit +TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests # Run all API unit tests. test: @@ -387,7 +389,7 @@ test_unit: @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit awx/network_ui/tests/unit + py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit test_ansible: @if [ "$(VENV_BASE)" ]; then \ @@ -560,7 +562,7 @@ docker-isolated: TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-isolated-override.yml create docker start tools_awx_1 docker start tools_isolated_1 - echo "__version__ = '`python setup.py --version`'" | docker exec -i tools_isolated_1 /bin/bash -c "cat > /venv/awx/lib/python2.7/site-packages/awx.py" + echo "__version__ = '`git describe --long | cut -d - -f 1-1`'" | docker exec -i tools_isolated_1 /bin/bash -c "cat > /venv/awx/lib/python2.7/site-packages/awx.py" if [ "`docker exec -i -t tools_isolated_1 cat /root/.ssh/authorized_keys`" == "`docker exec -t tools_awx_1 cat /root/.ssh/id_rsa.pub`" ]; then \ echo "SSH keys already copied to isolated instance"; \ else \ @@ -607,6 +609,10 @@ docker-compose-elk: docker-auth docker-compose-cluster-elk: docker-auth TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose-cluster.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate +minishift-dev: + ansible-playbook -i localhost, -e devtree_directory=$(CURWD) tools/clusterdevel/start_minishift_dev.yml + + clean-elk: docker stop tools_kibana_1 docker stop tools_logstash_1 diff --git a/awx/__init__.py b/awx/__init__.py index f107340385..2241a165a0 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -7,11 +7,18 @@ import sys import warnings from pkg_resources import get_distribution -from .celery import app as celery_app # noqa __version__ = get_distribution('awx').version +__all__ = ['__version__'] + + +# Isolated nodes do not have celery installed +try: + from .celery import app as celery_app # noqa + __all__.append('celery_app') +except ImportError: + pass -__all__ = ['__version__', 'celery_app'] # Check for the presence/absence of "devonly" module to determine if running # from a source code checkout or release packaage. diff --git a/awx/api/authentication.py b/awx/api/authentication.py index a09df703cd..0bd7f52f37 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -11,7 +11,7 @@ from django.utils.encoding import smart_text # Django REST Framework from rest_framework import authentication -# Django OAuth Toolkit +# Django-OAuth-Toolkit from oauth2_provider.contrib.rest_framework import OAuth2Authentication logger = logging.getLogger('awx.api.authentication') @@ -25,7 +25,7 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication): ret = super(LoggedBasicAuthentication, self).authenticate(request) if ret: username = ret[0].username if ret[0] else '' - logger.debug(smart_text(u"User {} performed a {} to {} through the API".format(username, request.method, request.path))) + logger.info(smart_text(u"User {} performed a {} to {} through the API".format(username, request.method, request.path))) return ret def authenticate_header(self, request): @@ -39,9 +39,6 @@ class SessionAuthentication(authentication.SessionAuthentication): def authenticate_header(self, request): return 'Session' - def enforce_csrf(self, request): - return None - class LoggedOAuth2Authentication(OAuth2Authentication): @@ -50,8 +47,8 @@ class LoggedOAuth2Authentication(OAuth2Authentication): if ret: user, token = ret username = user.username if user else '' - logger.debug(smart_text( - u"User {} performed a {} to {} through the API using OAuth token {}.".format( + logger.info(smart_text( + u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format( username, request.method, request.path, token.pk ) )) diff --git a/awx/api/conf.py b/awx/api/conf.py index 58aa9b4cc8..ccd1ddf08c 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -47,3 +47,15 @@ register( category=_('Authentication'), category_slug='authentication', ) +register( + 'ALLOW_OAUTH2_FOR_EXTERNAL_USERS', + field_class=fields.BooleanField, + default=False, + label=_('Allow External Users to Create OAuth2 Tokens'), + help_text=_('For security reasons, users from external auth providers (LDAP, SAML, ' + 'SSO, Radius, and others) are not allowed to create OAuth2 tokens. ' + 'To change this behavior, enable this setting. Existing tokens will ' + 'not be deleted when this setting is toggled off.'), + category=_('Authentication'), + category_slug='authentication', +) diff --git a/awx/api/exceptions.py b/awx/api/exceptions.py index 0c67be279b..7c7a182d06 100644 --- a/awx/api/exceptions.py +++ b/awx/api/exceptions.py @@ -12,7 +12,11 @@ class ActiveJobConflict(ValidationError): status_code = 409 def __init__(self, active_jobs): - super(ActiveJobConflict, self).__init__({ + # During APIException.__init__(), Django Rest Framework + # turn everything in self.detail into string by using force_text. + # Declare detail afterwards circumvent this behavior. + super(ActiveJobConflict, self).__init__() + self.detail = { "error": _("Resource is being used by running jobs."), "active_jobs": active_jobs - }) + } diff --git a/awx/api/filters.py b/awx/api/filters.py index 81290c377b..8f883191f3 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -4,6 +4,7 @@ # Python import re import json +from functools import reduce # Django from django.core.exceptions import FieldError, ValidationError @@ -238,7 +239,11 @@ class FieldLookupBackend(BaseFilterBackend): or_filters = [] chain_filters = [] role_filters = [] - search_filters = [] + search_filters = {} + # Can only have two values: 'AND', 'OR' + # If 'AND' is used, an iterm must satisfy all condition to show up in the results. + # If 'OR' is used, an item just need to satisfy one condition to appear in results. + search_filter_relation = 'OR' for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue @@ -262,11 +267,13 @@ class FieldLookupBackend(BaseFilterBackend): # Search across related objects. if key.endswith('__search'): + if values and ',' in values[0]: + search_filter_relation = 'AND' + values = reduce(lambda list1, list2: list1 + list2, [i.split(',') for i in values]) for value in values: search_value, new_keys = self.value_to_python(queryset.model, key, force_text(value)) assert isinstance(new_keys, list) - for new_key in new_keys: - search_filters.append((new_key, search_value)) + search_filters[search_value] = new_keys continue # Custom chain__ and or__ filters, mutually exclusive (both can @@ -355,11 +362,18 @@ class FieldLookupBackend(BaseFilterBackend): else: q |= Q(**{k:v}) args.append(q) - if search_filters: + if search_filters and search_filter_relation == 'OR': q = Q() - for k,v in search_filters: - q |= Q(**{k:v}) + for term, constrains in search_filters.iteritems(): + for constrain in constrains: + q |= Q(**{constrain: term}) args.append(q) + elif search_filters and search_filter_relation == 'AND': + for term, constrains in search_filters.iteritems(): + q_chain = Q() + for constrain in constrains: + q_chain |= Q(**{constrain: term}) + queryset = queryset.filter(q_chain) for n,k,v in chain_filters: if n: q = ~Q(**{k:v}) diff --git a/awx/api/generics.py b/awx/api/generics.py index c62f3cc6dd..f52ed95c46 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -23,14 +23,14 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import views as auth_views # Django REST Framework -from rest_framework.authentication import get_authorization_header -from rest_framework.exceptions import PermissionDenied, AuthenticationFailed, ParseError +from rest_framework.exceptions import PermissionDenied, AuthenticationFailed, ParseError, NotAcceptable, UnsupportedMediaType from rest_framework import generics from rest_framework.response import Response from rest_framework import status from rest_framework import views from rest_framework.permissions import AllowAny -from rest_framework.renderers import JSONRenderer +from rest_framework.renderers import StaticHTMLRenderer, JSONRenderer +from rest_framework.negotiation import DefaultContentNegotiation # cryptography from cryptography.fernet import InvalidToken @@ -64,21 +64,36 @@ analytics_logger = logging.getLogger('awx.analytics.performance') class LoggedLoginView(auth_views.LoginView): + def get(self, request, *args, **kwargs): + # The django.auth.contrib login form doesn't perform the content + # negotiation we've come to expect from DRF; add in code to catch + # situations where Accept != text/html (or */*) and reply with + # an HTTP 406 + try: + DefaultContentNegotiation().select_renderer( + request, + [StaticHTMLRenderer], + 'html' + ) + except NotAcceptable: + resp = Response(status=status.HTTP_406_NOT_ACCEPTABLE) + resp.accepted_renderer = StaticHTMLRenderer() + resp.accepted_media_type = 'text/plain' + resp.renderer_context = {} + return resp + return super(LoggedLoginView, self).get(request, *args, **kwargs) + def post(self, request, *args, **kwargs): - original_user = getattr(request, 'user', None) ret = super(LoggedLoginView, self).post(request, *args, **kwargs) current_user = getattr(request, 'user', None) - - if current_user and getattr(current_user, 'pk', None) and current_user != original_user: - logger.info("User {} logged in.".format(current_user.username)) if request.user.is_authenticated: - logger.info(smart_text(u"User {} logged in".format(self.request.user.username))) + logger.info(smart_text(u"User {} logged in.".format(self.request.user.username))) ret.set_cookie('userLoggedIn', 'true') current_user = UserSerializer(self.request.user) current_user = JSONRenderer().render(current_user.data) current_user = urllib.quote('%s' % current_user, '') ret.set_cookie('current_user', current_user) - + return ret else: ret.status_code = 401 @@ -175,9 +190,13 @@ class APIView(views.APIView): request.drf_request_user = getattr(drf_request, 'user', False) except AuthenticationFailed: request.drf_request_user = None - except ParseError as exc: + except (PermissionDenied, ParseError) as exc: request.drf_request_user = None self.__init_request_error__ = exc + except UnsupportedMediaType as exc: + exc.detail = _('You did not use correct Content-Type in your HTTP request. ' + 'If you are using our REST API, the Content-Type must be application/json') + self.__init_request_error__ = exc return drf_request def finalize_response(self, request, response, *args, **kwargs): @@ -190,6 +209,7 @@ class APIView(views.APIView): if hasattr(self, '__init_request_error__'): response = self.handle_exception(self.__init_request_error__) if response.status_code == 401: + response.data['detail'] += ' To establish a login session, visit /api/login/.' logger.info(status_msg) else: logger.warn(status_msg) @@ -208,26 +228,35 @@ class APIView(views.APIView): return response def get_authenticate_header(self, request): - """ - Determine the WWW-Authenticate header to use for 401 responses. Try to - use the request header as an indication for which authentication method - was attempted. - """ - for authenticator in self.get_authenticators(): - resp_hdr = authenticator.authenticate_header(request) - if not resp_hdr: - continue - req_hdr = get_authorization_header(request) - if not req_hdr: - continue - if resp_hdr.split()[0] and resp_hdr.split()[0] == req_hdr.split()[0]: - return resp_hdr - # If it can't be determined from the request, use the last - # authenticator (should be Basic). - try: - return authenticator.authenticate_header(request) - except NameError: - pass + # HTTP Basic auth is insecure by default, because the basic auth + # backend does not provide CSRF protection. + # + # If you visit `/api/v2/job_templates/` and we return + # `WWW-Authenticate: Basic ...`, your browser will prompt you for an + # HTTP basic auth username+password and will store it _in the browser_ + # for subsequent requests. Because basic auth does not require CSRF + # validation (because it's commonly used with e.g., tower-cli and other + # non-browser clients), browsers that save basic auth in this way are + # vulnerable to cross-site request forgery: + # + # 1. Visit `/api/v2/job_templates/` and specify a user+pass for basic auth. + # 2. Visit a nefarious website and submit a + # `
` + # 3. The browser will use your persisted user+pass and your login + # session is effectively hijacked. + # + # To prevent this, we will _no longer_ send `WWW-Authenticate: Basic ...` + # headers in responses; this means that unauthenticated /api/v2/... requests + # will now return HTTP 401 in-browser, rather than popping up an auth dialog. + # + # This means that people who wish to use the interactive API browser + # must _first_ login in via `/api/login/` to establish a session (which + # _does_ enforce CSRF). + # + # CLI users can _still_ specify basic auth credentials explicitly via + # a header or in the URL e.g., + # `curl https://user:pass@tower.example.org/api/v2/job_templates/N/launch/` + return 'Bearer realm=api authorization_url=/api/o/authorize/' def get_view_description(self, html=False): """ @@ -298,6 +327,12 @@ class APIView(views.APIView): kwargs.pop('version') return super(APIView, self).dispatch(request, *args, **kwargs) + def check_permissions(self, request): + if request.method not in ('GET', 'OPTIONS', 'HEAD'): + if 'write' not in getattr(request.user, 'oauth_scopes', ['write']): + raise PermissionDenied() + return super(APIView, self).check_permissions(request) + class GenericAPIView(generics.GenericAPIView, APIView): # Base class for all model-based views. @@ -726,6 +761,7 @@ class DeleteLastUnattachLabelMixin(object): when the last disassociate is called should inherit from this class. Further, the model should implement is_detached() ''' + def unattach(self, request, *args, **kwargs): (sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request) if res: @@ -801,6 +837,10 @@ class CopyAPIView(GenericAPIView): new_in_330 = True new_in_api_v2 = True + def v1_not_allowed(self): + return Response({'detail': 'Action only possible starting with v2 API.'}, + status=status.HTTP_404_NOT_FOUND) + def _get_copy_return_serializer(self, *args, **kwargs): if not self.copy_return_serializer_class: return self.get_serializer(*args, **kwargs) @@ -885,9 +925,11 @@ class CopyAPIView(GenericAPIView): # not work properly in non-request-response-cycle context. new_obj.created_by = creater new_obj.save() - for m2m in m2m_to_preserve: - for related_obj in m2m_to_preserve[m2m].all(): - getattr(new_obj, m2m).add(related_obj) + from awx.main.signals import disable_activity_stream + with disable_activity_stream(): + for m2m in m2m_to_preserve: + for related_obj in m2m_to_preserve[m2m].all(): + getattr(new_obj, m2m).add(related_obj) if not old_parent: sub_objects = [] for o2m in o2m_to_preserve: @@ -902,13 +944,21 @@ class CopyAPIView(GenericAPIView): return ret def get(self, request, *args, **kwargs): + if get_request_version(request) < 2: + return self.v1_not_allowed() obj = self.get_object() + if not request.user.can_access(obj.__class__, 'read', obj): + raise PermissionDenied() create_kwargs = self._build_create_dict(obj) for key in create_kwargs: create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key] - return Response({'can_copy': request.user.can_access(self.model, 'add', create_kwargs)}) + can_copy = request.user.can_access(self.model, 'add', create_kwargs) and \ + request.user.can_access(self.model, 'copy_related', obj) + return Response({'can_copy': can_copy}) def post(self, request, *args, **kwargs): + if get_request_version(request) < 2: + return self.v1_not_allowed() obj = self.get_object() create_kwargs = self._build_create_dict(obj) create_kwargs_check = {} @@ -916,6 +966,8 @@ class CopyAPIView(GenericAPIView): create_kwargs_check[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key] if not request.user.can_access(self.model, 'add', create_kwargs_check): raise PermissionDenied() + if not request.user.can_access(self.model, 'copy_related', obj): + raise PermissionDenied() serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -937,4 +989,5 @@ class CopyAPIView(GenericAPIView): permission_check_func=permission_check_func ) serializer = self._get_copy_return_serializer(new_obj) - return Response(serializer.data, status=status.HTTP_201_CREATED) + headers = {'Location': new_obj.get_absolute_url(request=request)} + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index e11474f27b..03e597b0d2 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -67,6 +67,8 @@ class Metadata(metadata.SimpleMetadata): if field.field_name == model_field.name: field_info['filterable'] = True break + else: + field_info['filterable'] = False # Indicate if a field has a default value. # FIXME: Still isn't showing all default values? diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3b750ec704..f5d853525c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -13,6 +13,7 @@ from collections import OrderedDict from datetime import timedelta # OAuth2 +from oauthlib import oauth2 from oauthlib.common import generate_token # Django @@ -29,6 +30,7 @@ from django.utils.functional import cached_property # Django REST Framework from rest_framework.exceptions import ValidationError, PermissionDenied +from rest_framework.relations import ManyRelatedField from rest_framework import fields from rest_framework import serializers from rest_framework import validators @@ -42,7 +44,7 @@ from awx.main.constants import ( SCHEDULEABLE_PROVIDERS, ANSI_SGR_PATTERN, ACTIVE_STATES, - TOKEN_CENSOR, + CENSOR_VALUE, CHOICES_PRIVILEGE_ESCALATION_METHODS, ) from awx.main.models import * # noqa @@ -53,9 +55,9 @@ from awx.main.utils import ( get_type_for_model, get_model_for_type, timestamp_apiformat, camelcase_to_underscore, getattrd, parse_yaml_or_json, has_model_field_prefetched, extract_ansible_vars, encrypt_dict, - prefetch_page_capabilities) + prefetch_page_capabilities, get_external_account) from awx.main.utils.filters import SmartFilter -from awx.main.redact import REPLACE_STR +from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.validators import vars_validate_or_raise @@ -76,7 +78,7 @@ DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'description')# , 'created_by', 'modifie SUMMARIZABLE_FK_FIELDS = { 'organization': DEFAULT_SUMMARY_FIELDS, 'user': ('id', 'username', 'first_name', 'last_name'), - 'application': ('id', 'name', 'client_id'), + 'application': ('id', 'name'), 'team': DEFAULT_SUMMARY_FIELDS, 'inventory': DEFAULT_SUMMARY_FIELDS + ('has_active_failures', 'total_hosts', @@ -277,6 +279,16 @@ class BaseSerializer(serializers.ModelSerializer): created = serializers.SerializerMethodField() modified = serializers.SerializerMethodField() + def __init__(self, *args, **kwargs): + super(BaseSerializer, self).__init__(*args, **kwargs) + # The following lines fix the problem of being able to pass JSON dict into PrimaryKeyRelatedField. + data = kwargs.get('data', False) + if data: + for field_name, field_instance in six.iteritems(self.fields): + if isinstance(field_instance, ManyRelatedField) and not field_instance.read_only: + if isinstance(data.get(field_name, False), dict): + raise serializers.ValidationError(_('Cannot use dictionary for %s' % field_name)) + @property def version(self): """ @@ -299,10 +311,11 @@ class BaseSerializer(serializers.ModelSerializer): 'system_job': _('Management Job'), 'workflow_job': _('Workflow Job'), 'workflow_job_template': _('Workflow Template'), + 'job_template': _('Job Template') } choices = [] for t in self.get_types(): - name = type_name_map.get(t, force_text(get_model_for_type(t)._meta.verbose_name).title()) + name = _(type_name_map.get(t, force_text(get_model_for_type(t)._meta.verbose_name).title())) choices.append((t, name)) return choices @@ -363,16 +376,17 @@ class BaseSerializer(serializers.ModelSerializer): isinstance(obj, Project)): continue - fkval = getattr(obj, fk, None) + try: + fkval = getattr(obj, fk, None) + except ObjectDoesNotExist: + continue if fkval is None: continue if fkval == obj: continue summary_fields[fk] = OrderedDict() for field in related_fields: - if ( - self.version < 2 and field == 'credential_type_id' and - fk in ['credential', 'vault_credential']): # TODO: remove version check in 3.3 + if self.version < 2 and field == 'credential_type_id': # TODO: remove version check in 3.3 continue fval = getattr(fkval, field, None) @@ -661,7 +675,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): else: return super(UnifiedJobTemplateSerializer, self).get_types() - def to_representation(self, obj): + def get_sub_serializer(self, obj): serializer_class = None if type(self) is UnifiedJobTemplateSerializer: if isinstance(obj, Project): @@ -674,6 +688,10 @@ class UnifiedJobTemplateSerializer(BaseSerializer): serializer_class = SystemJobTemplateSerializer elif isinstance(obj, WorkflowJobTemplate): serializer_class = WorkflowJobTemplateSerializer + return serializer_class + + def to_representation(self, obj): + serializer_class = self.get_sub_serializer(obj) if serializer_class: serializer = serializer_class(instance=obj, context=self.context) # preserve links for list view @@ -702,7 +720,8 @@ class UnifiedJobSerializer(BaseSerializer): model = UnifiedJob fields = ('*', 'unified_job_template', 'launch_type', 'status', 'failed', 'started', 'finished', 'elapsed', 'job_args', - 'job_cwd', 'job_env', 'job_explanation', 'execution_node', + 'job_cwd', 'job_env', 'job_explanation', + 'execution_node', 'controller_node', 'result_traceback', 'event_processing_finished') extra_kwargs = { 'unified_job_template': { @@ -755,7 +774,7 @@ class UnifiedJobSerializer(BaseSerializer): return summary_fields - def to_representation(self, obj): + def get_sub_serializer(self, obj): serializer_class = None if type(self) is UnifiedJobSerializer: if isinstance(obj, ProjectUpdate): @@ -770,6 +789,10 @@ class UnifiedJobSerializer(BaseSerializer): serializer_class = SystemJobSerializer elif isinstance(obj, WorkflowJob): serializer_class = WorkflowJobSerializer + return serializer_class + + def to_representation(self, obj): + serializer_class = self.get_sub_serializer(obj) if serializer_class: serializer = serializer_class(instance=obj, context=self.context) # preserve links for list view @@ -807,7 +830,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer): else: return super(UnifiedJobListSerializer, self).get_types() - def to_representation(self, obj): + def get_sub_serializer(self, obj): serializer_class = None if type(self) is UnifiedJobListSerializer: if isinstance(obj, ProjectUpdate): @@ -821,7 +844,11 @@ class UnifiedJobListSerializer(UnifiedJobSerializer): elif isinstance(obj, SystemJob): serializer_class = SystemJobListSerializer elif isinstance(obj, WorkflowJob): - serializer_class = WorkflowJobSerializer + serializer_class = WorkflowJobListSerializer + return serializer_class + + def to_representation(self, obj): + serializer_class = self.get_sub_serializer(obj) if serializer_class: serializer = serializer_class(instance=obj, context=self.context) ret = serializer.to_representation(obj) @@ -899,29 +926,14 @@ class UserSerializer(BaseSerializer): if new_password: obj.set_password(new_password) obj.save(update_fields=['password']) - UserSessionMembership.clear_session_for_user(obj) + if self.context['request'].user != obj: + UserSessionMembership.clear_session_for_user(obj) elif not obj.password: obj.set_unusable_password() obj.save(update_fields=['password']) def get_external_account(self, obj): - account_type = None - if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'): - try: - if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password(): - account_type = "ldap" - except AttributeError: - pass - if (getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None) or - getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY', None) or - getattr(settings, 'SOCIAL_AUTH_GITHUB_ORG_KEY', None) or - getattr(settings, 'SOCIAL_AUTH_GITHUB_TEAM_KEY', None) or - getattr(settings, 'SOCIAL_AUTH_SAML_ENABLED_IDPS', None)) and obj.social_auth.all(): - account_type = "social" - if (getattr(settings, 'RADIUS_SERVER', None) or - getattr(settings, 'TACACSPLUS_HOST', None)) and obj.enterprise_auth.all(): - account_type = "enterprise" - return account_type + return get_external_account(obj) def create(self, validated_data): new_password = validated_data.pop('password', None) @@ -948,7 +960,7 @@ class UserSerializer(BaseSerializer): access_list = self.reverse('api:user_access_list', kwargs={'pk': obj.pk}), tokens = self.reverse('api:o_auth2_token_list', kwargs={'pk': obj.pk}), authorized_tokens = self.reverse('api:user_authorized_token_list', kwargs={'pk': obj.pk}), - personal_tokens = self.reverse('api:o_auth2_personal_token_list', kwargs={'pk': obj.pk}), + personal_tokens = self.reverse('api:user_personal_token_list', kwargs={'pk': obj.pk}), )) return res @@ -984,18 +996,23 @@ class UserSerializer(BaseSerializer): return self._validate_ldap_managed_field(value, 'is_superuser') -class UserAuthorizedTokenSerializer(BaseSerializer): +class BaseOAuth2TokenSerializer(BaseSerializer): refresh_token = serializers.SerializerMethodField() token = serializers.SerializerMethodField() + ALLOWED_SCOPES = ['read', 'write'] class Meta: model = OAuth2AccessToken fields = ( '*', '-name', 'description', 'user', 'token', 'refresh_token', - 'expires', 'scope', 'application' + 'application', 'expires', 'scope', ) - read_only_fields = ('user', 'token', 'expires') + read_only_fields = ('user', 'token', 'expires', 'refresh_token') + extra_kwargs = { + 'scope': {'allow_null': False, 'required': False}, + 'user': {'allow_null': False, 'required': True} + } def get_token(self, obj): request = self.context.get('request', None) @@ -1003,19 +1020,71 @@ class UserAuthorizedTokenSerializer(BaseSerializer): if request.method == 'POST': return obj.token else: - return TOKEN_CENSOR + return CENSOR_VALUE except ObjectDoesNotExist: return '' def get_refresh_token(self, obj): request = self.context.get('request', None) try: - if request.method == 'POST': + if not obj.refresh_token: + return None + elif request.method == 'POST': return getattr(obj.refresh_token, 'token', '') else: - return TOKEN_CENSOR + return CENSOR_VALUE except ObjectDoesNotExist: - return '' + return None + + def get_related(self, obj): + ret = super(BaseOAuth2TokenSerializer, self).get_related(obj) + if obj.user: + ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk}) + if obj.application: + ret['application'] = self.reverse( + 'api:o_auth2_application_detail', kwargs={'pk': obj.application.pk} + ) + ret['activity_stream'] = self.reverse( + 'api:o_auth2_token_activity_stream_list', kwargs={'pk': obj.pk} + ) + return ret + + def _is_valid_scope(self, value): + if not value or (not isinstance(value, six.string_types)): + return False + words = value.split() + for word in words: + if words.count(word) > 1: + return False # do not allow duplicates + if word not in self.ALLOWED_SCOPES: + return False + return True + + def validate_scope(self, value): + if not self._is_valid_scope(value): + raise serializers.ValidationError(_( + 'Must be a simple space-separated string with allowed scopes {}.' + ).format(self.ALLOWED_SCOPES)) + return value + + def create(self, validated_data, from_command_line=False): + if not from_command_line: + current_user = self.context['request'].user + validated_data['user'] = current_user + try: + return super(BaseOAuth2TokenSerializer, self).create(validated_data) + except oauth2.AccessDeniedError as e: + raise PermissionDenied(str(e)) + + +class UserAuthorizedTokenSerializer(BaseOAuth2TokenSerializer): + + class Meta: + extra_kwargs = { + 'scope': {'allow_null': False, 'required': False}, + 'user': {'allow_null': False, 'required': True}, + 'application': {'allow_null': False, 'required': True} + } def create(self, validated_data): current_user = self.context['request'].user @@ -1024,9 +1093,9 @@ class UserAuthorizedTokenSerializer(BaseSerializer): validated_data['expires'] = now() + timedelta( seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] ) - obj = super(OAuth2TokenSerializer, self).create(validated_data) + obj = super(UserAuthorizedTokenSerializer, self).create(validated_data) obj.save() - if obj.application is not None: + if obj.application and obj.application.authorization_grant_type != 'implicit': RefreshToken.objects.create( user=current_user, token=generate_token(), @@ -1036,10 +1105,56 @@ class UserAuthorizedTokenSerializer(BaseSerializer): return obj +class OAuth2TokenSerializer(BaseOAuth2TokenSerializer): + + def create(self, validated_data): + current_user = self.context['request'].user + validated_data['user'] = current_user + validated_data['token'] = generate_token() + validated_data['expires'] = now() + timedelta( + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] + ) + obj = super(OAuth2TokenSerializer, self).create(validated_data) + if obj.application and obj.application.user: + obj.user = obj.application.user + obj.save() + if obj.application and obj.application.authorization_grant_type != 'implicit': + RefreshToken.objects.create( + user=current_user, + token=generate_token(), + application=obj.application, + access_token=obj + ) + return obj + + +class OAuth2TokenDetailSerializer(OAuth2TokenSerializer): + + class Meta: + read_only_fields = ('*', 'user', 'application') + + +class UserPersonalTokenSerializer(BaseOAuth2TokenSerializer): + + class Meta: + read_only_fields = ('user', 'token', 'expires', 'application') + + def create(self, validated_data): + validated_data['user'] = self.context['request'].user + validated_data['token'] = generate_token() + validated_data['expires'] = now() + timedelta( + seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] + ) + validated_data['application'] = None + obj = super(UserPersonalTokenSerializer, self).create(validated_data) + obj.save() + return obj + + class OAuth2ApplicationSerializer(BaseSerializer): - + show_capabilities = ['edit', 'delete'] - + class Meta: model = OAuth2Application fields = ( @@ -1051,35 +1166,47 @@ class OAuth2ApplicationSerializer(BaseSerializer): extra_kwargs = { 'user': {'allow_null': True, 'required': False}, 'organization': {'allow_null': False}, - 'authorization_grant_type': {'allow_null': False} - } - + 'authorization_grant_type': {'allow_null': False, 'label': _('Authorization Grant Type')}, + 'client_secret': { + 'label': _('Client Secret') + }, + 'client_type': { + 'label': _('Client Type') + }, + 'redirect_uris': { + 'label': _('Redirect URIs') + }, + 'skip_authorization': { + 'label': _('Skip Authorization') + }, + } + def to_representation(self, obj): ret = super(OAuth2ApplicationSerializer, self).to_representation(obj) + request = self.context.get('request', None) + if request.method != 'POST' and obj.client_type == 'confidential': + ret['client_secret'] = CENSOR_VALUE if obj.client_type == 'public': ret.pop('client_secret', None) return ret - + + def get_related(self, obj): + res = super(OAuth2ApplicationSerializer, self).get_related(obj) + res.update(dict( + tokens = self.reverse('api:o_auth2_application_token_list', kwargs={'pk': obj.pk}), + activity_stream = self.reverse( + 'api:o_auth2_application_activity_stream_list', kwargs={'pk': obj.pk} + ) + )) + return res def get_modified(self, obj): if obj is None: return None return obj.updated - def get_related(self, obj): - ret = super(OAuth2ApplicationSerializer, self).get_related(obj) - if obj.user: - ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk}) - ret['tokens'] = self.reverse( - 'api:o_auth2_application_token_list', kwargs={'pk': obj.pk} - ) - ret['activity_stream'] = self.reverse( - 'api:o_auth2_application_activity_stream_list', kwargs={'pk': obj.pk} - ) - return ret - def _summary_field_tokens(self, obj): - token_list = [{'id': x.pk, 'token': TOKEN_CENSOR, 'scope': x.scope} for x in obj.oauth2accesstoken_set.all()[:10]] + token_list = [{'id': x.pk, 'token': CENSOR_VALUE, 'scope': x.scope} for x in obj.oauth2accesstoken_set.all()[:10]] if has_model_field_prefetched(obj, 'oauth2accesstoken_set'): token_count = len(obj.oauth2accesstoken_set.all()) else: @@ -1095,224 +1222,6 @@ class OAuth2ApplicationSerializer(BaseSerializer): return ret -class OAuth2TokenSerializer(BaseSerializer): - - refresh_token = serializers.SerializerMethodField() - token = serializers.SerializerMethodField() - ALLOWED_SCOPES = ['read', 'write'] - - class Meta: - model = OAuth2AccessToken - fields = ( - '*', '-name', 'description', 'user', 'token', 'refresh_token', - 'application', 'expires', 'scope', - ) - read_only_fields = ('user', 'token', 'expires') - extra_kwargs = { - 'scope': {'allow_null': False, 'required': True}, - 'user': {'allow_null': False, 'required': True} - } - - def get_modified(self, obj): - if obj is None: - return None - return obj.updated - - def get_related(self, obj): - ret = super(OAuth2TokenSerializer, self).get_related(obj) - if obj.user: - ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk}) - if obj.application: - ret['application'] = self.reverse( - 'api:o_auth2_application_detail', kwargs={'pk': obj.application.pk} - ) - ret['activity_stream'] = self.reverse( - 'api:o_auth2_token_activity_stream_list', kwargs={'pk': obj.pk} - ) - return ret - - def get_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return obj.token - else: - return TOKEN_CENSOR - except ObjectDoesNotExist: - return '' - - def get_refresh_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return getattr(obj.refresh_token, 'token', '') - else: - return TOKEN_CENSOR - except ObjectDoesNotExist: - return '' - - def _is_valid_scope(self, value): - if not value or (not isinstance(value, six.string_types)): - return False - words = value.split() - for word in words: - if words.count(word) > 1: - return False # do not allow duplicates - if word not in self.ALLOWED_SCOPES: - return False - return True - - def validate_scope(self, value): - if not self._is_valid_scope(value): - raise serializers.ValidationError(_( - 'Must be a simple space-separated string with allowed scopes {}.' - ).format(self.ALLOWED_SCOPES)) - return value - - def create(self, validated_data, from_command_line=False): - if not from_command_line: - current_user = self.context['request'].user - validated_data['user'] = current_user - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta( - seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] - ) - obj = super(OAuth2TokenSerializer, self).create(validated_data) - if obj.application and obj.application.user: - obj.user = obj.application.user - obj.save() - if obj.application is not None: - RefreshToken.objects.create( - user=current_user, - token=generate_token(), - application=obj.application, - access_token=obj - ) - return obj - - -class OAuth2TokenDetailSerializer(OAuth2TokenSerializer): - - class Meta: - read_only_fields = ('*', 'user', 'application') - - -class OAuth2AuthorizedTokenSerializer(BaseSerializer): - - refresh_token = serializers.SerializerMethodField() - token = serializers.SerializerMethodField() - - class Meta: - model = OAuth2AccessToken - fields = ( - '*', '-name', 'description', '-user', 'token', 'refresh_token', - 'expires', 'scope', 'application', - ) - read_only_fields = ('user', 'token', 'expires') - extra_kwargs = { - 'scope': {'allow_null': False, 'required': True} - } - - def get_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return obj.token - else: - return TOKEN_CENSOR - except ObjectDoesNotExist: - return '' - - def get_refresh_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return getattr(obj.refresh_token, 'token', '') - else: - return TOKEN_CENSOR - except ObjectDoesNotExist: - return '' - - def create(self, validated_data): - current_user = self.context['request'].user - validated_data['user'] = current_user - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta( - seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] - ) - obj = super(OAuth2AuthorizedTokenSerializer, self).create(validated_data) - if obj.application and obj.application.user: - obj.user = obj.application.user - obj.save() - if obj.application is not None: - RefreshToken.objects.create( - user=current_user, - token=generate_token(), - application=obj.application, - access_token=obj - ) - return obj - - -class OAuth2PersonalTokenSerializer(BaseSerializer): - - refresh_token = serializers.SerializerMethodField() - token = serializers.SerializerMethodField() - - class Meta: - model = OAuth2AccessToken - fields = ( - '*', '-name', 'description', 'user', 'token', 'refresh_token', - 'application', 'expires', 'scope', - ) - read_only_fields = ('user', 'token', 'expires', 'application') - extra_kwargs = { - 'scope': {'allow_null': False, 'required': True} - } - - def get_modified(self, obj): - if obj is None: - return None - return obj.updated - - def get_related(self, obj): - ret = super(OAuth2PersonalTokenSerializer, self).get_related(obj) - if obj.user: - ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk}) - if obj.application: - ret['application'] = self.reverse( - 'api:o_auth2_application_detail', kwargs={'pk': obj.application.pk} - ) - ret['activity_stream'] = self.reverse( - 'api:o_auth2_token_activity_stream_list', kwargs={'pk': obj.pk} - ) - return ret - - def get_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return obj.token - else: - return TOKEN_CENSOR - except ObjectDoesNotExist: - return '' - - def get_refresh_token(self, obj): - return None - - def create(self, validated_data): - validated_data['user'] = self.context['request'].user - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta( - seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] - ) - validated_data['application'] = None - obj = super(OAuth2PersonalTokenSerializer, self).create(validated_data) - obj.save() - return obj - - class OrganizationSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] @@ -1407,6 +1316,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): 'admin', 'update', {'copy': 'organization.project_admin'} ] + scm_delete_on_next_update = serializers.BooleanField( + read_only=True, + help_text=_('This field has been deprecated and will be removed in a future release')) class Meta: model = Project @@ -1431,8 +1343,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): notification_templates_error = self.reverse('api:project_notification_templates_error_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:project_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:project_object_roles_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:project_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:project_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) @@ -1519,7 +1432,7 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer): class Meta: model = ProjectUpdate - fields = ('*', 'project', 'job_type') + fields = ('*', 'project', 'job_type', '-controller_node') def get_related(self, obj): res = super(ProjectUpdateSerializer, self).get_related(obj) @@ -1538,9 +1451,41 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer): return res +class ProjectUpdateDetailSerializer(ProjectUpdateSerializer): + + host_status_counts = serializers.SerializerMethodField( + help_text=_('A count of hosts uniquely assigned to each status.'), + ) + playbook_counts = serializers.SerializerMethodField( + help_text=_('A count of all plays and tasks for the job run.'), + ) + + class Meta: + model = ProjectUpdate + fields = ('*', 'host_status_counts', 'playbook_counts',) + + def get_playbook_counts(self, obj): + task_count = obj.project_update_events.filter(event='playbook_on_task_start').count() + play_count = obj.project_update_events.filter(event='playbook_on_play_start').count() + + data = {'play_count': play_count, 'task_count': task_count} + + return data + + def get_host_status_counts(self, obj): + try: + counts = obj.project_update_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() + except ProjectUpdateEvent.DoesNotExist: + counts = {} + + return counts + + class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer): - pass + class Meta: + model = ProjectUpdate + fields = ('*', '-controller_node') # field removal undone by UJ serializer class ProjectUpdateCancelSerializer(ProjectUpdateSerializer): @@ -1589,8 +1534,9 @@ class InventorySerializer(BaseSerializerWithVariables): access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk}) if obj.insights_credential: res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk}) if obj.organization: @@ -1924,7 +1870,7 @@ class CustomInventoryScriptSerializer(BaseSerializer): script = serializers.CharField(trim_whitespace=False) show_capabilities = ['edit', 'delete', 'copy'] capabilities_prefetch = [ - {'edit': 'organization.admin'} + {'edit': 'admin'} ] class Meta: @@ -1952,8 +1898,9 @@ class CustomInventoryScriptSerializer(BaseSerializer): res = super(CustomInventoryScriptSerializer, self).get_related(obj) res.update(dict( object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) @@ -2020,8 +1967,10 @@ class InventorySourceOptionsSerializer(BaseSerializer): if cred: summary_fields['credential'] = { 'id': cred.id, 'name': cred.name, 'description': cred.description, - 'kind': cred.kind, 'cloud': True, 'credential_type_id': cred.credential_type_id + 'kind': cred.kind, 'cloud': True } + if self.version > 1: + summary_fields['credential']['credential_type_id'] = cred.credential_type_id else: summary_fields.pop('credential') return summary_fields @@ -2052,7 +2001,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt update = self.reverse('api:inventory_source_update_view', kwargs={'pk': obj.pk}), inventory_updates = self.reverse('api:inventory_source_updates_list', kwargs={'pk': obj.pk}), schedules = self.reverse('api:inventory_source_schedules_list', kwargs={'pk': obj.pk}), - credentials = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}), activity_stream = self.reverse('api:inventory_source_activity_stream_list', kwargs={'pk': obj.pk}), hosts = self.reverse('api:inventory_source_hosts_list', kwargs={'pk': obj.pk}), groups = self.reverse('api:inventory_source_groups_list', kwargs={'pk': obj.pk}), @@ -2074,6 +2022,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if self.version == 1: # TODO: remove in 3.3 if obj.deprecated_group: res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.deprecated_group.pk}) + else: + res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}) return res def get_fields(self): # TODO: remove in 3.3 @@ -2167,14 +2117,14 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt def _update_deprecated_fields(self, fields, obj): if 'credential' in fields: new_cred = fields['credential'] - existing_creds = obj.credentials.exclude(credential_type__kind='vault') - for cred in existing_creds: - # Remove all other cloud credentials - if cred != new_cred: + existing = obj.credentials.all() + if new_cred not in existing: + for cred in existing: + # Remove all other cloud credentials obj.credentials.remove(cred) - if new_cred: - # Add new credential - obj.credentials.add(new_cred) + if new_cred: + # Add new credential + obj.credentials.add(new_cred) def validate(self, attrs): deprecated_fields = {} @@ -2210,7 +2160,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt cred ) if cred_error: - raise serializers.ValidationError({"detail": cred_error}) + raise serializers.ValidationError({"credential": cred_error}) return attrs @@ -2227,7 +2177,8 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri class Meta: model = InventoryUpdate - fields = ('*', 'inventory_source', 'license_error', 'source_project_update') + fields = ('*', 'inventory', 'inventory_source', 'license_error', 'source_project_update', + '-controller_node',) def get_related(self, obj): res = super(InventoryUpdateSerializer, self).get_related(obj) @@ -2242,18 +2193,25 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri res.update(dict( cancel = self.reverse('api:inventory_update_cancel', kwargs={'pk': obj.pk}), notifications = self.reverse('api:inventory_update_notifications_list', kwargs={'pk': obj.pk}), - credentials = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk}), events = self.reverse('api:inventory_update_events_list', kwargs={'pk': obj.pk}), )) if obj.source_project_update_id: res['source_project_update'] = self.reverse('api:project_update_detail', kwargs={'pk': obj.source_project_update.pk}) + if obj.inventory: + res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) + + if self.version > 1: + res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk}) + return res class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSerializer): - pass + class Meta: + model = InventoryUpdate + fields = ('*', '-controller_node') # field removal undone by UJ serializer class InventoryUpdateCancelSerializer(InventoryUpdateSerializer): @@ -2496,6 +2454,7 @@ class CredentialTypeSerializer(BaseSerializer): # translate labels and help_text for credential fields "managed by Tower" if value.get('managed_by_tower'): + value['name'] = _(value['name']) for field in value.get('inputs', {}).get('fields', []): field['label'] = _(field['label']) if 'help_text' in field: @@ -2543,6 +2502,12 @@ class V2CredentialFields(BaseSerializer): model = Credential fields = ('*', 'credential_type', 'inputs') + extra_kwargs = { + 'credential_type': { + 'label': _('Credential Type'), + }, + } + class CredentialSerializer(BaseSerializer): show_capabilities = ['edit', 'delete', 'copy'] @@ -2589,8 +2554,9 @@ class CredentialSerializer(BaseSerializer): object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}), owner_users = self.reverse('api:credential_owner_users_list', kwargs={'pk': obj.pk}), owner_teams = self.reverse('api:credential_owner_teams_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}) # TODO: remove when API v1 is removed if self.version > 1: @@ -2961,11 +2927,12 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): ('network_credential', obj.network_credentials), ): if key in fields: - for cred in existing: - obj.credentials.remove(cred) - if fields[key]: - obj.credentials.add(fields[key]) - obj.save() + new_cred = fields[key] + if new_cred not in existing: + for cred in existing: + obj.credentials.remove(cred) + if new_cred: + obj.credentials.add(new_cred) def validate(self, attrs): v1_credentials = {} @@ -3064,8 +3031,9 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO labels = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}) if obj.host_config_key: res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk}) return res @@ -3096,6 +3064,11 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO def get_summary_fields(self, obj): summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj) all_creds = [] + # Organize credential data into multitude of deprecated fields + # TODO: remove most of this as v1 is removed + vault_credential = None + credential = None + extra_creds = [] if obj.pk: for cred in obj.credentials.all(): summarized_cred = { @@ -3103,20 +3076,17 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 'name': cred.name, 'description': cred.description, 'kind': cred.kind, - 'credential_type_id': cred.credential_type_id + 'cloud': cred.credential_type.kind == 'cloud' } + if self.version > 1: + summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) - # Organize credential data into multitude of deprecated fields - extra_creds = [] - vault_credential = None - credential = None - for summarized_cred in all_creds: - if summarized_cred['kind'] in ('cloud', 'net'): - extra_creds.append(summarized_cred) - elif summarized_cred['kind'] == 'ssh': - credential = summarized_cred - elif summarized_cred['kind'] == 'vault': - vault_credential = summarized_cred + if cred.credential_type.kind in ('cloud', 'net'): + extra_creds.append(summarized_cred) + elif summarized_cred['kind'] == 'ssh': + credential = summarized_cred + elif summarized_cred['kind'] == 'vault': + vault_credential = summarized_cred # Selectively apply those fields, depending on view deetails if (self.is_detail_view or self.version == 1) and credential: summary_fields['credential'] = credential @@ -3184,7 +3154,8 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): ) except ObjectDoesNotExist: pass - res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}) + if self.version > 1: + res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}) res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk}) return res @@ -3233,30 +3204,78 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): def get_summary_fields(self, obj): summary_fields = super(JobSerializer, self).get_summary_fields(obj) - if self.is_detail_view: # TODO: remove version check in 3.3 - all_creds = [] - extra_creds = [] + all_creds = [] + # Organize credential data into multitude of deprecated fields + # TODO: remove most of this as v1 is removed + vault_credential = None + credential = None + extra_creds = [] + if obj.pk: for cred in obj.credentials.all(): summarized_cred = { 'id': cred.pk, 'name': cred.name, 'description': cred.description, 'kind': cred.kind, - 'credential_type_id': cred.credential_type_id + 'cloud': cred.credential_type.kind == 'cloud' } + if self.version > 1: + summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) if cred.credential_type.kind in ('cloud', 'net'): extra_creds.append(summarized_cred) - elif cred.credential_type.kind == 'ssh': - summary_fields['credential'] = summarized_cred - elif cred.credential_type.kind == 'vault': - summary_fields['vault_credential'] = summarized_cred - if self.version > 1: + elif summarized_cred['kind'] == 'ssh': + credential = summarized_cred + elif summarized_cred['kind'] == 'vault': + vault_credential = summarized_cred + # Selectively apply those fields, depending on view deetails + if (self.is_detail_view or self.version == 1) and credential: + summary_fields['credential'] = credential + else: + # Credential could be an empty dictionary in this case + summary_fields.pop('credential', None) + if (self.is_detail_view or self.version == 1) and vault_credential: + summary_fields['vault_credential'] = vault_credential + else: + # vault credential could be empty dictionary + summary_fields.pop('vault_credential', None) + if self.version > 1: + if self.is_detail_view: summary_fields['extra_credentials'] = extra_creds - summary_fields['credentials'] = all_creds + summary_fields['credentials'] = all_creds return summary_fields +class JobDetailSerializer(JobSerializer): + + host_status_counts = serializers.SerializerMethodField( + help_text=_('A count of hosts uniquely assigned to each status.'), + ) + playbook_counts = serializers.SerializerMethodField( + help_text=_('A count of all plays and tasks for the job run.'), + ) + + class Meta: + model = Job + fields = ('*', 'host_status_counts', 'playbook_counts',) + + def get_playbook_counts(self, obj): + task_count = obj.job_events.filter(event='playbook_on_task_start').count() + play_count = obj.job_events.filter(event='playbook_on_play_start').count() + + data = {'play_count': play_count, 'task_count': task_count} + + return data + + def get_host_status_counts(self, obj): + try: + counts = obj.job_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() + except JobEvent.DoesNotExist: + counts = {} + + return counts + + class JobCancelSerializer(BaseSerializer): can_cancel = serializers.BooleanField(read_only=True) @@ -3400,10 +3419,10 @@ class AdHocCommandSerializer(UnifiedJobSerializer): def get_related(self, obj): res = super(AdHocCommandSerializer, self).get_related(obj) - if obj.inventory: - res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) - if obj.credential: - res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk}) + if obj.inventory_id: + res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory_id}) + if obj.credential_id: + res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential_id}) res.update(dict( events = self.reverse('api:ad_hoc_command_ad_hoc_command_events_list', kwargs={'pk': obj.pk}), activity_stream = self.reverse('api:ad_hoc_command_activity_stream_list', kwargs={'pk': obj.pk}), @@ -3415,9 +3434,9 @@ class AdHocCommandSerializer(UnifiedJobSerializer): def to_representation(self, obj): ret = super(AdHocCommandSerializer, self).to_representation(obj) - if 'inventory' in ret and not obj.inventory: + if 'inventory' in ret and not obj.inventory_id: ret['inventory'] = None - if 'credential' in ret and not obj.credential: + if 'credential' in ret and not obj.credential_id: ret['credential'] = None # For the UI, only module_name is returned for name, instead of the # longer module name + module_args format. @@ -3434,6 +3453,25 @@ class AdHocCommandSerializer(UnifiedJobSerializer): return vars_validate_or_raise(value) +class AdHocCommandDetailSerializer(AdHocCommandSerializer): + + host_status_counts = serializers.SerializerMethodField( + help_text=_('A count of hosts uniquely assigned to each status.'), + ) + + class Meta: + model = AdHocCommand + fields = ('*', 'host_status_counts',) + + def get_host_status_counts(self, obj): + try: + counts = obj.ad_hoc_command_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() + except AdHocCommandEvent.DoesNotExist: + counts = {} + + return counts + + class AdHocCommandCancelSerializer(AdHocCommandSerializer): can_cancel = serializers.BooleanField(read_only=True) @@ -3480,7 +3518,7 @@ class SystemJobSerializer(UnifiedJobSerializer): class Meta: model = SystemJob - fields = ('*', 'system_job_template', 'job_type', 'extra_vars', 'result_stdout') + fields = ('*', 'system_job_template', 'job_type', 'extra_vars', 'result_stdout', '-controller_node',) def get_related(self, obj): res = super(SystemJobSerializer, self).get_related(obj) @@ -3530,7 +3568,6 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo workflow_jobs = self.reverse('api:workflow_job_template_jobs_list', kwargs={'pk': obj.pk}), schedules = self.reverse('api:workflow_job_template_schedules_list', kwargs={'pk': obj.pk}), launch = self.reverse('api:workflow_job_template_launch', kwargs={'pk': obj.pk}), - copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}), workflow_nodes = self.reverse('api:workflow_job_template_workflow_nodes_list', kwargs={'pk': obj.pk}), labels = self.reverse('api:workflow_job_template_label_list', kwargs={'pk': obj.pk}), activity_stream = self.reverse('api:workflow_job_template_activity_stream_list', kwargs={'pk': obj.pk}), @@ -3541,6 +3578,8 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}), survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res @@ -3549,18 +3588,12 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo return vars_validate_or_raise(value) -# TODO: -class WorkflowJobTemplateListSerializer(WorkflowJobTemplateSerializer): - pass - - -# TODO: class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): class Meta: model = WorkflowJob fields = ('*', 'workflow_job_template', 'extra_vars', 'allow_simultaneous', - '-execution_node', '-event_processing_finished',) + '-execution_node', '-event_processing_finished', '-controller_node',) def get_related(self, obj): res = super(WorkflowJobSerializer, self).get_related(obj) @@ -3585,11 +3618,10 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): return ret -# TODO: class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer): class Meta: - fields = ('*', '-execution_node',) + fields = ('*', '-execution_node', '-controller_node',) class WorkflowJobCancelSerializer(WorkflowJobSerializer): @@ -3652,6 +3684,10 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): return summary_fields def validate(self, attrs): + db_extra_data = {} + if self.instance: + db_extra_data = parse_yaml_or_json(self.instance.extra_data) + attrs = super(LaunchConfigurationBaseSerializer, self).validate(attrs) ujt = None @@ -3660,39 +3696,38 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): elif self.instance: ujt = self.instance.unified_job_template - # Replace $encrypted$ submissions with db value if exists # build additional field survey_passwords to track redacted variables + password_dict = {} + extra_data = parse_yaml_or_json(attrs.get('extra_data', {})) + if hasattr(ujt, 'survey_password_variables'): + # Prepare additional field survey_passwords for save + for key in ujt.survey_password_variables(): + if key in extra_data: + password_dict[key] = REPLACE_STR + + # Replace $encrypted$ submissions with db value if exists if 'extra_data' in attrs: - extra_data = parse_yaml_or_json(attrs.get('extra_data', {})) - if hasattr(ujt, 'survey_password_variables'): - # Prepare additional field survey_passwords for save - password_dict = {} - for key in ujt.survey_password_variables(): - if key in extra_data: - password_dict[key] = REPLACE_STR + if password_dict: if not self.instance or password_dict != self.instance.survey_passwords: attrs['survey_passwords'] = password_dict.copy() # Force dict type (cannot preserve YAML formatting if passwords are involved) - if not isinstance(attrs['extra_data'], dict): - attrs['extra_data'] = parse_yaml_or_json(attrs['extra_data']) # Encrypt the extra_data for save, only current password vars in JT survey + # but first, make a copy or else this is referenced by request.data, and + # user could get encrypted string in form data in API browser + attrs['extra_data'] = extra_data.copy() encrypt_dict(attrs['extra_data'], password_dict.keys()) # For any raw $encrypted$ string, either # - replace with existing DB value # - raise a validation error - # - remove key from extra_data if survey default is present - if self.instance: - db_extra_data = parse_yaml_or_json(self.instance.extra_data) - else: - db_extra_data = {} + # - ignore, if default present for key in password_dict.keys(): if attrs['extra_data'].get(key, None) == REPLACE_STR: if key not in db_extra_data: element = ujt.pivot_spec(ujt.survey_spec)[key] - if 'default' in element and element['default']: - attrs['survey_passwords'].pop(key, None) - attrs['extra_data'].pop(key, None) - else: + # NOTE: validation _of_ the default values of password type + # questions not done here or on launch, but doing so could + # leak info about values, so it should not be added + if not ('default' in element and element['default']): raise serializers.ValidationError( {"extra_data": _('Provided variable {} has no database value to replace with.').format(key)}) else: @@ -3703,6 +3738,18 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): accepted, rejected, errors = ujt._accept_or_ignore_job_kwargs( _exclude_errors=self.exclude_errors, **mock_obj.prompts_dict()) + # Remove all unprocessed $encrypted$ strings, indicating default usage + if 'extra_data' in attrs and password_dict: + for key, value in attrs['extra_data'].copy().items(): + if value == REPLACE_STR: + if key in password_dict: + attrs['extra_data'].pop(key) + attrs.get('survey_passwords', {}).pop(key, None) + else: + errors.setdefault('extra_vars', []).append( + _('"$encrypted$ is a reserved keyword, may not be used for {var_name}."'.format(key)) + ) + # Launch configs call extra_vars extra_data for historical reasons if 'extra_vars' in errors: errors['extra_data'] = errors.pop('extra_vars') @@ -3800,10 +3847,13 @@ class WorkflowJobTemplateNodeSerializer(LaunchConfigurationBaseSerializer): deprecated_fields['credential'] = validated_data.pop('credential') obj = super(WorkflowJobTemplateNodeSerializer, self).update(obj, validated_data) if 'credential' in deprecated_fields: - for cred in obj.credentials.filter(credential_type__kind='ssh'): - obj.credentials.remove(cred) - if deprecated_fields['credential']: - obj.credentials.add(deprecated_fields['credential']) + existing = obj.credentials.filter(credential_type__kind='ssh') + new_cred = deprecated_fields['credential'] + if new_cred not in existing: + for cred in existing: + obj.credentials.remove(cred) + if new_cred: + obj.credentials.add(new_cred) return obj @@ -3865,7 +3915,10 @@ class AdHocCommandListSerializer(AdHocCommandSerializer, UnifiedJobListSerialize class SystemJobListSerializer(SystemJobSerializer, UnifiedJobListSerializer): - pass + + class Meta: + model = SystemJob + fields = ('*', '-controller_node') # field removal undone by UJ serializer class JobHostSummarySerializer(BaseSerializer): @@ -3975,6 +4028,8 @@ class JobEventWebSocketSerializer(JobEventSerializer): class ProjectUpdateEventSerializer(JobEventSerializer): + stdout = serializers.SerializerMethodField() + event_data = serializers.SerializerMethodField() class Meta: model = ProjectUpdateEvent @@ -3988,6 +4043,20 @@ class ProjectUpdateEventSerializer(JobEventSerializer): ) return res + def get_stdout(self, obj): + return UriCleaner.remove_sensitive(obj.stdout) + + def get_event_data(self, obj): + try: + return json.loads( + UriCleaner.remove_sensitive( + json.dumps(obj.event_data) + ) + ) + except Exception: + logger.exception("Failed to sanitize event_data") + return {} + class ProjectUpdateEventWebSocketSerializer(ProjectUpdateEventSerializer): created = serializers.SerializerMethodField() @@ -4243,6 +4312,10 @@ class JobLaunchSerializer(BaseSerializer): errors.setdefault('credentials', []).append(_( 'Cannot assign multiple {} credentials.' ).format(cred.unique_hash(display=True))) + if cred.credential_type.kind not in ('ssh', 'vault', 'cloud', 'net'): + errors.setdefault('credentials', []).append(_( + 'Cannot assign a Credential of kind `{}`' + ).format(cred.credential_type.kind)) distinct_cred_kinds.append(cred.unique_hash()) # Prohibit removing credentials from the JT list (unsupported for now) @@ -4256,7 +4329,7 @@ class JobLaunchSerializer(BaseSerializer): errors.setdefault('credentials', []).append(_( 'Removing {} credential at launch time without replacement is not supported. ' 'Provided list lacked credential(s): {}.' - ).format(cred.unique_hash(display=True), ', '.join([str(c) for c in removed_creds]))) + ).format(cred.unique_hash(display=True), ', '.join([six.text_type(c) for c in removed_creds]))) # verify that credentials (either provided or existing) don't # require launch-time passwords that have not been provided @@ -4354,8 +4427,9 @@ class NotificationTemplateSerializer(BaseSerializer): res.update(dict( test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}), notifications = self.reverse('api:notification_template_notification_list', kwargs={'pk': obj.pk}), - copy = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}), )) + if self.version > 1: + res['copy'] = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res @@ -4564,14 +4638,17 @@ class InstanceSerializer(BaseSerializer): 'are targeted for this instance'), read_only=True ) - + jobs_total = serializers.IntegerField( + help_text=_('Count of all jobs that target this instance'), + read_only=True + ) class Meta: model = Instance read_only_fields = ('uuid', 'hostname', 'version') fields = ("id", "type", "url", "related", "uuid", "hostname", "created", "modified", 'capacity_adjustment', - "version", "capacity", "consumed_capacity", "percent_capacity_remaining", "jobs_running", - "cpu", "memory", "cpu_capacity", "mem_capacity", "enabled") + "version", "capacity", "consumed_capacity", "percent_capacity_remaining", "jobs_running", "jobs_total", + "cpu", "memory", "cpu_capacity", "mem_capacity", "enabled", "managed_by_policy") def get_related(self, obj): res = super(InstanceSerializer, self).get_related(obj) @@ -4594,22 +4671,33 @@ class InstanceGroupSerializer(BaseSerializer): committed_capacity = serializers.SerializerMethodField() consumed_capacity = serializers.SerializerMethodField() percent_capacity_remaining = serializers.SerializerMethodField() - jobs_running = serializers.SerializerMethodField() + jobs_running = serializers.IntegerField( + help_text=_('Count of jobs in the running or waiting state that ' + 'are targeted for this instance group'), + read_only=True + ) + jobs_total = serializers.IntegerField( + help_text=_('Count of all jobs that target this instance group'), + read_only=True + ) instances = serializers.SerializerMethodField() # NOTE: help_text is duplicated from field definitions, no obvious way of # both defining field details here and also getting the field's help_text policy_instance_percentage = serializers.IntegerField( default=0, min_value=0, max_value=100, required=False, initial=0, + label=_('Policy Instance Percentage'), help_text=_("Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online.") ) policy_instance_minimum = serializers.IntegerField( default=0, min_value=0, required=False, initial=0, + label=_('Policy Instance Minimum'), help_text=_("Static minimum number of Instances that will be automatically assign to " "this group when new instances come online.") ) policy_instance_list = serializers.ListField( child=serializers.CharField(), required=False, + label=_('Policy Instance List'), help_text=_("List of exact-match Instances that will be assigned to this group") ) @@ -4617,7 +4705,8 @@ class InstanceGroupSerializer(BaseSerializer): model = InstanceGroup fields = ("id", "type", "url", "related", "name", "created", "modified", "capacity", "committed_capacity", "consumed_capacity", - "percent_capacity_remaining", "jobs_running", "instances", "controller", + "percent_capacity_remaining", "jobs_running", "jobs_total", + "instances", "controller", "policy_instance_percentage", "policy_instance_minimum", "policy_instance_list") def get_related(self, obj): @@ -4634,6 +4723,10 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('Duplicate entry {}.').format(instance_name)) if not Instance.objects.filter(hostname=instance_name).exists(): raise serializers.ValidationError(_('{} is not a valid hostname of an existing instance.').format(instance_name)) + if Instance.objects.get(hostname=instance_name).is_isolated(): + raise serializers.ValidationError(_('Isolated instances may not be added or removed from instances groups via the API.')) + if self.instance and self.instance.controller_id is not None: + raise serializers.ValidationError(_('Isolated instance group membership may not be managed via the API.')) return value def validate_name(self, value): @@ -4641,21 +4734,15 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('tower instance group name may not be changed.')) return value - def get_jobs_qs(self): - # Store running jobs queryset in context, so it will be shared in ListView - if 'running_jobs' not in self.context: - self.context['running_jobs'] = UnifiedJob.objects.filter( - status__in=('running', 'waiting')) - return self.context['running_jobs'] - def get_capacity_dict(self): # Store capacity values (globally computed) in the context if 'capacity_map' not in self.context: ig_qs = None + jobs_qs = UnifiedJob.objects.filter(status__in=('running', 'waiting')) if self.parent: # Is ListView: ig_qs = self.parent.instance self.context['capacity_map'] = InstanceGroup.objects.capacity_values( - qs=ig_qs, tasks=self.get_jobs_qs(), breakdown=True) + qs=ig_qs, tasks=jobs_qs, breakdown=True) return self.context['capacity_map'] def get_consumed_capacity(self, obj): @@ -4675,10 +4762,6 @@ class InstanceGroupSerializer(BaseSerializer): ((float(obj.capacity) - float(consumed)) / (float(obj.capacity))) * 100) ) - def get_jobs_running(self, obj): - jobs_qs = self.get_jobs_qs() - return sum(1 for job in jobs_qs if job.instance_group_id == obj.id) - def get_instances(self, obj): return obj.instances.count() @@ -4739,11 +4822,21 @@ class ActivityStreamSerializer(BaseSerializer): return {} def get_object_association(self, obj): + if not obj.object_relationship_type: + return "" + elif obj.object_relationship_type.endswith('_role'): + # roles: these values look like + # "awx.main.models.inventory.Inventory.admin_role" + # due to historical reasons the UI expects just "role" here + return "role" + # default case: these values look like + # "awx.main.models.organization.Organization_notification_templates_success" + # so instead of splitting on period we have to take after the first underscore try: - return obj.object_relationship_type.split(".")[-1].split("_")[1] + return obj.object_relationship_type.split(".")[-1].split("_", 1)[1] except Exception: - pass - return "" + logger.debug('Failed to parse activity stream relationship type {}'.format(obj.object_relationship_type)) + return "" def get_related(self, obj): rel = {} @@ -4828,6 +4921,9 @@ class ActivityStreamSerializer(BaseSerializer): username = obj.actor.username, first_name = obj.actor.first_name, last_name = obj.actor.last_name) + elif obj.deleted_actor: + summary_fields['actor'] = obj.deleted_actor.copy() + summary_fields['actor']['id'] = None if obj.setting: summary_fields['setting'] = [obj.setting] return summary_fields diff --git a/awx/api/urls/oauth.py b/awx/api/urls/oauth.py deleted file mode 100644 index 542c06cd36..0000000000 --- a/awx/api/urls/oauth.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2017 Ansible, Inc. -# All Rights Reserved. - -from django.conf.urls import url - -from oauth2_provider.urls import base_urlpatterns - -from awx.api.views import ( - ApiOAuthAuthorizationRootView, -) - - -urls = [ - url(r'^$', ApiOAuthAuthorizationRootView.as_view(), name='oauth_authorization_root_view'), -] + base_urlpatterns - - -__all__ = ['urls'] diff --git a/awx/api/urls/user_oauth.py b/awx/api/urls/oauth2.py similarity index 90% rename from awx/api/urls/user_oauth.py rename to awx/api/urls/oauth2.py index bec5c4332b..6e9eea3d9f 100644 --- a/awx/api/urls/user_oauth.py +++ b/awx/api/urls/oauth2.py @@ -11,7 +11,6 @@ from awx.api.views import ( OAuth2TokenList, OAuth2TokenDetail, OAuth2TokenActivityStreamList, - OAuth2PersonalTokenList ) @@ -42,8 +41,7 @@ urls = [ r'^tokens/(?P[0-9]+)/activity_stream/$', OAuth2TokenActivityStreamList.as_view(), name='o_auth2_token_activity_stream_list' - ), - url(r'^personal_tokens/$', OAuth2PersonalTokenList.as_view(), name='o_auth2_personal_token_list'), + ), ] __all__ = ['urls'] diff --git a/awx/api/urls/oauth2_root.py b/awx/api/urls/oauth2_root.py new file mode 100644 index 0000000000..4b5b8d619a --- /dev/null +++ b/awx/api/urls/oauth2_root.py @@ -0,0 +1,31 @@ +# Copyright (c) 2017 Ansible, Inc. +# All Rights Reserved. + +from django.conf.urls import url + +from oauthlib import oauth2 +from oauth2_provider import views + +from awx.api.views import ( + ApiOAuthAuthorizationRootView, +) + + +class TokenView(views.TokenView): + + def create_token_response(self, request): + try: + return super(TokenView, self).create_token_response(request) + except oauth2.AccessDeniedError as e: + return request.build_absolute_uri(), {}, str(e), '403' + + +urls = [ + url(r'^$', ApiOAuthAuthorizationRootView.as_view(), name='oauth_authorization_root_view'), + url(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"), + url(r"^token/$", TokenView.as_view(), name="token"), + url(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), +] + + +__all__ = ['urls'] diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index e282a73e5f..52e9ef1cf0 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -67,8 +67,8 @@ from .schedule import urls as schedule_urls from .activity_stream import urls as activity_stream_urls from .instance import urls as instance_urls from .instance_group import urls as instance_group_urls -from .user_oauth import urls as user_oauth_urls -from .oauth import urls as oauth_urls +from .oauth2 import urls as oauth2_urls +from .oauth2_root import urls as oauth2_root_urls v1_urls = [ @@ -130,7 +130,7 @@ v2_urls = [ url(r'^applications/(?P[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), url(r'^applications/(?P[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'), url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'), - url(r'^', include(user_oauth_urls)), + url(r'^', include(oauth2_urls)), ] app_name = 'api' @@ -145,7 +145,7 @@ urlpatterns = [ url(r'^logout/$', LoggedLogoutView.as_view( next_page='/api/', redirect_field_name='next' ), name='logout'), - url(r'^o/', include(oauth_urls)), + url(r'^o/', include(oauth2_root_urls)), ] if settings.SETTINGS_MODULE == 'awx.settings.development': from awx.api.swagger import SwaggerSchemaView diff --git a/awx/api/urls/user.py b/awx/api/urls/user.py index 9ecebbb044..ca8d531f46 100644 --- a/awx/api/urls/user.py +++ b/awx/api/urls/user.py @@ -16,7 +16,7 @@ from awx.api.views import ( UserAccessList, OAuth2ApplicationList, OAuth2UserTokenList, - OAuth2PersonalTokenList, + UserPersonalTokenList, UserAuthorizedTokenList, ) @@ -34,7 +34,7 @@ urls = [ url(r'^(?P[0-9]+)/applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), url(r'^(?P[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'), url(r'^(?P[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'), - url(r'^(?P[0-9]+)/personal_tokens/$', OAuth2PersonalTokenList.as_view(), name='o_auth2_personal_token_list'), + url(r'^(?P[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'), ] diff --git a/awx/api/views.py b/awx/api/views.py index 5f1d8b22af..1692968c75 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -24,7 +24,8 @@ from django.shortcuts import get_object_or_404 from django.utils.encoding import smart_text from django.utils.safestring import mark_safe from django.utils.timezone import now -from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from django.template.loader import render_to_string from django.http import HttpResponse from django.contrib.contenttypes.models import ContentType @@ -60,7 +61,7 @@ import pytz from wsgiref.util import FileWrapper # AWX -from awx.main.tasks import send_notifications, handle_ha_toplogy_changes +from awx.main.tasks import send_notifications from awx.main.access import get_user_queryset from awx.main.ha import is_ha_environment from awx.api.filters import V1CredentialFilterBackend @@ -104,6 +105,8 @@ def api_exception_handler(exc, context): exc = ParseError(exc.args[0]) if isinstance(exc, FieldError): exc = ParseError(exc.args[0]) + if isinstance(context['view'], UnifiedJobStdout): + context['view'].renderer_classes = [BrowsableAPIRenderer, renderers.JSONRenderer] return exception_handler(exc, context) @@ -176,29 +179,64 @@ class InstanceGroupMembershipMixin(object): sub_id, res = self.attach_validate(request) if status.is_success(response.status_code): if self.parent_model is Instance: - ig_obj = get_object_or_400(self.model, pk=sub_id) inst_name = ig_obj.hostname else: - ig_obj = self.get_parent_object() inst_name = get_object_or_400(self.model, pk=sub_id).hostname - if inst_name not in ig_obj.policy_instance_list: - ig_obj.policy_instance_list.append(inst_name) - ig_obj.save() + with transaction.atomic(): + ig_qs = InstanceGroup.objects.select_for_update() + if self.parent_model is Instance: + ig_obj = get_object_or_400(ig_qs, pk=sub_id) + else: + # similar to get_parent_object, but selected for update + parent_filter = { + self.lookup_field: self.kwargs.get(self.lookup_field, None), + } + ig_obj = get_object_or_404(ig_qs, **parent_filter) + if inst_name not in ig_obj.policy_instance_list: + ig_obj.policy_instance_list.append(inst_name) + ig_obj.save(update_fields=['policy_instance_list']) return response + def is_valid_relation(self, parent, sub, created=False): + if sub.is_isolated(): + return {'error': _('Isolated instances may not be added or removed from instances groups via the API.')} + if self.parent_model is InstanceGroup: + ig_obj = self.get_parent_object() + if ig_obj.controller_id is not None: + return {'error': _('Isolated instance group membership may not be managed via the API.')} + return None + + def unattach_validate(self, request): + (sub_id, res) = super(InstanceGroupMembershipMixin, self).unattach_validate(request) + if res: + return (sub_id, res) + sub = get_object_or_400(self.model, pk=sub_id) + attach_errors = self.is_valid_relation(None, sub) + if attach_errors: + return (sub_id, Response(attach_errors, status=status.HTTP_400_BAD_REQUEST)) + return (sub_id, res) + def unattach(self, request, *args, **kwargs): response = super(InstanceGroupMembershipMixin, self).unattach(request, *args, **kwargs) - sub_id, res = self.attach_validate(request) if status.is_success(response.status_code): + sub_id = request.data.get('id', None) if self.parent_model is Instance: - ig_obj = get_object_or_400(self.model, pk=sub_id) inst_name = self.get_parent_object().hostname else: - ig_obj = self.get_parent_object() inst_name = get_object_or_400(self.model, pk=sub_id).hostname - if inst_name in ig_obj.policy_instance_list: - ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name)) - ig_obj.save() + with transaction.atomic(): + ig_qs = InstanceGroup.objects.select_for_update() + if self.parent_model is Instance: + ig_obj = get_object_or_400(ig_qs, pk=sub_id) + else: + # similar to get_parent_object, but selected for update + parent_filter = { + self.lookup_field: self.kwargs.get(self.lookup_field, None), + } + ig_obj = get_object_or_404(ig_qs, **parent_filter) + if inst_name in ig_obj.policy_instance_list: + ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name)) + ig_obj.save(update_fields=['policy_instance_list']) return response @@ -227,20 +265,20 @@ class ApiRootView(APIView): versioning_class = None swagger_topic = 'Versioning' + @method_decorator(ensure_csrf_cookie) def get(self, request, format=None): ''' List supported API versions ''' v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'}) v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'}) - data = dict( - description = _('AWX REST API'), - current_version = v2, - available_versions = dict(v1 = v1, v2 = v2), - ) + data = OrderedDict() + data['description'] = _('AWX REST API') + data['current_version'] = v2 + data['available_versions'] = dict(v1 = v1, v2 = v2) + data['oauth2'] = drf_reverse('api:oauth_authorization_root_view') if feature_enabled('rebranding'): data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO - data['oauth2'] = drf_reverse('api:oauth_authorization_root_view') return Response(data) @@ -631,7 +669,6 @@ class InstanceDetail(RetrieveUpdateAPIView): else: obj.capacity = 0 obj.save() - handle_ha_toplogy_changes.apply_async() r.data = InstanceSerializer(obj, context=self.get_serializer_context()).to_representation(obj) return r @@ -640,7 +677,7 @@ class InstanceUnifiedJobsList(SubListAPIView): view_name = _("Instance Jobs") model = UnifiedJob - serializer_class = UnifiedJobSerializer + serializer_class = UnifiedJobListSerializer parent_model = Instance def get_queryset(self): @@ -687,7 +724,7 @@ class InstanceGroupUnifiedJobsList(SubListAPIView): view_name = _("Instance Group Running Jobs") model = UnifiedJob - serializer_class = UnifiedJobSerializer + serializer_class = UnifiedJobListSerializer parent_model = InstanceGroup relationship = "unifiedjob_set" @@ -720,6 +757,7 @@ class SchedulePreview(GenericAPIView): model = Schedule view_name = _('Schedule Recurrence Rule Preview') serializer_class = SchedulePreviewSerializer + permission_classes = (IsAuthenticated,) def post(self, request): serializer = self.get_serializer(data=request.data) @@ -797,7 +835,7 @@ class ScheduleCredentialsList(LaunchConfigCredentialsBase): class ScheduleUnifiedJobsList(SubListAPIView): model = UnifiedJob - serializer_class = UnifiedJobSerializer + serializer_class = UnifiedJobListSerializer parent_model = Schedule relationship = 'unifiedjob_set' view_name = _('Schedule Jobs List') @@ -1055,7 +1093,7 @@ class OrganizationProjectsList(SubListCreateAttachDetachAPIView): class OrganizationWorkflowJobTemplatesList(SubListCreateAttachDetachAPIView): model = WorkflowJobTemplate - serializer_class = WorkflowJobTemplateListSerializer + serializer_class = WorkflowJobTemplateSerializer parent_model = Organization relationship = 'workflows' parent_key = 'organization' @@ -1144,11 +1182,6 @@ class TeamList(ListCreateAPIView): model = Team serializer_class = TeamSerializer - def get_queryset(self): - qs = Team.accessible_objects(self.request.user, 'read_role').order_by() - qs = qs.select_related('admin_role', 'read_role', 'member_role', 'organization') - return qs - class TeamDetail(RetrieveUpdateDestroyAPIView): @@ -1186,8 +1219,8 @@ class TeamRolesList(SubListAttachDetachAPIView): role = get_object_or_400(Role, pk=sub_id) org_content_type = ContentType.objects.get_for_model(Organization) - if role.content_type == org_content_type: - data = dict(msg=_("You cannot assign an Organization role as a child role for a Team.")) + if role.content_type == org_content_type and role.role_field in ['member_role', 'admin_role']: + data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) if role.is_singleton(): @@ -1377,7 +1410,7 @@ class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): class ProjectUpdatesList(SubListAPIView): model = ProjectUpdate - serializer_class = ProjectUpdateSerializer + serializer_class = ProjectUpdateListSerializer parent_model = Project relationship = 'project_updates' @@ -1415,7 +1448,7 @@ class ProjectUpdateList(ListAPIView): class ProjectUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = ProjectUpdate - serializer_class = ProjectUpdateSerializer + serializer_class = ProjectUpdateDetailSerializer class ProjectUpdateEventsList(SubListAPIView): @@ -1488,7 +1521,7 @@ class ProjectUpdateScmInventoryUpdates(SubListCreateAPIView): view_name = _("Project Update SCM Inventory Updates") model = InventoryUpdate - serializer_class = InventoryUpdateSerializer + serializer_class = InventoryUpdateListSerializer parent_model = ProjectUpdate relationship = 'scm_inventory_updates' parent_key = 'source_project_update' @@ -1568,6 +1601,10 @@ class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView): serializer_class = OAuth2ApplicationSerializer swagger_topic = 'Authentication' + def update_raw_data(self, data): + data.pop('client_secret', None) + return super(OAuth2ApplicationDetail, self).update_raw_data(data) + class ApplicationOAuth2TokenList(SubListCreateAPIView): @@ -1610,29 +1647,14 @@ class OAuth2UserTokenList(SubListCreateAPIView): relationship = 'main_oauth2accesstoken' parent_key = 'user' swagger_topic = 'Authentication' - - -class OAuth2AuthorizedTokenList(SubListCreateAPIView): - view_name = _("OAuth2 Authorized Access Tokens") - - model = OAuth2AccessToken - serializer_class = OAuth2AuthorizedTokenSerializer - parent_model = OAuth2Application - relationship = 'oauth2accesstoken_set' - parent_key = 'application' - swagger_topic = 'Authentication' - def get_queryset(self): - return get_access_token_model().objects.filter(application__isnull=False, user=self.request.user) - - class UserAuthorizedTokenList(SubListCreateAPIView): view_name = _("OAuth2 User Authorized Access Tokens") - + model = OAuth2AccessToken - serializer_class = OAuth2AuthorizedTokenSerializer + serializer_class = UserAuthorizedTokenSerializer parent_model = User relationship = 'oauth2accesstoken_set' parent_key = 'user' @@ -1640,12 +1662,12 @@ class UserAuthorizedTokenList(SubListCreateAPIView): def get_queryset(self): return get_access_token_model().objects.filter(application__isnull=False, user=self.request.user) - + class OrganizationApplicationList(SubListCreateAPIView): view_name = _("Organization OAuth2 Applications") - + model = OAuth2Application serializer_class = OAuth2ApplicationSerializer parent_model = Organization @@ -1654,17 +1676,17 @@ class OrganizationApplicationList(SubListCreateAPIView): swagger_topic = 'Authentication' -class OAuth2PersonalTokenList(SubListCreateAPIView): - +class UserPersonalTokenList(SubListCreateAPIView): + view_name = _("OAuth2 Personal Access Tokens") - + model = OAuth2AccessToken - serializer_class = OAuth2PersonalTokenSerializer + serializer_class = UserPersonalTokenSerializer parent_model = User relationship = 'main_oauth2accesstoken' parent_key = 'user' swagger_topic = 'Authentication' - + def get_queryset(self): return get_access_token_model().objects.filter(application__isnull=True, user=self.request.user) @@ -2233,6 +2255,12 @@ class HostDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUp model = Host serializer_class = HostSerializer + def delete(self, request, *args, **kwargs): + if self.get_object().inventory.pending_deletion: + return Response({"error": _("The inventory for this host is already being deleted.")}, + status=status.HTTP_400_BAD_REQUEST) + return super(HostDetail, self).delete(request, *args, **kwargs) + class HostAnsibleFactsDetail(RetrieveAPIView): @@ -2842,7 +2870,7 @@ class InventorySourceGroupsList(SubListDestroyAPIView): class InventorySourceUpdatesList(SubListAPIView): model = InventoryUpdate - serializer_class = InventoryUpdateSerializer + serializer_class = InventoryUpdateListSerializer parent_model = InventorySource relationship = 'inventory_updates' @@ -3011,12 +3039,12 @@ class JobTemplateLaunch(RetrieveAPIView): if fd not in modern_data and id_fd in modern_data: modern_data[fd] = modern_data[id_fd] - # This block causes `extra_credentials` to _always_ be ignored for + # This block causes `extra_credentials` to _always_ raise error if # the launch endpoint if we're accessing `/api/v1/` if get_request_version(self.request) == 1 and 'extra_credentials' in modern_data: - extra_creds = modern_data.pop('extra_credentials', None) - if extra_creds is not None: - ignored_fields['extra_credentials'] = extra_creds + raise ParseError({"extra_credentials": _( + "Field is not allowed for use with v1 API." + )}) # Automatically convert legacy launch credential arguments into a list of `.credentials` if 'credentials' in modern_data and ( @@ -3037,10 +3065,10 @@ class JobTemplateLaunch(RetrieveAPIView): existing_credentials = obj.credentials.all() template_credentials = list(existing_credentials) # save copy of existing new_credentials = [] - for key, conditional in ( - ('credential', lambda cred: cred.credential_type.kind != 'ssh'), - ('vault_credential', lambda cred: cred.credential_type.kind != 'vault'), - ('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net')) + for key, conditional, _type, type_repr in ( + ('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'), + ('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'), + ('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list') ): if key in modern_data: # if a specific deprecated key is specified, remove all @@ -3049,6 +3077,13 @@ class JobTemplateLaunch(RetrieveAPIView): existing_credentials = filter(conditional, existing_credentials) prompted_value = modern_data.pop(key) + # validate type, since these are not covered by a serializer + if not isinstance(prompted_value, _type): + msg = _( + "Incorrect type. Expected {}, received {}." + ).format(type_repr, prompted_value.__class__.__name__) + raise ParseError({key: [msg], 'credentials': [msg]}) + # add the deprecated credential specified in the request if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, basestring): prompted_value = [prompted_value] @@ -3108,7 +3143,8 @@ class JobTemplateLaunch(RetrieveAPIView): data['job'] = new_job.id data['ignored_fields'] = self.sanitize_for_response(ignored_fields) data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) - return Response(data, status=status.HTTP_201_CREATED) + headers = {'Location': new_job.get_absolute_url(request)} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) def sanitize_for_response(self, data): @@ -3320,6 +3356,9 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): if sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]: return {"error": _("Cannot assign multiple {credential_type} credentials.".format( credential_type=sub.unique_hash(display=True)))} + kind = sub.credential_type.kind + if kind not in ('ssh', 'vault', 'cloud', 'net'): + return {'error': _('Cannot assign a Credential of kind `{}`.').format(kind)} return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created) @@ -3713,7 +3752,7 @@ class WorkflowJobNodeAlwaysNodesList(WorkflowJobNodeChildrenBaseList): class WorkflowJobTemplateList(WorkflowsEnforcementMixin, ListCreateAPIView): model = WorkflowJobTemplate - serializer_class = WorkflowJobTemplateListSerializer + serializer_class = WorkflowJobTemplateSerializer always_allow_superuser = False @@ -3730,7 +3769,11 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView): copy_return_serializer_class = WorkflowJobTemplateSerializer def get(self, request, *args, **kwargs): + if get_request_version(request) < 2: + return self.v1_not_allowed() obj = self.get_object() + if not request.user.can_access(obj.__class__, 'read', obj): + raise PermissionDenied() can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj) data = OrderedDict([ ('can_copy', can_copy), ('can_copy_without_user_input', can_copy), @@ -3806,7 +3849,8 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView): data['workflow_job'] = new_job.id data['ignored_fields'] = ignored_fields data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) - return Response(data, status=status.HTTP_201_CREATED) + headers = {'Location': new_job.get_absolute_url(request)} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView): @@ -4022,7 +4066,8 @@ class SystemJobTemplateLaunch(GenericAPIView): data = OrderedDict() data['system_job'] = new_job.id data.update(SystemJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) - return Response(data, status=status.HTTP_201_CREATED) + headers = {'Location': new_job.get_absolute_url(request)} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) class SystemJobTemplateSchedulesList(SubListCreateAPIView): @@ -4094,7 +4139,30 @@ class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView): model = Job metadata_class = JobTypeMetadata - serializer_class = JobSerializer + serializer_class = JobDetailSerializer + + # NOTE: When removing the V1 API in 3.4, delete the following four methods, + # and let this class inherit from RetrieveDestroyAPIView instead of + # RetrieveUpdateDestroyAPIView. + @property + def allowed_methods(self): + methods = super(JobDetail, self).allowed_methods + if get_request_version(getattr(self, 'request', None)) > 1: + methods.remove('PUT') + methods.remove('PATCH') + return methods + + def put(self, request, *args, **kwargs): + if get_request_version(self.request) > 1: + return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + return super(JobDetail, self).put(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + if get_request_version(self.request) > 1: + return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")}, + status=status.HTTP_405_METHOD_NOT_ALLOWED) + return super(JobDetail, self).patch(request, *args, **kwargs) def update(self, request, *args, **kwargs): obj = self.get_object() @@ -4220,7 +4288,6 @@ class JobRelaunch(RetrieveAPIView): data.pop('credential_passwords', None) return data - @csrf_exempt @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): return super(JobRelaunch, self).dispatch(*args, **kwargs) @@ -4466,7 +4533,6 @@ class AdHocCommandList(ListCreateAPIView): serializer_class = AdHocCommandListSerializer always_allow_superuser = False - @csrf_exempt @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): return super(AdHocCommandList, self).dispatch(*args, **kwargs) @@ -4538,7 +4604,7 @@ class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = AdHocCommand - serializer_class = AdHocCommandSerializer + serializer_class = AdHocCommandDetailSerializer class AdHocCommandCancel(RetrieveAPIView): @@ -4564,7 +4630,6 @@ class AdHocCommandRelaunch(GenericAPIView): # FIXME: Figure out why OPTIONS request still shows all fields. - @csrf_exempt @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): return super(AdHocCommandRelaunch, self).dispatch(*args, **kwargs) @@ -5041,8 +5106,8 @@ class RoleTeamsList(SubListAttachDetachAPIView): role = Role.objects.get(pk=self.kwargs['pk']) organization_content_type = ContentType.objects.get_for_model(Organization) - if role.content_type == organization_content_type: - data = dict(msg=_("You cannot assign an Organization role as a child role for a Team.")) + if role.content_type == organization_content_type and role.role_field in ['member_role', 'admin_role']: + data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) credential_content_type = ContentType.objects.get_for_model(Credential) diff --git a/awx/conf/migrations/0005_v330_rename_two_session_settings.py b/awx/conf/migrations/0005_v330_rename_two_session_settings.py new file mode 100644 index 0000000000..4e4b561ec4 --- /dev/null +++ b/awx/conf/migrations/0005_v330_rename_two_session_settings.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.db import migrations +from awx.conf.migrations import _rename_setting + + +def copy_session_settings(apps, schema_editor): + _rename_setting.rename_setting(apps, schema_editor, old_key='AUTH_TOKEN_PER_USER', new_key='SESSIONS_PER_USER') + _rename_setting.rename_setting(apps, schema_editor, old_key='AUTH_TOKEN_EXPIRATION', new_key='SESSION_COOKIE_AGE') + + +def reverse_copy_session_settings(apps, schema_editor): + _rename_setting.rename_setting(apps, schema_editor, old_key='SESSION_COOKIE_AGE', new_key='AUTH_TOKEN_EXPIRATION') + _rename_setting.rename_setting(apps, schema_editor, old_key='SESSIONS_PER_USER', new_key='AUTH_TOKEN_PER_USER') + + +class Migration(migrations.Migration): + + dependencies = [ + ('conf', '0004_v320_reencrypt'), + ] + + operations = [ + migrations.RunPython(copy_session_settings, reverse_copy_session_settings), + ] + \ No newline at end of file diff --git a/awx/conf/migrations/_rename_setting.py b/awx/conf/migrations/_rename_setting.py new file mode 100644 index 0000000000..dbbc347edf --- /dev/null +++ b/awx/conf/migrations/_rename_setting.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import logging +from django.utils.timezone import now +from django.conf import settings + +logger = logging.getLogger('awx.conf.settings') + +__all__ = ['rename_setting'] + + +def rename_setting(apps, schema_editor, old_key, new_key): + + old_setting = None + Setting = apps.get_model('conf', 'Setting') + if Setting.objects.filter(key=new_key).exists() or hasattr(settings, new_key): + logger.info('Setting ' + new_key + ' unexpectedly exists before this migration, it will be replaced by the value of the ' + old_key + ' setting.') + Setting.objects.filter(key=new_key).delete() + # Look for db setting, which wouldn't be picked up by SettingsWrapper because the register method is gone + if Setting.objects.filter(key=old_key).exists(): + old_setting = Setting.objects.filter(key=old_key).last().value + Setting.objects.filter(key=old_key).delete() + # Look for "on-disk" setting (/etc/tower/conf.d) + if hasattr(settings, old_key): + old_setting = getattr(settings, old_key) + if old_setting is not None: + Setting.objects.create(key=new_key, + value=old_setting, + created=now(), + modified=now() + ) + diff --git a/awx/conf/models.py b/awx/conf/models.py index 7dae8bc77e..d37b634fe0 100644 --- a/awx/conf/models.py +++ b/awx/conf/models.py @@ -78,6 +78,14 @@ class Setting(CreatedModifiedModel): def get_cache_id_key(self, key): return '{}_ID'.format(key) + def display_value(self): + if self.key == 'LICENSE' and 'license_key' in self.value: + # don't log the license key in activity stream + value = self.value.copy() + value['license_key'] = '********' + return value + return self.value + import awx.conf.signals # noqa diff --git a/awx/conf/settings.py b/awx/conf/settings.py index 2f7970ec2b..986e09037d 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -15,7 +15,7 @@ from django.conf import LazySettings from django.conf import settings, UserSettingsHolder from django.core.cache import cache as django_cache from django.core.exceptions import ImproperlyConfigured -from django.db import ProgrammingError, OperationalError +from django.db import ProgrammingError, OperationalError, transaction, connection from django.utils.functional import cached_property # Django REST Framework @@ -61,24 +61,66 @@ __all__ = ['SettingsWrapper', 'get_settings_to_cache', 'SETTING_CACHE_NOTSET'] @contextlib.contextmanager -def _log_database_error(): +def _ctit_db_wrapper(trans_safe=False): + ''' + Wrapper to avoid undesired actions by Django ORM when managing settings + if only getting a setting, can use trans_safe=True, which will avoid + throwing errors if the prior context was a broken transaction. + Any database errors will be logged, but exception will be suppressed. + ''' + rollback_set = None + is_atomic = None try: + if trans_safe: + is_atomic = connection.in_atomic_block + if is_atomic: + rollback_set = transaction.get_rollback() + if rollback_set: + logger.debug('Obtaining database settings in spite of broken transaction.') + transaction.set_rollback(False) yield except (ProgrammingError, OperationalError): if 'migrate' in sys.argv and get_tower_migration_version() < '310': logger.info('Using default settings until version 3.1 migration.') else: - # Somewhat ugly - craming the full stack trace into the log message - # the available exc_info does not give information about the real caller - # TODO: replace in favor of stack_info kwarg in python 3 - sio = StringIO.StringIO() - traceback.print_stack(file=sio) - sinfo = sio.getvalue() - sio.close() - sinfo = sinfo.strip('\n') - logger.warning('Database settings are not available, using defaults, logged from:\n{}'.format(sinfo)) + # We want the _full_ traceback with the context + # First we get the current call stack, which constitutes the "top", + # it has the context up to the point where the context manager is used + top_stack = StringIO.StringIO() + traceback.print_stack(file=top_stack) + top_lines = top_stack.getvalue().strip('\n').split('\n') + top_stack.close() + # Get "bottom" stack from the local error that happened + # inside of the "with" block this wraps + exc_type, exc_value, exc_traceback = sys.exc_info() + bottom_stack = StringIO.StringIO() + traceback.print_tb(exc_traceback, file=bottom_stack) + bottom_lines = bottom_stack.getvalue().strip('\n').split('\n') + # Glue together top and bottom where overlap is found + bottom_cutoff = 0 + for i, line in enumerate(bottom_lines): + if line in top_lines: + # start of overlapping section, take overlap from bottom + top_lines = top_lines[:top_lines.index(line)] + bottom_cutoff = i + break + bottom_lines = bottom_lines[bottom_cutoff:] + tb_lines = top_lines + bottom_lines + + tb_string = '\n'.join( + ['Traceback (most recent call last):'] + + tb_lines + + ['{}: {}'.format(exc_type.__name__, str(exc_value))] + ) + bottom_stack.close() + # Log the combined stack + if trans_safe: + logger.warning('Database settings are not available, using defaults, error:\n{}'.format(tb_string)) + else: + logger.error('Error modifying something related to database settings.\n{}'.format(tb_string)) finally: - pass + if trans_safe and is_atomic and rollback_set: + transaction.set_rollback(rollback_set) def filter_sensitive(registry, key, value): @@ -398,7 +440,7 @@ class SettingsWrapper(UserSettingsHolder): def __getattr__(self, name): value = empty if name in self.all_supported_settings: - with _log_database_error(): + with _ctit_db_wrapper(trans_safe=True): value = self._get_local(name) if value is not empty: return value @@ -430,7 +472,7 @@ class SettingsWrapper(UserSettingsHolder): def __setattr__(self, name, value): if name in self.all_supported_settings: - with _log_database_error(): + with _ctit_db_wrapper(): self._set_local(name, value) else: setattr(self.default_settings, name, value) @@ -446,14 +488,14 @@ class SettingsWrapper(UserSettingsHolder): def __delattr__(self, name): if name in self.all_supported_settings: - with _log_database_error(): + with _ctit_db_wrapper(): self._del_local(name) else: delattr(self.default_settings, name) def __dir__(self): keys = [] - with _log_database_error(): + with _ctit_db_wrapper(trans_safe=True): for setting in Setting.objects.filter( key__in=self.all_supported_settings, user__isnull=True): # Skip returning settings that have been overridden but are @@ -470,7 +512,7 @@ class SettingsWrapper(UserSettingsHolder): def is_overridden(self, setting): set_locally = False if setting in self.all_supported_settings: - with _log_database_error(): + with _ctit_db_wrapper(trans_safe=True): set_locally = Setting.objects.filter(key=setting, user__isnull=True).exists() set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting) return (set_locally or set_on_default) diff --git a/awx/locale/django.pot b/awx/locale/django.pot index af37525098..572cf9ef2c 100644 --- a/awx/locale/django.pot +++ b/awx/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-07 21:24+0000\n" +"POT-Creation-Date: 2018-08-03 19:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -28,7 +28,7 @@ msgid "" msgstr "" #: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 -#: awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/api/conf.py:59 awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 #: awx/sso/conf.py:123 msgid "Authentication" msgstr "" @@ -63,7 +63,19 @@ msgid "" "authorization grants in the number of seconds." msgstr "" -#: awx/api/exceptions.py:16 +#: awx/api/conf.py:54 +msgid "Allow External Users to Create OAuth2 Tokens" +msgstr "" + +#: awx/api/conf.py:55 +msgid "" +"For security reasons, users from external auth providers (LDAP, SAML, SSO, " +"Radius, and others) are not allowed to create OAuth2 tokens. To change this " +"behavior, enable this setting. Existing tokens will not be deleted when this " +"setting is toggled off." +msgstr "" + +#: awx/api/exceptions.py:20 msgid "Resource is being used by running jobs." msgstr "" @@ -76,46 +88,52 @@ msgstr "" msgid "Credential {} does not exist" msgstr "" -#: awx/api/filters.py:96 +#: awx/api/filters.py:97 msgid "No related model for field {}." msgstr "" -#: awx/api/filters.py:113 +#: awx/api/filters.py:114 msgid "Filtering on password fields is not allowed." msgstr "" -#: awx/api/filters.py:125 awx/api/filters.py:127 +#: awx/api/filters.py:126 awx/api/filters.py:128 #, python-format msgid "Filtering on %s is not allowed." msgstr "" -#: awx/api/filters.py:130 +#: awx/api/filters.py:131 msgid "Loops not allowed in filters, detected on field {}." msgstr "" -#: awx/api/filters.py:159 +#: awx/api/filters.py:160 msgid "Query string field name not provided." msgstr "" -#: awx/api/filters.py:186 +#: awx/api/filters.py:187 #, python-brace-format msgid "Invalid {field_name} id: {field_id}" msgstr "" -#: awx/api/filters.py:319 +#: awx/api/filters.py:326 #, python-format msgid "cannot filter on kind %s" msgstr "" -#: awx/api/generics.py:600 awx/api/generics.py:662 +#: awx/api/generics.py:197 +msgid "" +"You did not use correct Content-Type in your HTTP request. If you are using " +"our REST API, the Content-Type must be application/json" +msgstr "" + +#: awx/api/generics.py:629 awx/api/generics.py:691 msgid "\"id\" field must be an integer." msgstr "" -#: awx/api/generics.py:659 +#: awx/api/generics.py:688 msgid "\"id\" is required to disassociate" msgstr "" -#: awx/api/generics.py:710 +#: awx/api/generics.py:739 msgid "{} 'id' field is missing." msgstr "" @@ -166,1049 +184,1162 @@ msgid "" "Possible cause: trailing comma." msgstr "" -#: awx/api/serializers.py:153 +#: awx/api/serializers.py:155 msgid "" "The original object is already named {}, a copy from it cannot have the same " "name." msgstr "" -#: awx/api/serializers.py:295 +#: awx/api/serializers.py:290 +#, python-format +msgid "Cannot use dictionary for %s" +msgstr "" + +#: awx/api/serializers.py:307 msgid "Playbook Run" msgstr "" -#: awx/api/serializers.py:296 +#: awx/api/serializers.py:308 msgid "Command" msgstr "" -#: awx/api/serializers.py:297 awx/main/models/unified_jobs.py:525 +#: awx/api/serializers.py:309 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "" -#: awx/api/serializers.py:298 +#: awx/api/serializers.py:310 msgid "Inventory Sync" msgstr "" -#: awx/api/serializers.py:299 +#: awx/api/serializers.py:311 msgid "Management Job" msgstr "" -#: awx/api/serializers.py:300 +#: awx/api/serializers.py:312 msgid "Workflow Job" msgstr "" -#: awx/api/serializers.py:301 +#: awx/api/serializers.py:313 msgid "Workflow Template" msgstr "" -#: awx/api/serializers.py:696 +#: awx/api/serializers.py:314 +msgid "Job Template" +msgstr "" + +#: awx/api/serializers.py:714 msgid "" "Indicates whether all of the events generated by this unified job have been " "saved to the database." msgstr "" -#: awx/api/serializers.py:852 +#: awx/api/serializers.py:879 msgid "Write-only field used to change the password." msgstr "" -#: awx/api/serializers.py:854 +#: awx/api/serializers.py:881 msgid "Set if the account is managed by an external service" msgstr "" -#: awx/api/serializers.py:878 +#: awx/api/serializers.py:905 msgid "Password required for new User." msgstr "" -#: awx/api/serializers.py:969 +#: awx/api/serializers.py:981 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "" -#: awx/api/serializers.py:1169 +#: awx/api/serializers.py:1067 msgid "Must be a simple space-separated string with allowed scopes {}." msgstr "" -#: awx/api/serializers.py:1386 +#: awx/api/serializers.py:1167 +msgid "Authorization Grant Type" +msgstr "" + +#: awx/api/serializers.py:1169 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "" + +#: awx/api/serializers.py:1172 +msgid "Client Type" +msgstr "" + +#: awx/api/serializers.py:1175 +msgid "Redirect URIs" +msgstr "" + +#: awx/api/serializers.py:1178 +msgid "Skip Authorization" +msgstr "" + +#: awx/api/serializers.py:1290 msgid "This path is already being used by another manual project." msgstr "" -#: awx/api/serializers.py:1467 +#: awx/api/serializers.py:1316 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "" + +#: awx/api/serializers.py:1375 msgid "Organization is missing" msgstr "" -#: awx/api/serializers.py:1471 +#: awx/api/serializers.py:1379 msgid "Update options must be set to false for manual projects." msgstr "" -#: awx/api/serializers.py:1477 +#: awx/api/serializers.py:1385 msgid "Array of playbooks available within this project." msgstr "" -#: awx/api/serializers.py:1496 +#: awx/api/serializers.py:1404 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." msgstr "" -#: awx/api/serializers.py:1629 +#: awx/api/serializers.py:1452 awx/api/serializers.py:3247 +#: awx/api/serializers.py:3454 +msgid "A count of hosts uniquely assigned to each status." +msgstr "" + +#: awx/api/serializers.py:1455 awx/api/serializers.py:3250 +msgid "A count of all plays and tasks for the job run." +msgstr "" + +#: awx/api/serializers.py:1570 msgid "Smart inventories must specify host_filter" msgstr "" -#: awx/api/serializers.py:1733 +#: awx/api/serializers.py:1674 #, python-format msgid "Invalid port specification: %s" msgstr "" -#: awx/api/serializers.py:1744 +#: awx/api/serializers.py:1685 msgid "Cannot create Host for Smart Inventory" msgstr "" -#: awx/api/serializers.py:1856 +#: awx/api/serializers.py:1797 msgid "Invalid group name." msgstr "" -#: awx/api/serializers.py:1861 +#: awx/api/serializers.py:1802 msgid "Cannot create Group for Smart Inventory" msgstr "" -#: awx/api/serializers.py:1936 +#: awx/api/serializers.py:1877 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" -#: awx/api/serializers.py:1984 +#: awx/api/serializers.py:1926 msgid "`{}` is a prohibited environment variable" msgstr "" -#: awx/api/serializers.py:1995 +#: awx/api/serializers.py:1937 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "" -#: awx/api/serializers.py:2001 +#: awx/api/serializers.py:1943 msgid "Must provide an inventory." msgstr "" -#: awx/api/serializers.py:2005 +#: awx/api/serializers.py:1947 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" -#: awx/api/serializers.py:2007 +#: awx/api/serializers.py:1949 msgid "'source_script' doesn't exist." msgstr "" -#: awx/api/serializers.py:2041 +#: awx/api/serializers.py:1985 msgid "Automatic group relationship, will be removed in 3.3" msgstr "" -#: awx/api/serializers.py:2127 +#: awx/api/serializers.py:2072 msgid "Cannot use manual project for SCM-based inventory." msgstr "" -#: awx/api/serializers.py:2133 +#: awx/api/serializers.py:2078 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." msgstr "" -#: awx/api/serializers.py:2138 +#: awx/api/serializers.py:2083 msgid "Setting not compatible with existing schedules." msgstr "" -#: awx/api/serializers.py:2143 +#: awx/api/serializers.py:2088 msgid "Cannot create Inventory Source for Smart Inventory" msgstr "" -#: awx/api/serializers.py:2194 +#: awx/api/serializers.py:2139 #, python-format msgid "Cannot set %s if not SCM type." msgstr "" -#: awx/api/serializers.py:2461 +#: awx/api/serializers.py:2414 msgid "Modifications not allowed for managed credential types" msgstr "" -#: awx/api/serializers.py:2466 +#: awx/api/serializers.py:2419 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "" -#: awx/api/serializers.py:2472 +#: awx/api/serializers.py:2425 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "" -#: awx/api/serializers.py:2478 +#: awx/api/serializers.py:2431 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "" -#: awx/api/serializers.py:2656 +#: awx/api/serializers.py:2502 +msgid "Credential Type" +msgstr "" + +#: awx/api/serializers.py:2617 #, python-format msgid "\"%s\" is not a valid choice" msgstr "" -#: awx/api/serializers.py:2675 +#: awx/api/serializers.py:2636 #, python-brace-format msgid "'{field_name}' is not a valid field for {credential_type_name}" msgstr "" -#: awx/api/serializers.py:2696 +#: awx/api/serializers.py:2657 msgid "" "You cannot change the credential type of the credential, as it may break the " "functionality of the resources using it." msgstr "" -#: awx/api/serializers.py:2708 +#: awx/api/serializers.py:2669 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:2713 +#: awx/api/serializers.py:2674 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:2718 +#: awx/api/serializers.py:2679 msgid "" "Inherit permissions from organization roles. If provided on creation, do not " "give either user or team." msgstr "" -#: awx/api/serializers.py:2734 +#: awx/api/serializers.py:2695 msgid "Missing 'user', 'team', or 'organization'." msgstr "" -#: awx/api/serializers.py:2774 +#: awx/api/serializers.py:2735 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" -#: awx/api/serializers.py:2974 +#: awx/api/serializers.py:2936 msgid "You must provide a cloud credential." msgstr "" -#: awx/api/serializers.py:2975 +#: awx/api/serializers.py:2937 msgid "You must provide a network credential." msgstr "" -#: awx/api/serializers.py:2976 awx/main/models/jobs.py:155 +#: awx/api/serializers.py:2938 awx/main/models/jobs.py:155 msgid "You must provide an SSH credential." msgstr "" -#: awx/api/serializers.py:2977 +#: awx/api/serializers.py:2939 msgid "You must provide a vault credential." msgstr "" -#: awx/api/serializers.py:2996 +#: awx/api/serializers.py:2958 msgid "This field is required." msgstr "" -#: awx/api/serializers.py:2998 awx/api/serializers.py:3000 +#: awx/api/serializers.py:2960 awx/api/serializers.py:2962 msgid "Playbook not found for project." msgstr "" -#: awx/api/serializers.py:3002 +#: awx/api/serializers.py:2964 msgid "Must select playbook for project." msgstr "" -#: awx/api/serializers.py:3082 +#: awx/api/serializers.py:3045 msgid "Cannot enable provisioning callback without an inventory set." msgstr "" -#: awx/api/serializers.py:3085 +#: awx/api/serializers.py:3048 msgid "Must either set a default value or ask to prompt on launch." msgstr "" -#: awx/api/serializers.py:3087 awx/main/models/jobs.py:310 +#: awx/api/serializers.py:3050 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "" -#: awx/api/serializers.py:3203 +#: awx/api/serializers.py:3169 msgid "Invalid job template." msgstr "" -#: awx/api/serializers.py:3276 +#: awx/api/serializers.py:3290 msgid "No change to job limit" msgstr "" -#: awx/api/serializers.py:3277 +#: awx/api/serializers.py:3291 msgid "All failed and unreachable hosts" msgstr "" -#: awx/api/serializers.py:3292 +#: awx/api/serializers.py:3306 msgid "Missing passwords needed to start: {}" msgstr "" -#: awx/api/serializers.py:3311 +#: awx/api/serializers.py:3325 msgid "Relaunch by host status not available until job finishes running." msgstr "" -#: awx/api/serializers.py:3325 +#: awx/api/serializers.py:3339 msgid "Job Template Project is missing or undefined." msgstr "" -#: awx/api/serializers.py:3327 +#: awx/api/serializers.py:3341 msgid "Job Template Inventory is missing or undefined." msgstr "" -#: awx/api/serializers.py:3365 +#: awx/api/serializers.py:3379 msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "" -#: awx/api/serializers.py:3432 awx/main/tasks.py:2238 +#: awx/api/serializers.py:3446 awx/main/tasks.py:2297 msgid "{} are prohibited from use in ad hoc commands." msgstr "" -#: awx/api/serializers.py:3501 awx/api/views.py:4763 +#: awx/api/serializers.py:3534 awx/api/views.py:4893 #, python-brace-format msgid "" "Standard Output too large to display ({text_size} bytes), only download " "supported for sizes over {supported_size} bytes." msgstr "" -#: awx/api/serializers.py:3697 +#: awx/api/serializers.py:3727 msgid "Provided variable {} has no database value to replace with." msgstr "" -#: awx/api/serializers.py:3773 +#: awx/api/serializers.py:3745 +#, python-brace-format +msgid "\"$encrypted$ is a reserved keyword, may not be used for {var_name}.\"" +msgstr "" + +#: awx/api/serializers.py:3815 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "" -#: awx/api/serializers.py:3780 awx/api/views.py:776 +#: awx/api/serializers.py:3822 awx/api/views.py:818 msgid "Related template is not configured to accept credentials on launch." msgstr "" -#: awx/api/serializers.py:4234 +#: awx/api/serializers.py:4282 msgid "The inventory associated with this Job Template is being deleted." msgstr "" -#: awx/api/serializers.py:4236 +#: awx/api/serializers.py:4284 msgid "The provided inventory is being deleted." msgstr "" -#: awx/api/serializers.py:4244 +#: awx/api/serializers.py:4292 msgid "Cannot assign multiple {} credentials." msgstr "" -#: awx/api/serializers.py:4257 +#: awx/api/serializers.py:4296 +msgid "Cannot assign a Credential of kind `{}`" +msgstr "" + +#: awx/api/serializers.py:4309 msgid "" "Removing {} credential at launch time without replacement is not supported. " "Provided list lacked credential(s): {}." msgstr "" -#: awx/api/serializers.py:4382 +#: awx/api/serializers.py:4435 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "" -#: awx/api/serializers.py:4405 +#: awx/api/serializers.py:4458 msgid "No values specified for field '{}'" msgstr "" -#: awx/api/serializers.py:4410 +#: awx/api/serializers.py:4463 msgid "Missing required fields for Notification Configuration: {}." msgstr "" -#: awx/api/serializers.py:4413 +#: awx/api/serializers.py:4466 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "" -#: awx/api/serializers.py:4475 +#: awx/api/serializers.py:4528 msgid "" "Valid DTSTART required in rrule. Value should start with: DTSTART:" "YYYYMMDDTHHMMSSZ" msgstr "" -#: awx/api/serializers.py:4477 +#: awx/api/serializers.py:4530 msgid "" "DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "" -#: awx/api/serializers.py:4479 +#: awx/api/serializers.py:4532 msgid "Multiple DTSTART is not supported." msgstr "" -#: awx/api/serializers.py:4481 +#: awx/api/serializers.py:4534 msgid "RRULE required in rrule." msgstr "" -#: awx/api/serializers.py:4483 +#: awx/api/serializers.py:4536 msgid "Multiple RRULE is not supported." msgstr "" -#: awx/api/serializers.py:4485 +#: awx/api/serializers.py:4538 msgid "INTERVAL required in rrule." msgstr "" -#: awx/api/serializers.py:4487 +#: awx/api/serializers.py:4540 msgid "SECONDLY is not supported." msgstr "" -#: awx/api/serializers.py:4489 +#: awx/api/serializers.py:4542 msgid "Multiple BYMONTHDAYs not supported." msgstr "" -#: awx/api/serializers.py:4491 +#: awx/api/serializers.py:4544 msgid "Multiple BYMONTHs not supported." msgstr "" -#: awx/api/serializers.py:4493 +#: awx/api/serializers.py:4546 msgid "BYDAY with numeric prefix not supported." msgstr "" -#: awx/api/serializers.py:4495 +#: awx/api/serializers.py:4548 msgid "BYYEARDAY not supported." msgstr "" -#: awx/api/serializers.py:4497 +#: awx/api/serializers.py:4550 msgid "BYWEEKNO not supported." msgstr "" -#: awx/api/serializers.py:4499 +#: awx/api/serializers.py:4552 msgid "RRULE may not contain both COUNT and UNTIL" msgstr "" -#: awx/api/serializers.py:4503 +#: awx/api/serializers.py:4556 msgid "COUNT > 999 is unsupported." msgstr "" -#: awx/api/serializers.py:4507 +#: awx/api/serializers.py:4560 msgid "rrule parsing failed validation: {}" msgstr "" -#: awx/api/serializers.py:4538 +#: awx/api/serializers.py:4601 msgid "Inventory Source must be a cloud resource." msgstr "" -#: awx/api/serializers.py:4540 +#: awx/api/serializers.py:4603 msgid "Manual Project cannot have a schedule set." msgstr "" -#: awx/api/serializers.py:4553 +#: awx/api/serializers.py:4616 msgid "" "Count of jobs in the running or waiting state that are targeted for this " "instance" msgstr "" -#: awx/api/serializers.py:4593 +#: awx/api/serializers.py:4621 +msgid "Count of all jobs that target this instance" +msgstr "" + +#: awx/api/serializers.py:4654 +msgid "" +"Count of jobs in the running or waiting state that are targeted for this " +"instance group" +msgstr "" + +#: awx/api/serializers.py:4659 +msgid "Count of all jobs that target this instance group" +msgstr "" + +#: awx/api/serializers.py:4667 +msgid "Policy Instance Percentage" +msgstr "" + +#: awx/api/serializers.py:4668 msgid "" "Minimum percentage of all instances that will be automatically assigned to " "this group when new instances come online." msgstr "" -#: awx/api/serializers.py:4598 +#: awx/api/serializers.py:4673 +msgid "Policy Instance Minimum" +msgstr "" + +#: awx/api/serializers.py:4674 msgid "" "Static minimum number of Instances that will be automatically assign to this " "group when new instances come online." msgstr "" -#: awx/api/serializers.py:4603 +#: awx/api/serializers.py:4679 +msgid "Policy Instance List" +msgstr "" + +#: awx/api/serializers.py:4680 msgid "List of exact-match Instances that will be assigned to this group" msgstr "" -#: awx/api/serializers.py:4624 +#: awx/api/serializers.py:4702 msgid "Duplicate entry {}." msgstr "" -#: awx/api/serializers.py:4626 +#: awx/api/serializers.py:4704 msgid "{} is not a valid hostname of an existing instance." msgstr "" -#: awx/api/serializers.py:4631 +#: awx/api/serializers.py:4706 awx/api/views.py:202 +msgid "" +"Isolated instances may not be added or removed from instances groups via the " +"API." +msgstr "" + +#: awx/api/serializers.py:4708 awx/api/views.py:206 +msgid "Isolated instance group membership may not be managed via the API." +msgstr "" + +#: awx/api/serializers.py:4713 msgid "tower instance group name may not be changed." msgstr "" -#: awx/api/serializers.py:4711 +#: awx/api/serializers.py:4783 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "" -#: awx/api/serializers.py:4713 +#: awx/api/serializers.py:4785 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 "" -#: awx/api/serializers.py:4716 +#: awx/api/serializers.py:4788 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 "" -#: awx/api/serializers.py:4719 +#: awx/api/serializers.py:4791 msgid "The action taken with respect to the given object(s)." msgstr "" -#: awx/api/views.py:116 +#: awx/api/views.py:119 msgid "Your license does not allow use of the activity stream." msgstr "" -#: awx/api/views.py:126 +#: awx/api/views.py:129 msgid "Your license does not permit use of system tracking." msgstr "" -#: awx/api/views.py:136 +#: awx/api/views.py:139 msgid "Your license does not allow use of workflows." msgstr "" -#: awx/api/views.py:150 +#: awx/api/views.py:153 msgid "Cannot delete job resource when associated workflow job is running." msgstr "" -#: awx/api/views.py:155 +#: awx/api/views.py:158 msgid "Cannot delete running job resource." msgstr "" -#: awx/api/views.py:160 +#: awx/api/views.py:163 msgid "Job has not finished processing events." msgstr "" -#: awx/api/views.py:219 +#: awx/api/views.py:257 msgid "Related job {} is still processing events." msgstr "" -#: awx/api/views.py:226 awx/templates/rest_framework/api.html:28 +#: awx/api/views.py:264 awx/templates/rest_framework/api.html:28 msgid "REST API" msgstr "" -#: awx/api/views.py:236 awx/templates/rest_framework/api.html:4 +#: awx/api/views.py:275 awx/templates/rest_framework/api.html:4 msgid "AWX REST API" msgstr "" -#: awx/api/views.py:250 +#: awx/api/views.py:288 msgid "API OAuth 2 Authorization Root" msgstr "" -#: awx/api/views.py:315 +#: awx/api/views.py:353 msgid "Version 1" msgstr "" -#: awx/api/views.py:319 +#: awx/api/views.py:357 msgid "Version 2" msgstr "" -#: awx/api/views.py:328 +#: awx/api/views.py:366 msgid "Ping" msgstr "" -#: awx/api/views.py:359 awx/conf/apps.py:10 +#: awx/api/views.py:397 awx/conf/apps.py:10 msgid "Configuration" msgstr "" -#: awx/api/views.py:414 +#: awx/api/views.py:454 msgid "Invalid license data" msgstr "" -#: awx/api/views.py:416 +#: awx/api/views.py:456 msgid "Missing 'eula_accepted' property" msgstr "" -#: awx/api/views.py:420 +#: awx/api/views.py:460 msgid "'eula_accepted' value is invalid" msgstr "" -#: awx/api/views.py:423 +#: awx/api/views.py:463 msgid "'eula_accepted' must be True" msgstr "" -#: awx/api/views.py:430 +#: awx/api/views.py:470 msgid "Invalid JSON" msgstr "" -#: awx/api/views.py:438 +#: awx/api/views.py:478 msgid "Invalid License" msgstr "" -#: awx/api/views.py:448 +#: awx/api/views.py:488 msgid "Invalid license" msgstr "" -#: awx/api/views.py:456 +#: awx/api/views.py:496 #, python-format msgid "Failed to remove license (%s)" msgstr "" -#: awx/api/views.py:461 +#: awx/api/views.py:501 msgid "Dashboard" msgstr "" -#: awx/api/views.py:560 +#: awx/api/views.py:600 msgid "Dashboard Jobs Graphs" msgstr "" -#: awx/api/views.py:596 +#: awx/api/views.py:636 #, python-format msgid "Unknown period \"%s\"" msgstr "" -#: awx/api/views.py:610 +#: awx/api/views.py:650 msgid "Instances" msgstr "" -#: awx/api/views.py:617 +#: awx/api/views.py:658 msgid "Instance Detail" msgstr "" -#: awx/api/views.py:638 +#: awx/api/views.py:678 msgid "Instance Jobs" msgstr "" -#: awx/api/views.py:652 +#: awx/api/views.py:692 msgid "Instance's Instance Groups" msgstr "" -#: awx/api/views.py:661 +#: awx/api/views.py:701 msgid "Instance Groups" msgstr "" -#: awx/api/views.py:669 +#: awx/api/views.py:709 msgid "Instance Group Detail" msgstr "" -#: awx/api/views.py:677 +#: awx/api/views.py:717 msgid "Isolated Groups can not be removed from the API" msgstr "" -#: awx/api/views.py:679 +#: awx/api/views.py:719 msgid "" "Instance Groups acting as a controller for an Isolated Group can not be " "removed from the API" msgstr "" -#: awx/api/views.py:685 +#: awx/api/views.py:725 msgid "Instance Group Running Jobs" msgstr "" -#: awx/api/views.py:694 +#: awx/api/views.py:734 msgid "Instance Group's Instances" msgstr "" -#: awx/api/views.py:703 +#: awx/api/views.py:744 msgid "Schedules" msgstr "" -#: awx/api/views.py:717 +#: awx/api/views.py:758 msgid "Schedule Recurrence Rule Preview" msgstr "" -#: awx/api/views.py:763 +#: awx/api/views.py:805 msgid "Cannot assign credential when related template is null." msgstr "" -#: awx/api/views.py:768 +#: awx/api/views.py:810 msgid "Related template cannot accept {} on launch." msgstr "" -#: awx/api/views.py:770 +#: awx/api/views.py:812 msgid "" "Credential that requires user input on launch cannot be used in saved launch " "configuration." msgstr "" -#: awx/api/views.py:778 +#: awx/api/views.py:820 #, python-brace-format msgid "" "This launch configuration already provides a {credential_type} credential." msgstr "" -#: awx/api/views.py:781 +#: awx/api/views.py:823 #, python-brace-format msgid "Related template already uses {credential_type} credential." msgstr "" -#: awx/api/views.py:799 +#: awx/api/views.py:841 msgid "Schedule Jobs List" msgstr "" -#: awx/api/views.py:954 +#: awx/api/views.py:996 msgid "Your license only permits a single organization to exist." msgstr "" -#: awx/api/views.py:1183 awx/api/views.py:4972 -msgid "You cannot assign an Organization role as a child role for a Team." +#: awx/api/views.py:1223 awx/api/views.py:5106 +msgid "" +"You cannot assign an Organization participation role as a child role for a " +"Team." msgstr "" -#: awx/api/views.py:1187 awx/api/views.py:4986 +#: awx/api/views.py:1227 awx/api/views.py:5120 msgid "You cannot grant system-level permissions to a team." msgstr "" -#: awx/api/views.py:1194 awx/api/views.py:4978 +#: awx/api/views.py:1234 awx/api/views.py:5112 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" msgstr "" -#: awx/api/views.py:1306 +#: awx/api/views.py:1348 msgid "Project Schedules" msgstr "" -#: awx/api/views.py:1317 +#: awx/api/views.py:1359 msgid "Project SCM Inventory Sources" msgstr "" -#: awx/api/views.py:1417 +#: awx/api/views.py:1460 msgid "Project Update Events List" msgstr "" -#: awx/api/views.py:1430 +#: awx/api/views.py:1474 msgid "System Job Events List" msgstr "" -#: awx/api/views.py:1443 +#: awx/api/views.py:1488 msgid "Inventory Update Events List" msgstr "" -#: awx/api/views.py:1475 +#: awx/api/views.py:1522 msgid "Project Update SCM Inventory Updates" msgstr "" -#: awx/api/views.py:1533 +#: awx/api/views.py:1581 msgid "Me" msgstr "" -#: awx/api/views.py:1541 +#: awx/api/views.py:1589 msgid "OAuth 2 Applications" msgstr "" -#: awx/api/views.py:1550 +#: awx/api/views.py:1598 msgid "OAuth 2 Application Detail" msgstr "" -#: awx/api/views.py:1559 +#: awx/api/views.py:1607 msgid "OAuth 2 Application Tokens" msgstr "" -#: awx/api/views.py:1580 +#: awx/api/views.py:1629 msgid "OAuth2 Tokens" msgstr "" -#: awx/api/views.py:1589 -msgid "OAuth2 Authorized Access Tokens" +#: awx/api/views.py:1638 +msgid "OAuth2 User Tokens" msgstr "" -#: awx/api/views.py:1604 +#: awx/api/views.py:1650 msgid "OAuth2 User Authorized Access Tokens" msgstr "" -#: awx/api/views.py:1619 +#: awx/api/views.py:1665 msgid "Organization OAuth2 Applications" msgstr "" -#: awx/api/views.py:1631 +#: awx/api/views.py:1677 msgid "OAuth2 Personal Access Tokens" msgstr "" -#: awx/api/views.py:1646 +#: awx/api/views.py:1692 msgid "OAuth Token Detail" msgstr "" -#: awx/api/views.py:1704 awx/api/views.py:4939 +#: awx/api/views.py:1752 awx/api/views.py:5073 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" msgstr "" -#: awx/api/views.py:1708 awx/api/views.py:4943 +#: awx/api/views.py:1756 awx/api/views.py:5077 msgid "You cannot grant private credential access to another user" msgstr "" -#: awx/api/views.py:1805 +#: awx/api/views.py:1854 #, python-format msgid "Cannot change %s." msgstr "" -#: awx/api/views.py:1811 +#: awx/api/views.py:1860 msgid "Cannot delete user." msgstr "" -#: awx/api/views.py:1835 +#: awx/api/views.py:1884 msgid "Deletion not allowed for managed credential types" msgstr "" -#: awx/api/views.py:1837 +#: awx/api/views.py:1886 msgid "Credential types that are in use cannot be deleted" msgstr "" -#: awx/api/views.py:2009 +#: awx/api/views.py:2061 msgid "Cannot delete inventory script." msgstr "" -#: awx/api/views.py:2099 +#: awx/api/views.py:2152 #, python-brace-format msgid "{0}" msgstr "" -#: awx/api/views.py:2326 +#: awx/api/views.py:2256 +msgid "The inventory for this host is already being deleted." +msgstr "" + +#: awx/api/views.py:2389 msgid "Fact not found." msgstr "" -#: awx/api/views.py:2348 +#: awx/api/views.py:2411 msgid "SSLError while trying to connect to {}" msgstr "" -#: awx/api/views.py:2350 +#: awx/api/views.py:2413 msgid "Request to {} timed out." msgstr "" -#: awx/api/views.py:2352 +#: awx/api/views.py:2415 msgid "Unknown exception {} while trying to GET {}" msgstr "" -#: awx/api/views.py:2355 +#: awx/api/views.py:2418 msgid "" "Unauthorized access. Please check your Insights Credential username and " "password." msgstr "" -#: awx/api/views.py:2358 +#: awx/api/views.py:2421 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" msgstr "" -#: awx/api/views.py:2365 +#: awx/api/views.py:2428 msgid "Expected JSON response from Insights but instead got {}" msgstr "" -#: awx/api/views.py:2372 +#: awx/api/views.py:2435 msgid "This host is not recognized as an Insights host." msgstr "" -#: awx/api/views.py:2377 +#: awx/api/views.py:2440 msgid "The Insights Credential for \"{}\" was not found." msgstr "" -#: awx/api/views.py:2445 +#: awx/api/views.py:2508 msgid "Cyclical Group association." msgstr "" -#: awx/api/views.py:2658 +#: awx/api/views.py:2722 msgid "Inventory Source List" msgstr "" -#: awx/api/views.py:2670 +#: awx/api/views.py:2734 msgid "Inventory Sources Update" msgstr "" -#: awx/api/views.py:2703 +#: awx/api/views.py:2767 msgid "Could not start because `can_update` returned False" msgstr "" -#: awx/api/views.py:2711 +#: awx/api/views.py:2775 msgid "No inventory sources to update." msgstr "" -#: awx/api/views.py:2740 +#: awx/api/views.py:2804 msgid "Inventory Source Schedules" msgstr "" -#: awx/api/views.py:2767 +#: awx/api/views.py:2832 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" -#: awx/api/views.py:2822 +#: awx/api/views.py:2887 msgid "Vault credentials are not yet supported for inventory sources." msgstr "" -#: awx/api/views.py:2827 +#: awx/api/views.py:2892 msgid "Source already has cloud credential assigned." msgstr "" -#: awx/api/views.py:2986 +#: awx/api/views.py:3042 +msgid "Field is not allowed for use with v1 API." +msgstr "" + +#: awx/api/views.py:3052 msgid "" "'credentials' cannot be used in combination with 'credential', " "'vault_credential', or 'extra_credentials'." msgstr "" -#: awx/api/views.py:3098 +#: awx/api/views.py:3079 +msgid "Incorrect type. Expected {}, received {}." +msgstr "" + +#: awx/api/views.py:3172 msgid "Job Template Schedules" msgstr "" -#: awx/api/views.py:3116 awx/api/views.py:3127 +#: awx/api/views.py:3190 awx/api/views.py:3201 msgid "Your license does not allow adding surveys." msgstr "" -#: awx/api/views.py:3146 +#: awx/api/views.py:3220 msgid "Field '{}' is missing from survey spec." msgstr "" -#: awx/api/views.py:3148 +#: awx/api/views.py:3222 msgid "Expected {} for field '{}', received {} type." msgstr "" -#: awx/api/views.py:3152 +#: awx/api/views.py:3226 msgid "'spec' doesn't contain any items." msgstr "" -#: awx/api/views.py:3161 +#: awx/api/views.py:3235 #, python-format msgid "Survey question %s is not a json object." msgstr "" -#: awx/api/views.py:3163 +#: awx/api/views.py:3237 #, python-format msgid "'type' missing from survey question %s." msgstr "" -#: awx/api/views.py:3165 +#: awx/api/views.py:3239 #, python-format msgid "'question_name' missing from survey question %s." msgstr "" -#: awx/api/views.py:3167 +#: awx/api/views.py:3241 #, python-format msgid "'variable' missing from survey question %s." msgstr "" -#: awx/api/views.py:3169 +#: awx/api/views.py:3243 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "" -#: awx/api/views.py:3174 +#: awx/api/views.py:3248 #, python-format msgid "'required' missing from survey question %s." msgstr "" -#: awx/api/views.py:3179 +#: awx/api/views.py:3253 #, python-brace-format msgid "Value {question_default} for '{variable_name}' expected to be a string." msgstr "" -#: awx/api/views.py:3189 +#: awx/api/views.py:3263 #, python-brace-format msgid "" "$encrypted$ is a reserved keyword for password question defaults, survey " "question {question_position} is type {question_type}." msgstr "" -#: awx/api/views.py:3205 +#: awx/api/views.py:3279 #, python-brace-format msgid "" "$encrypted$ is a reserved keyword, may not be used for new default in " "position {question_position}." msgstr "" -#: awx/api/views.py:3278 +#: awx/api/views.py:3353 #, python-brace-format msgid "Cannot assign multiple {credential_type} credentials." msgstr "" -#: awx/api/views.py:3296 +#: awx/api/views.py:3357 +msgid "Cannot assign a Credential of kind `{}`." +msgstr "" + +#: awx/api/views.py:3374 msgid "Extra credentials must be network or cloud." msgstr "" -#: awx/api/views.py:3318 +#: awx/api/views.py:3396 msgid "Maximum number of labels for {} reached." msgstr "" -#: awx/api/views.py:3441 +#: awx/api/views.py:3519 msgid "No matching host could be found!" msgstr "" -#: awx/api/views.py:3444 +#: awx/api/views.py:3522 msgid "Multiple hosts matched the request!" msgstr "" -#: awx/api/views.py:3449 +#: awx/api/views.py:3527 msgid "Cannot start automatically, user input required!" msgstr "" -#: awx/api/views.py:3456 +#: awx/api/views.py:3534 msgid "Host callback job already pending." msgstr "" -#: awx/api/views.py:3471 awx/api/views.py:4212 +#: awx/api/views.py:3549 awx/api/views.py:4336 msgid "Error starting job!" msgstr "" -#: awx/api/views.py:3587 +#: awx/api/views.py:3669 #, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr "" -#: awx/api/views.py:3612 +#: awx/api/views.py:3694 msgid "Multiple parent relationship not allowed." msgstr "" -#: awx/api/views.py:3617 +#: awx/api/views.py:3699 msgid "Cycle detected." msgstr "" -#: awx/api/views.py:3807 +#: awx/api/views.py:3902 msgid "Workflow Job Template Schedules" msgstr "" -#: awx/api/views.py:3938 awx/api/views.py:4610 +#: awx/api/views.py:4038 awx/api/views.py:4740 msgid "Superuser privileges needed." msgstr "" -#: awx/api/views.py:3970 +#: awx/api/views.py:4071 msgid "System Job Template Schedules" msgstr "" -#: awx/api/views.py:4028 +#: awx/api/views.py:4129 msgid "POST not allowed for Job launching in version 2 of the api" msgstr "" -#: awx/api/views.py:4195 +#: awx/api/views.py:4153 awx/api/views.py:4159 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "" + +#: awx/api/views.py:4319 #, python-brace-format msgid "Wait until job finishes before retrying on {status_value} hosts." msgstr "" -#: awx/api/views.py:4200 +#: awx/api/views.py:4324 #, python-brace-format msgid "Cannot retry on {status_value} hosts, playbook stats not available." msgstr "" -#: awx/api/views.py:4205 +#: awx/api/views.py:4329 #, python-brace-format msgid "Cannot relaunch because previous job had 0 {status_value} hosts." msgstr "" -#: awx/api/views.py:4234 +#: awx/api/views.py:4358 msgid "Cannot create schedule because job requires credential passwords." msgstr "" -#: awx/api/views.py:4239 +#: awx/api/views.py:4363 msgid "Cannot create schedule because job was launched by legacy method." msgstr "" -#: awx/api/views.py:4241 +#: awx/api/views.py:4365 msgid "Cannot create schedule because a related resource is missing." msgstr "" -#: awx/api/views.py:4295 +#: awx/api/views.py:4420 msgid "Job Host Summaries List" msgstr "" -#: awx/api/views.py:4342 +#: awx/api/views.py:4469 msgid "Job Event Children List" msgstr "" -#: awx/api/views.py:4351 +#: awx/api/views.py:4479 msgid "Job Event Hosts List" msgstr "" -#: awx/api/views.py:4360 +#: awx/api/views.py:4488 msgid "Job Events List" msgstr "" -#: awx/api/views.py:4570 +#: awx/api/views.py:4697 msgid "Ad Hoc Command Events List" msgstr "" -#: awx/api/views.py:4809 +#: awx/api/views.py:4939 msgid "Delete not allowed while there are pending notifications" msgstr "" -#: awx/api/views.py:4817 +#: awx/api/views.py:4947 msgid "Notification Template Test" msgstr "" @@ -1364,7 +1495,7 @@ msgstr "" msgid "User" msgstr "" -#: awx/conf/fields.py:60 awx/sso/fields.py:583 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 #, python-brace-format msgid "" "Expected None, True, False, a string or list of strings but got {input_type} " @@ -1475,9 +1606,9 @@ msgstr "" #: awx/conf/tests/unit/test_settings.py:411 #: awx/conf/tests/unit/test_settings.py:430 #: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:52 -#: awx/main/conf.py:61 awx/main/conf.py:73 awx/main/conf.py:86 -#: awx/main/conf.py:99 awx/main/conf.py:124 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "" @@ -1501,92 +1632,96 @@ msgstr "" msgid "Logging Connectivity Test" msgstr "" -#: awx/main/access.py:57 +#: awx/main/access.py:59 #, python-format msgid "Required related field %s for permission check." msgstr "" -#: awx/main/access.py:73 +#: awx/main/access.py:75 #, python-format msgid "Bad data found in related field %s." msgstr "" -#: awx/main/access.py:293 +#: awx/main/access.py:304 msgid "License is missing." msgstr "" -#: awx/main/access.py:295 +#: awx/main/access.py:306 msgid "License has expired." msgstr "" -#: awx/main/access.py:303 +#: awx/main/access.py:314 #, python-format msgid "License count of %s instances has been reached." msgstr "" -#: awx/main/access.py:305 +#: awx/main/access.py:316 #, python-format msgid "License count of %s instances has been exceeded." msgstr "" -#: awx/main/access.py:307 +#: awx/main/access.py:318 msgid "Host count exceeds available instances." msgstr "" -#: awx/main/access.py:311 +#: awx/main/access.py:322 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "" -#: awx/main/access.py:313 +#: awx/main/access.py:324 msgid "Features not found in active license." msgstr "" -#: awx/main/access.py:823 +#: awx/main/access.py:837 msgid "Unable to change inventory on a host." msgstr "" -#: awx/main/access.py:840 awx/main/access.py:885 +#: awx/main/access.py:854 awx/main/access.py:899 msgid "Cannot associate two items from different inventories." msgstr "" -#: awx/main/access.py:873 +#: awx/main/access.py:887 msgid "Unable to change inventory on a group." msgstr "" -#: awx/main/access.py:1131 +#: awx/main/access.py:1148 msgid "Unable to change organization on a team." msgstr "" -#: awx/main/access.py:1148 +#: awx/main/access.py:1165 msgid "The {} role cannot be assigned to a team" msgstr "" -#: awx/main/access.py:1150 +#: awx/main/access.py:1167 msgid "The admin_role for a User cannot be assigned to a team" msgstr "" -#: awx/main/access.py:1517 +#: awx/main/access.py:1533 awx/main/access.py:1967 +msgid "Job was launched with prompts provided by another user." +msgstr "" + +#: awx/main/access.py:1553 msgid "Job has been orphaned from its job template." msgstr "" -#: awx/main/access.py:1519 +#: awx/main/access.py:1555 msgid "Job was launched with unknown prompted fields." msgstr "" -#: awx/main/access.py:1521 +#: awx/main/access.py:1557 msgid "Job was launched with prompted fields." msgstr "" -#: awx/main/access.py:1523 +#: awx/main/access.py:1559 msgid " Organization level permissions required." msgstr "" -#: awx/main/access.py:1525 +#: awx/main/access.py:1561 msgid " You do not have permission to related resources." msgstr "" -#: awx/main/access.py:1935 +#: awx/main/access.py:1981 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1620,56 +1755,56 @@ msgstr "" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." -msgstr "" - -#: awx/main/conf.py:49 -msgid "Organization Admins Can Manage Users and Teams" +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." msgstr "" #: awx/main/conf.py:50 +msgid "Organization Admins Can Manage Users and Teams" +msgstr "" + +#: awx/main/conf.py:51 msgid "" "Controls whether any Organization Admin has the privileges to create and " "manage users and teams. You may want to disable this ability if you are " "using an LDAP or SAML integration." msgstr "" -#: awx/main/conf.py:59 +#: awx/main/conf.py:60 msgid "Enable Administrator Alerts" msgstr "" -#: awx/main/conf.py:60 +#: awx/main/conf.py:61 msgid "Email Admin users for system events that may require attention." msgstr "" -#: awx/main/conf.py:70 +#: awx/main/conf.py:71 msgid "Base URL of the Tower host" msgstr "" -#: awx/main/conf.py:71 +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to " "the Tower host." msgstr "" -#: awx/main/conf.py:80 +#: awx/main/conf.py:81 msgid "Remote Host Headers" msgstr "" -#: awx/main/conf.py:81 +#: awx/main/conf.py:82 msgid "" "HTTP headers and meta keys to search to determine remote host name or IP. " "Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " "behind a reverse proxy. See the \"Proxy Support\" section of the " -"Adminstrator guide formore details." -msgstr "" - -#: awx/main/conf.py:93 -msgid "Proxy IP Whitelist" +"Adminstrator guide for more details." msgstr "" #: awx/main/conf.py:94 +msgid "Proxy IP Whitelist" +msgstr "" + +#: awx/main/conf.py:95 msgid "" "If Tower is behind a reverse proxy/load balancer, use this setting to " "whitelist the proxy IP addresses from which Tower should trust custom " @@ -1678,51 +1813,51 @@ msgid "" "unconditionally')" msgstr "" -#: awx/main/conf.py:120 +#: awx/main/conf.py:121 msgid "License" msgstr "" -#: awx/main/conf.py:121 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use /api/" "v1/config/ to update or change the license." msgstr "" -#: awx/main/conf.py:131 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "" -#: awx/main/conf.py:132 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "" -#: awx/main/conf.py:133 awx/main/conf.py:155 awx/main/conf.py:164 -#: awx/main/conf.py:175 awx/main/conf.py:185 awx/main/conf.py:195 -#: awx/main/conf.py:205 awx/main/conf.py:216 awx/main/conf.py:228 -#: awx/main/conf.py:240 awx/main/conf.py:253 awx/main/conf.py:265 -#: awx/main/conf.py:275 awx/main/conf.py:286 awx/main/conf.py:297 -#: awx/main/conf.py:307 awx/main/conf.py:317 awx/main/conf.py:329 -#: awx/main/conf.py:341 awx/main/conf.py:353 awx/main/conf.py:367 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "" -#: awx/main/conf.py:142 +#: awx/main/conf.py:143 msgid "Always" msgstr "" -#: awx/main/conf.py:143 +#: awx/main/conf.py:144 msgid "Never" msgstr "" -#: awx/main/conf.py:144 +#: awx/main/conf.py:145 msgid "Only On Job Template Definitions" msgstr "" -#: awx/main/conf.py:147 +#: awx/main/conf.py:148 msgid "When can extra variables contain Jinja templates?" msgstr "" -#: awx/main/conf.py:149 +#: awx/main/conf.py:150 msgid "" "Ansible allows variable substitution via the Jinja2 templating language for " "--extra-vars. This poses a potential security risk where Tower users with " @@ -1731,185 +1866,185 @@ msgid "" "to \"template\" or \"never\"." msgstr "" -#: awx/main/conf.py:162 +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "" -#: awx/main/conf.py:163 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." msgstr "" -#: awx/main/conf.py:171 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "" -#: awx/main/conf.py:172 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " "scripts)." msgstr "" -#: awx/main/conf.py:183 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" msgstr "" -#: awx/main/conf.py:184 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." msgstr "" -#: awx/main/conf.py:193 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" msgstr "" -#: awx/main/conf.py:194 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." msgstr "" -#: awx/main/conf.py:203 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "" -#: awx/main/conf.py:204 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." msgstr "" -#: awx/main/conf.py:213 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "" -#: awx/main/conf.py:214 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " "isolated instance." msgstr "" -#: awx/main/conf.py:225 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "" -#: awx/main/conf.py:226 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " "network latency." msgstr "" -#: awx/main/conf.py:236 +#: awx/main/conf.py:237 msgid "Generate RSA keys for isolated instances" msgstr "" -#: awx/main/conf.py:237 +#: awx/main/conf.py:238 msgid "" "If set, a random RSA key will be generated and distributed to isolated " "instances. To disable this behavior and manage authentication for isolated " "instances outside of Tower, disable this setting." msgstr "" -#: awx/main/conf.py:251 awx/main/conf.py:252 +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "" -#: awx/main/conf.py:263 awx/main/conf.py:264 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "" -#: awx/main/conf.py:273 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "" -#: awx/main/conf.py:274 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." msgstr "" -#: awx/main/conf.py:284 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:285 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." msgstr "" -#: awx/main/conf.py:294 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:296 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." msgstr "" -#: awx/main/conf.py:305 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" msgstr "" -#: awx/main/conf.py:306 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." msgstr "" -#: awx/main/conf.py:315 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "" -#: awx/main/conf.py:316 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." msgstr "" -#: awx/main/conf.py:326 +#: awx/main/conf.py:327 msgid "Default Job Timeout" msgstr "" -#: awx/main/conf.py:327 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " "template will override this." msgstr "" -#: awx/main/conf.py:338 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "" -#: awx/main/conf.py:339 +#: awx/main/conf.py:340 msgid "" "Maximum time in seconds to allow inventory updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " "inventory source will override this." msgstr "" -#: awx/main/conf.py:350 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" msgstr "" -#: awx/main/conf.py:351 +#: awx/main/conf.py:352 msgid "" "Maximum time in seconds to allow project updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " "project will override this." msgstr "" -#: awx/main/conf.py:362 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "" -#: awx/main/conf.py:363 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be " @@ -1918,62 +2053,62 @@ msgid "" "timeout should be imposed." msgstr "" -#: awx/main/conf.py:376 +#: awx/main/conf.py:377 msgid "Logging Aggregator" msgstr "" -#: awx/main/conf.py:377 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." msgstr "" -#: awx/main/conf.py:378 awx/main/conf.py:389 awx/main/conf.py:401 -#: awx/main/conf.py:411 awx/main/conf.py:423 awx/main/conf.py:438 -#: awx/main/conf.py:450 awx/main/conf.py:459 awx/main/conf.py:469 -#: awx/main/conf.py:479 awx/main/conf.py:490 awx/main/conf.py:502 -#: awx/main/conf.py:515 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:482 awx/main/conf.py:493 awx/main/conf.py:505 +#: awx/main/conf.py:518 msgid "Logging" msgstr "" -#: awx/main/conf.py:386 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "" -#: awx/main/conf.py:387 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." msgstr "" -#: awx/main/conf.py:399 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" msgstr "" -#: awx/main/conf.py:400 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." msgstr "" -#: awx/main/conf.py:409 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "" -#: awx/main/conf.py:410 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "" -#: awx/main/conf.py:421 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "" -#: awx/main/conf.py:422 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "" -#: awx/main/conf.py:431 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "" -#: awx/main/conf.py:432 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include " "any or all of: \n" @@ -1983,11 +2118,11 @@ msgid "" "system_tracking - facts gathered from scan jobs." msgstr "" -#: awx/main/conf.py:445 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" msgstr "" -#: awx/main/conf.py:446 +#: awx/main/conf.py:447 msgid "" "If set, system tracking facts will be sent for each package, service, or " "other item found in a scan, allowing for greater search query granularity. " @@ -1995,45 +2130,47 @@ msgid "" "efficiency in fact processing." msgstr "" -#: awx/main/conf.py:457 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "" -#: awx/main/conf.py:458 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "" -#: awx/main/conf.py:467 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "" -#: awx/main/conf.py:468 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." msgstr "" -#: awx/main/conf.py:477 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "" -#: awx/main/conf.py:478 -msgid "Protocol used to communicate with log aggregator." +#: awx/main/conf.py:479 +msgid "" +"Protocol used to communicate with log aggregator. HTTPS/HTTP assumes HTTPS " +"unless http:// is explicitly used in the Logging Aggregator hostname." msgstr "" -#: awx/main/conf.py:486 +#: awx/main/conf.py:489 msgid "TCP Connection Timeout" msgstr "" -#: awx/main/conf.py:487 +#: awx/main/conf.py:490 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." msgstr "" -#: awx/main/conf.py:497 +#: awx/main/conf.py:500 msgid "Enable/disable HTTPS certificate verification" msgstr "" -#: awx/main/conf.py:498 +#: awx/main/conf.py:501 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -2041,11 +2178,11 @@ msgid "" "connection." msgstr "" -#: awx/main/conf.py:510 +#: awx/main/conf.py:513 msgid "Logging Aggregator Level Threshold" msgstr "" -#: awx/main/conf.py:511 +#: awx/main/conf.py:514 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -2053,7 +2190,7 @@ msgid "" "anlytics ignore this setting)" msgstr "" -#: awx/main/conf.py:534 awx/sso/conf.py:1262 +#: awx/main/conf.py:537 awx/sso/conf.py:1264 msgid "\n" msgstr "" @@ -2102,99 +2239,108 @@ msgstr "" msgid "'{value}' is not one of ['{allowed_values}']" msgstr "" -#: awx/main/fields.py:418 +#: awx/main/fields.py:421 #, python-brace-format msgid "{type} provided in relative path {path}, expected {expected_type}" msgstr "" -#: awx/main/fields.py:423 +#: awx/main/fields.py:426 #, python-brace-format msgid "{type} provided, expected {expected_type}" msgstr "" -#: awx/main/fields.py:428 +#: awx/main/fields.py:431 #, python-brace-format msgid "Schema validation error in relative path {path} ({error})" msgstr "" -#: awx/main/fields.py:549 +#: awx/main/fields.py:552 msgid "secret values must be of type string, not {}" msgstr "" -#: awx/main/fields.py:584 +#: awx/main/fields.py:587 #, python-format msgid "cannot be set unless \"%s\" is set" msgstr "" -#: awx/main/fields.py:600 +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "" -#: awx/main/fields.py:624 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "" -#: awx/main/fields.py:630 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "" -#: awx/main/fields.py:688 +#: awx/main/fields.py:691 msgid "'dependencies' is not supported for custom credentials." msgstr "" -#: awx/main/fields.py:702 +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "" -#: awx/main/fields.py:709 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "" -#: awx/main/fields.py:722 +#: awx/main/fields.py:725 msgid "become_method is a reserved type name" msgstr "" -#: awx/main/fields.py:733 +#: awx/main/fields.py:736 #, python-brace-format msgid "{sub_key} not allowed for {element_type} type ({element_id})" msgstr "" -#: awx/main/fields.py:813 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "" + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "" + +#: awx/main/fields.py:827 msgid "Must use multi-file syntax when injecting multiple files" msgstr "" -#: awx/main/fields.py:831 +#: awx/main/fields.py:844 #, python-brace-format msgid "{sub_key} uses an undefined field ({error_msg})" msgstr "" -#: awx/main/fields.py:838 +#: awx/main/fields.py:851 #, python-brace-format msgid "" "Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" msgstr "" -#: awx/main/middleware.py:146 +#: awx/main/middleware.py:160 msgid "Formats of all available named urls" msgstr "" -#: awx/main/middleware.py:147 +#: awx/main/middleware.py:161 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." msgstr "" -#: awx/main/middleware.py:149 awx/main/middleware.py:159 +#: awx/main/middleware.py:163 awx/main/middleware.py:173 msgid "Named URL" msgstr "" -#: awx/main/middleware.py:156 +#: awx/main/middleware.py:170 msgid "List of all named url graph nodes." msgstr "" -#: awx/main/middleware.py:157 +#: awx/main/middleware.py:171 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use " "this list to programmatically generate named URLs for resources" @@ -2297,6 +2443,17 @@ msgid "The hostname or IP address to use." msgstr "" #: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "" @@ -2305,6 +2462,16 @@ msgid "Username for this credential." msgstr "" #: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "" @@ -2411,18 +2578,22 @@ msgid "" msgstr "" #: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "" #: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "" #: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "" #: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "" @@ -2431,6 +2602,7 @@ msgid "Cloud" msgstr "" #: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "" @@ -2446,127 +2618,372 @@ msgstr "" msgid "adding %s credential type" msgstr "" -#: awx/main/models/events.py:71 awx/main/models/events.py:598 +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying " +"the --become-method Ansible parameter." +msgstr "" + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the --vault-" +"id Ansible parameter for providing multiple Vault passwords. Note: this " +"feature only works in Ansible 2.4+." +msgstr "" + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"Management (IAM) users." +msgstr "" + +#: awx/main/models/credential/__init__.py:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, https://openstack.business.com/" +"v2.0/" +msgstr "" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "" + +#: awx/main/models/credential/__init__.py:926 +msgid "" +"Enter the hostname or IP address that corresponds to your VMware vCenter." +msgstr "" + +#: awx/main/models/credential/__init__.py:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"example, https://satellite.example.org" +msgstr "" + +#: awx/main/models/credential/__init__.py:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"instance. For example, https://cloudforms.example.org" +msgstr "" + +#: awx/main/models/credential/__init__.py:1004 awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "" + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:1028 +msgid "" +"Paste the contents of the PEM file associated with the service account email." +msgstr "" + +#: awx/main/models/credential/__init__.py:1040 awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "" + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "" + +#: awx/main/models/credential/__init__.py:1115 awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "" + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "" + +#: awx/main/models/credential/__init__.py:1136 +msgid "Absolute file path to the CA file to use (optional)" +msgstr "" + +#: awx/main/models/credential/__init__.py:1167 awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "" + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "" + +#: awx/main/models/events.py:105 awx/main/models/events.py:630 msgid "Host Failed" msgstr "" -#: awx/main/models/events.py:72 awx/main/models/events.py:599 +#: awx/main/models/events.py:106 awx/main/models/events.py:631 msgid "Host OK" msgstr "" -#: awx/main/models/events.py:73 +#: awx/main/models/events.py:107 msgid "Host Failure" msgstr "" -#: awx/main/models/events.py:74 awx/main/models/events.py:605 +#: awx/main/models/events.py:108 awx/main/models/events.py:637 msgid "Host Skipped" msgstr "" -#: awx/main/models/events.py:75 awx/main/models/events.py:600 +#: awx/main/models/events.py:109 awx/main/models/events.py:632 msgid "Host Unreachable" msgstr "" -#: awx/main/models/events.py:76 awx/main/models/events.py:90 +#: awx/main/models/events.py:110 awx/main/models/events.py:124 msgid "No Hosts Remaining" msgstr "" -#: awx/main/models/events.py:77 +#: awx/main/models/events.py:111 msgid "Host Polling" msgstr "" -#: awx/main/models/events.py:78 +#: awx/main/models/events.py:112 msgid "Host Async OK" msgstr "" -#: awx/main/models/events.py:79 +#: awx/main/models/events.py:113 msgid "Host Async Failure" msgstr "" -#: awx/main/models/events.py:80 +#: awx/main/models/events.py:114 msgid "Item OK" msgstr "" -#: awx/main/models/events.py:81 +#: awx/main/models/events.py:115 msgid "Item Failed" msgstr "" -#: awx/main/models/events.py:82 +#: awx/main/models/events.py:116 msgid "Item Skipped" msgstr "" -#: awx/main/models/events.py:83 +#: awx/main/models/events.py:117 msgid "Host Retry" msgstr "" -#: awx/main/models/events.py:85 +#: awx/main/models/events.py:119 msgid "File Difference" msgstr "" -#: awx/main/models/events.py:86 +#: awx/main/models/events.py:120 msgid "Playbook Started" msgstr "" -#: awx/main/models/events.py:87 +#: awx/main/models/events.py:121 msgid "Running Handlers" msgstr "" -#: awx/main/models/events.py:88 +#: awx/main/models/events.py:122 msgid "Including File" msgstr "" -#: awx/main/models/events.py:89 +#: awx/main/models/events.py:123 msgid "No Hosts Matched" msgstr "" -#: awx/main/models/events.py:91 +#: awx/main/models/events.py:125 msgid "Task Started" msgstr "" -#: awx/main/models/events.py:93 +#: awx/main/models/events.py:127 msgid "Variables Prompted" msgstr "" -#: awx/main/models/events.py:94 +#: awx/main/models/events.py:128 msgid "Gathering Facts" msgstr "" -#: awx/main/models/events.py:95 +#: awx/main/models/events.py:129 msgid "internal: on Import for Host" msgstr "" -#: awx/main/models/events.py:96 +#: awx/main/models/events.py:130 msgid "internal: on Not Import for Host" msgstr "" -#: awx/main/models/events.py:97 +#: awx/main/models/events.py:131 msgid "Play Started" msgstr "" -#: awx/main/models/events.py:98 +#: awx/main/models/events.py:132 msgid "Playbook Complete" msgstr "" -#: awx/main/models/events.py:102 awx/main/models/events.py:615 +#: awx/main/models/events.py:136 awx/main/models/events.py:647 msgid "Debug" msgstr "" -#: awx/main/models/events.py:103 awx/main/models/events.py:616 +#: awx/main/models/events.py:137 awx/main/models/events.py:648 msgid "Verbose" msgstr "" -#: awx/main/models/events.py:104 awx/main/models/events.py:617 +#: awx/main/models/events.py:138 awx/main/models/events.py:649 msgid "Deprecated" msgstr "" -#: awx/main/models/events.py:105 awx/main/models/events.py:618 +#: awx/main/models/events.py:139 awx/main/models/events.py:650 msgid "Warning" msgstr "" -#: awx/main/models/events.py:106 awx/main/models/events.py:619 +#: awx/main/models/events.py:140 awx/main/models/events.py:651 msgid "System Warning" msgstr "" -#: awx/main/models/events.py:107 awx/main/models/events.py:620 +#: awx/main/models/events.py:141 awx/main/models/events.py:652 #: awx/main/models/unified_jobs.py:67 msgid "Error" msgstr "" @@ -2585,24 +3002,24 @@ msgid "" "host." msgstr "" -#: awx/main/models/ha.py:129 +#: awx/main/models/ha.py:181 msgid "Instances that are members of this InstanceGroup" msgstr "" -#: awx/main/models/ha.py:134 +#: awx/main/models/ha.py:186 msgid "Instance Group to remotely control this group." msgstr "" -#: awx/main/models/ha.py:141 +#: awx/main/models/ha.py:193 msgid "Percentage of Instances to automatically assign to this group" msgstr "" -#: awx/main/models/ha.py:145 +#: awx/main/models/ha.py:197 msgid "" "Static minimum number of Instances to automatically assign to this group" msgstr "" -#: awx/main/models/ha.py:150 +#: awx/main/models/ha.py:202 msgid "" "List of exact-match Instances that will always be automatically assigned to " "this group" @@ -2766,7 +3183,7 @@ msgid "Inventory source(s) that created or modified this group." msgstr "" #: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 -#: awx/main/models/unified_jobs.py:518 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "" @@ -2782,38 +3199,6 @@ msgstr "" msgid "Amazon EC2" msgstr "" -#: awx/main/models/inventory.py:985 -msgid "Google Compute Engine" -msgstr "" - -#: awx/main/models/inventory.py:986 -msgid "Microsoft Azure Resource Manager" -msgstr "" - -#: awx/main/models/inventory.py:987 -msgid "VMware vCenter" -msgstr "" - -#: awx/main/models/inventory.py:988 -msgid "Red Hat Satellite 6" -msgstr "" - -#: awx/main/models/inventory.py:989 -msgid "Red Hat CloudForms" -msgstr "" - -#: awx/main/models/inventory.py:990 -msgid "OpenStack" -msgstr "" - -#: awx/main/models/inventory.py:991 -msgid "Red Hat Virtualization" -msgstr "" - -#: awx/main/models/inventory.py:992 -msgid "Ansible Tower" -msgstr "" - #: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "" @@ -2960,16 +3345,16 @@ msgstr "" msgid "Cannot set source_path if not SCM type." msgstr "" -#: awx/main/models/inventory.py:1615 +#: awx/main/models/inventory.py:1622 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "" -#: awx/main/models/inventory.py:1725 +#: awx/main/models/inventory.py:1732 msgid "Inventory script contents" msgstr "" -#: awx/main/models/inventory.py:1730 +#: awx/main/models/inventory.py:1737 msgid "Organization owning this inventory script" msgstr "" @@ -2994,59 +3379,59 @@ msgstr "" msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "" -#: awx/main/models/jobs.py:403 +#: awx/main/models/jobs.py:398 msgid "Field is not configured to prompt on launch." msgstr "" -#: awx/main/models/jobs.py:407 +#: awx/main/models/jobs.py:404 msgid "Saved launch configurations cannot provide passwords needed to start." msgstr "" -#: awx/main/models/jobs.py:415 +#: awx/main/models/jobs.py:412 msgid "Job Template {} is missing or undefined." msgstr "" -#: awx/main/models/jobs.py:496 awx/main/models/projects.py:276 +#: awx/main/models/jobs.py:493 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "" -#: awx/main/models/jobs.py:497 +#: awx/main/models/jobs.py:494 msgid "The SCM Revision from the Project used for this job, if available" msgstr "" -#: awx/main/models/jobs.py:505 +#: awx/main/models/jobs.py:502 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" msgstr "" -#: awx/main/models/jobs.py:632 +#: awx/main/models/jobs.py:629 #, python-brace-format msgid "{status_value} is not a valid status option." msgstr "" -#: awx/main/models/jobs.py:991 +#: awx/main/models/jobs.py:1005 msgid "job host summaries" msgstr "" -#: awx/main/models/jobs.py:1062 +#: awx/main/models/jobs.py:1077 msgid "Remove jobs older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1063 +#: awx/main/models/jobs.py:1078 msgid "Remove activity stream entries older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1064 +#: awx/main/models/jobs.py:1079 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "" -#: awx/main/models/jobs.py:1134 +#: awx/main/models/jobs.py:1149 #, python-brace-format msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "" -#: awx/main/models/jobs.py:1149 +#: awx/main/models/jobs.py:1164 msgid "days must be a positive integer." msgstr "" @@ -3061,7 +3446,11 @@ msgid "" "Launch setting on the Job Template to include Extra Variables." msgstr "" -#: awx/main/models/mixins.py:446 +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "" + +#: awx/main/models/mixins.py:447 msgid "{} is not a valid virtualenv in {}" msgstr "" @@ -3085,73 +3474,75 @@ msgstr "" msgid "status_str must be either succeeded or failed" msgstr "" -#: awx/main/models/oauth.py:27 +#: awx/main/models/oauth.py:29 msgid "application" msgstr "" -#: awx/main/models/oauth.py:32 +#: awx/main/models/oauth.py:35 msgid "Confidential" msgstr "" -#: awx/main/models/oauth.py:33 +#: awx/main/models/oauth.py:36 msgid "Public" msgstr "" -#: awx/main/models/oauth.py:41 +#: awx/main/models/oauth.py:43 msgid "Authorization code" msgstr "" -#: awx/main/models/oauth.py:42 +#: awx/main/models/oauth.py:44 msgid "Implicit" msgstr "" -#: awx/main/models/oauth.py:43 +#: awx/main/models/oauth.py:45 msgid "Resource owner password-based" msgstr "" -#: awx/main/models/oauth.py:44 -msgid "Client credentials" -msgstr "" - -#: awx/main/models/oauth.py:59 +#: awx/main/models/oauth.py:60 msgid "Organization containing this application." msgstr "" -#: awx/main/models/oauth.py:68 +#: awx/main/models/oauth.py:69 msgid "" "Used for more stringent verification of access to an application when " "creating a token." msgstr "" -#: awx/main/models/oauth.py:73 +#: awx/main/models/oauth.py:74 msgid "" "Set to Public or Confidential depending on how secure the client device is." msgstr "" -#: awx/main/models/oauth.py:77 +#: awx/main/models/oauth.py:78 msgid "" "Set True to skip authorization step for completely trusted applications." msgstr "" -#: awx/main/models/oauth.py:82 +#: awx/main/models/oauth.py:83 msgid "" "The Grant type the user must use for acquire tokens for this application." msgstr "" -#: awx/main/models/oauth.py:90 +#: awx/main/models/oauth.py:91 msgid "access token" msgstr "" -#: awx/main/models/oauth.py:98 +#: awx/main/models/oauth.py:99 msgid "The user representing the token owner" msgstr "" -#: awx/main/models/oauth.py:112 +#: awx/main/models/oauth.py:114 msgid "" "Allowed scopes, further restricts user's permissions. Must be a simple space-" "separated string with allowed scopes ['read', 'write']." msgstr "" +#: awx/main/models/oauth.py:133 +msgid "" +"OAuth2 Tokens cannot be created by users associated with an external " +"authentication provider ({})" +msgstr "" + #: awx/main/models/projects.py:54 msgid "Git" msgstr "" @@ -3226,33 +3617,33 @@ msgstr "" msgid "Invalid credential." msgstr "" -#: awx/main/models/projects.py:262 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "" -#: awx/main/models/projects.py:267 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." msgstr "" -#: awx/main/models/projects.py:277 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "" -#: awx/main/models/projects.py:284 +#: awx/main/models/projects.py:285 msgid "Playbook Files" msgstr "" -#: awx/main/models/projects.py:285 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "" -#: awx/main/models/projects.py:292 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "" -#: awx/main/models/projects.py:293 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "" @@ -3286,141 +3677,150 @@ msgid "Credential Admin" msgstr "" #: awx/main/models/rbac.py:43 -msgid "Workflow Admin" +msgid "Job Template Admin" msgstr "" #: awx/main/models/rbac.py:44 -msgid "Notification Admin" +msgid "Workflow Admin" msgstr "" #: awx/main/models/rbac.py:45 -msgid "Auditor" +msgid "Notification Admin" msgstr "" #: awx/main/models/rbac.py:46 -msgid "Execute" +msgid "Auditor" msgstr "" #: awx/main/models/rbac.py:47 -msgid "Member" +msgid "Execute" msgstr "" #: awx/main/models/rbac.py:48 -msgid "Read" +msgid "Member" msgstr "" #: awx/main/models/rbac.py:49 -msgid "Update" +msgid "Read" msgstr "" #: awx/main/models/rbac.py:50 +msgid "Update" +msgstr "" + +#: awx/main/models/rbac.py:51 msgid "Use" msgstr "" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:55 msgid "Can manage all aspects of the system" msgstr "" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:56 msgid "Can view all settings on the system" msgstr "" -#: awx/main/models/rbac.py:56 -msgid "May run ad hoc commands on an inventory" -msgstr "" - #: awx/main/models/rbac.py:57 -#, python-format -msgid "Can manage all aspects of the %s" +msgid "May run ad hoc commands on an inventory" msgstr "" #: awx/main/models/rbac.py:58 #, python-format -msgid "Can manage all projects of the %s" +msgid "Can manage all aspects of the %s" msgstr "" #: awx/main/models/rbac.py:59 #, python-format -msgid "Can manage all inventories of the %s" +msgid "Can manage all projects of the %s" msgstr "" #: awx/main/models/rbac.py:60 #, python-format -msgid "Can manage all credentials of the %s" +msgid "Can manage all inventories of the %s" msgstr "" #: awx/main/models/rbac.py:61 #, python-format -msgid "Can manage all workflows of the %s" +msgid "Can manage all credentials of the %s" msgstr "" #: awx/main/models/rbac.py:62 #, python-format -msgid "Can manage all notifications of the %s" +msgid "Can manage all job templates of the %s" msgstr "" #: awx/main/models/rbac.py:63 #, python-format -msgid "Can view all settings for the %s" +msgid "Can manage all workflows of the %s" +msgstr "" + +#: awx/main/models/rbac.py:64 +#, python-format +msgid "Can manage all notifications of the %s" msgstr "" #: awx/main/models/rbac.py:65 -msgid "May run any executable resources in the organization" +#, python-format +msgid "Can view all settings for the %s" msgstr "" -#: awx/main/models/rbac.py:66 -#, python-format -msgid "May run the %s" +#: awx/main/models/rbac.py:67 +msgid "May run any executable resources in the organization" msgstr "" #: awx/main/models/rbac.py:68 #, python-format +msgid "May run the %s" +msgstr "" + +#: awx/main/models/rbac.py:70 +#, python-format msgid "User is a member of the %s" msgstr "" -#: awx/main/models/rbac.py:69 +#: awx/main/models/rbac.py:71 #, python-format msgid "May view settings for the %s" msgstr "" -#: awx/main/models/rbac.py:70 +#: awx/main/models/rbac.py:72 msgid "" "May update project or inventory or group using the configured source update " "system" msgstr "" -#: awx/main/models/rbac.py:71 +#: awx/main/models/rbac.py:73 #, python-format msgid "Can use the %s in a job template" msgstr "" -#: awx/main/models/rbac.py:135 +#: awx/main/models/rbac.py:137 msgid "roles" msgstr "" -#: awx/main/models/rbac.py:441 +#: awx/main/models/rbac.py:443 msgid "role_ancestors" msgstr "" -#: awx/main/models/schedules.py:72 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "" -#: awx/main/models/schedules.py:78 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." msgstr "" -#: awx/main/models/schedules.py:84 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." msgstr "" -#: awx/main/models/schedules.py:88 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "" -#: awx/main/models/schedules.py:94 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." msgstr "" @@ -3470,53 +3870,57 @@ msgid "" "Variables {list_of_keys} provided, but this template cannot accept variables." msgstr "" -#: awx/main/models/unified_jobs.py:519 +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "" -#: awx/main/models/unified_jobs.py:520 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "" -#: awx/main/models/unified_jobs.py:521 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "" -#: awx/main/models/unified_jobs.py:522 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "" -#: awx/main/models/unified_jobs.py:523 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "" -#: awx/main/models/unified_jobs.py:524 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "" -#: awx/main/models/unified_jobs.py:572 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "" -#: awx/main/models/unified_jobs.py:598 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "" + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." msgstr "" -#: awx/main/models/unified_jobs.py:604 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." msgstr "" -#: awx/main/models/unified_jobs.py:610 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." msgstr "" -#: awx/main/models/unified_jobs.py:632 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and " "capture stdout" msgstr "" -#: awx/main/models/unified_jobs.py:661 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "" @@ -3528,7 +3932,7 @@ msgid "" "{error_text}" msgstr "" -#: awx/main/models/workflow.py:387 +#: awx/main/models/workflow.py:393 msgid "Field is not allowed for use in workflows." msgstr "" @@ -3580,31 +3984,31 @@ msgstr "" msgid "Error sending notification webhook: {}" msgstr "" -#: awx/main/scheduler/task_manager.py:200 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" msgstr "" -#: awx/main/scheduler/task_manager.py:204 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" msgstr "" -#: awx/main/signals.py:617 +#: awx/main/signals.py:632 msgid "limit_reached" msgstr "" -#: awx/main/tasks.py:273 +#: awx/main/tasks.py:305 msgid "Ansible Tower host usage over 90%" msgstr "" -#: awx/main/tasks.py:278 +#: awx/main/tasks.py:310 msgid "Ansible Tower license will expire soon" msgstr "" -#: awx/main/tasks.py:1321 +#: awx/main/tasks.py:1358 msgid "Job could not start because it does not have a valid inventory." msgstr "" @@ -3613,53 +4017,53 @@ msgstr "" msgid "Unable to convert \"%s\" to boolean" msgstr "" -#: awx/main/utils/common.py:251 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "" -#: awx/main/utils/common.py:258 awx/main/utils/common.py:270 -#: awx/main/utils/common.py:289 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" msgstr "" -#: awx/main/utils/common.py:260 awx/main/utils/common.py:299 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "" -#: awx/main/utils/common.py:301 +#: awx/main/utils/common.py:304 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "" -#: awx/main/utils/common.py:303 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "" -#: awx/main/utils/common.py:321 +#: awx/main/utils/common.py:324 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "" -#: awx/main/utils/common.py:327 +#: awx/main/utils/common.py:330 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "" -#: awx/main/utils/common.py:608 +#: awx/main/utils/common.py:611 #, python-brace-format msgid "Input type `{data_type}` is not a dictionary" msgstr "" -#: awx/main/utils/common.py:641 +#: awx/main/utils/common.py:644 #, python-brace-format msgid "Variables not compatible with JSON standard (error: {json_error})" msgstr "" -#: awx/main/utils/common.py:647 +#: awx/main/utils/common.py:650 #, python-brace-format msgid "" "Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." @@ -3771,287 +4175,287 @@ msgstr "" msgid "A server error has occurred." msgstr "" -#: awx/settings/defaults.py:722 +#: awx/settings/defaults.py:725 msgid "US East (Northern Virginia)" msgstr "" -#: awx/settings/defaults.py:723 +#: awx/settings/defaults.py:726 msgid "US East (Ohio)" msgstr "" -#: awx/settings/defaults.py:724 +#: awx/settings/defaults.py:727 msgid "US West (Oregon)" msgstr "" -#: awx/settings/defaults.py:725 +#: awx/settings/defaults.py:728 msgid "US West (Northern California)" msgstr "" -#: awx/settings/defaults.py:726 +#: awx/settings/defaults.py:729 msgid "Canada (Central)" msgstr "" -#: awx/settings/defaults.py:727 +#: awx/settings/defaults.py:730 msgid "EU (Frankfurt)" msgstr "" -#: awx/settings/defaults.py:728 +#: awx/settings/defaults.py:731 msgid "EU (Ireland)" msgstr "" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:732 msgid "EU (London)" msgstr "" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Singapore)" msgstr "" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:734 msgid "Asia Pacific (Sydney)" msgstr "" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:735 msgid "Asia Pacific (Tokyo)" msgstr "" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:736 msgid "Asia Pacific (Seoul)" msgstr "" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:737 msgid "Asia Pacific (Mumbai)" msgstr "" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:738 msgid "South America (Sao Paulo)" msgstr "" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:739 msgid "US West (GovCloud)" msgstr "" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:740 msgid "China (Beijing)" msgstr "" -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:789 msgid "US East 1 (B)" msgstr "" -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:790 msgid "US East 1 (C)" msgstr "" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:791 msgid "US East 1 (D)" msgstr "" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:792 msgid "US East 4 (A)" msgstr "" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:793 msgid "US East 4 (B)" msgstr "" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:794 msgid "US East 4 (C)" msgstr "" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:795 msgid "US Central (A)" msgstr "" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:796 msgid "US Central (B)" msgstr "" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:797 msgid "US Central (C)" msgstr "" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:798 msgid "US Central (F)" msgstr "" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:799 msgid "US West (A)" msgstr "" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:800 msgid "US West (B)" msgstr "" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:801 msgid "US West (C)" msgstr "" -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:802 msgid "Europe West 1 (B)" msgstr "" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:803 msgid "Europe West 1 (C)" msgstr "" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:804 msgid "Europe West 1 (D)" msgstr "" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:805 msgid "Europe West 2 (A)" msgstr "" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:806 msgid "Europe West 2 (B)" msgstr "" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:807 msgid "Europe West 2 (C)" msgstr "" -#: awx/settings/defaults.py:805 +#: awx/settings/defaults.py:808 msgid "Asia East (A)" msgstr "" -#: awx/settings/defaults.py:806 +#: awx/settings/defaults.py:809 msgid "Asia East (B)" msgstr "" -#: awx/settings/defaults.py:807 +#: awx/settings/defaults.py:810 msgid "Asia East (C)" msgstr "" -#: awx/settings/defaults.py:808 +#: awx/settings/defaults.py:811 msgid "Asia Southeast (A)" msgstr "" -#: awx/settings/defaults.py:809 +#: awx/settings/defaults.py:812 msgid "Asia Southeast (B)" msgstr "" -#: awx/settings/defaults.py:810 +#: awx/settings/defaults.py:813 msgid "Asia Northeast (A)" msgstr "" -#: awx/settings/defaults.py:811 +#: awx/settings/defaults.py:814 msgid "Asia Northeast (B)" msgstr "" -#: awx/settings/defaults.py:812 +#: awx/settings/defaults.py:815 msgid "Asia Northeast (C)" msgstr "" -#: awx/settings/defaults.py:813 +#: awx/settings/defaults.py:816 msgid "Australia Southeast (A)" msgstr "" -#: awx/settings/defaults.py:814 +#: awx/settings/defaults.py:817 msgid "Australia Southeast (B)" msgstr "" -#: awx/settings/defaults.py:815 +#: awx/settings/defaults.py:818 msgid "Australia Southeast (C)" msgstr "" -#: awx/settings/defaults.py:837 +#: awx/settings/defaults.py:840 msgid "US East" msgstr "" -#: awx/settings/defaults.py:838 +#: awx/settings/defaults.py:841 msgid "US East 2" msgstr "" -#: awx/settings/defaults.py:839 +#: awx/settings/defaults.py:842 msgid "US Central" msgstr "" -#: awx/settings/defaults.py:840 +#: awx/settings/defaults.py:843 msgid "US North Central" msgstr "" -#: awx/settings/defaults.py:841 +#: awx/settings/defaults.py:844 msgid "US South Central" msgstr "" -#: awx/settings/defaults.py:842 +#: awx/settings/defaults.py:845 msgid "US West Central" msgstr "" -#: awx/settings/defaults.py:843 +#: awx/settings/defaults.py:846 msgid "US West" msgstr "" -#: awx/settings/defaults.py:844 +#: awx/settings/defaults.py:847 msgid "US West 2" msgstr "" -#: awx/settings/defaults.py:845 +#: awx/settings/defaults.py:848 msgid "Canada East" msgstr "" -#: awx/settings/defaults.py:846 +#: awx/settings/defaults.py:849 msgid "Canada Central" msgstr "" -#: awx/settings/defaults.py:847 +#: awx/settings/defaults.py:850 msgid "Brazil South" msgstr "" -#: awx/settings/defaults.py:848 +#: awx/settings/defaults.py:851 msgid "Europe North" msgstr "" -#: awx/settings/defaults.py:849 +#: awx/settings/defaults.py:852 msgid "Europe West" msgstr "" -#: awx/settings/defaults.py:850 +#: awx/settings/defaults.py:853 msgid "UK West" msgstr "" -#: awx/settings/defaults.py:851 +#: awx/settings/defaults.py:854 msgid "UK South" msgstr "" -#: awx/settings/defaults.py:852 +#: awx/settings/defaults.py:855 msgid "Asia East" msgstr "" -#: awx/settings/defaults.py:853 +#: awx/settings/defaults.py:856 msgid "Asia Southeast" msgstr "" -#: awx/settings/defaults.py:854 +#: awx/settings/defaults.py:857 msgid "Australia East" msgstr "" -#: awx/settings/defaults.py:855 +#: awx/settings/defaults.py:858 msgid "Australia Southeast" msgstr "" -#: awx/settings/defaults.py:856 +#: awx/settings/defaults.py:859 msgid "India West" msgstr "" -#: awx/settings/defaults.py:857 +#: awx/settings/defaults.py:860 msgid "India South" msgstr "" -#: awx/settings/defaults.py:858 +#: awx/settings/defaults.py:861 msgid "Japan East" msgstr "" -#: awx/settings/defaults.py:859 +#: awx/settings/defaults.py:862 msgid "Japan West" msgstr "" -#: awx/settings/defaults.py:860 +#: awx/settings/defaults.py:863 msgid "Korea Central" msgstr "" -#: awx/settings/defaults.py:861 +#: awx/settings/defaults.py:864 msgid "Korea South" msgstr "" @@ -4602,7 +5006,7 @@ msgstr "" #: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 #: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 #: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 -#: awx/sso/conf.py:1211 awx/sso/models.py:16 +#: awx/sso/conf.py:1213 awx/sso/models.py:16 msgid "SAML" msgstr "" @@ -4735,11 +5139,11 @@ msgstr "" msgid "Used to translate user organization membership into Tower." msgstr "" -#: awx/sso/conf.py:1209 +#: awx/sso/conf.py:1211 msgid "SAML Team Attribute Mapping" msgstr "" -#: awx/sso/conf.py:1210 +#: awx/sso/conf.py:1212 msgid "Used to translate user team membership into Tower." msgstr "" @@ -4748,96 +5152,96 @@ msgstr "" msgid "Invalid connection option(s): {invalid_options}." msgstr "" -#: awx/sso/fields.py:254 +#: awx/sso/fields.py:266 msgid "Base" msgstr "" -#: awx/sso/fields.py:255 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "" -#: awx/sso/fields.py:256 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "" -#: awx/sso/fields.py:274 +#: awx/sso/fields.py:286 #, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "" -#: awx/sso/fields.py:275 +#: awx/sso/fields.py:287 #, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:311 +#: awx/sso/fields.py:323 #, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " "instead." msgstr "" -#: awx/sso/fields.py:349 +#: awx/sso/fields.py:361 #, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "" -#: awx/sso/fields.py:366 +#: awx/sso/fields.py:378 #, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:406 awx/sso/fields.py:453 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 #, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "" -#: awx/sso/fields.py:431 +#: awx/sso/fields.py:443 #, python-brace-format msgid "Invalid user flag: \"{invalid_flag}\"." msgstr "" -#: awx/sso/fields.py:452 +#: awx/sso/fields.py:464 #, python-brace-format msgid "Missing key(s): {missing_keys}." msgstr "" -#: awx/sso/fields.py:502 awx/sso/fields.py:619 +#: awx/sso/fields.py:514 awx/sso/fields.py:631 #, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:520 +#: awx/sso/fields.py:532 #, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:521 awx/sso/fields.py:638 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 #, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:637 +#: awx/sso/fields.py:649 #, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "" -#: awx/sso/fields.py:655 +#: awx/sso/fields.py:667 #, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "" -#: awx/sso/fields.py:668 +#: awx/sso/fields.py:680 #, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "" -#: awx/sso/fields.py:687 +#: awx/sso/fields.py:699 #, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "" -#: awx/sso/fields.py:699 +#: awx/sso/fields.py:711 #, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "" diff --git a/awx/locale/en-us/LC_MESSAGES/django.po b/awx/locale/en-us/LC_MESSAGES/django.po index 141b9ce4ea..572cf9ef2c 100644 --- a/awx/locale/en-us/LC_MESSAGES/django.po +++ b/awx/locale/en-us/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-07-28 18:57+0000\n" +"POT-Creation-Date: 2018-08-03 19:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,86 +17,123 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: awx/api/authentication.py:67 -msgid "Invalid token header. No credentials provided." -msgstr "" - -#: awx/api/authentication.py:70 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "" - -#: awx/api/authentication.py:105 -msgid "User inactive or deleted" -msgstr "" - -#: awx/api/authentication.py:161 -msgid "Invalid task token" -msgstr "" - -#: awx/api/conf.py:12 +#: awx/api/conf.py:15 msgid "Idle Time Force Log Out" msgstr "" -#: awx/api/conf.py:13 +#: awx/api/conf.py:16 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." msgstr "" -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:85 -#: awx/sso/conf.py:96 awx/sso/conf.py:108 awx/sso/conf.py:123 +#: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 +#: awx/api/conf.py:59 awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/sso/conf.py:123 msgid "Authentication" msgstr "" -#: awx/api/conf.py:22 -msgid "Maximum number of simultaneous logins" +#: awx/api/conf.py:24 +msgid "Maximum number of simultaneous logged in sessions" msgstr "" -#: awx/api/conf.py:23 +#: awx/api/conf.py:25 msgid "" -"Maximum number of simultaneous logins a user may have. To disable enter -1." -msgstr "" - -#: awx/api/conf.py:31 -msgid "Enable HTTP Basic Auth" +"Maximum number of simultaneous logged in sessions a user may have. To " +"disable enter -1." msgstr "" #: awx/api/conf.py:32 +msgid "Enable HTTP Basic Auth" +msgstr "" + +#: awx/api/conf.py:33 msgid "Enable HTTP Basic Auth for the API Browser." msgstr "" -#: awx/api/filters.py:129 +#: awx/api/conf.py:42 +msgid "OAuth 2 Timeout Settings" +msgstr "" + +#: awx/api/conf.py:43 +msgid "" +"Dictionary for customizing OAuth 2 timeouts, available items are " +"`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number " +"of seconds, and `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of " +"authorization grants in the number of seconds." +msgstr "" + +#: awx/api/conf.py:54 +msgid "Allow External Users to Create OAuth2 Tokens" +msgstr "" + +#: awx/api/conf.py:55 +msgid "" +"For security reasons, users from external auth providers (LDAP, SAML, SSO, " +"Radius, and others) are not allowed to create OAuth2 tokens. To change this " +"behavior, enable this setting. Existing tokens will not be deleted when this " +"setting is toggled off." +msgstr "" + +#: awx/api/exceptions.py:20 +msgid "Resource is being used by running jobs." +msgstr "" + +#: awx/api/fields.py:81 +#, python-brace-format +msgid "Invalid key names: {invalid_key_names}" +msgstr "" + +#: awx/api/fields.py:107 +msgid "Credential {} does not exist" +msgstr "" + +#: awx/api/filters.py:97 +msgid "No related model for field {}." +msgstr "" + +#: awx/api/filters.py:114 msgid "Filtering on password fields is not allowed." msgstr "" -#: awx/api/filters.py:141 awx/api/filters.py:143 +#: awx/api/filters.py:126 awx/api/filters.py:128 #, python-format msgid "Filtering on %s is not allowed." msgstr "" -#: awx/api/filters.py:146 +#: awx/api/filters.py:131 msgid "Loops not allowed in filters, detected on field {}." msgstr "" -#: awx/api/filters.py:297 +#: awx/api/filters.py:160 +msgid "Query string field name not provided." +msgstr "" + +#: awx/api/filters.py:187 +#, python-brace-format +msgid "Invalid {field_name} id: {field_id}" +msgstr "" + +#: awx/api/filters.py:326 #, python-format msgid "cannot filter on kind %s" msgstr "" -#: awx/api/filters.py:404 -#, python-format -msgid "cannot order by field %s" +#: awx/api/generics.py:197 +msgid "" +"You did not use correct Content-Type in your HTTP request. If you are using " +"our REST API, the Content-Type must be application/json" msgstr "" -#: awx/api/generics.py:515 awx/api/generics.py:577 +#: awx/api/generics.py:629 awx/api/generics.py:691 msgid "\"id\" field must be an integer." msgstr "" -#: awx/api/generics.py:574 +#: awx/api/generics.py:688 msgid "\"id\" is required to disassociate" msgstr "" -#: awx/api/generics.py:625 +#: awx/api/generics.py:739 msgid "{} 'id' field is missing." msgstr "" @@ -136,842 +173,1173 @@ msgstr "" msgid "Timestamp when this {} was last modified." msgstr "" -#: awx/api/parsers.py:64 +#: awx/api/parsers.py:33 msgid "JSON parse error - not a JSON object" msgstr "" -#: awx/api/parsers.py:67 +#: awx/api/parsers.py:36 #, python-format msgid "" "JSON parse error - %s\n" "Possible cause: trailing comma." msgstr "" -#: awx/api/serializers.py:268 +#: awx/api/serializers.py:155 +msgid "" +"The original object is already named {}, a copy from it cannot have the same " +"name." +msgstr "" + +#: awx/api/serializers.py:290 +#, python-format +msgid "Cannot use dictionary for %s" +msgstr "" + +#: awx/api/serializers.py:307 msgid "Playbook Run" msgstr "" -#: awx/api/serializers.py:269 +#: awx/api/serializers.py:308 msgid "Command" msgstr "" -#: awx/api/serializers.py:270 awx/main/models/unified_jobs.py:434 +#: awx/api/serializers.py:309 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "" -#: awx/api/serializers.py:271 +#: awx/api/serializers.py:310 msgid "Inventory Sync" msgstr "" -#: awx/api/serializers.py:272 +#: awx/api/serializers.py:311 msgid "Management Job" msgstr "" -#: awx/api/serializers.py:273 +#: awx/api/serializers.py:312 msgid "Workflow Job" msgstr "" -#: awx/api/serializers.py:274 +#: awx/api/serializers.py:313 msgid "Workflow Template" msgstr "" -#: awx/api/serializers.py:697 awx/api/serializers.py:755 awx/api/views.py:4330 -#, python-format -msgid "" -"Standard Output too large to display (%(text_size)d bytes), only download " -"supported for sizes over %(supported_size)d bytes" +#: awx/api/serializers.py:314 +msgid "Job Template" msgstr "" -#: awx/api/serializers.py:770 +#: awx/api/serializers.py:714 +msgid "" +"Indicates whether all of the events generated by this unified job have been " +"saved to the database." +msgstr "" + +#: awx/api/serializers.py:879 msgid "Write-only field used to change the password." msgstr "" -#: awx/api/serializers.py:772 +#: awx/api/serializers.py:881 msgid "Set if the account is managed by an external service" msgstr "" -#: awx/api/serializers.py:796 +#: awx/api/serializers.py:905 msgid "Password required for new User." msgstr "" -#: awx/api/serializers.py:882 +#: awx/api/serializers.py:981 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "" -#: awx/api/serializers.py:1046 +#: awx/api/serializers.py:1067 +msgid "Must be a simple space-separated string with allowed scopes {}." +msgstr "" + +#: awx/api/serializers.py:1167 +msgid "Authorization Grant Type" +msgstr "" + +#: awx/api/serializers.py:1169 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "" + +#: awx/api/serializers.py:1172 +msgid "Client Type" +msgstr "" + +#: awx/api/serializers.py:1175 +msgid "Redirect URIs" +msgstr "" + +#: awx/api/serializers.py:1178 +msgid "Skip Authorization" +msgstr "" + +#: awx/api/serializers.py:1290 +msgid "This path is already being used by another manual project." +msgstr "" + +#: awx/api/serializers.py:1316 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "" + +#: awx/api/serializers.py:1375 msgid "Organization is missing" msgstr "" -#: awx/api/serializers.py:1050 +#: awx/api/serializers.py:1379 msgid "Update options must be set to false for manual projects." msgstr "" -#: awx/api/serializers.py:1056 +#: awx/api/serializers.py:1385 msgid "Array of playbooks available within this project." msgstr "" -#: awx/api/serializers.py:1075 +#: awx/api/serializers.py:1404 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." msgstr "" -#: awx/api/serializers.py:1283 +#: awx/api/serializers.py:1452 awx/api/serializers.py:3247 +#: awx/api/serializers.py:3454 +msgid "A count of hosts uniquely assigned to each status." +msgstr "" + +#: awx/api/serializers.py:1455 awx/api/serializers.py:3250 +msgid "A count of all plays and tasks for the job run." +msgstr "" + +#: awx/api/serializers.py:1570 +msgid "Smart inventories must specify host_filter" +msgstr "" + +#: awx/api/serializers.py:1674 #, python-format msgid "Invalid port specification: %s" msgstr "" -#: awx/api/serializers.py:1311 awx/api/serializers.py:3208 -#: awx/api/serializers.py:3293 awx/main/validators.py:198 -msgid "Must be valid JSON or YAML." +#: awx/api/serializers.py:1685 +msgid "Cannot create Host for Smart Inventory" msgstr "" -#: awx/api/serializers.py:1407 +#: awx/api/serializers.py:1797 msgid "Invalid group name." msgstr "" -#: awx/api/serializers.py:1479 +#: awx/api/serializers.py:1802 +msgid "Cannot create Group for Smart Inventory" +msgstr "" + +#: awx/api/serializers.py:1877 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" -#: awx/api/serializers.py:1525 +#: awx/api/serializers.py:1926 msgid "`{}` is a prohibited environment variable" msgstr "" -#: awx/api/serializers.py:1536 +#: awx/api/serializers.py:1937 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "" -#: awx/api/serializers.py:1542 +#: awx/api/serializers.py:1943 msgid "Must provide an inventory." msgstr "" -#: awx/api/serializers.py:1546 +#: awx/api/serializers.py:1947 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" -#: awx/api/serializers.py:1548 +#: awx/api/serializers.py:1949 msgid "'source_script' doesn't exist." msgstr "" -#: awx/api/serializers.py:1572 +#: awx/api/serializers.py:1985 msgid "Automatic group relationship, will be removed in 3.3" msgstr "" -#: awx/api/serializers.py:1649 +#: awx/api/serializers.py:2072 msgid "Cannot use manual project for SCM-based inventory." msgstr "" -#: awx/api/serializers.py:1655 +#: awx/api/serializers.py:2078 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." msgstr "" -#: awx/api/serializers.py:1660 +#: awx/api/serializers.py:2083 msgid "Setting not compatible with existing schedules." msgstr "" -#: awx/api/serializers.py:1673 -msgid "Cannot set source_path if not SCM type." +#: awx/api/serializers.py:2088 +msgid "Cannot create Inventory Source for Smart Inventory" msgstr "" -#: awx/api/serializers.py:1676 -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." +#: awx/api/serializers.py:2139 +#, python-format +msgid "Cannot set %s if not SCM type." msgstr "" -#: awx/api/serializers.py:1680 -msgid "Inventory controlled by project-following SCM." -msgstr "" - -#: awx/api/serializers.py:1683 -msgid "SCM type sources must set `overwrite_vars` to `true`." -msgstr "" - -#: awx/api/serializers.py:1925 +#: awx/api/serializers.py:2414 msgid "Modifications not allowed for managed credential types" msgstr "" -#: awx/api/serializers.py:1930 +#: awx/api/serializers.py:2419 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "" -#: awx/api/serializers.py:1936 +#: awx/api/serializers.py:2425 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "" -#: awx/api/serializers.py:1942 +#: awx/api/serializers.py:2431 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "" -#: awx/api/serializers.py:2115 +#: awx/api/serializers.py:2502 +msgid "Credential Type" +msgstr "" + +#: awx/api/serializers.py:2617 #, python-format msgid "\"%s\" is not a valid choice" msgstr "" -#: awx/api/serializers.py:2134 -#, python-format -msgid "'%s' is not a valid field for %s" +#: awx/api/serializers.py:2636 +#, python-brace-format +msgid "'{field_name}' is not a valid field for {credential_type_name}" msgstr "" -#: awx/api/serializers.py:2146 +#: awx/api/serializers.py:2657 +msgid "" +"You cannot change the credential type of the credential, as it may break the " +"functionality of the resources using it." +msgstr "" + +#: awx/api/serializers.py:2669 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:2151 +#: awx/api/serializers.py:2674 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." msgstr "" -#: awx/api/serializers.py:2156 +#: awx/api/serializers.py:2679 msgid "" "Inherit permissions from organization roles. If provided on creation, do not " "give either user or team." msgstr "" -#: awx/api/serializers.py:2172 +#: awx/api/serializers.py:2695 msgid "Missing 'user', 'team', or 'organization'." msgstr "" -#: awx/api/serializers.py:2212 +#: awx/api/serializers.py:2735 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" -#: awx/api/serializers.py:2374 +#: awx/api/serializers.py:2936 msgid "You must provide a cloud credential." msgstr "" -#: awx/api/serializers.py:2375 +#: awx/api/serializers.py:2937 msgid "You must provide a network credential." msgstr "" -#: awx/api/serializers.py:2391 +#: awx/api/serializers.py:2938 awx/main/models/jobs.py:155 +msgid "You must provide an SSH credential." +msgstr "" + +#: awx/api/serializers.py:2939 +msgid "You must provide a vault credential." +msgstr "" + +#: awx/api/serializers.py:2958 msgid "This field is required." msgstr "" -#: awx/api/serializers.py:2393 awx/api/serializers.py:2395 +#: awx/api/serializers.py:2960 awx/api/serializers.py:2962 msgid "Playbook not found for project." msgstr "" -#: awx/api/serializers.py:2397 +#: awx/api/serializers.py:2964 msgid "Must select playbook for project." msgstr "" -#: awx/api/serializers.py:2466 +#: awx/api/serializers.py:3045 +msgid "Cannot enable provisioning callback without an inventory set." +msgstr "" + +#: awx/api/serializers.py:3048 msgid "Must either set a default value or ask to prompt on launch." msgstr "" -#: awx/api/serializers.py:2468 awx/main/models/jobs.py:325 +#: awx/api/serializers.py:3050 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "" -#: awx/api/serializers.py:2539 +#: awx/api/serializers.py:3169 msgid "Invalid job template." msgstr "" -#: awx/api/serializers.py:2620 -msgid "Credential not found or deleted." +#: awx/api/serializers.py:3290 +msgid "No change to job limit" msgstr "" -#: awx/api/serializers.py:2622 +#: awx/api/serializers.py:3291 +msgid "All failed and unreachable hosts" +msgstr "" + +#: awx/api/serializers.py:3306 +msgid "Missing passwords needed to start: {}" +msgstr "" + +#: awx/api/serializers.py:3325 +msgid "Relaunch by host status not available until job finishes running." +msgstr "" + +#: awx/api/serializers.py:3339 msgid "Job Template Project is missing or undefined." msgstr "" -#: awx/api/serializers.py:2624 +#: awx/api/serializers.py:3341 msgid "Job Template Inventory is missing or undefined." msgstr "" -#: awx/api/serializers.py:2911 -#, python-format -msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." +#: awx/api/serializers.py:3379 +msgid "Unknown, job may have been ran before launch configurations were saved." msgstr "" -#: awx/api/serializers.py:2916 -msgid "Workflow job template is missing during creation." +#: awx/api/serializers.py:3446 awx/main/tasks.py:2297 +msgid "{} are prohibited from use in ad hoc commands." msgstr "" -#: awx/api/serializers.py:2921 +#: awx/api/serializers.py:3534 awx/api/views.py:4893 +#, python-brace-format +msgid "" +"Standard Output too large to display ({text_size} bytes), only download " +"supported for sizes over {supported_size} bytes." +msgstr "" + +#: awx/api/serializers.py:3727 +msgid "Provided variable {} has no database value to replace with." +msgstr "" + +#: awx/api/serializers.py:3745 +#, python-brace-format +msgid "\"$encrypted$ is a reserved keyword, may not be used for {var_name}.\"" +msgstr "" + +#: awx/api/serializers.py:3815 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "" -#: awx/api/serializers.py:3178 -#, python-format -msgid "Job Template '%s' is missing or undefined." +#: awx/api/serializers.py:3822 awx/api/views.py:818 +msgid "Related template is not configured to accept credentials on launch." msgstr "" -#: awx/api/serializers.py:3181 +#: awx/api/serializers.py:4282 msgid "The inventory associated with this Job Template is being deleted." msgstr "" -#: awx/api/serializers.py:3222 awx/api/views.py:2998 -#, python-format -msgid "Cannot assign multiple %s credentials." +#: awx/api/serializers.py:4284 +msgid "The provided inventory is being deleted." msgstr "" -#: awx/api/serializers.py:3224 awx/api/views.py:3001 -msgid "Extra credentials must be network or cloud." +#: awx/api/serializers.py:4292 +msgid "Cannot assign multiple {} credentials." msgstr "" -#: awx/api/serializers.py:3361 +#: awx/api/serializers.py:4296 +msgid "Cannot assign a Credential of kind `{}`" +msgstr "" + +#: awx/api/serializers.py:4309 +msgid "" +"Removing {} credential at launch time without replacement is not supported. " +"Provided list lacked credential(s): {}." +msgstr "" + +#: awx/api/serializers.py:4435 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "" -#: awx/api/serializers.py:3384 +#: awx/api/serializers.py:4458 msgid "No values specified for field '{}'" msgstr "" -#: awx/api/serializers.py:3389 +#: awx/api/serializers.py:4463 msgid "Missing required fields for Notification Configuration: {}." msgstr "" -#: awx/api/serializers.py:3392 +#: awx/api/serializers.py:4466 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "" -#: awx/api/serializers.py:3445 -msgid "Inventory Source must be a cloud resource." -msgstr "" - -#: awx/api/serializers.py:3447 -msgid "Manual Project cannot have a schedule set." -msgstr "" - -#: awx/api/serializers.py:3450 +#: awx/api/serializers.py:4528 msgid "" -"Inventory sources with `update_on_project_update` cannot be scheduled. " -"Schedule its source project `{}` instead." +"Valid DTSTART required in rrule. Value should start with: DTSTART:" +"YYYYMMDDTHHMMSSZ" msgstr "" -#: awx/api/serializers.py:3469 -msgid "Projects and inventory updates cannot accept extra variables." +#: awx/api/serializers.py:4530 +msgid "" +"DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." msgstr "" -#: awx/api/serializers.py:3491 -msgid "DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" -msgstr "" - -#: awx/api/serializers.py:3493 +#: awx/api/serializers.py:4532 msgid "Multiple DTSTART is not supported." msgstr "" -#: awx/api/serializers.py:3495 -msgid "RRULE require in rrule." +#: awx/api/serializers.py:4534 +msgid "RRULE required in rrule." msgstr "" -#: awx/api/serializers.py:3497 +#: awx/api/serializers.py:4536 msgid "Multiple RRULE is not supported." msgstr "" -#: awx/api/serializers.py:3499 +#: awx/api/serializers.py:4538 msgid "INTERVAL required in rrule." msgstr "" -#: awx/api/serializers.py:3501 -msgid "TZID is not supported." -msgstr "" - -#: awx/api/serializers.py:3503 +#: awx/api/serializers.py:4540 msgid "SECONDLY is not supported." msgstr "" -#: awx/api/serializers.py:3505 +#: awx/api/serializers.py:4542 msgid "Multiple BYMONTHDAYs not supported." msgstr "" -#: awx/api/serializers.py:3507 +#: awx/api/serializers.py:4544 msgid "Multiple BYMONTHs not supported." msgstr "" -#: awx/api/serializers.py:3509 +#: awx/api/serializers.py:4546 msgid "BYDAY with numeric prefix not supported." msgstr "" -#: awx/api/serializers.py:3511 +#: awx/api/serializers.py:4548 msgid "BYYEARDAY not supported." msgstr "" -#: awx/api/serializers.py:3513 +#: awx/api/serializers.py:4550 msgid "BYWEEKNO not supported." msgstr "" -#: awx/api/serializers.py:3517 +#: awx/api/serializers.py:4552 +msgid "RRULE may not contain both COUNT and UNTIL" +msgstr "" + +#: awx/api/serializers.py:4556 msgid "COUNT > 999 is unsupported." msgstr "" -#: awx/api/serializers.py:3521 -msgid "rrule parsing failed validation." +#: awx/api/serializers.py:4560 +msgid "rrule parsing failed validation: {}" msgstr "" -#: awx/api/serializers.py:3621 +#: awx/api/serializers.py:4601 +msgid "Inventory Source must be a cloud resource." +msgstr "" + +#: awx/api/serializers.py:4603 +msgid "Manual Project cannot have a schedule set." +msgstr "" + +#: awx/api/serializers.py:4616 +msgid "" +"Count of jobs in the running or waiting state that are targeted for this " +"instance" +msgstr "" + +#: awx/api/serializers.py:4621 +msgid "Count of all jobs that target this instance" +msgstr "" + +#: awx/api/serializers.py:4654 +msgid "" +"Count of jobs in the running or waiting state that are targeted for this " +"instance group" +msgstr "" + +#: awx/api/serializers.py:4659 +msgid "Count of all jobs that target this instance group" +msgstr "" + +#: awx/api/serializers.py:4667 +msgid "Policy Instance Percentage" +msgstr "" + +#: awx/api/serializers.py:4668 +msgid "" +"Minimum percentage of all instances that will be automatically assigned to " +"this group when new instances come online." +msgstr "" + +#: awx/api/serializers.py:4673 +msgid "Policy Instance Minimum" +msgstr "" + +#: awx/api/serializers.py:4674 +msgid "" +"Static minimum number of Instances that will be automatically assign to this " +"group when new instances come online." +msgstr "" + +#: awx/api/serializers.py:4679 +msgid "Policy Instance List" +msgstr "" + +#: awx/api/serializers.py:4680 +msgid "List of exact-match Instances that will be assigned to this group" +msgstr "" + +#: awx/api/serializers.py:4702 +msgid "Duplicate entry {}." +msgstr "" + +#: awx/api/serializers.py:4704 +msgid "{} is not a valid hostname of an existing instance." +msgstr "" + +#: awx/api/serializers.py:4706 awx/api/views.py:202 +msgid "" +"Isolated instances may not be added or removed from instances groups via the " +"API." +msgstr "" + +#: awx/api/serializers.py:4708 awx/api/views.py:206 +msgid "Isolated instance group membership may not be managed via the API." +msgstr "" + +#: awx/api/serializers.py:4713 +msgid "tower instance group name may not be changed." +msgstr "" + +#: awx/api/serializers.py:4783 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "" -#: awx/api/serializers.py:3623 +#: awx/api/serializers.py:4785 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 "" -#: awx/api/serializers.py:3626 +#: awx/api/serializers.py:4788 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 "" -#: awx/api/serializers.py:3629 +#: awx/api/serializers.py:4791 msgid "The action taken with respect to the given object(s)." msgstr "" -#: awx/api/serializers.py:3732 -msgid "Unable to login with provided credentials." -msgstr "" - -#: awx/api/serializers.py:3734 -msgid "Must include \"username\" and \"password\"." -msgstr "" - -#: awx/api/views.py:107 +#: awx/api/views.py:119 msgid "Your license does not allow use of the activity stream." msgstr "" -#: awx/api/views.py:117 +#: awx/api/views.py:129 msgid "Your license does not permit use of system tracking." msgstr "" -#: awx/api/views.py:127 +#: awx/api/views.py:139 msgid "Your license does not allow use of workflows." msgstr "" -#: awx/api/views.py:135 awx/templates/rest_framework/api.html:28 +#: awx/api/views.py:153 +msgid "Cannot delete job resource when associated workflow job is running." +msgstr "" + +#: awx/api/views.py:158 +msgid "Cannot delete running job resource." +msgstr "" + +#: awx/api/views.py:163 +msgid "Job has not finished processing events." +msgstr "" + +#: awx/api/views.py:257 +msgid "Related job {} is still processing events." +msgstr "" + +#: awx/api/views.py:264 awx/templates/rest_framework/api.html:28 msgid "REST API" msgstr "" -#: awx/api/views.py:144 -msgid "Ansible Tower REST API" +#: awx/api/views.py:275 awx/templates/rest_framework/api.html:4 +msgid "AWX REST API" msgstr "" -#: awx/api/views.py:206 +#: awx/api/views.py:288 +msgid "API OAuth 2 Authorization Root" +msgstr "" + +#: awx/api/views.py:353 msgid "Version 1" msgstr "" -#: awx/api/views.py:210 +#: awx/api/views.py:357 msgid "Version 2" msgstr "" -#: awx/api/views.py:221 +#: awx/api/views.py:366 msgid "Ping" msgstr "" -#: awx/api/views.py:256 awx/conf/apps.py:12 +#: awx/api/views.py:397 awx/conf/apps.py:10 msgid "Configuration" msgstr "" -#: awx/api/views.py:309 +#: awx/api/views.py:454 msgid "Invalid license data" msgstr "" -#: awx/api/views.py:311 +#: awx/api/views.py:456 msgid "Missing 'eula_accepted' property" msgstr "" -#: awx/api/views.py:315 +#: awx/api/views.py:460 msgid "'eula_accepted' value is invalid" msgstr "" -#: awx/api/views.py:318 +#: awx/api/views.py:463 msgid "'eula_accepted' must be True" msgstr "" -#: awx/api/views.py:325 +#: awx/api/views.py:470 msgid "Invalid JSON" msgstr "" -#: awx/api/views.py:333 +#: awx/api/views.py:478 msgid "Invalid License" msgstr "" -#: awx/api/views.py:343 +#: awx/api/views.py:488 msgid "Invalid license" msgstr "" -#: awx/api/views.py:351 +#: awx/api/views.py:496 #, python-format msgid "Failed to remove license (%s)" msgstr "" -#: awx/api/views.py:356 +#: awx/api/views.py:501 msgid "Dashboard" msgstr "" -#: awx/api/views.py:455 +#: awx/api/views.py:600 msgid "Dashboard Jobs Graphs" msgstr "" -#: awx/api/views.py:491 +#: awx/api/views.py:636 #, python-format msgid "Unknown period \"%s\"" msgstr "" -#: awx/api/views.py:505 +#: awx/api/views.py:650 msgid "Instances" msgstr "" -#: awx/api/views.py:513 +#: awx/api/views.py:658 msgid "Instance Detail" msgstr "" -#: awx/api/views.py:521 -msgid "Instance Running Jobs" +#: awx/api/views.py:678 +msgid "Instance Jobs" msgstr "" -#: awx/api/views.py:536 +#: awx/api/views.py:692 msgid "Instance's Instance Groups" msgstr "" -#: awx/api/views.py:546 +#: awx/api/views.py:701 msgid "Instance Groups" msgstr "" -#: awx/api/views.py:554 +#: awx/api/views.py:709 msgid "Instance Group Detail" msgstr "" -#: awx/api/views.py:562 +#: awx/api/views.py:717 +msgid "Isolated Groups can not be removed from the API" +msgstr "" + +#: awx/api/views.py:719 +msgid "" +"Instance Groups acting as a controller for an Isolated Group can not be " +"removed from the API" +msgstr "" + +#: awx/api/views.py:725 msgid "Instance Group Running Jobs" msgstr "" -#: awx/api/views.py:572 +#: awx/api/views.py:734 msgid "Instance Group's Instances" msgstr "" -#: awx/api/views.py:582 +#: awx/api/views.py:744 msgid "Schedules" msgstr "" -#: awx/api/views.py:601 +#: awx/api/views.py:758 +msgid "Schedule Recurrence Rule Preview" +msgstr "" + +#: awx/api/views.py:805 +msgid "Cannot assign credential when related template is null." +msgstr "" + +#: awx/api/views.py:810 +msgid "Related template cannot accept {} on launch." +msgstr "" + +#: awx/api/views.py:812 +msgid "" +"Credential that requires user input on launch cannot be used in saved launch " +"configuration." +msgstr "" + +#: awx/api/views.py:820 +#, python-brace-format +msgid "" +"This launch configuration already provides a {credential_type} credential." +msgstr "" + +#: awx/api/views.py:823 +#, python-brace-format +msgid "Related template already uses {credential_type} credential." +msgstr "" + +#: awx/api/views.py:841 msgid "Schedule Jobs List" msgstr "" -#: awx/api/views.py:825 +#: awx/api/views.py:996 msgid "Your license only permits a single organization to exist." msgstr "" -#: awx/api/views.py:1061 awx/api/views.py:4624 -msgid "You cannot assign an Organization role as a child role for a Team." +#: awx/api/views.py:1223 awx/api/views.py:5106 +msgid "" +"You cannot assign an Organization participation role as a child role for a " +"Team." msgstr "" -#: awx/api/views.py:1065 awx/api/views.py:4638 +#: awx/api/views.py:1227 awx/api/views.py:5120 msgid "You cannot grant system-level permissions to a team." msgstr "" -#: awx/api/views.py:1072 awx/api/views.py:4630 +#: awx/api/views.py:1234 awx/api/views.py:5112 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" msgstr "" -#: awx/api/views.py:1162 -msgid "Cannot delete project." -msgstr "" - -#: awx/api/views.py:1197 +#: awx/api/views.py:1348 msgid "Project Schedules" msgstr "" -#: awx/api/views.py:1209 +#: awx/api/views.py:1359 msgid "Project SCM Inventory Sources" msgstr "" -#: awx/api/views.py:1312 awx/api/views.py:2685 awx/api/views.py:3767 -msgid "Cannot delete job resource when associated workflow job is running." +#: awx/api/views.py:1460 +msgid "Project Update Events List" msgstr "" -#: awx/api/views.py:1345 +#: awx/api/views.py:1474 +msgid "System Job Events List" +msgstr "" + +#: awx/api/views.py:1488 +msgid "Inventory Update Events List" +msgstr "" + +#: awx/api/views.py:1522 msgid "Project Update SCM Inventory Updates" msgstr "" -#: awx/api/views.py:1400 +#: awx/api/views.py:1581 msgid "Me" msgstr "" -#: awx/api/views.py:1444 awx/api/views.py:4581 -msgid "You may not perform any action with your own admin_role." +#: awx/api/views.py:1589 +msgid "OAuth 2 Applications" msgstr "" -#: awx/api/views.py:1450 awx/api/views.py:4585 -msgid "You may not change the membership of a users admin_role" +#: awx/api/views.py:1598 +msgid "OAuth 2 Application Detail" msgstr "" -#: awx/api/views.py:1455 awx/api/views.py:4590 +#: awx/api/views.py:1607 +msgid "OAuth 2 Application Tokens" +msgstr "" + +#: awx/api/views.py:1629 +msgid "OAuth2 Tokens" +msgstr "" + +#: awx/api/views.py:1638 +msgid "OAuth2 User Tokens" +msgstr "" + +#: awx/api/views.py:1650 +msgid "OAuth2 User Authorized Access Tokens" +msgstr "" + +#: awx/api/views.py:1665 +msgid "Organization OAuth2 Applications" +msgstr "" + +#: awx/api/views.py:1677 +msgid "OAuth2 Personal Access Tokens" +msgstr "" + +#: awx/api/views.py:1692 +msgid "OAuth Token Detail" +msgstr "" + +#: awx/api/views.py:1752 awx/api/views.py:5073 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" msgstr "" -#: awx/api/views.py:1459 awx/api/views.py:4594 +#: awx/api/views.py:1756 awx/api/views.py:5077 msgid "You cannot grant private credential access to another user" msgstr "" -#: awx/api/views.py:1557 +#: awx/api/views.py:1854 #, python-format msgid "Cannot change %s." msgstr "" -#: awx/api/views.py:1563 +#: awx/api/views.py:1860 msgid "Cannot delete user." msgstr "" -#: awx/api/views.py:1592 +#: awx/api/views.py:1884 msgid "Deletion not allowed for managed credential types" msgstr "" -#: awx/api/views.py:1594 +#: awx/api/views.py:1886 msgid "Credential types that are in use cannot be deleted" msgstr "" -#: awx/api/views.py:1772 +#: awx/api/views.py:2061 msgid "Cannot delete inventory script." msgstr "" -#: awx/api/views.py:1857 +#: awx/api/views.py:2152 +#, python-brace-format msgid "{0}" msgstr "" -#: awx/api/views.py:2078 +#: awx/api/views.py:2256 +msgid "The inventory for this host is already being deleted." +msgstr "" + +#: awx/api/views.py:2389 msgid "Fact not found." msgstr "" -#: awx/api/views.py:2102 +#: awx/api/views.py:2411 msgid "SSLError while trying to connect to {}" msgstr "" -#: awx/api/views.py:2104 +#: awx/api/views.py:2413 msgid "Request to {} timed out." msgstr "" -#: awx/api/views.py:2106 -msgid "Unkown exception {} while trying to GET {}" +#: awx/api/views.py:2415 +msgid "Unknown exception {} while trying to GET {}" msgstr "" -#: awx/api/views.py:2109 +#: awx/api/views.py:2418 +msgid "" +"Unauthorized access. Please check your Insights Credential username and " +"password." +msgstr "" + +#: awx/api/views.py:2421 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" msgstr "" -#: awx/api/views.py:2115 +#: awx/api/views.py:2428 msgid "Expected JSON response from Insights but instead got {}" msgstr "" -#: awx/api/views.py:2122 +#: awx/api/views.py:2435 msgid "This host is not recognized as an Insights host." msgstr "" -#: awx/api/views.py:2127 +#: awx/api/views.py:2440 msgid "The Insights Credential for \"{}\" was not found." msgstr "" -#: awx/api/views.py:2196 +#: awx/api/views.py:2508 msgid "Cyclical Group association." msgstr "" -#: awx/api/views.py:2473 +#: awx/api/views.py:2722 msgid "Inventory Source List" msgstr "" -#: awx/api/views.py:2486 +#: awx/api/views.py:2734 msgid "Inventory Sources Update" msgstr "" -#: awx/api/views.py:2514 -msgid "You do not have permission to update project `{}`" -msgstr "" - -#: awx/api/views.py:2526 +#: awx/api/views.py:2767 msgid "Could not start because `can_update` returned False" msgstr "" -#: awx/api/views.py:2534 +#: awx/api/views.py:2775 msgid "No inventory sources to update." msgstr "" -#: awx/api/views.py:2566 -msgid "Cannot delete inventory source." -msgstr "" - -#: awx/api/views.py:2574 +#: awx/api/views.py:2804 msgid "Inventory Source Schedules" msgstr "" -#: awx/api/views.py:2604 +#: awx/api/views.py:2832 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" -#: awx/api/views.py:2833 +#: awx/api/views.py:2887 +msgid "Vault credentials are not yet supported for inventory sources." +msgstr "" + +#: awx/api/views.py:2892 +msgid "Source already has cloud credential assigned." +msgstr "" + +#: awx/api/views.py:3042 +msgid "Field is not allowed for use with v1 API." +msgstr "" + +#: awx/api/views.py:3052 +msgid "" +"'credentials' cannot be used in combination with 'credential', " +"'vault_credential', or 'extra_credentials'." +msgstr "" + +#: awx/api/views.py:3079 +msgid "Incorrect type. Expected {}, received {}." +msgstr "" + +#: awx/api/views.py:3172 msgid "Job Template Schedules" msgstr "" -#: awx/api/views.py:2853 awx/api/views.py:2869 +#: awx/api/views.py:3190 awx/api/views.py:3201 msgid "Your license does not allow adding surveys." msgstr "" -#: awx/api/views.py:2876 -msgid "'name' missing from survey spec." +#: awx/api/views.py:3220 +msgid "Field '{}' is missing from survey spec." msgstr "" -#: awx/api/views.py:2878 -msgid "'description' missing from survey spec." +#: awx/api/views.py:3222 +msgid "Expected {} for field '{}', received {} type." msgstr "" -#: awx/api/views.py:2880 -msgid "'spec' missing from survey spec." -msgstr "" - -#: awx/api/views.py:2882 -msgid "'spec' must be a list of items." -msgstr "" - -#: awx/api/views.py:2884 +#: awx/api/views.py:3226 msgid "'spec' doesn't contain any items." msgstr "" -#: awx/api/views.py:2890 +#: awx/api/views.py:3235 #, python-format msgid "Survey question %s is not a json object." msgstr "" -#: awx/api/views.py:2892 +#: awx/api/views.py:3237 #, python-format msgid "'type' missing from survey question %s." msgstr "" -#: awx/api/views.py:2894 +#: awx/api/views.py:3239 #, python-format msgid "'question_name' missing from survey question %s." msgstr "" -#: awx/api/views.py:2896 +#: awx/api/views.py:3241 #, python-format msgid "'variable' missing from survey question %s." msgstr "" -#: awx/api/views.py:2898 +#: awx/api/views.py:3243 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "" -#: awx/api/views.py:2903 +#: awx/api/views.py:3248 #, python-format msgid "'required' missing from survey question %s." msgstr "" -#: awx/api/views.py:2908 -msgid "" -"$encrypted$ is reserved keyword and may not be used as a default for " -"password {}." +#: awx/api/views.py:3253 +#, python-brace-format +msgid "Value {question_default} for '{variable_name}' expected to be a string." msgstr "" -#: awx/api/views.py:3024 +#: awx/api/views.py:3263 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword for password question defaults, survey " +"question {question_position} is type {question_type}." +msgstr "" + +#: awx/api/views.py:3279 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword, may not be used for new default in " +"position {question_position}." +msgstr "" + +#: awx/api/views.py:3353 +#, python-brace-format +msgid "Cannot assign multiple {credential_type} credentials." +msgstr "" + +#: awx/api/views.py:3357 +msgid "Cannot assign a Credential of kind `{}`." +msgstr "" + +#: awx/api/views.py:3374 +msgid "Extra credentials must be network or cloud." +msgstr "" + +#: awx/api/views.py:3396 msgid "Maximum number of labels for {} reached." msgstr "" -#: awx/api/views.py:3143 +#: awx/api/views.py:3519 msgid "No matching host could be found!" msgstr "" -#: awx/api/views.py:3146 +#: awx/api/views.py:3522 msgid "Multiple hosts matched the request!" msgstr "" -#: awx/api/views.py:3151 +#: awx/api/views.py:3527 msgid "Cannot start automatically, user input required!" msgstr "" -#: awx/api/views.py:3158 +#: awx/api/views.py:3534 msgid "Host callback job already pending." msgstr "" -#: awx/api/views.py:3171 +#: awx/api/views.py:3549 awx/api/views.py:4336 msgid "Error starting job!" msgstr "" -#: awx/api/views.py:3278 +#: awx/api/views.py:3669 +#, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr "" -#: awx/api/views.py:3303 +#: awx/api/views.py:3694 msgid "Multiple parent relationship not allowed." msgstr "" -#: awx/api/views.py:3308 +#: awx/api/views.py:3699 msgid "Cycle detected." msgstr "" -#: awx/api/views.py:3512 +#: awx/api/views.py:3902 msgid "Workflow Job Template Schedules" msgstr "" -#: awx/api/views.py:3657 awx/api/views.py:4233 +#: awx/api/views.py:4038 awx/api/views.py:4740 msgid "Superuser privileges needed." msgstr "" -#: awx/api/views.py:3689 +#: awx/api/views.py:4071 msgid "System Job Template Schedules" msgstr "" -#: awx/api/views.py:3907 +#: awx/api/views.py:4129 +msgid "POST not allowed for Job launching in version 2 of the api" +msgstr "" + +#: awx/api/views.py:4153 awx/api/views.py:4159 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "" + +#: awx/api/views.py:4319 +#, python-brace-format +msgid "Wait until job finishes before retrying on {status_value} hosts." +msgstr "" + +#: awx/api/views.py:4324 +#, python-brace-format +msgid "Cannot retry on {status_value} hosts, playbook stats not available." +msgstr "" + +#: awx/api/views.py:4329 +#, python-brace-format +msgid "Cannot relaunch because previous job had 0 {status_value} hosts." +msgstr "" + +#: awx/api/views.py:4358 +msgid "Cannot create schedule because job requires credential passwords." +msgstr "" + +#: awx/api/views.py:4363 +msgid "Cannot create schedule because job was launched by legacy method." +msgstr "" + +#: awx/api/views.py:4365 +msgid "Cannot create schedule because a related resource is missing." +msgstr "" + +#: awx/api/views.py:4420 msgid "Job Host Summaries List" msgstr "" -#: awx/api/views.py:3954 +#: awx/api/views.py:4469 msgid "Job Event Children List" msgstr "" -#: awx/api/views.py:3963 +#: awx/api/views.py:4479 msgid "Job Event Hosts List" msgstr "" -#: awx/api/views.py:3973 +#: awx/api/views.py:4488 msgid "Job Events List" msgstr "" -#: awx/api/views.py:4187 +#: awx/api/views.py:4697 msgid "Ad Hoc Command Events List" msgstr "" -#: awx/api/views.py:4395 -msgid "Error generating stdout download file: {}" -msgstr "" - -#: awx/api/views.py:4408 -#, python-format -msgid "Error generating stdout download file: %s" -msgstr "" - -#: awx/api/views.py:4453 +#: awx/api/views.py:4939 msgid "Delete not allowed while there are pending notifications" msgstr "" -#: awx/api/views.py:4460 +#: awx/api/views.py:4947 msgid "Notification Template Test" msgstr "" @@ -1123,19 +1491,32 @@ msgstr "" msgid "Example setting which can be different for each user." msgstr "" -#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:56 +#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:55 msgid "User" msgstr "" -#: awx/conf/fields.py:62 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 +#, python-brace-format +msgid "" +"Expected None, True, False, a string or list of strings but got {input_type} " +"instead." +msgstr "" + +#: awx/conf/fields.py:104 msgid "Enter a valid URL" msgstr "" -#: awx/conf/fields.py:94 +#: awx/conf/fields.py:136 +#, python-brace-format msgid "\"{input}\" is not a valid string." msgstr "" -#: awx/conf/license.py:19 +#: awx/conf/fields.py:151 +#, python-brace-format +msgid "Expected a list of tuples of max length 2 but got {input_type} instead." +msgstr "" + +#: awx/conf/license.py:22 msgid "Your Tower license does not allow that." msgstr "" @@ -1151,7 +1532,11 @@ msgstr "" msgid "Skip commenting out settings in files." msgstr "" -#: awx/conf/management/commands/migrate_to_database_settings.py:61 +#: awx/conf/management/commands/migrate_to_database_settings.py:62 +msgid "Skip migrating and only comment out settings in files." +msgstr "" + +#: awx/conf/management/commands/migrate_to_database_settings.py:68 msgid "Backup existing settings files with this suffix." msgstr "" @@ -1175,7 +1560,7 @@ msgstr "" msgid "User-Defaults" msgstr "" -#: awx/conf/registry.py:150 +#: awx/conf/registry.py:154 msgid "This value has been set manually in a settings file." msgstr "" @@ -1218,11 +1603,12 @@ msgstr "" #: awx/conf/tests/unit/test_settings.py:360 #: awx/conf/tests/unit/test_settings.py:374 #: awx/conf/tests/unit/test_settings.py:398 -#: awx/conf/tests/unit/test_settings.py:412 -#: awx/conf/tests/unit/test_settings.py:448 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:51 -#: awx/main/conf.py:63 awx/main/conf.py:81 awx/main/conf.py:96 -#: awx/main/conf.py:121 +#: awx/conf/tests/unit/test_settings.py:411 +#: awx/conf/tests/unit/test_settings.py:430 +#: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "" @@ -1234,11 +1620,11 @@ msgstr "" msgid "OtherSystem" msgstr "" -#: awx/conf/views.py:48 +#: awx/conf/views.py:47 msgid "Setting Categories" msgstr "" -#: awx/conf/views.py:73 +#: awx/conf/views.py:71 msgid "Setting Detail" msgstr "" @@ -1246,93 +1632,96 @@ msgstr "" msgid "Logging Connectivity Test" msgstr "" -#: awx/main/access.py:224 +#: awx/main/access.py:59 +#, python-format +msgid "Required related field %s for permission check." +msgstr "" + +#: awx/main/access.py:75 #, python-format msgid "Bad data found in related field %s." msgstr "" -#: awx/main/access.py:268 +#: awx/main/access.py:304 msgid "License is missing." msgstr "" -#: awx/main/access.py:270 +#: awx/main/access.py:306 msgid "License has expired." msgstr "" -#: awx/main/access.py:278 +#: awx/main/access.py:314 #, python-format msgid "License count of %s instances has been reached." msgstr "" -#: awx/main/access.py:280 +#: awx/main/access.py:316 #, python-format msgid "License count of %s instances has been exceeded." msgstr "" -#: awx/main/access.py:282 +#: awx/main/access.py:318 msgid "Host count exceeds available instances." msgstr "" -#: awx/main/access.py:286 +#: awx/main/access.py:322 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "" -#: awx/main/access.py:288 +#: awx/main/access.py:324 msgid "Features not found in active license." msgstr "" -#: awx/main/access.py:534 awx/main/access.py:617 awx/main/access.py:746 -#: awx/main/access.py:803 awx/main/access.py:1065 awx/main/access.py:1265 -#: awx/main/access.py:1708 -msgid "Resource is being used by running jobs" -msgstr "" - -#: awx/main/access.py:673 +#: awx/main/access.py:837 msgid "Unable to change inventory on a host." msgstr "" -#: awx/main/access.py:690 awx/main/access.py:735 +#: awx/main/access.py:854 awx/main/access.py:899 msgid "Cannot associate two items from different inventories." msgstr "" -#: awx/main/access.py:723 +#: awx/main/access.py:887 msgid "Unable to change inventory on a group." msgstr "" -#: awx/main/access.py:985 +#: awx/main/access.py:1148 msgid "Unable to change organization on a team." msgstr "" -#: awx/main/access.py:998 +#: awx/main/access.py:1165 msgid "The {} role cannot be assigned to a team" msgstr "" -#: awx/main/access.py:1000 +#: awx/main/access.py:1167 msgid "The admin_role for a User cannot be assigned to a team" msgstr "" -#: awx/main/access.py:1426 +#: awx/main/access.py:1533 awx/main/access.py:1967 +msgid "Job was launched with prompts provided by another user." +msgstr "" + +#: awx/main/access.py:1553 msgid "Job has been orphaned from its job template." msgstr "" -#: awx/main/access.py:1428 -msgid "You do not have execute permission to related job template." +#: awx/main/access.py:1555 +msgid "Job was launched with unknown prompted fields." msgstr "" -#: awx/main/access.py:1431 +#: awx/main/access.py:1557 msgid "Job was launched with prompted fields." msgstr "" -#: awx/main/access.py:1433 +#: awx/main/access.py:1559 msgid " Organization level permissions required." msgstr "" -#: awx/main/access.py:1435 +#: awx/main/access.py:1561 msgid " You do not have permission to related resources." msgstr "" -#: awx/main/access.py:1781 +#: awx/main/access.py:1981 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1366,322 +1755,360 @@ msgstr "" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." -msgstr "" - -#: awx/main/conf.py:49 -msgid "Enable Administrator Alerts" +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." msgstr "" #: awx/main/conf.py:50 -msgid "Email Admin users for system events that may require attention." +msgid "Organization Admins Can Manage Users and Teams" +msgstr "" + +#: awx/main/conf.py:51 +msgid "" +"Controls whether any Organization Admin has the privileges to create and " +"manage users and teams. You may want to disable this ability if you are " +"using an LDAP or SAML integration." msgstr "" #: awx/main/conf.py:60 -msgid "Base URL of the Tower host" +msgid "Enable Administrator Alerts" msgstr "" #: awx/main/conf.py:61 +msgid "Email Admin users for system events that may require attention." +msgstr "" + +#: awx/main/conf.py:71 +msgid "Base URL of the Tower host" +msgstr "" + +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to " "the Tower host." msgstr "" -#: awx/main/conf.py:70 +#: awx/main/conf.py:81 msgid "Remote Host Headers" msgstr "" -#: awx/main/conf.py:71 +#: awx/main/conf.py:82 msgid "" "HTTP headers and meta keys to search to determine remote host name or IP. " "Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " -"behind a reverse proxy.\n" -"\n" -"Note: The headers will be searched in order and the first found remote host " -"name or IP will be used.\n" -"\n" -"In the below example 8.8.8.7 would be the chosen IP address.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"behind a reverse proxy. See the \"Proxy Support\" section of the " +"Adminstrator guide for more details." msgstr "" -#: awx/main/conf.py:88 +#: awx/main/conf.py:94 msgid "Proxy IP Whitelist" msgstr "" -#: awx/main/conf.py:89 +#: awx/main/conf.py:95 msgid "" "If Tower is behind a reverse proxy/load balancer, use this setting to " "whitelist the proxy IP addresses from which Tower should trust custom " -"REMOTE_HOST_HEADERS header values\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', " -"'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"If this setting is an empty list (the default), the headers specified by " -"REMOTE_HOST_HEADERS will be trusted unconditionally')" +"REMOTE_HOST_HEADERS header values. If this setting is an empty list (the " +"default), the headers specified by REMOTE_HOST_HEADERS will be trusted " +"unconditionally')" msgstr "" -#: awx/main/conf.py:117 +#: awx/main/conf.py:121 msgid "License" msgstr "" -#: awx/main/conf.py:118 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use /api/" "v1/config/ to update or change the license." msgstr "" -#: awx/main/conf.py:128 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "" -#: awx/main/conf.py:129 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "" -#: awx/main/conf.py:130 awx/main/conf.py:140 awx/main/conf.py:151 -#: awx/main/conf.py:161 awx/main/conf.py:171 awx/main/conf.py:181 -#: awx/main/conf.py:192 awx/main/conf.py:204 awx/main/conf.py:216 -#: awx/main/conf.py:227 awx/main/conf.py:237 awx/main/conf.py:248 -#: awx/main/conf.py:258 awx/main/conf.py:268 awx/main/conf.py:278 -#: awx/main/conf.py:290 awx/main/conf.py:302 awx/main/conf.py:314 -#: awx/main/conf.py:327 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "" -#: awx/main/conf.py:138 +#: awx/main/conf.py:143 +msgid "Always" +msgstr "" + +#: awx/main/conf.py:144 +msgid "Never" +msgstr "" + +#: awx/main/conf.py:145 +msgid "Only On Job Template Definitions" +msgstr "" + +#: awx/main/conf.py:148 +msgid "When can extra variables contain Jinja templates?" +msgstr "" + +#: awx/main/conf.py:150 +msgid "" +"Ansible allows variable substitution via the Jinja2 templating language for " +"--extra-vars. This poses a potential security risk where Tower users with " +"the ability to specify extra vars at job launch time can use Jinja2 " +"templates to run arbitrary Python. It is recommended that this value be set " +"to \"template\" or \"never\"." +msgstr "" + +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "" -#: awx/main/conf.py:139 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." msgstr "" -#: awx/main/conf.py:147 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "" -#: awx/main/conf.py:148 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " "scripts)." msgstr "" -#: awx/main/conf.py:159 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" msgstr "" -#: awx/main/conf.py:160 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." msgstr "" -#: awx/main/conf.py:169 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" msgstr "" -#: awx/main/conf.py:170 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." msgstr "" -#: awx/main/conf.py:179 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "" -#: awx/main/conf.py:180 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." msgstr "" -#: awx/main/conf.py:189 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "" -#: awx/main/conf.py:190 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " "isolated instance." msgstr "" -#: awx/main/conf.py:201 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "" -#: awx/main/conf.py:202 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " "network latency." msgstr "" -#: awx/main/conf.py:214 awx/main/conf.py:215 +#: awx/main/conf.py:237 +msgid "Generate RSA keys for isolated instances" +msgstr "" + +#: awx/main/conf.py:238 +msgid "" +"If set, a random RSA key will be generated and distributed to isolated " +"instances. To disable this behavior and manage authentication for isolated " +"instances outside of Tower, disable this setting." +msgstr "" + +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "" -#: awx/main/conf.py:225 awx/main/conf.py:226 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "" -#: awx/main/conf.py:235 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "" -#: awx/main/conf.py:236 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." msgstr "" -#: awx/main/conf.py:246 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:247 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." msgstr "" -#: awx/main/conf.py:256 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "" -#: awx/main/conf.py:257 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." msgstr "" -#: awx/main/conf.py:266 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" msgstr "" -#: awx/main/conf.py:267 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." msgstr "" -#: awx/main/conf.py:276 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "" -#: awx/main/conf.py:277 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." msgstr "" -#: awx/main/conf.py:287 +#: awx/main/conf.py:327 msgid "Default Job Timeout" msgstr "" -#: awx/main/conf.py:288 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " "template will override this." msgstr "" -#: awx/main/conf.py:299 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "" -#: awx/main/conf.py:300 +#: awx/main/conf.py:340 msgid "" -"Maximum time to allow inventory updates to run. Use value of 0 to indicate " -"that no timeout should be imposed. A timeout set on an individual inventory " -"source will override this." +"Maximum time in seconds to allow inventory updates to run. Use value of 0 to " +"indicate that no timeout should be imposed. A timeout set on an individual " +"inventory source will override this." msgstr "" -#: awx/main/conf.py:311 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" msgstr "" -#: awx/main/conf.py:312 +#: awx/main/conf.py:352 msgid "" -"Maximum time to allow project updates to run. Use value of 0 to indicate " -"that no timeout should be imposed. A timeout set on an individual project " -"will override this." +"Maximum time in seconds to allow project updates to run. Use value of 0 to " +"indicate that no timeout should be imposed. A timeout set on an individual " +"project will override this." msgstr "" -#: awx/main/conf.py:323 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "" -#: awx/main/conf.py:324 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be " "accessible by a playbook. Note, this does not influence the deletion of " -"ansible_facts from the database." +"ansible_facts from the database. Use a value of 0 to indicate that no " +"timeout should be imposed." msgstr "" -#: awx/main/conf.py:335 +#: awx/main/conf.py:377 msgid "Logging Aggregator" msgstr "" -#: awx/main/conf.py:336 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." msgstr "" -#: awx/main/conf.py:337 awx/main/conf.py:347 awx/main/conf.py:358 -#: awx/main/conf.py:368 awx/main/conf.py:380 awx/main/conf.py:395 -#: awx/main/conf.py:407 awx/main/conf.py:416 awx/main/conf.py:426 -#: awx/main/conf.py:436 awx/main/conf.py:447 awx/main/conf.py:459 -#: awx/main/conf.py:472 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:482 awx/main/conf.py:493 awx/main/conf.py:505 +#: awx/main/conf.py:518 msgid "Logging" msgstr "" -#: awx/main/conf.py:344 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "" -#: awx/main/conf.py:345 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." msgstr "" -#: awx/main/conf.py:356 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" msgstr "" -#: awx/main/conf.py:357 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." msgstr "" -#: awx/main/conf.py:366 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "" -#: awx/main/conf.py:367 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "" -#: awx/main/conf.py:378 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "" -#: awx/main/conf.py:379 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "" -#: awx/main/conf.py:388 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "" -#: awx/main/conf.py:389 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include " "any or all of: \n" @@ -1691,57 +2118,59 @@ msgid "" "system_tracking - facts gathered from scan jobs." msgstr "" -#: awx/main/conf.py:402 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" msgstr "" -#: awx/main/conf.py:403 +#: awx/main/conf.py:447 msgid "" -"If set, system tracking facts will be sent for each package, service, " -"orother item found in a scan, allowing for greater search query granularity. " +"If set, system tracking facts will be sent for each package, service, or " +"other item found in a scan, allowing for greater search query granularity. " "If unset, facts will be sent as a single dictionary, allowing for greater " "efficiency in fact processing." msgstr "" -#: awx/main/conf.py:414 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "" -#: awx/main/conf.py:415 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "" -#: awx/main/conf.py:424 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "" -#: awx/main/conf.py:425 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." msgstr "" -#: awx/main/conf.py:434 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "" -#: awx/main/conf.py:435 -msgid "Protocol used to communicate with log aggregator." +#: awx/main/conf.py:479 +msgid "" +"Protocol used to communicate with log aggregator. HTTPS/HTTP assumes HTTPS " +"unless http:// is explicitly used in the Logging Aggregator hostname." msgstr "" -#: awx/main/conf.py:443 +#: awx/main/conf.py:489 msgid "TCP Connection Timeout" msgstr "" -#: awx/main/conf.py:444 +#: awx/main/conf.py:490 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." msgstr "" -#: awx/main/conf.py:454 +#: awx/main/conf.py:500 msgid "Enable/disable HTTPS certificate verification" msgstr "" -#: awx/main/conf.py:455 +#: awx/main/conf.py:501 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -1749,11 +2178,11 @@ msgid "" "connection." msgstr "" -#: awx/main/conf.py:467 +#: awx/main/conf.py:513 msgid "Logging Aggregator Level Threshold" msgstr "" -#: awx/main/conf.py:468 +#: awx/main/conf.py:514 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -1761,384 +2190,804 @@ msgid "" "anlytics ignore this setting)" msgstr "" -#: awx/main/conf.py:491 awx/sso/conf.py:1112 +#: awx/main/conf.py:537 awx/sso/conf.py:1264 msgid "\n" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:17 msgid "Sudo" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:17 msgid "Su" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:17 msgid "Pbrun" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:17 msgid "Pfexec" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:18 msgid "DZDO" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:18 msgid "Pmrun" msgstr "" -#: awx/main/constants.py:8 +#: awx/main/constants.py:18 msgid "Runas" msgstr "" -#: awx/main/fields.py:55 -#, python-format -msgid "'%s' is not one of ['%s']" +#: awx/main/constants.py:19 +msgid "Enable" msgstr "" -#: awx/main/fields.py:520 +#: awx/main/constants.py:19 +msgid "Doas" +msgstr "" + +#: awx/main/constants.py:21 +msgid "None" +msgstr "" + +#: awx/main/fields.py:62 +#, python-brace-format +msgid "'{value}' is not one of ['{allowed_values}']" +msgstr "" + +#: awx/main/fields.py:421 +#, python-brace-format +msgid "{type} provided in relative path {path}, expected {expected_type}" +msgstr "" + +#: awx/main/fields.py:426 +#, python-brace-format +msgid "{type} provided, expected {expected_type}" +msgstr "" + +#: awx/main/fields.py:431 +#, python-brace-format +msgid "Schema validation error in relative path {path} ({error})" +msgstr "" + +#: awx/main/fields.py:552 +msgid "secret values must be of type string, not {}" +msgstr "" + +#: awx/main/fields.py:587 +#, python-format +msgid "cannot be set unless \"%s\" is set" +msgstr "" + +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "" -#: awx/main/fields.py:544 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "" -#: awx/main/fields.py:547 -msgid "should not be set when SSH key is empty." -msgstr "" - -#: awx/main/fields.py:549 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "" -#: awx/main/fields.py:613 +#: awx/main/fields.py:691 +msgid "'dependencies' is not supported for custom credentials." +msgstr "" + +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "" -#: awx/main/fields.py:620 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "" -#: awx/main/fields.py:633 -#, python-format -msgid "%s not allowed for %s type (%s)" +#: awx/main/fields.py:725 +msgid "become_method is a reserved type name" msgstr "" -#: awx/main/fields.py:717 -#, python-format -msgid "%s uses an undefined field (%s)" +#: awx/main/fields.py:736 +#, python-brace-format +msgid "{sub_key} not allowed for {element_type} type ({element_id})" msgstr "" -#: awx/main/middleware.py:121 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "" + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "" + +#: awx/main/fields.py:827 +msgid "Must use multi-file syntax when injecting multiple files" +msgstr "" + +#: awx/main/fields.py:844 +#, python-brace-format +msgid "{sub_key} uses an undefined field ({error_msg})" +msgstr "" + +#: awx/main/fields.py:851 +#, python-brace-format +msgid "" +"Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" +msgstr "" + +#: awx/main/middleware.py:160 msgid "Formats of all available named urls" msgstr "" -#: awx/main/middleware.py:122 +#: awx/main/middleware.py:161 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." msgstr "" -#: awx/main/middleware.py:124 awx/main/middleware.py:134 +#: awx/main/middleware.py:163 awx/main/middleware.py:173 msgid "Named URL" msgstr "" -#: awx/main/middleware.py:131 +#: awx/main/middleware.py:170 msgid "List of all named url graph nodes." msgstr "" -#: awx/main/middleware.py:132 +#: awx/main/middleware.py:171 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use " "this list to programmatically generate named URLs for resources" msgstr "" -#: awx/main/migrations/_reencrypt.py:23 awx/main/models/notifications.py:33 +#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:35 msgid "Email" msgstr "" -#: awx/main/migrations/_reencrypt.py:24 awx/main/models/notifications.py:34 +#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:36 msgid "Slack" msgstr "" -#: awx/main/migrations/_reencrypt.py:25 awx/main/models/notifications.py:35 +#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:37 msgid "Twilio" msgstr "" -#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:36 +#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:38 msgid "Pagerduty" msgstr "" -#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:37 +#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:39 msgid "HipChat" msgstr "" -#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:38 +#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:41 +msgid "Mattermost" +msgstr "" + +#: awx/main/migrations/_reencrypt.py:32 awx/main/models/notifications.py:40 msgid "Webhook" msgstr "" -#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:39 +#: awx/main/migrations/_reencrypt.py:33 awx/main/models/notifications.py:43 msgid "IRC" msgstr "" -#: awx/main/models/activity_stream.py:24 +#: awx/main/models/activity_stream.py:25 msgid "Entity Created" msgstr "" -#: awx/main/models/activity_stream.py:25 +#: awx/main/models/activity_stream.py:26 msgid "Entity Updated" msgstr "" -#: awx/main/models/activity_stream.py:26 +#: awx/main/models/activity_stream.py:27 msgid "Entity Deleted" msgstr "" -#: awx/main/models/activity_stream.py:27 +#: awx/main/models/activity_stream.py:28 msgid "Entity Associated with another Entity" msgstr "" -#: awx/main/models/activity_stream.py:28 +#: awx/main/models/activity_stream.py:29 msgid "Entity was Disassociated with another Entity" msgstr "" -#: awx/main/models/ad_hoc_commands.py:100 +#: awx/main/models/ad_hoc_commands.py:95 msgid "No valid inventory." msgstr "" -#: awx/main/models/ad_hoc_commands.py:107 +#: awx/main/models/ad_hoc_commands.py:102 msgid "You must provide a machine / SSH credential." msgstr "" -#: awx/main/models/ad_hoc_commands.py:118 -#: awx/main/models/ad_hoc_commands.py:126 +#: awx/main/models/ad_hoc_commands.py:113 +#: awx/main/models/ad_hoc_commands.py:121 msgid "Invalid type for ad hoc command" msgstr "" -#: awx/main/models/ad_hoc_commands.py:121 +#: awx/main/models/ad_hoc_commands.py:116 msgid "Unsupported module for ad hoc commands." msgstr "" -#: awx/main/models/ad_hoc_commands.py:129 +#: awx/main/models/ad_hoc_commands.py:124 #, python-format msgid "No argument passed to %s module." msgstr "" -#: awx/main/models/ad_hoc_commands.py:243 awx/main/models/jobs.py:900 -msgid "Host Failed" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:244 awx/main/models/jobs.py:901 -msgid "Host OK" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:245 awx/main/models/jobs.py:904 -msgid "Host Unreachable" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:250 awx/main/models/jobs.py:903 -msgid "Host Skipped" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:260 awx/main/models/jobs.py:931 -msgid "Debug" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:261 awx/main/models/jobs.py:932 -msgid "Verbose" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:262 awx/main/models/jobs.py:933 -msgid "Deprecated" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:263 awx/main/models/jobs.py:934 -msgid "Warning" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:264 awx/main/models/jobs.py:935 -msgid "System Warning" -msgstr "" - -#: awx/main/models/ad_hoc_commands.py:265 awx/main/models/jobs.py:936 -#: awx/main/models/unified_jobs.py:63 -msgid "Error" -msgstr "" - -#: awx/main/models/base.py:40 awx/main/models/base.py:46 -#: awx/main/models/base.py:51 +#: awx/main/models/base.py:33 awx/main/models/base.py:39 +#: awx/main/models/base.py:44 awx/main/models/base.py:49 msgid "Run" msgstr "" -#: awx/main/models/base.py:41 awx/main/models/base.py:47 -#: awx/main/models/base.py:52 +#: awx/main/models/base.py:34 awx/main/models/base.py:40 +#: awx/main/models/base.py:45 awx/main/models/base.py:50 msgid "Check" msgstr "" -#: awx/main/models/base.py:42 +#: awx/main/models/base.py:35 msgid "Scan" msgstr "" -#: awx/main/models/credential.py:82 +#: awx/main/models/credential/__init__.py:110 msgid "Host" msgstr "" -#: awx/main/models/credential.py:83 +#: awx/main/models/credential/__init__.py:111 msgid "The hostname or IP address to use." msgstr "" -#: awx/main/models/credential.py:89 +#: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "" -#: awx/main/models/credential.py:90 +#: awx/main/models/credential/__init__.py:118 msgid "Username for this credential." msgstr "" -#: awx/main/models/credential.py:96 +#: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "" -#: awx/main/models/credential.py:97 +#: awx/main/models/credential/__init__.py:125 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." msgstr "" -#: awx/main/models/credential.py:104 +#: awx/main/models/credential/__init__.py:132 msgid "Security Token" msgstr "" -#: awx/main/models/credential.py:105 +#: awx/main/models/credential/__init__.py:133 msgid "Security Token for this credential" msgstr "" -#: awx/main/models/credential.py:111 +#: awx/main/models/credential/__init__.py:139 msgid "Project" msgstr "" -#: awx/main/models/credential.py:112 +#: awx/main/models/credential/__init__.py:140 msgid "The identifier for the project." msgstr "" -#: awx/main/models/credential.py:118 +#: awx/main/models/credential/__init__.py:146 msgid "Domain" msgstr "" -#: awx/main/models/credential.py:119 +#: awx/main/models/credential/__init__.py:147 msgid "The identifier for the domain." msgstr "" -#: awx/main/models/credential.py:124 +#: awx/main/models/credential/__init__.py:152 msgid "SSH private key" msgstr "" -#: awx/main/models/credential.py:125 +#: awx/main/models/credential/__init__.py:153 msgid "RSA or DSA private key to be used instead of password." msgstr "" -#: awx/main/models/credential.py:131 +#: awx/main/models/credential/__init__.py:159 msgid "SSH key unlock" msgstr "" -#: awx/main/models/credential.py:132 +#: awx/main/models/credential/__init__.py:160 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." msgstr "" -#: awx/main/models/credential.py:139 -msgid "None" -msgstr "" - -#: awx/main/models/credential.py:140 +#: awx/main/models/credential/__init__.py:168 msgid "Privilege escalation method." msgstr "" -#: awx/main/models/credential.py:146 +#: awx/main/models/credential/__init__.py:174 msgid "Privilege escalation username." msgstr "" -#: awx/main/models/credential.py:152 +#: awx/main/models/credential/__init__.py:180 msgid "Password for privilege escalation method." msgstr "" -#: awx/main/models/credential.py:158 +#: awx/main/models/credential/__init__.py:186 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "" -#: awx/main/models/credential.py:162 +#: awx/main/models/credential/__init__.py:190 msgid "Whether to use the authorize mechanism." msgstr "" -#: awx/main/models/credential.py:168 +#: awx/main/models/credential/__init__.py:196 msgid "Password used by the authorize mechanism." msgstr "" -#: awx/main/models/credential.py:174 +#: awx/main/models/credential/__init__.py:202 msgid "Client Id or Application Id for the credential" msgstr "" -#: awx/main/models/credential.py:180 +#: awx/main/models/credential/__init__.py:208 msgid "Secret Token for this credential" msgstr "" -#: awx/main/models/credential.py:186 +#: awx/main/models/credential/__init__.py:214 msgid "Subscription identifier for this credential" msgstr "" -#: awx/main/models/credential.py:192 +#: awx/main/models/credential/__init__.py:220 msgid "Tenant identifier for this credential" msgstr "" -#: awx/main/models/credential.py:216 +#: awx/main/models/credential/__init__.py:244 msgid "" "Specify the type of credential you want to create. Refer to the Ansible " "Tower documentation for details on each type." msgstr "" -#: awx/main/models/credential.py:230 awx/main/models/credential.py:416 +#: awx/main/models/credential/__init__.py:258 +#: awx/main/models/credential/__init__.py:476 msgid "" "Enter inputs 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 "" -#: awx/main/models/credential.py:397 +#: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "" -#: awx/main/models/credential.py:398 +#: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "" -#: awx/main/models/credential.py:399 +#: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "" -#: awx/main/models/credential.py:400 +#: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "" -#: awx/main/models/credential.py:401 +#: awx/main/models/credential/__init__.py:461 msgid "Cloud" msgstr "" -#: awx/main/models/credential.py:402 +#: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "" -#: awx/main/models/credential.py:423 +#: awx/main/models/credential/__init__.py:483 msgid "" "Enter injectors 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 "" +#: awx/main/models/credential/__init__.py:534 +#, python-format +msgid "adding %s credential type" +msgstr "" + +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying " +"the --become-method Ansible parameter." +msgstr "" + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the --vault-" +"id Ansible parameter for providing multiple Vault passwords. Note: this " +"feature only works in Ansible 2.4+." +msgstr "" + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"Management (IAM) users." +msgstr "" + +#: awx/main/models/credential/__init__.py:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, https://openstack.business.com/" +"v2.0/" +msgstr "" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "" + +#: awx/main/models/credential/__init__.py:926 +msgid "" +"Enter the hostname or IP address that corresponds to your VMware vCenter." +msgstr "" + +#: awx/main/models/credential/__init__.py:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"example, https://satellite.example.org" +msgstr "" + +#: awx/main/models/credential/__init__.py:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"instance. For example, https://cloudforms.example.org" +msgstr "" + +#: awx/main/models/credential/__init__.py:1004 awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "" + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "" + +#: awx/main/models/credential/__init__.py:1028 +msgid "" +"Paste the contents of the PEM file associated with the service account email." +msgstr "" + +#: awx/main/models/credential/__init__.py:1040 awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "" + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "" + +#: awx/main/models/credential/__init__.py:1115 awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "" + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "" + +#: awx/main/models/credential/__init__.py:1136 +msgid "Absolute file path to the CA file to use (optional)" +msgstr "" + +#: awx/main/models/credential/__init__.py:1167 awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "" + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "" + +#: awx/main/models/events.py:105 awx/main/models/events.py:630 +msgid "Host Failed" +msgstr "" + +#: awx/main/models/events.py:106 awx/main/models/events.py:631 +msgid "Host OK" +msgstr "" + +#: awx/main/models/events.py:107 +msgid "Host Failure" +msgstr "" + +#: awx/main/models/events.py:108 awx/main/models/events.py:637 +msgid "Host Skipped" +msgstr "" + +#: awx/main/models/events.py:109 awx/main/models/events.py:632 +msgid "Host Unreachable" +msgstr "" + +#: awx/main/models/events.py:110 awx/main/models/events.py:124 +msgid "No Hosts Remaining" +msgstr "" + +#: awx/main/models/events.py:111 +msgid "Host Polling" +msgstr "" + +#: awx/main/models/events.py:112 +msgid "Host Async OK" +msgstr "" + +#: awx/main/models/events.py:113 +msgid "Host Async Failure" +msgstr "" + +#: awx/main/models/events.py:114 +msgid "Item OK" +msgstr "" + +#: awx/main/models/events.py:115 +msgid "Item Failed" +msgstr "" + +#: awx/main/models/events.py:116 +msgid "Item Skipped" +msgstr "" + +#: awx/main/models/events.py:117 +msgid "Host Retry" +msgstr "" + +#: awx/main/models/events.py:119 +msgid "File Difference" +msgstr "" + +#: awx/main/models/events.py:120 +msgid "Playbook Started" +msgstr "" + +#: awx/main/models/events.py:121 +msgid "Running Handlers" +msgstr "" + +#: awx/main/models/events.py:122 +msgid "Including File" +msgstr "" + +#: awx/main/models/events.py:123 +msgid "No Hosts Matched" +msgstr "" + +#: awx/main/models/events.py:125 +msgid "Task Started" +msgstr "" + +#: awx/main/models/events.py:127 +msgid "Variables Prompted" +msgstr "" + +#: awx/main/models/events.py:128 +msgid "Gathering Facts" +msgstr "" + +#: awx/main/models/events.py:129 +msgid "internal: on Import for Host" +msgstr "" + +#: awx/main/models/events.py:130 +msgid "internal: on Not Import for Host" +msgstr "" + +#: awx/main/models/events.py:131 +msgid "Play Started" +msgstr "" + +#: awx/main/models/events.py:132 +msgid "Playbook Complete" +msgstr "" + +#: awx/main/models/events.py:136 awx/main/models/events.py:647 +msgid "Debug" +msgstr "" + +#: awx/main/models/events.py:137 awx/main/models/events.py:648 +msgid "Verbose" +msgstr "" + +#: awx/main/models/events.py:138 awx/main/models/events.py:649 +msgid "Deprecated" +msgstr "" + +#: awx/main/models/events.py:139 awx/main/models/events.py:650 +msgid "Warning" +msgstr "" + +#: awx/main/models/events.py:140 awx/main/models/events.py:651 +msgid "System Warning" +msgstr "" + +#: awx/main/models/events.py:141 awx/main/models/events.py:652 +#: awx/main/models/unified_jobs.py:67 +msgid "Error" +msgstr "" + #: awx/main/models/fact.py:25 msgid "Host for the facts that the fact scan captured." msgstr "" @@ -2153,624 +3002,648 @@ msgid "" "host." msgstr "" -#: awx/main/models/ha.py:76 +#: awx/main/models/ha.py:181 msgid "Instances that are members of this InstanceGroup" msgstr "" -#: awx/main/models/ha.py:81 +#: awx/main/models/ha.py:186 msgid "Instance Group to remotely control this group." msgstr "" -#: awx/main/models/inventory.py:51 +#: awx/main/models/ha.py:193 +msgid "Percentage of Instances to automatically assign to this group" +msgstr "" + +#: awx/main/models/ha.py:197 +msgid "" +"Static minimum number of Instances to automatically assign to this group" +msgstr "" + +#: awx/main/models/ha.py:202 +msgid "" +"List of exact-match Instances that will always be automatically assigned to " +"this group" +msgstr "" + +#: awx/main/models/inventory.py:61 msgid "Hosts have a direct link to this inventory." msgstr "" -#: awx/main/models/inventory.py:52 +#: awx/main/models/inventory.py:62 msgid "Hosts for inventory generated using the host_filter property." msgstr "" -#: awx/main/models/inventory.py:57 +#: awx/main/models/inventory.py:67 msgid "inventories" msgstr "" -#: awx/main/models/inventory.py:64 +#: awx/main/models/inventory.py:74 msgid "Organization containing this inventory." msgstr "" -#: awx/main/models/inventory.py:71 +#: awx/main/models/inventory.py:81 msgid "Inventory variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:76 +#: awx/main/models/inventory.py:86 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "" -#: awx/main/models/inventory.py:81 +#: awx/main/models/inventory.py:91 msgid "Total number of hosts in this inventory." msgstr "" -#: awx/main/models/inventory.py:86 +#: awx/main/models/inventory.py:96 msgid "Number of hosts in this inventory with active failures." msgstr "" -#: awx/main/models/inventory.py:91 +#: awx/main/models/inventory.py:101 msgid "Total number of groups in this inventory." msgstr "" -#: awx/main/models/inventory.py:96 +#: awx/main/models/inventory.py:106 msgid "Number of groups in this inventory with active failures." msgstr "" -#: awx/main/models/inventory.py:101 +#: awx/main/models/inventory.py:111 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "" -#: awx/main/models/inventory.py:106 +#: awx/main/models/inventory.py:116 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "" -#: awx/main/models/inventory.py:111 +#: awx/main/models/inventory.py:121 msgid "Number of external inventory sources in this inventory with failures." msgstr "" -#: awx/main/models/inventory.py:118 +#: awx/main/models/inventory.py:128 msgid "Kind of inventory being represented." msgstr "" -#: awx/main/models/inventory.py:124 +#: awx/main/models/inventory.py:134 msgid "Filter that will be applied to the hosts of this inventory." msgstr "" -#: awx/main/models/inventory.py:151 +#: awx/main/models/inventory.py:161 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." msgstr "" -#: awx/main/models/inventory.py:160 +#: awx/main/models/inventory.py:170 msgid "Flag indicating the inventory is being deleted." msgstr "" -#: awx/main/models/inventory.py:373 +#: awx/main/models/inventory.py:459 msgid "Assignment not allowed for Smart Inventory" msgstr "" -#: awx/main/models/inventory.py:375 awx/main/models/projects.py:148 +#: awx/main/models/inventory.py:461 awx/main/models/projects.py:159 msgid "Credential kind must be 'insights'." msgstr "" -#: awx/main/models/inventory.py:439 +#: awx/main/models/inventory.py:546 msgid "Is this host online and available for running jobs?" msgstr "" -#: awx/main/models/inventory.py:445 +#: awx/main/models/inventory.py:552 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "" -#: awx/main/models/inventory.py:450 +#: awx/main/models/inventory.py:557 msgid "Host variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:472 +#: awx/main/models/inventory.py:579 msgid "Flag indicating whether the last job failed for this host." msgstr "" -#: awx/main/models/inventory.py:477 +#: awx/main/models/inventory.py:584 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." msgstr "" -#: awx/main/models/inventory.py:483 +#: awx/main/models/inventory.py:590 msgid "Inventory source(s) that created or modified this host." msgstr "" -#: awx/main/models/inventory.py:488 +#: awx/main/models/inventory.py:595 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "" -#: awx/main/models/inventory.py:494 +#: awx/main/models/inventory.py:601 msgid "The date and time ansible_facts was last modified." msgstr "" -#: awx/main/models/inventory.py:501 +#: awx/main/models/inventory.py:608 msgid "Red Hat Insights host unique identifier." msgstr "" -#: awx/main/models/inventory.py:629 +#: awx/main/models/inventory.py:743 msgid "Group variables in JSON or YAML format." msgstr "" -#: awx/main/models/inventory.py:635 +#: awx/main/models/inventory.py:749 msgid "Hosts associated directly with this group." msgstr "" -#: awx/main/models/inventory.py:640 +#: awx/main/models/inventory.py:754 msgid "Total number of hosts directly or indirectly in this group." msgstr "" -#: awx/main/models/inventory.py:645 +#: awx/main/models/inventory.py:759 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "" -#: awx/main/models/inventory.py:650 +#: awx/main/models/inventory.py:764 msgid "Number of hosts in this group with active failures." msgstr "" -#: awx/main/models/inventory.py:655 +#: awx/main/models/inventory.py:769 msgid "Total number of child groups contained within this group." msgstr "" -#: awx/main/models/inventory.py:660 +#: awx/main/models/inventory.py:774 msgid "Number of child groups within this group that have active failures." msgstr "" -#: awx/main/models/inventory.py:665 +#: awx/main/models/inventory.py:779 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." msgstr "" -#: awx/main/models/inventory.py:671 +#: awx/main/models/inventory.py:785 msgid "Inventory source(s) that created or modified this group." msgstr "" -#: awx/main/models/inventory.py:861 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:427 +#: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "" -#: awx/main/models/inventory.py:862 +#: awx/main/models/inventory.py:982 msgid "File, Directory or Script" msgstr "" -#: awx/main/models/inventory.py:863 +#: awx/main/models/inventory.py:983 msgid "Sourced from a Project" msgstr "" -#: awx/main/models/inventory.py:864 +#: awx/main/models/inventory.py:984 msgid "Amazon EC2" msgstr "" -#: awx/main/models/inventory.py:865 -msgid "Google Compute Engine" -msgstr "" - -#: awx/main/models/inventory.py:866 -msgid "Microsoft Azure Classic (deprecated)" -msgstr "" - -#: awx/main/models/inventory.py:867 -msgid "Microsoft Azure Resource Manager" -msgstr "" - -#: awx/main/models/inventory.py:868 -msgid "VMware vCenter" -msgstr "" - -#: awx/main/models/inventory.py:869 -msgid "Red Hat Satellite 6" -msgstr "" - -#: awx/main/models/inventory.py:870 -msgid "Red Hat CloudForms" -msgstr "" - -#: awx/main/models/inventory.py:871 -msgid "OpenStack" -msgstr "" - -#: awx/main/models/inventory.py:872 +#: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "" -#: awx/main/models/inventory.py:989 +#: awx/main/models/inventory.py:1110 msgid "Inventory source variables in YAML or JSON format." msgstr "" -#: awx/main/models/inventory.py:1008 +#: awx/main/models/inventory.py:1121 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." msgstr "" -#: awx/main/models/inventory.py:1014 +#: awx/main/models/inventory.py:1127 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "" -#: awx/main/models/inventory.py:1018 +#: awx/main/models/inventory.py:1131 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "" -#: awx/main/models/inventory.py:1022 +#: awx/main/models/inventory.py:1135 msgid "Overwrite local variables from remote inventory source." msgstr "" -#: awx/main/models/inventory.py:1027 awx/main/models/jobs.py:159 -#: awx/main/models/projects.py:117 +#: awx/main/models/inventory.py:1140 awx/main/models/jobs.py:140 +#: awx/main/models/projects.py:128 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "" -#: awx/main/models/inventory.py:1060 -msgid "Availability Zone" -msgstr "" - -#: awx/main/models/inventory.py:1061 +#: awx/main/models/inventory.py:1173 msgid "Image ID" msgstr "" -#: awx/main/models/inventory.py:1062 +#: awx/main/models/inventory.py:1174 +msgid "Availability Zone" +msgstr "" + +#: awx/main/models/inventory.py:1175 +msgid "Account" +msgstr "" + +#: awx/main/models/inventory.py:1176 msgid "Instance ID" msgstr "" -#: awx/main/models/inventory.py:1063 +#: awx/main/models/inventory.py:1177 +msgid "Instance State" +msgstr "" + +#: awx/main/models/inventory.py:1178 +msgid "Platform" +msgstr "" + +#: awx/main/models/inventory.py:1179 msgid "Instance Type" msgstr "" -#: awx/main/models/inventory.py:1064 +#: awx/main/models/inventory.py:1180 msgid "Key Name" msgstr "" -#: awx/main/models/inventory.py:1065 +#: awx/main/models/inventory.py:1181 msgid "Region" msgstr "" -#: awx/main/models/inventory.py:1066 +#: awx/main/models/inventory.py:1182 msgid "Security Group" msgstr "" -#: awx/main/models/inventory.py:1067 +#: awx/main/models/inventory.py:1183 msgid "Tags" msgstr "" -#: awx/main/models/inventory.py:1068 -msgid "VPC ID" -msgstr "" - -#: awx/main/models/inventory.py:1069 +#: awx/main/models/inventory.py:1184 msgid "Tag None" msgstr "" -#: awx/main/models/inventory.py:1132 +#: awx/main/models/inventory.py:1185 +msgid "VPC ID" +msgstr "" + +#: awx/main/models/inventory.py:1253 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "" -#: awx/main/models/inventory.py:1139 +#: awx/main/models/inventory.py:1259 msgid "Credential is required for a cloud source." msgstr "" -#: awx/main/models/inventory.py:1161 +#: awx/main/models/inventory.py:1262 +msgid "" +"Credentials of type machine, source control, insights and vault are " +"disallowed for custom inventory sources." +msgstr "" + +#: awx/main/models/inventory.py:1314 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "" -#: awx/main/models/inventory.py:1185 +#: awx/main/models/inventory.py:1338 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "" -#: awx/main/models/inventory.py:1206 +#: awx/main/models/inventory.py:1359 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "" -#: awx/main/models/inventory.py:1241 +#: awx/main/models/inventory.py:1394 msgid "Project containing inventory file used as source." msgstr "" -#: awx/main/models/inventory.py:1389 +#: awx/main/models/inventory.py:1555 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." msgstr "" -#: awx/main/models/inventory.py:1414 +#: awx/main/models/inventory.py:1565 +msgid "" +"More than one SCM-based inventory source with update on project update per-" +"inventory not allowed." +msgstr "" + +#: awx/main/models/inventory.py:1572 +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 "" + +#: awx/main/models/inventory.py:1579 +msgid "SCM type sources must set `overwrite_vars` to `true` until Ansible 2.5." +msgstr "" + +#: awx/main/models/inventory.py:1584 +msgid "Cannot set source_path if not SCM type." +msgstr "" + +#: awx/main/models/inventory.py:1622 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "" -#: awx/main/models/inventory.py:1528 +#: awx/main/models/inventory.py:1732 msgid "Inventory script contents" msgstr "" -#: awx/main/models/inventory.py:1533 +#: awx/main/models/inventory.py:1737 msgid "Organization owning this inventory script" msgstr "" -#: awx/main/models/jobs.py:65 +#: awx/main/models/jobs.py:66 msgid "" "If enabled, textual changes made to any templated files on the host are " "shown in the standard output" msgstr "" -#: awx/main/models/jobs.py:163 +#: awx/main/models/jobs.py:145 msgid "" "If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts " "at the end of a playbook run to the database and caching facts for use by " "Ansible." msgstr "" -#: awx/main/models/jobs.py:172 -msgid "You must provide an SSH credential." -msgstr "" - -#: awx/main/models/jobs.py:180 +#: awx/main/models/jobs.py:163 msgid "You must provide a Vault credential." msgstr "" -#: awx/main/models/jobs.py:316 +#: awx/main/models/jobs.py:308 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "" -#: awx/main/models/jobs.py:320 -msgid "Job Template must provide 'credential' or allow prompting for it." +#: awx/main/models/jobs.py:398 +msgid "Field is not configured to prompt on launch." msgstr "" -#: awx/main/models/jobs.py:421 -msgid "Cannot override job_type to or from a scan job." +#: awx/main/models/jobs.py:404 +msgid "Saved launch configurations cannot provide passwords needed to start." msgstr "" -#: awx/main/models/jobs.py:487 awx/main/models/projects.py:263 +#: awx/main/models/jobs.py:412 +msgid "Job Template {} is missing or undefined." +msgstr "" + +#: awx/main/models/jobs.py:493 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "" -#: awx/main/models/jobs.py:488 +#: awx/main/models/jobs.py:494 msgid "The SCM Revision from the Project used for this job, if available" msgstr "" -#: awx/main/models/jobs.py:496 +#: awx/main/models/jobs.py:502 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" msgstr "" -#: awx/main/models/jobs.py:799 +#: awx/main/models/jobs.py:629 +#, python-brace-format +msgid "{status_value} is not a valid status option." +msgstr "" + +#: awx/main/models/jobs.py:1005 msgid "job host summaries" msgstr "" -#: awx/main/models/jobs.py:902 -msgid "Host Failure" -msgstr "" - -#: awx/main/models/jobs.py:905 awx/main/models/jobs.py:919 -msgid "No Hosts Remaining" -msgstr "" - -#: awx/main/models/jobs.py:906 -msgid "Host Polling" -msgstr "" - -#: awx/main/models/jobs.py:907 -msgid "Host Async OK" -msgstr "" - -#: awx/main/models/jobs.py:908 -msgid "Host Async Failure" -msgstr "" - -#: awx/main/models/jobs.py:909 -msgid "Item OK" -msgstr "" - -#: awx/main/models/jobs.py:910 -msgid "Item Failed" -msgstr "" - -#: awx/main/models/jobs.py:911 -msgid "Item Skipped" -msgstr "" - -#: awx/main/models/jobs.py:912 -msgid "Host Retry" -msgstr "" - -#: awx/main/models/jobs.py:914 -msgid "File Difference" -msgstr "" - -#: awx/main/models/jobs.py:915 -msgid "Playbook Started" -msgstr "" - -#: awx/main/models/jobs.py:916 -msgid "Running Handlers" -msgstr "" - -#: awx/main/models/jobs.py:917 -msgid "Including File" -msgstr "" - -#: awx/main/models/jobs.py:918 -msgid "No Hosts Matched" -msgstr "" - -#: awx/main/models/jobs.py:920 -msgid "Task Started" -msgstr "" - -#: awx/main/models/jobs.py:922 -msgid "Variables Prompted" -msgstr "" - -#: awx/main/models/jobs.py:923 -msgid "Gathering Facts" -msgstr "" - -#: awx/main/models/jobs.py:924 -msgid "internal: on Import for Host" -msgstr "" - -#: awx/main/models/jobs.py:925 -msgid "internal: on Not Import for Host" -msgstr "" - -#: awx/main/models/jobs.py:926 -msgid "Play Started" -msgstr "" - -#: awx/main/models/jobs.py:927 -msgid "Playbook Complete" -msgstr "" - -#: awx/main/models/jobs.py:1337 +#: awx/main/models/jobs.py:1077 msgid "Remove jobs older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1338 +#: awx/main/models/jobs.py:1078 msgid "Remove activity stream entries older than a certain number of days" msgstr "" -#: awx/main/models/jobs.py:1339 +#: awx/main/models/jobs.py:1079 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "" +#: awx/main/models/jobs.py:1149 +#, python-brace-format +msgid "Variables {list_of_keys} are not allowed for system jobs." +msgstr "" + +#: awx/main/models/jobs.py:1164 +msgid "days must be a positive integer." +msgstr "" + #: awx/main/models/label.py:29 msgid "Organization this label belongs to." msgstr "" -#: awx/main/models/notifications.py:138 awx/main/models/unified_jobs.py:58 +#: awx/main/models/mixins.py:309 +#, python-brace-format +msgid "" +"Variables {list_of_keys} are not allowed on launch. Check the Prompt on " +"Launch setting on the Job Template to include Extra Variables." +msgstr "" + +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "" + +#: awx/main/models/mixins.py:447 +msgid "{} is not a valid virtualenv in {}" +msgstr "" + +#: awx/main/models/notifications.py:42 +msgid "Rocket.Chat" +msgstr "" + +#: awx/main/models/notifications.py:142 awx/main/models/unified_jobs.py:62 msgid "Pending" msgstr "" -#: awx/main/models/notifications.py:139 awx/main/models/unified_jobs.py:61 +#: awx/main/models/notifications.py:143 awx/main/models/unified_jobs.py:65 msgid "Successful" msgstr "" -#: awx/main/models/notifications.py:140 awx/main/models/unified_jobs.py:62 +#: awx/main/models/notifications.py:144 awx/main/models/unified_jobs.py:66 msgid "Failed" msgstr "" -#: awx/main/models/organization.py:132 -msgid "Token not invalidated" +#: awx/main/models/notifications.py:218 +msgid "status_str must be either succeeded or failed" msgstr "" -#: awx/main/models/organization.py:133 -msgid "Token is expired" +#: awx/main/models/oauth.py:29 +msgid "application" msgstr "" -#: awx/main/models/organization.py:134 -msgid "The maximum number of allowed sessions for this user has been exceeded." +#: awx/main/models/oauth.py:35 +msgid "Confidential" msgstr "" -#: awx/main/models/organization.py:137 -msgid "Invalid token" +#: awx/main/models/oauth.py:36 +msgid "Public" msgstr "" -#: awx/main/models/organization.py:155 -msgid "Reason the auth token was invalidated." +#: awx/main/models/oauth.py:43 +msgid "Authorization code" msgstr "" -#: awx/main/models/organization.py:194 -msgid "Invalid reason specified" +#: awx/main/models/oauth.py:44 +msgid "Implicit" msgstr "" -#: awx/main/models/projects.py:43 +#: awx/main/models/oauth.py:45 +msgid "Resource owner password-based" +msgstr "" + +#: awx/main/models/oauth.py:60 +msgid "Organization containing this application." +msgstr "" + +#: awx/main/models/oauth.py:69 +msgid "" +"Used for more stringent verification of access to an application when " +"creating a token." +msgstr "" + +#: awx/main/models/oauth.py:74 +msgid "" +"Set to Public or Confidential depending on how secure the client device is." +msgstr "" + +#: awx/main/models/oauth.py:78 +msgid "" +"Set True to skip authorization step for completely trusted applications." +msgstr "" + +#: awx/main/models/oauth.py:83 +msgid "" +"The Grant type the user must use for acquire tokens for this application." +msgstr "" + +#: awx/main/models/oauth.py:91 +msgid "access token" +msgstr "" + +#: awx/main/models/oauth.py:99 +msgid "The user representing the token owner" +msgstr "" + +#: awx/main/models/oauth.py:114 +msgid "" +"Allowed scopes, further restricts user's permissions. Must be a simple space-" +"separated string with allowed scopes ['read', 'write']." +msgstr "" + +#: awx/main/models/oauth.py:133 +msgid "" +"OAuth2 Tokens cannot be created by users associated with an external " +"authentication provider ({})" +msgstr "" + +#: awx/main/models/projects.py:54 msgid "Git" msgstr "" -#: awx/main/models/projects.py:44 +#: awx/main/models/projects.py:55 msgid "Mercurial" msgstr "" -#: awx/main/models/projects.py:45 +#: awx/main/models/projects.py:56 msgid "Subversion" msgstr "" -#: awx/main/models/projects.py:46 +#: awx/main/models/projects.py:57 msgid "Red Hat Insights" msgstr "" -#: awx/main/models/projects.py:72 +#: awx/main/models/projects.py:83 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." msgstr "" -#: awx/main/models/projects.py:81 +#: awx/main/models/projects.py:92 msgid "SCM Type" msgstr "" -#: awx/main/models/projects.py:82 +#: awx/main/models/projects.py:93 msgid "Specifies the source control system used to store the project." msgstr "" -#: awx/main/models/projects.py:88 +#: awx/main/models/projects.py:99 msgid "SCM URL" msgstr "" -#: awx/main/models/projects.py:89 +#: awx/main/models/projects.py:100 msgid "The location where the project is stored." msgstr "" -#: awx/main/models/projects.py:95 +#: awx/main/models/projects.py:106 msgid "SCM Branch" msgstr "" -#: awx/main/models/projects.py:96 +#: awx/main/models/projects.py:107 msgid "Specific branch, tag or commit to checkout." msgstr "" -#: awx/main/models/projects.py:100 +#: awx/main/models/projects.py:111 msgid "Discard any local changes before syncing the project." msgstr "" -#: awx/main/models/projects.py:104 +#: awx/main/models/projects.py:115 msgid "Delete the project before syncing." msgstr "" -#: awx/main/models/projects.py:133 +#: awx/main/models/projects.py:144 msgid "Invalid SCM URL." msgstr "" -#: awx/main/models/projects.py:136 +#: awx/main/models/projects.py:147 msgid "SCM URL is required." msgstr "" -#: awx/main/models/projects.py:144 +#: awx/main/models/projects.py:155 msgid "Insights Credential is required for an Insights Project." msgstr "" -#: awx/main/models/projects.py:150 +#: awx/main/models/projects.py:161 msgid "Credential kind must be 'scm'." msgstr "" -#: awx/main/models/projects.py:167 +#: awx/main/models/projects.py:178 msgid "Invalid credential." msgstr "" -#: awx/main/models/projects.py:249 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "" -#: awx/main/models/projects.py:254 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." msgstr "" -#: awx/main/models/projects.py:264 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "" -#: awx/main/models/projects.py:271 +#: awx/main/models/projects.py:285 msgid "Playbook Files" msgstr "" -#: awx/main/models/projects.py:272 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "" -#: awx/main/models/projects.py:279 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "" -#: awx/main/models/projects.py:280 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "" @@ -2792,219 +3665,288 @@ msgid "Admin" msgstr "" #: awx/main/models/rbac.py:40 -msgid "Auditor" +msgid "Project Admin" msgstr "" #: awx/main/models/rbac.py:41 -msgid "Execute" +msgid "Inventory Admin" msgstr "" #: awx/main/models/rbac.py:42 -msgid "Member" +msgid "Credential Admin" msgstr "" #: awx/main/models/rbac.py:43 -msgid "Read" +msgid "Job Template Admin" msgstr "" #: awx/main/models/rbac.py:44 -msgid "Update" +msgid "Workflow Admin" msgstr "" #: awx/main/models/rbac.py:45 -msgid "Use" +msgid "Notification Admin" +msgstr "" + +#: awx/main/models/rbac.py:46 +msgid "Auditor" +msgstr "" + +#: awx/main/models/rbac.py:47 +msgid "Execute" +msgstr "" + +#: awx/main/models/rbac.py:48 +msgid "Member" msgstr "" #: awx/main/models/rbac.py:49 -msgid "Can manage all aspects of the system" +msgid "Read" msgstr "" #: awx/main/models/rbac.py:50 -msgid "Can view all settings on the system" +msgid "Update" msgstr "" #: awx/main/models/rbac.py:51 +msgid "Use" +msgstr "" + +#: awx/main/models/rbac.py:55 +msgid "Can manage all aspects of the system" +msgstr "" + +#: awx/main/models/rbac.py:56 +msgid "Can view all settings on the system" +msgstr "" + +#: awx/main/models/rbac.py:57 msgid "May run ad hoc commands on an inventory" msgstr "" -#: awx/main/models/rbac.py:52 +#: awx/main/models/rbac.py:58 #, python-format msgid "Can manage all aspects of the %s" msgstr "" -#: awx/main/models/rbac.py:53 +#: awx/main/models/rbac.py:59 +#, python-format +msgid "Can manage all projects of the %s" +msgstr "" + +#: awx/main/models/rbac.py:60 +#, python-format +msgid "Can manage all inventories of the %s" +msgstr "" + +#: awx/main/models/rbac.py:61 +#, python-format +msgid "Can manage all credentials of the %s" +msgstr "" + +#: awx/main/models/rbac.py:62 +#, python-format +msgid "Can manage all job templates of the %s" +msgstr "" + +#: awx/main/models/rbac.py:63 +#, python-format +msgid "Can manage all workflows of the %s" +msgstr "" + +#: awx/main/models/rbac.py:64 +#, python-format +msgid "Can manage all notifications of the %s" +msgstr "" + +#: awx/main/models/rbac.py:65 #, python-format msgid "Can view all settings for the %s" msgstr "" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:67 +msgid "May run any executable resources in the organization" +msgstr "" + +#: awx/main/models/rbac.py:68 #, python-format msgid "May run the %s" msgstr "" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:70 #, python-format msgid "User is a member of the %s" msgstr "" -#: awx/main/models/rbac.py:56 +#: awx/main/models/rbac.py:71 #, python-format msgid "May view settings for the %s" msgstr "" -#: awx/main/models/rbac.py:57 +#: awx/main/models/rbac.py:72 msgid "" "May update project or inventory or group using the configured source update " "system" msgstr "" -#: awx/main/models/rbac.py:58 +#: awx/main/models/rbac.py:73 #, python-format msgid "Can use the %s in a job template" msgstr "" -#: awx/main/models/rbac.py:122 +#: awx/main/models/rbac.py:137 msgid "roles" msgstr "" -#: awx/main/models/rbac.py:434 +#: awx/main/models/rbac.py:443 msgid "role_ancestors" msgstr "" -#: awx/main/models/schedules.py:71 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "" -#: awx/main/models/schedules.py:77 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." msgstr "" -#: awx/main/models/schedules.py:83 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." msgstr "" -#: awx/main/models/schedules.py:87 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "" -#: awx/main/models/schedules.py:93 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." msgstr "" -#: awx/main/models/schedules.py:109 -msgid "Expected JSON" -msgstr "" - -#: awx/main/models/schedules.py:121 -msgid "days must be a positive integer." -msgstr "" - -#: awx/main/models/unified_jobs.py:57 +#: awx/main/models/unified_jobs.py:61 msgid "New" msgstr "" -#: awx/main/models/unified_jobs.py:59 +#: awx/main/models/unified_jobs.py:63 msgid "Waiting" msgstr "" -#: awx/main/models/unified_jobs.py:60 +#: awx/main/models/unified_jobs.py:64 msgid "Running" msgstr "" -#: awx/main/models/unified_jobs.py:64 +#: awx/main/models/unified_jobs.py:68 msgid "Canceled" msgstr "" -#: awx/main/models/unified_jobs.py:68 +#: awx/main/models/unified_jobs.py:72 msgid "Never Updated" msgstr "" -#: awx/main/models/unified_jobs.py:72 awx/ui/templates/ui/index.html:67 -#: awx/ui/templates/ui/index.html.py:86 +#: awx/main/models/unified_jobs.py:76 msgid "OK" msgstr "" -#: awx/main/models/unified_jobs.py:73 +#: awx/main/models/unified_jobs.py:77 msgid "Missing" msgstr "" -#: awx/main/models/unified_jobs.py:77 +#: awx/main/models/unified_jobs.py:81 msgid "No External Source" msgstr "" -#: awx/main/models/unified_jobs.py:84 +#: awx/main/models/unified_jobs.py:88 msgid "Updating" msgstr "" -#: awx/main/models/unified_jobs.py:428 +#: awx/main/models/unified_jobs.py:427 +msgid "Field is not allowed on launch." +msgstr "" + +#: awx/main/models/unified_jobs.py:455 +#, python-brace-format +msgid "" +"Variables {list_of_keys} provided, but this template cannot accept variables." +msgstr "" + +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "" -#: awx/main/models/unified_jobs.py:429 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "" -#: awx/main/models/unified_jobs.py:430 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "" -#: awx/main/models/unified_jobs.py:431 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "" -#: awx/main/models/unified_jobs.py:432 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "" -#: awx/main/models/unified_jobs.py:433 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "" -#: awx/main/models/unified_jobs.py:480 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "" -#: awx/main/models/unified_jobs.py:506 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "" + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." msgstr "" -#: awx/main/models/unified_jobs.py:512 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." msgstr "" -#: awx/main/models/unified_jobs.py:518 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." msgstr "" -#: awx/main/models/unified_jobs.py:540 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and " "capture stdout" msgstr "" -#: awx/main/models/unified_jobs.py:579 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "" -#: awx/main/notifications/base.py:17 +#: awx/main/models/workflow.py:203 +#, python-brace-format +msgid "" +"Bad launch configuration starting template {template_pk} as part of workflow " +"{workflow_pk}. Errors:\n" +"{error_text}" +msgstr "" + +#: awx/main/models/workflow.py:393 +msgid "Field is not allowed for use in workflows." +msgstr "" + +#: awx/main/notifications/base.py:17 awx/main/notifications/email_backend.py:28 msgid "" "{} #{} had status {}, view details at {}\n" "\n" msgstr "" -#: awx/main/notifications/email_backend.py:28 -msgid "" -"{} #{} had status {} on Ansible Tower, view details at {}\n" -"\n" -msgstr "" - -#: awx/main/notifications/hipchat_backend.py:47 +#: awx/main/notifications/hipchat_backend.py:48 msgid "Error sending messages: {}" msgstr "" -#: awx/main/notifications/hipchat_backend.py:49 +#: awx/main/notifications/hipchat_backend.py:50 msgid "Error sending message to hipchat: {}" msgstr "" @@ -3012,16 +3954,27 @@ msgstr "" msgid "Exception connecting to irc server: {}" msgstr "" +#: awx/main/notifications/mattermost_backend.py:48 +#: awx/main/notifications/mattermost_backend.py:50 +msgid "Error sending notification mattermost: {}" +msgstr "" + #: awx/main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" msgstr "" #: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 +#: awx/main/notifications/slack_backend.py:82 +#: awx/main/notifications/slack_backend.py:99 #: awx/main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" msgstr "" +#: awx/main/notifications/rocketchat_backend.py:46 +#: awx/main/notifications/rocketchat_backend.py:49 +msgid "Error sending notification rocket.chat: {}" +msgstr "" + #: awx/main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" msgstr "" @@ -3031,140 +3984,156 @@ msgstr "" msgid "Error sending notification webhook: {}" msgstr "" -#: awx/main/scheduler/__init__.py:153 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" msgstr "" -#: awx/main/scheduler/__init__.py:157 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" msgstr "" -#: awx/main/tasks.py:175 +#: awx/main/signals.py:632 +msgid "limit_reached" +msgstr "" + +#: awx/main/tasks.py:305 msgid "Ansible Tower host usage over 90%" msgstr "" -#: awx/main/tasks.py:180 +#: awx/main/tasks.py:310 msgid "Ansible Tower license will expire soon" msgstr "" -#: awx/main/tasks.py:308 -msgid "status_str must be either succeeded or failed" +#: awx/main/tasks.py:1358 +msgid "Job could not start because it does not have a valid inventory." msgstr "" -#: awx/main/tasks.py:1498 -msgid "Dependent inventory update {} was canceled." -msgstr "" - -#: awx/main/utils/common.py:89 +#: awx/main/utils/common.py:97 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "" -#: awx/main/utils/common.py:209 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "" -#: awx/main/utils/common.py:216 awx/main/utils/common.py:228 -#: awx/main/utils/common.py:247 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" msgstr "" -#: awx/main/utils/common.py:218 awx/main/utils/common.py:257 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "" -#: awx/main/utils/common.py:259 +#: awx/main/utils/common.py:304 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "" -#: awx/main/utils/common.py:261 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "" -#: awx/main/utils/common.py:279 +#: awx/main/utils/common.py:324 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "" -#: awx/main/utils/common.py:285 +#: awx/main/utils/common.py:330 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "" -#: awx/main/validators.py:60 +#: awx/main/utils/common.py:611 +#, python-brace-format +msgid "Input type `{data_type}` is not a dictionary" +msgstr "" + +#: awx/main/utils/common.py:644 +#, python-brace-format +msgid "Variables not compatible with JSON standard (error: {json_error})" +msgstr "" + +#: awx/main/utils/common.py:650 +#, python-brace-format +msgid "" +"Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." +msgstr "" + +#: awx/main/validators.py:67 #, python-format msgid "Invalid certificate or key: %s..." msgstr "" -#: awx/main/validators.py:74 +#: awx/main/validators.py:83 #, python-format msgid "Invalid private key: unsupported type \"%s\"" msgstr "" -#: awx/main/validators.py:78 +#: awx/main/validators.py:87 #, python-format msgid "Unsupported PEM object type: \"%s\"" msgstr "" -#: awx/main/validators.py:103 +#: awx/main/validators.py:112 msgid "Invalid base64-encoded data" msgstr "" -#: awx/main/validators.py:122 +#: awx/main/validators.py:131 msgid "Exactly one private key is required." msgstr "" -#: awx/main/validators.py:124 +#: awx/main/validators.py:133 msgid "At least one private key is required." msgstr "" -#: awx/main/validators.py:126 +#: awx/main/validators.py:135 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d provided." msgstr "" -#: awx/main/validators.py:129 +#: awx/main/validators.py:138 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." msgstr "" -#: awx/main/validators.py:131 +#: awx/main/validators.py:140 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." msgstr "" -#: awx/main/validators.py:136 +#: awx/main/validators.py:145 msgid "Exactly one certificate is required." msgstr "" -#: awx/main/validators.py:138 +#: awx/main/validators.py:147 msgid "At least one certificate is required." msgstr "" -#: awx/main/validators.py:140 +#: awx/main/validators.py:149 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " "provided." msgstr "" -#: awx/main/validators.py:143 +#: awx/main/validators.py:152 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." msgstr "" -#: awx/main/validators.py:145 +#: awx/main/validators.py:154 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d provided." @@ -3206,287 +4175,287 @@ msgstr "" msgid "A server error has occurred." msgstr "" -#: awx/settings/defaults.py:663 +#: awx/settings/defaults.py:725 msgid "US East (Northern Virginia)" msgstr "" -#: awx/settings/defaults.py:664 +#: awx/settings/defaults.py:726 msgid "US East (Ohio)" msgstr "" -#: awx/settings/defaults.py:665 +#: awx/settings/defaults.py:727 msgid "US West (Oregon)" msgstr "" -#: awx/settings/defaults.py:666 +#: awx/settings/defaults.py:728 msgid "US West (Northern California)" msgstr "" -#: awx/settings/defaults.py:667 +#: awx/settings/defaults.py:729 msgid "Canada (Central)" msgstr "" -#: awx/settings/defaults.py:668 +#: awx/settings/defaults.py:730 msgid "EU (Frankfurt)" msgstr "" -#: awx/settings/defaults.py:669 +#: awx/settings/defaults.py:731 msgid "EU (Ireland)" msgstr "" -#: awx/settings/defaults.py:670 +#: awx/settings/defaults.py:732 msgid "EU (London)" msgstr "" -#: awx/settings/defaults.py:671 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Singapore)" msgstr "" -#: awx/settings/defaults.py:672 +#: awx/settings/defaults.py:734 msgid "Asia Pacific (Sydney)" msgstr "" -#: awx/settings/defaults.py:673 +#: awx/settings/defaults.py:735 msgid "Asia Pacific (Tokyo)" msgstr "" -#: awx/settings/defaults.py:674 +#: awx/settings/defaults.py:736 msgid "Asia Pacific (Seoul)" msgstr "" -#: awx/settings/defaults.py:675 +#: awx/settings/defaults.py:737 msgid "Asia Pacific (Mumbai)" msgstr "" -#: awx/settings/defaults.py:676 +#: awx/settings/defaults.py:738 msgid "South America (Sao Paulo)" msgstr "" -#: awx/settings/defaults.py:677 +#: awx/settings/defaults.py:739 msgid "US West (GovCloud)" msgstr "" -#: awx/settings/defaults.py:678 +#: awx/settings/defaults.py:740 msgid "China (Beijing)" msgstr "" -#: awx/settings/defaults.py:727 +#: awx/settings/defaults.py:789 msgid "US East 1 (B)" msgstr "" -#: awx/settings/defaults.py:728 +#: awx/settings/defaults.py:790 msgid "US East 1 (C)" msgstr "" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:791 msgid "US East 1 (D)" msgstr "" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:792 msgid "US East 4 (A)" msgstr "" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:793 msgid "US East 4 (B)" msgstr "" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:794 msgid "US East 4 (C)" msgstr "" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:795 msgid "US Central (A)" msgstr "" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:796 msgid "US Central (B)" msgstr "" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:797 msgid "US Central (C)" msgstr "" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:798 msgid "US Central (F)" msgstr "" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:799 msgid "US West (A)" msgstr "" -#: awx/settings/defaults.py:738 +#: awx/settings/defaults.py:800 msgid "US West (B)" msgstr "" -#: awx/settings/defaults.py:739 +#: awx/settings/defaults.py:801 msgid "US West (C)" msgstr "" -#: awx/settings/defaults.py:740 +#: awx/settings/defaults.py:802 msgid "Europe West 1 (B)" msgstr "" -#: awx/settings/defaults.py:741 +#: awx/settings/defaults.py:803 msgid "Europe West 1 (C)" msgstr "" -#: awx/settings/defaults.py:742 +#: awx/settings/defaults.py:804 msgid "Europe West 1 (D)" msgstr "" -#: awx/settings/defaults.py:743 +#: awx/settings/defaults.py:805 msgid "Europe West 2 (A)" msgstr "" -#: awx/settings/defaults.py:744 +#: awx/settings/defaults.py:806 msgid "Europe West 2 (B)" msgstr "" -#: awx/settings/defaults.py:745 +#: awx/settings/defaults.py:807 msgid "Europe West 2 (C)" msgstr "" -#: awx/settings/defaults.py:746 +#: awx/settings/defaults.py:808 msgid "Asia East (A)" msgstr "" -#: awx/settings/defaults.py:747 +#: awx/settings/defaults.py:809 msgid "Asia East (B)" msgstr "" -#: awx/settings/defaults.py:748 +#: awx/settings/defaults.py:810 msgid "Asia East (C)" msgstr "" -#: awx/settings/defaults.py:749 +#: awx/settings/defaults.py:811 msgid "Asia Southeast (A)" msgstr "" -#: awx/settings/defaults.py:750 +#: awx/settings/defaults.py:812 msgid "Asia Southeast (B)" msgstr "" -#: awx/settings/defaults.py:751 +#: awx/settings/defaults.py:813 msgid "Asia Northeast (A)" msgstr "" -#: awx/settings/defaults.py:752 +#: awx/settings/defaults.py:814 msgid "Asia Northeast (B)" msgstr "" -#: awx/settings/defaults.py:753 +#: awx/settings/defaults.py:815 msgid "Asia Northeast (C)" msgstr "" -#: awx/settings/defaults.py:754 +#: awx/settings/defaults.py:816 msgid "Australia Southeast (A)" msgstr "" -#: awx/settings/defaults.py:755 +#: awx/settings/defaults.py:817 msgid "Australia Southeast (B)" msgstr "" -#: awx/settings/defaults.py:756 +#: awx/settings/defaults.py:818 msgid "Australia Southeast (C)" msgstr "" -#: awx/settings/defaults.py:780 +#: awx/settings/defaults.py:840 msgid "US East" msgstr "" -#: awx/settings/defaults.py:781 +#: awx/settings/defaults.py:841 msgid "US East 2" msgstr "" -#: awx/settings/defaults.py:782 +#: awx/settings/defaults.py:842 msgid "US Central" msgstr "" -#: awx/settings/defaults.py:783 +#: awx/settings/defaults.py:843 msgid "US North Central" msgstr "" -#: awx/settings/defaults.py:784 +#: awx/settings/defaults.py:844 msgid "US South Central" msgstr "" -#: awx/settings/defaults.py:785 +#: awx/settings/defaults.py:845 msgid "US West Central" msgstr "" -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:846 msgid "US West" msgstr "" -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:847 msgid "US West 2" msgstr "" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:848 msgid "Canada East" msgstr "" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:849 msgid "Canada Central" msgstr "" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:850 msgid "Brazil South" msgstr "" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:851 msgid "Europe North" msgstr "" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:852 msgid "Europe West" msgstr "" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:853 msgid "UK West" msgstr "" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:854 msgid "UK South" msgstr "" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:855 msgid "Asia East" msgstr "" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:856 msgid "Asia Southeast" msgstr "" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:857 msgid "Australia East" msgstr "" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:858 msgid "Australia Southeast" msgstr "" -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:859 msgid "India West" msgstr "" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:860 msgid "India South" msgstr "" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:861 msgid "Japan East" msgstr "" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:862 msgid "Japan West" msgstr "" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:863 msgid "Korea Central" msgstr "" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:864 msgid "Korea South" msgstr "" @@ -3498,8 +4467,10 @@ msgstr "" msgid "" "Mapping to organization admins/users from social auth accounts. This " "setting\n" -"controls which users are placed into which Tower organizations based on\n" -"their username and email address. Configuration details are available in\n" +"controls which users are placed into which Tower organizations based on " +"their\n" +"username and email address. Configuration details are available in the " +"Ansible\n" "Tower documentation." msgstr "" @@ -3538,11 +4509,11 @@ msgid "" "have a user account with a matching email address will be able to login." msgstr "" -#: awx/sso/conf.py:137 +#: awx/sso/conf.py:141 msgid "LDAP Server URI" msgstr "" -#: awx/sso/conf.py:138 +#: awx/sso/conf.py:142 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be " @@ -3550,47 +4521,47 @@ msgid "" "disabled if this parameter is empty." msgstr "" -#: awx/sso/conf.py:142 awx/sso/conf.py:160 awx/sso/conf.py:172 -#: awx/sso/conf.py:184 awx/sso/conf.py:200 awx/sso/conf.py:220 -#: awx/sso/conf.py:242 awx/sso/conf.py:258 awx/sso/conf.py:277 -#: awx/sso/conf.py:294 awx/sso/conf.py:311 awx/sso/conf.py:327 -#: awx/sso/conf.py:344 awx/sso/conf.py:361 awx/sso/conf.py:387 +#: awx/sso/conf.py:146 awx/sso/conf.py:162 awx/sso/conf.py:174 +#: awx/sso/conf.py:186 awx/sso/conf.py:202 awx/sso/conf.py:222 +#: awx/sso/conf.py:244 awx/sso/conf.py:259 awx/sso/conf.py:277 +#: awx/sso/conf.py:294 awx/sso/conf.py:306 awx/sso/conf.py:332 +#: awx/sso/conf.py:348 awx/sso/conf.py:362 awx/sso/conf.py:380 +#: awx/sso/conf.py:406 msgid "LDAP" msgstr "" -#: awx/sso/conf.py:154 +#: awx/sso/conf.py:158 msgid "LDAP Bind DN" msgstr "" -#: awx/sso/conf.py:155 +#: awx/sso/conf.py:159 msgid "" -"DN (Distinguished Name) of user to bind for all search queries. Normally in " -"the format \"CN=Some User,OU=Users,DC=example,DC=com\" but may also be " -"specified as \"DOMAIN\\username\" for Active Directory. This is the system " -"user account we will use to login to query LDAP for other user information." +"DN (Distinguished Name) of user to bind for all search queries. This is the " +"system user account we will use to login to query LDAP for other user " +"information. Refer to the Ansible Tower documentation for example syntax." msgstr "" -#: awx/sso/conf.py:170 +#: awx/sso/conf.py:172 msgid "LDAP Bind Password" msgstr "" -#: awx/sso/conf.py:171 +#: awx/sso/conf.py:173 msgid "Password used to bind LDAP user account." msgstr "" -#: awx/sso/conf.py:182 +#: awx/sso/conf.py:184 msgid "LDAP Start TLS" msgstr "" -#: awx/sso/conf.py:183 +#: awx/sso/conf.py:185 msgid "Whether to enable TLS when the LDAP connection is not using SSL." msgstr "" -#: awx/sso/conf.py:193 +#: awx/sso/conf.py:195 msgid "LDAP Connection Options" msgstr "" -#: awx/sso/conf.py:194 +#: awx/sso/conf.py:196 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -3599,52 +4570,52 @@ msgid "" "values that can be set." msgstr "" -#: awx/sso/conf.py:213 +#: awx/sso/conf.py:215 msgid "LDAP User Search" msgstr "" -#: awx/sso/conf.py:214 +#: awx/sso/conf.py:216 msgid "" "LDAP search query to find users. Any user that matches the given pattern " -"will be able to login to Tower. The user should also be mapped into an " -"Tower organization (as defined in the AUTH_LDAP_ORGANIZATION_MAP setting). " -"If multiple search queries need to be supported use of \"LDAPUnion\" is " +"will be able to login to Tower. The user should also be mapped into a Tower " +"organization (as defined in the AUTH_LDAP_ORGANIZATION_MAP setting). If " +"multiple search queries need to be supported use of \"LDAPUnion\" is " "possible. See Tower documentation for details." msgstr "" -#: awx/sso/conf.py:236 +#: awx/sso/conf.py:238 msgid "LDAP User DN Template" msgstr "" -#: awx/sso/conf.py:237 +#: awx/sso/conf.py:239 msgid "" "Alternative to user search, if user DNs are all of the same format. This " -"approach will be more efficient for user lookups than searching if it is " -"usable in your organizational environment. If this setting has a value it " -"will be used instead of AUTH_LDAP_USER_SEARCH." +"approach is more efficient for user lookups than searching if it is usable " +"in your organizational environment. If this setting has a value it will be " +"used instead of AUTH_LDAP_USER_SEARCH." msgstr "" -#: awx/sso/conf.py:252 +#: awx/sso/conf.py:254 msgid "LDAP User Attribute Map" msgstr "" -#: awx/sso/conf.py:253 +#: awx/sso/conf.py:255 msgid "" -"Mapping of LDAP user schema to Tower API user attributes (key is user " -"attribute name, value is LDAP attribute name). The default setting is valid " -"for ActiveDirectory but users with other LDAP configurations may need to " -"change the values (not the keys) of the dictionary/hash-table." -msgstr "" - -#: awx/sso/conf.py:272 -msgid "LDAP Group Search" +"Mapping of LDAP user schema to Tower API user attributes. The default " +"setting is valid for ActiveDirectory but users with other LDAP " +"configurations may need to change the values. Refer to the Ansible Tower " +"documentation for additional details." msgstr "" #: awx/sso/conf.py:273 +msgid "LDAP Group Search" +msgstr "" + +#: awx/sso/conf.py:274 msgid "" "Users are mapped to organizations based on their membership in LDAP groups. " -"This setting defines the LDAP search query to find groups. Note that this, " -"unlike the user search above, does not support LDAPSearchUnion." +"This setting defines the LDAP search query to find groups. Unlike the user " +"search, group search does not support LDAPSearchUnion." msgstr "" #: awx/sso/conf.py:290 @@ -3654,255 +4625,293 @@ msgstr "" #: awx/sso/conf.py:291 msgid "" "The group type may need to be changed based on the type of the LDAP server. " -"Values are listed at: http://pythonhosted.org/django-auth-ldap/groups." -"html#types-of-groups" +"Values are listed at: https://django-auth-ldap.readthedocs.io/en/stable/" +"groups.html#types-of-groups" msgstr "" -#: awx/sso/conf.py:306 +#: awx/sso/conf.py:304 +msgid "LDAP Group Type Parameters" +msgstr "" + +#: awx/sso/conf.py:305 +msgid "Key value parameters to send the chosen group type init method." +msgstr "" + +#: awx/sso/conf.py:327 msgid "LDAP Require Group" msgstr "" -#: awx/sso/conf.py:307 +#: awx/sso/conf.py:328 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " "search will be able to login via Tower. Only one require group is supported." msgstr "" -#: awx/sso/conf.py:323 +#: awx/sso/conf.py:344 msgid "LDAP Deny Group" msgstr "" -#: awx/sso/conf.py:324 +#: awx/sso/conf.py:345 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." msgstr "" -#: awx/sso/conf.py:337 +#: awx/sso/conf.py:358 msgid "LDAP User Flags By Group" msgstr "" -#: awx/sso/conf.py:338 +#: awx/sso/conf.py:359 msgid "" -"User profile flags updated from group membership (key is user attribute " -"name, value is group DN). These are boolean fields that are matched based " -"on whether the user is a member of the given group. So far only " -"is_superuser and is_system_auditor are settable via this method. This flag " -"is set both true and false at login time based on current LDAP settings." +"Retrieve users from a given group. At this time, superuser and system " +"auditors are the only groups supported. Refer to the Ansible Tower " +"documentation for more detail." msgstr "" -#: awx/sso/conf.py:356 +#: awx/sso/conf.py:375 msgid "LDAP Organization Map" msgstr "" -#: awx/sso/conf.py:357 +#: awx/sso/conf.py:376 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " -"what users are placed into what Tower organizations relative to their LDAP " -"group memberships. Configuration details are available in Tower " +"which users are placed into which Tower organizations relative to their LDAP " +"group memberships. Configuration details are available in the Ansible Tower " "documentation." msgstr "" -#: awx/sso/conf.py:384 +#: awx/sso/conf.py:403 msgid "LDAP Team Map" msgstr "" -#: awx/sso/conf.py:385 +#: awx/sso/conf.py:404 msgid "" -"Mapping between team members (users) and LDAP groups.Configuration details " -"are available in Tower documentation." +"Mapping between team members (users) and LDAP groups. Configuration details " +"are available in the Ansible Tower documentation." msgstr "" -#: awx/sso/conf.py:413 +#: awx/sso/conf.py:440 msgid "RADIUS Server" msgstr "" -#: awx/sso/conf.py:414 +#: awx/sso/conf.py:441 msgid "" -"Hostname/IP of RADIUS server. RADIUS authentication will be disabled if this " +"Hostname/IP of RADIUS server. RADIUS authentication is disabled if this " "setting is empty." msgstr "" -#: awx/sso/conf.py:416 awx/sso/conf.py:430 awx/sso/conf.py:442 +#: awx/sso/conf.py:443 awx/sso/conf.py:457 awx/sso/conf.py:469 #: awx/sso/models.py:14 msgid "RADIUS" msgstr "" -#: awx/sso/conf.py:428 +#: awx/sso/conf.py:455 msgid "RADIUS Port" msgstr "" -#: awx/sso/conf.py:429 +#: awx/sso/conf.py:456 msgid "Port of RADIUS server." msgstr "" -#: awx/sso/conf.py:440 +#: awx/sso/conf.py:467 msgid "RADIUS Secret" msgstr "" -#: awx/sso/conf.py:441 +#: awx/sso/conf.py:468 msgid "Shared secret for authenticating to RADIUS server." msgstr "" -#: awx/sso/conf.py:457 +#: awx/sso/conf.py:484 msgid "TACACS+ Server" msgstr "" -#: awx/sso/conf.py:458 +#: awx/sso/conf.py:485 msgid "Hostname of TACACS+ server." msgstr "" -#: awx/sso/conf.py:459 awx/sso/conf.py:472 awx/sso/conf.py:485 -#: awx/sso/conf.py:498 awx/sso/conf.py:510 awx/sso/models.py:15 +#: awx/sso/conf.py:486 awx/sso/conf.py:499 awx/sso/conf.py:512 +#: awx/sso/conf.py:525 awx/sso/conf.py:537 awx/sso/models.py:15 msgid "TACACS+" msgstr "" -#: awx/sso/conf.py:470 +#: awx/sso/conf.py:497 msgid "TACACS+ Port" msgstr "" -#: awx/sso/conf.py:471 +#: awx/sso/conf.py:498 msgid "Port number of TACACS+ server." msgstr "" -#: awx/sso/conf.py:483 +#: awx/sso/conf.py:510 msgid "TACACS+ Secret" msgstr "" -#: awx/sso/conf.py:484 +#: awx/sso/conf.py:511 msgid "Shared secret for authenticating to TACACS+ server." msgstr "" -#: awx/sso/conf.py:496 +#: awx/sso/conf.py:523 msgid "TACACS+ Auth Session Timeout" msgstr "" -#: awx/sso/conf.py:497 +#: awx/sso/conf.py:524 msgid "TACACS+ session timeout value in seconds, 0 disables timeout." msgstr "" -#: awx/sso/conf.py:508 +#: awx/sso/conf.py:535 msgid "TACACS+ Authentication Protocol" msgstr "" -#: awx/sso/conf.py:509 +#: awx/sso/conf.py:536 msgid "Choose the authentication protocol used by TACACS+ client." msgstr "" -#: awx/sso/conf.py:524 +#: awx/sso/conf.py:551 msgid "Google OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:525 +#: awx/sso/conf.py:552 awx/sso/conf.py:645 awx/sso/conf.py:710 msgid "" -"Create a project at https://console.developers.google.com/ to obtain an " -"OAuth2 key and secret for a web application. Ensure that the Google+ API is " -"enabled. Provide this URL as the callback URL for your application." +"Provide this URL as the callback URL for your application as part of your " +"registration process. Refer to the Ansible Tower documentation for more " +"detail." msgstr "" -#: awx/sso/conf.py:529 awx/sso/conf.py:540 awx/sso/conf.py:551 -#: awx/sso/conf.py:564 awx/sso/conf.py:578 awx/sso/conf.py:590 -#: awx/sso/conf.py:602 +#: awx/sso/conf.py:555 awx/sso/conf.py:567 awx/sso/conf.py:579 +#: awx/sso/conf.py:592 awx/sso/conf.py:606 awx/sso/conf.py:618 +#: awx/sso/conf.py:630 msgid "Google OAuth2" msgstr "" -#: awx/sso/conf.py:538 +#: awx/sso/conf.py:565 msgid "Google OAuth2 Key" msgstr "" -#: awx/sso/conf.py:539 -msgid "" -"The OAuth2 key from your web application at https://console.developers." -"google.com/." +#: awx/sso/conf.py:566 +msgid "The OAuth2 key from your web application." msgstr "" -#: awx/sso/conf.py:549 +#: awx/sso/conf.py:577 msgid "Google OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:550 -msgid "" -"The OAuth2 secret from your web application at https://console.developers." -"google.com/." +#: awx/sso/conf.py:578 +msgid "The OAuth2 secret from your web application." msgstr "" -#: awx/sso/conf.py:561 +#: awx/sso/conf.py:589 msgid "Google OAuth2 Whitelisted Domains" msgstr "" -#: awx/sso/conf.py:562 +#: awx/sso/conf.py:590 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." msgstr "" -#: awx/sso/conf.py:573 +#: awx/sso/conf.py:601 msgid "Google OAuth2 Extra Arguments" msgstr "" -#: awx/sso/conf.py:574 +#: awx/sso/conf.py:602 msgid "" -"Extra arguments for Google OAuth2 login. When only allowing a single domain " -"to authenticate, set to `{\"hd\": \"yourdomain.com\"}` and Google will not " -"display any other accounts even if the user is logged in with multiple " -"Google accounts." -msgstr "" - -#: awx/sso/conf.py:588 -msgid "Google OAuth2 Organization Map" -msgstr "" - -#: awx/sso/conf.py:600 -msgid "Google OAuth2 Team Map" +"Extra arguments for Google OAuth2 login. You can restrict it to only allow a " +"single domain to authenticate, even if the user is logged in with multple " +"Google accounts. Refer to the Ansible Tower documentation for more detail." msgstr "" #: awx/sso/conf.py:616 +msgid "Google OAuth2 Organization Map" +msgstr "" + +#: awx/sso/conf.py:628 +msgid "Google OAuth2 Team Map" +msgstr "" + +#: awx/sso/conf.py:644 msgid "GitHub OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:617 -msgid "" -"Create a developer application at https://github.com/settings/developers to " -"obtain an OAuth2 key (Client ID) and secret (Client Secret). Provide this " -"URL as the callback URL for your application." -msgstr "" - -#: awx/sso/conf.py:621 awx/sso/conf.py:632 awx/sso/conf.py:642 -#: awx/sso/conf.py:654 awx/sso/conf.py:666 +#: awx/sso/conf.py:648 awx/sso/conf.py:660 awx/sso/conf.py:671 +#: awx/sso/conf.py:683 awx/sso/conf.py:695 msgid "GitHub OAuth2" msgstr "" -#: awx/sso/conf.py:630 +#: awx/sso/conf.py:658 msgid "GitHub OAuth2 Key" msgstr "" -#: awx/sso/conf.py:631 +#: awx/sso/conf.py:659 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "" -#: awx/sso/conf.py:640 +#: awx/sso/conf.py:669 msgid "GitHub OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:641 +#: awx/sso/conf.py:670 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "" -#: awx/sso/conf.py:652 +#: awx/sso/conf.py:681 msgid "GitHub OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:664 +#: awx/sso/conf.py:693 msgid "GitHub OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:680 +#: awx/sso/conf.py:709 msgid "GitHub Organization OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:681 awx/sso/conf.py:756 +#: awx/sso/conf.py:713 awx/sso/conf.py:725 awx/sso/conf.py:736 +#: awx/sso/conf.py:749 awx/sso/conf.py:760 awx/sso/conf.py:772 +msgid "GitHub Organization OAuth2" +msgstr "" + +#: awx/sso/conf.py:723 +msgid "GitHub Organization OAuth2 Key" +msgstr "" + +#: awx/sso/conf.py:724 awx/sso/conf.py:802 +msgid "The OAuth2 key (Client ID) from your GitHub organization application." +msgstr "" + +#: awx/sso/conf.py:734 +msgid "GitHub Organization OAuth2 Secret" +msgstr "" + +#: awx/sso/conf.py:735 awx/sso/conf.py:813 +msgid "" +"The OAuth2 secret (Client Secret) from your GitHub organization application." +msgstr "" + +#: awx/sso/conf.py:746 +msgid "GitHub Organization Name" +msgstr "" + +#: awx/sso/conf.py:747 +msgid "" +"The name of your GitHub organization, as used in your organization's URL: " +"https://github.com//." +msgstr "" + +#: awx/sso/conf.py:758 +msgid "GitHub Organization OAuth2 Organization Map" +msgstr "" + +#: awx/sso/conf.py:770 +msgid "GitHub Organization OAuth2 Team Map" +msgstr "" + +#: awx/sso/conf.py:786 +msgid "GitHub Team OAuth2 Callback URL" +msgstr "" + +#: awx/sso/conf.py:787 msgid "" "Create an organization-owned application at https://github.com/organizations/" "/settings/applications and obtain an OAuth2 key (Client ID) and " @@ -3910,314 +4919,339 @@ msgid "" "application." msgstr "" -#: awx/sso/conf.py:685 awx/sso/conf.py:696 awx/sso/conf.py:706 -#: awx/sso/conf.py:718 awx/sso/conf.py:729 awx/sso/conf.py:741 -msgid "GitHub Organization OAuth2" -msgstr "" - -#: awx/sso/conf.py:694 -msgid "GitHub Organization OAuth2 Key" -msgstr "" - -#: awx/sso/conf.py:695 awx/sso/conf.py:770 -msgid "The OAuth2 key (Client ID) from your GitHub organization application." -msgstr "" - -#: awx/sso/conf.py:704 -msgid "GitHub Organization OAuth2 Secret" -msgstr "" - -#: awx/sso/conf.py:705 awx/sso/conf.py:780 -msgid "" -"The OAuth2 secret (Client Secret) from your GitHub organization application." -msgstr "" - -#: awx/sso/conf.py:715 -msgid "GitHub Organization Name" -msgstr "" - -#: awx/sso/conf.py:716 -msgid "" -"The name of your GitHub organization, as used in your organization's URL: " -"https://github.com//." -msgstr "" - -#: awx/sso/conf.py:727 -msgid "GitHub Organization OAuth2 Organization Map" -msgstr "" - -#: awx/sso/conf.py:739 -msgid "GitHub Organization OAuth2 Team Map" -msgstr "" - -#: awx/sso/conf.py:755 -msgid "GitHub Team OAuth2 Callback URL" -msgstr "" - -#: awx/sso/conf.py:760 awx/sso/conf.py:771 awx/sso/conf.py:781 -#: awx/sso/conf.py:793 awx/sso/conf.py:804 awx/sso/conf.py:816 +#: awx/sso/conf.py:791 awx/sso/conf.py:803 awx/sso/conf.py:814 +#: awx/sso/conf.py:827 awx/sso/conf.py:838 awx/sso/conf.py:850 msgid "GitHub Team OAuth2" msgstr "" -#: awx/sso/conf.py:769 +#: awx/sso/conf.py:801 msgid "GitHub Team OAuth2 Key" msgstr "" -#: awx/sso/conf.py:779 +#: awx/sso/conf.py:812 msgid "GitHub Team OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:790 +#: awx/sso/conf.py:824 msgid "GitHub Team ID" msgstr "" -#: awx/sso/conf.py:791 +#: awx/sso/conf.py:825 msgid "" "Find the numeric team ID using the Github API: http://fabian-kostadinov." "github.io/2015/01/16/how-to-find-a-github-team-id/." msgstr "" -#: awx/sso/conf.py:802 +#: awx/sso/conf.py:836 msgid "GitHub Team OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:814 +#: awx/sso/conf.py:848 msgid "GitHub Team OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:830 +#: awx/sso/conf.py:864 msgid "Azure AD OAuth2 Callback URL" msgstr "" -#: awx/sso/conf.py:831 +#: awx/sso/conf.py:865 msgid "" -"Register an Azure AD application as described by https://msdn.microsoft.com/" -"en-us/library/azure/dn132599.aspx and obtain an OAuth2 key (Client ID) and " -"secret (Client Secret). Provide this URL as the callback URL for your " -"application." +"Provide this URL as the callback URL for your application as part of your " +"registration process. Refer to the Ansible Tower documentation for more " +"detail. " msgstr "" -#: awx/sso/conf.py:835 awx/sso/conf.py:846 awx/sso/conf.py:856 -#: awx/sso/conf.py:868 awx/sso/conf.py:880 +#: awx/sso/conf.py:868 awx/sso/conf.py:880 awx/sso/conf.py:891 +#: awx/sso/conf.py:903 awx/sso/conf.py:915 msgid "Azure AD OAuth2" msgstr "" -#: awx/sso/conf.py:844 +#: awx/sso/conf.py:878 msgid "Azure AD OAuth2 Key" msgstr "" -#: awx/sso/conf.py:845 +#: awx/sso/conf.py:879 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "" -#: awx/sso/conf.py:854 +#: awx/sso/conf.py:889 msgid "Azure AD OAuth2 Secret" msgstr "" -#: awx/sso/conf.py:855 +#: awx/sso/conf.py:890 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "" -#: awx/sso/conf.py:866 +#: awx/sso/conf.py:901 msgid "Azure AD OAuth2 Organization Map" msgstr "" -#: awx/sso/conf.py:878 +#: awx/sso/conf.py:913 msgid "Azure AD OAuth2 Team Map" msgstr "" -#: awx/sso/conf.py:903 +#: awx/sso/conf.py:938 msgid "SAML Assertion Consumer Service (ACS) URL" msgstr "" -#: awx/sso/conf.py:904 +#: awx/sso/conf.py:939 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this ACS URL for your " "application." msgstr "" -#: awx/sso/conf.py:907 awx/sso/conf.py:921 awx/sso/conf.py:935 -#: awx/sso/conf.py:950 awx/sso/conf.py:964 awx/sso/conf.py:982 -#: awx/sso/conf.py:1004 awx/sso/conf.py:1023 awx/sso/conf.py:1043 -#: awx/sso/conf.py:1077 awx/sso/conf.py:1090 awx/sso/models.py:16 +#: awx/sso/conf.py:942 awx/sso/conf.py:956 awx/sso/conf.py:970 +#: awx/sso/conf.py:985 awx/sso/conf.py:999 awx/sso/conf.py:1012 +#: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 +#: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 +#: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 +#: awx/sso/conf.py:1213 awx/sso/models.py:16 msgid "SAML" msgstr "" -#: awx/sso/conf.py:918 +#: awx/sso/conf.py:953 msgid "SAML Service Provider Metadata URL" msgstr "" -#: awx/sso/conf.py:919 +#: awx/sso/conf.py:954 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." msgstr "" -#: awx/sso/conf.py:931 +#: awx/sso/conf.py:966 msgid "SAML Service Provider Entity ID" msgstr "" -#: awx/sso/conf.py:932 +#: awx/sso/conf.py:967 msgid "" "The application-defined unique identifier used as the audience of the SAML " "service provider (SP) configuration. This is usually the URL for Tower." msgstr "" -#: awx/sso/conf.py:947 +#: awx/sso/conf.py:982 msgid "SAML Service Provider Public Certificate" msgstr "" -#: awx/sso/conf.py:948 +#: awx/sso/conf.py:983 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the " "certificate content here." msgstr "" -#: awx/sso/conf.py:961 +#: awx/sso/conf.py:996 msgid "SAML Service Provider Private Key" msgstr "" -#: awx/sso/conf.py:962 +#: awx/sso/conf.py:997 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the " "private key content here." msgstr "" -#: awx/sso/conf.py:980 +#: awx/sso/conf.py:1009 msgid "SAML Service Provider Organization Info" msgstr "" -#: awx/sso/conf.py:981 -msgid "Configure this setting with information about your app." +#: awx/sso/conf.py:1010 +msgid "" +"Provide the URL, display name, and the name of your app. Refer to the " +"Ansible Tower documentation for example syntax." msgstr "" -#: awx/sso/conf.py:1002 +#: awx/sso/conf.py:1029 msgid "SAML Service Provider Technical Contact" msgstr "" -#: awx/sso/conf.py:1003 awx/sso/conf.py:1022 -msgid "Configure this setting with your contact information." +#: awx/sso/conf.py:1030 +msgid "" +"Provide the name and email address of the technical contact for your service " +"provider. Refer to the Ansible Tower documentation for example syntax." msgstr "" -#: awx/sso/conf.py:1021 +#: awx/sso/conf.py:1047 msgid "SAML Service Provider Support Contact" msgstr "" -#: awx/sso/conf.py:1036 +#: awx/sso/conf.py:1048 +msgid "" +"Provide the name and email address of the support contact for your service " +"provider. Refer to the Ansible Tower documentation for example syntax." +msgstr "" + +#: awx/sso/conf.py:1064 msgid "SAML Enabled Identity Providers" msgstr "" -#: awx/sso/conf.py:1037 +#: awx/sso/conf.py:1065 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " -"data using attribute names that differ from the default OIDs (https://github." -"com/omab/python-social-auth/blob/master/social/backends/saml.py#L16). " -"Attribute names may be overridden for each IdP." +"data using attribute names that differ from the default OIDs. Attribute " +"names may be overridden for each IdP. Refer to the Ansible documentation for " +"additional details and syntax." msgstr "" -#: awx/sso/conf.py:1075 +#: awx/sso/conf.py:1102 +msgid "SAML Security Config" +msgstr "" + +#: awx/sso/conf.py:1103 +msgid "" +"A dict of key value pairs that are passed to the underlying python-saml " +"security setting https://github.com/onelogin/python-saml#settings" +msgstr "" + +#: awx/sso/conf.py:1135 +msgid "SAML Service Provider extra configuration data" +msgstr "" + +#: awx/sso/conf.py:1136 +msgid "" +"A dict of key value pairs to be passed to the underlying python-saml Service " +"Provider configuration setting." +msgstr "" + +#: awx/sso/conf.py:1149 +msgid "SAML IDP to extra_data attribute mapping" +msgstr "" + +#: awx/sso/conf.py:1150 +msgid "" +"A list of tuples that maps IDP attributes to extra_attributes. Each " +"attribute will be a list of values, even if only 1 value." +msgstr "" + +#: awx/sso/conf.py:1167 msgid "SAML Organization Map" msgstr "" -#: awx/sso/conf.py:1088 +#: awx/sso/conf.py:1180 msgid "SAML Team Map" msgstr "" -#: awx/sso/fields.py:123 +#: awx/sso/conf.py:1193 +msgid "SAML Organization Attribute Mapping" +msgstr "" + +#: awx/sso/conf.py:1194 +msgid "Used to translate user organization membership into Tower." +msgstr "" + +#: awx/sso/conf.py:1211 +msgid "SAML Team Attribute Mapping" +msgstr "" + +#: awx/sso/conf.py:1212 +msgid "Used to translate user team membership into Tower." +msgstr "" + +#: awx/sso/fields.py:183 +#, python-brace-format msgid "Invalid connection option(s): {invalid_options}." msgstr "" -#: awx/sso/fields.py:194 +#: awx/sso/fields.py:266 msgid "Base" msgstr "" -#: awx/sso/fields.py:195 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "" -#: awx/sso/fields.py:196 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "" -#: awx/sso/fields.py:214 +#: awx/sso/fields.py:286 +#, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "" -#: awx/sso/fields.py:215 +#: awx/sso/fields.py:287 +#, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:251 +#: awx/sso/fields.py:323 +#, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " "instead." msgstr "" -#: awx/sso/fields.py:289 +#: awx/sso/fields.py:361 +#, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "" -#: awx/sso/fields.py:306 +#: awx/sso/fields.py:378 +#, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" -#: awx/sso/fields.py:334 -msgid "Invalid user flag: \"{invalid_flag}\"." -msgstr "" - -#: awx/sso/fields.py:350 awx/sso/fields.py:517 -msgid "" -"Expected None, True, False, a string or list of strings but got {input_type} " -"instead." -msgstr "" - -#: awx/sso/fields.py:386 -msgid "Missing key(s): {missing_keys}." -msgstr "" - -#: awx/sso/fields.py:387 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 +#, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "" -#: awx/sso/fields.py:436 awx/sso/fields.py:553 +#: awx/sso/fields.py:443 +#, python-brace-format +msgid "Invalid user flag: \"{invalid_flag}\"." +msgstr "" + +#: awx/sso/fields.py:464 +#, python-brace-format +msgid "Missing key(s): {missing_keys}." +msgstr "" + +#: awx/sso/fields.py:514 awx/sso/fields.py:631 +#, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:454 +#: awx/sso/fields.py:532 +#, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:455 awx/sso/fields.py:572 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 +#, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "" -#: awx/sso/fields.py:571 +#: awx/sso/fields.py:649 +#, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "" -#: awx/sso/fields.py:589 +#: awx/sso/fields.py:667 +#, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "" -#: awx/sso/fields.py:602 +#: awx/sso/fields.py:680 +#, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "" -#: awx/sso/fields.py:621 +#: awx/sso/fields.py:699 +#, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "" -#: awx/sso/fields.py:633 +#: awx/sso/fields.py:711 +#, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "" -#: awx/sso/pipeline.py:24 +#: awx/sso/pipeline.py:31 +#, python-brace-format msgid "An account cannot be found for {0}" msgstr "" -#: awx/sso/pipeline.py:30 +#: awx/sso/pipeline.py:37 msgid "Your account is inactive" msgstr "" @@ -4244,71 +5278,48 @@ msgstr "" msgid "AWX" msgstr "" -#: awx/templates/rest_framework/api.html:4 -msgid "AWX REST API" -msgstr "" - -#: awx/templates/rest_framework/api.html:39 +#: awx/templates/rest_framework/api.html:42 msgid "Ansible Tower API Guide" msgstr "" -#: awx/templates/rest_framework/api.html:40 +#: awx/templates/rest_framework/api.html:43 msgid "Back to Ansible Tower" msgstr "" -#: awx/templates/rest_framework/api.html:41 +#: awx/templates/rest_framework/api.html:44 msgid "Resize" msgstr "" +#: awx/templates/rest_framework/base.html:37 +msgid "navbar" +msgstr "" + +#: awx/templates/rest_framework/base.html:75 +msgid "content" +msgstr "" + #: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 -#, python-format -msgid "Make a GET request on the %(name)s resource" +msgid "request form" msgstr "" -#: awx/templates/rest_framework/base.html:80 -msgid "Specify a format for the GET request" -msgstr "" - -#: awx/templates/rest_framework/base.html:86 -#, python-format -msgid "" -"Make a GET request on the %(name)s resource with the format set to `" -"%(format)s`" -msgstr "" - -#: awx/templates/rest_framework/base.html:100 -#, python-format -msgid "Make an OPTIONS request on the %(name)s resource" -msgstr "" - -#: awx/templates/rest_framework/base.html:106 -#, python-format -msgid "Make a DELETE request on the %(name)s resource" -msgstr "" - -#: awx/templates/rest_framework/base.html:113 +#: awx/templates/rest_framework/base.html:134 msgid "Filters" msgstr "" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 -#, python-format -msgid "Make a POST request on the %(name)s resource" +#: awx/templates/rest_framework/base.html:139 +msgid "main content" msgstr "" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 -#, python-format -msgid "Make a PUT request on the %(name)s resource" +#: awx/templates/rest_framework/base.html:155 +msgid "request info" msgstr "" -#: awx/templates/rest_framework/base.html:233 -#, python-format -msgid "Make a PATCH request on the %(name)s resource" +#: awx/templates/rest_framework/base.html:159 +msgid "response info" msgstr "" -#: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:38 awx/ui/conf.py:53 +#: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:36 awx/ui/conf.py:51 +#: awx/ui/conf.py:63 awx/ui/conf.py:73 msgid "UI" msgstr "" @@ -4341,21 +5352,39 @@ msgid "" "If needed, you can add specific information (such as a legal notice or a " "disclaimer) to a text box in the login modal using this setting. Any content " "added must be in plain text, as custom HTML or other markup languages are " -"not supported. If multiple paragraphs of text are needed, new lines " -"(paragraphs) must be escaped as `\\n` within the block of text." +"not supported." msgstr "" -#: awx/ui/conf.py:48 +#: awx/ui/conf.py:46 msgid "Custom Logo" msgstr "" -#: awx/ui/conf.py:49 +#: awx/ui/conf.py:47 msgid "" "To set up a custom logo, provide a file that you create. For the custom logo " "to look its best, use a .png file with a transparent background. GIF, PNG " "and JPEG formats are supported." msgstr "" +#: awx/ui/conf.py:60 +msgid "Max Job Events Retrieved by UI" +msgstr "" + +#: awx/ui/conf.py:61 +msgid "" +"Maximum number of job events for the UI to retrieve within a single request." +msgstr "" + +#: awx/ui/conf.py:70 +msgid "Enable Live Updates in the UI" +msgstr "" + +#: awx/ui/conf.py:71 +msgid "" +"If disabled, the page will not refresh when events are received. Reloading " +"the page will be required to get the latest details." +msgstr "" + #: awx/ui/fields.py:29 msgid "" "Invalid format for custom logo. Must be a data URL with a base64-encoded " @@ -4365,69 +5394,3 @@ msgstr "" #: awx/ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "" - -#: awx/ui/templates/ui/index.html:31 -msgid "" -"Your session will expire in 60 seconds, would you like to continue?" -msgstr "" - -#: awx/ui/templates/ui/index.html:46 -msgid "CANCEL" -msgstr "" - -#: awx/ui/templates/ui/index.html:98 -msgid "Set how many days of data should be retained." -msgstr "" - -#: awx/ui/templates/ui/index.html:104 -msgid "" -"Please enter an integer that is not " -"negative that is lower than 9999." -msgstr "" - -#: awx/ui/templates/ui/index.html:109 -msgid "" -"For facts collected older than the time period specified, save one fact scan " -"(snapshot) per time window (frequency). For example, facts older than 30 " -"days are purged, while one weekly fact scan is kept.\n" -"
\n" -"
CAUTION: Setting both numerical variables to \"0\" " -"will delete all facts.\n" -"
\n" -"
" -msgstr "" - -#: awx/ui/templates/ui/index.html:118 -msgid "Select a time period after which to remove old facts" -msgstr "" - -#: awx/ui/templates/ui/index.html:132 -msgid "" -"Please enter an integer " -"that is not negative " -"that is lower than 9999." -msgstr "" - -#: awx/ui/templates/ui/index.html:137 -msgid "Select a frequency for snapshot retention" -msgstr "" - -#: awx/ui/templates/ui/index.html:151 -msgid "" -"Please enter an integer that is not negative that is " -"lower than 9999." -msgstr "" - -#: awx/ui/templates/ui/index.html:157 -msgid "working..." -msgstr "" diff --git a/awx/locale/es/LC_MESSAGES/django.po b/awx/locale/es/LC_MESSAGES/django.po index d24f549215..fd848e2482 100644 --- a/awx/locale/es/LC_MESSAGES/django.po +++ b/awx/locale/es/LC_MESSAGES/django.po @@ -8,12 +8,14 @@ # mkim , 2017. #zanata # plocatelli , 2017. #zanata # trh01 , 2017. #zanata +# edrh01 , 2018. #zanata +# trh01 , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-30 20:23+0000\n" -"PO-Revision-Date: 2017-09-01 06:52+0000\n" +"POT-Creation-Date: 2018-06-14 18:30+0000\n" +"PO-Revision-Date: 2018-06-15 10:02+0000\n" "Last-Translator: edrh01 \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -21,30 +23,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Zanata 4.3.2\n" +"X-Generator: Zanata 4.6.0\n" -#: awx/api/authentication.py:67 -msgid "Invalid token header. No credentials provided." -msgstr "Cabecera token inválida. Credenciales no indicados." - -#: awx/api/authentication.py:70 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "" -"Cabecera token inválida. La cadena de texto token no debe contener espacios." - -#: awx/api/authentication.py:105 -msgid "User inactive or deleted" -msgstr "Usuario inactivo o eliminado" - -#: awx/api/authentication.py:161 -msgid "Invalid task token" -msgstr "Token de tarea inválido" - -#: awx/api/conf.py:12 +#: awx/api/conf.py:15 msgid "Idle Time Force Log Out" msgstr "Tiempo de inactividad fuerza desconexión" -#: awx/api/conf.py:13 +#: awx/api/conf.py:16 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." @@ -52,112 +37,147 @@ msgstr "" "Número de segundos que un usuario es inactivo antes de que ellos vuelvan a " "conectarse de nuevo." -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:85 -#: awx/sso/conf.py:96 awx/sso/conf.py:108 awx/sso/conf.py:123 +#: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 +#: awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/sso/conf.py:123 msgid "Authentication" -msgstr "Autentificación" +msgstr "Autenticación" -#: awx/api/conf.py:22 -msgid "Maximum number of simultaneous logins" -msgstr "Número máximo de inicios de sesión simultáneos" +#: awx/api/conf.py:24 +msgid "Maximum number of simultaneous logged in sessions" +msgstr "Número máximo de sesiones activas en simultáneo" -#: awx/api/conf.py:23 +#: awx/api/conf.py:25 msgid "" -"Maximum number of simultaneous logins a user may have. To disable enter -1." +"Maximum number of simultaneous logged in sessions a user may have. To " +"disable enter -1." msgstr "" -"Número máximo de inicio de sesión simultáneos que un usuario puede tener. " -"Para deshabilitar introduzca -1." - -#: awx/api/conf.py:31 -msgid "Enable HTTP Basic Auth" -msgstr "Habilitar autentificación básica HTTP" +"Número máximo de sesiones activas en simultáneo que un usuario puede tener. " +"Para deshabilitar, introduzca -1." #: awx/api/conf.py:32 -msgid "Enable HTTP Basic Auth for the API Browser." -msgstr "Habilitar autentificación básica HTTP para la navegación API." +msgid "Enable HTTP Basic Auth" +msgstr "Habilitar autenticación básica HTTP" -#: awx/api/filters.py:129 +#: awx/api/conf.py:33 +msgid "Enable HTTP Basic Auth for the API Browser." +msgstr "Habilitar autenticación básica HTTP para la navegación API." + +#: awx/api/conf.py:42 +msgid "OAuth 2 Timeout Settings" +msgstr "Configuración de tiempo de expiración OAuth 2" + +#: awx/api/conf.py:43 +msgid "" +"Dictionary for customizing OAuth 2 timeouts, available items are " +"`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number " +"of seconds, and `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of " +"authorization grants in the number of seconds." +msgstr "" +"Diccionario para personalizar los tiempos de expiración OAuth 2; los " +"elementos disponibles son `ACCESS_TOKEN_EXPIRE_SECONDS`: duración de los " +"tokens de acceso en cantidad de segundos y " +"`AUTHORIZATION_CODE_EXPIRE_SECONDS`: duración de las autorizaciones " +"otorgadas en cantidad de segundos." + +#: awx/api/exceptions.py:20 +msgid "Resource is being used by running jobs." +msgstr "El recurso está siendo usado por tareas en ejecución." + +#: awx/api/fields.py:81 +#, python-brace-format +msgid "Invalid key names: {invalid_key_names}" +msgstr "Nombres de claves no válidos: {invalid_key_names}" + +#: awx/api/fields.py:107 +msgid "Credential {} does not exist" +msgstr "La credencial {} no existe" + +#: awx/api/filters.py:96 +msgid "No related model for field {}." +msgstr "Sin modelo relacionado para el campo {}." + +#: awx/api/filters.py:113 msgid "Filtering on password fields is not allowed." msgstr "Filtrar sobre campos de contraseña no está permitido." -#: awx/api/filters.py:141 awx/api/filters.py:143 +#: awx/api/filters.py:125 awx/api/filters.py:127 #, python-format msgid "Filtering on %s is not allowed." msgstr "Filtrar sobre %s no está permitido." -#: awx/api/filters.py:146 +#: awx/api/filters.py:130 msgid "Loops not allowed in filters, detected on field {}." -msgstr "Bucles no permitidos en los filtros, detectados en el campo {}." +msgstr "Bucles no permitidos en los filtros; detectados en el campo {}." -#: awx/api/filters.py:171 +#: awx/api/filters.py:159 +msgid "Query string field name not provided." +msgstr "Nombre de campo de la cadena de petición no provisto." + +#: awx/api/filters.py:186 #, python-brace-format msgid "Invalid {field_name} id: {field_id}" -msgstr "" +msgstr "ID de {field_name} no válida: {field_id}" -#: awx/api/filters.py:302 +#: awx/api/filters.py:319 #, python-format msgid "cannot filter on kind %s" msgstr "no se puede filtrar en el tipo %s" -#: awx/api/filters.py:409 -#, python-format -msgid "cannot order by field %s" -msgstr "no se puede ordenar por el campo %s" - -#: awx/api/generics.py:550 awx/api/generics.py:612 +#: awx/api/generics.py:620 awx/api/generics.py:682 msgid "\"id\" field must be an integer." msgstr "El campo \"id\" debe ser un número entero." -#: awx/api/generics.py:609 +#: awx/api/generics.py:679 msgid "\"id\" is required to disassociate" msgstr "\"id\" es necesario para desasociar" -#: awx/api/generics.py:660 +#: awx/api/generics.py:730 msgid "{} 'id' field is missing." msgstr "Falta el campo {} 'id'." #: awx/api/metadata.py:51 msgid "Database ID for this {}." -msgstr "ID de la base de datos para esto {}" +msgstr "ID de la base de datos para esto {}." #: awx/api/metadata.py:52 msgid "Name of this {}." -msgstr "Nombre de esto {}" +msgstr "Nombre de esto {}." #: awx/api/metadata.py:53 msgid "Optional description of this {}." -msgstr "Descripción opcional de esto {}" +msgstr "Descripción opcional de esto {}." #: awx/api/metadata.py:54 msgid "Data type for this {}." -msgstr "Tipo de datos para esto {}" +msgstr "Tipo de datos para esto {}." #: awx/api/metadata.py:55 msgid "URL for this {}." -msgstr "URL para esto {}" +msgstr "URL para esto {}." #: awx/api/metadata.py:56 msgid "Data structure with URLs of related resources." -msgstr "Estructura de datos con URLs de recursos relacionados." +msgstr "Estructura de datos con URL de recursos relacionados." #: awx/api/metadata.py:57 msgid "Data structure with name/description for related resources." msgstr "" -"Estructura de datos con nombre/descripción para recursos relacionados.arga" +"Estructura de datos con nombre/descripción para recursos relacionados." #: awx/api/metadata.py:58 msgid "Timestamp when this {} was created." -msgstr "Fecha y hora cuando este {} fue creado." +msgstr "Fecha y hora en que se creó esto {}." #: awx/api/metadata.py:59 msgid "Timestamp when this {} was last modified." -msgstr "Fecha y hora cuando este {} fue modificado más recientemente." +msgstr "Fecha y hora en que se modificó esto {} más recientemente." -#: awx/api/parsers.py:64 +#: awx/api/parsers.py:33 msgid "JSON parse error - not a JSON object" msgstr "Error de análisis JSON; no es un objeto JSON" -#: awx/api/parsers.py:67 +#: awx/api/parsers.py:36 #, python-format msgid "" "JSON parse error - %s\n" @@ -166,76 +186,119 @@ msgstr "" "Error de análisis JSON - %s\n" "Posible causa: coma final." -#: awx/api/serializers.py:268 +#: awx/api/serializers.py:153 +msgid "" +"The original object is already named {}, a copy from it cannot have the same" +" name." +msgstr "" +"El objeto original ya tiene el nombre {}, por lo que una copia de este no " +"puede tener el mismo nombre." + +#: awx/api/serializers.py:295 msgid "Playbook Run" msgstr "Ejecutar Playbook" -#: awx/api/serializers.py:269 +#: awx/api/serializers.py:296 msgid "Command" msgstr "Comando" -#: awx/api/serializers.py:270 awx/main/models/unified_jobs.py:435 +#: awx/api/serializers.py:297 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "Actualización SCM" -#: awx/api/serializers.py:271 +#: awx/api/serializers.py:298 msgid "Inventory Sync" msgstr "Sincronizar inventario" -#: awx/api/serializers.py:272 +#: awx/api/serializers.py:299 msgid "Management Job" msgstr "Trabajo de gestión" -#: awx/api/serializers.py:273 +#: awx/api/serializers.py:300 msgid "Workflow Job" msgstr "Tarea en flujo de trabajo" -#: awx/api/serializers.py:274 +#: awx/api/serializers.py:301 msgid "Workflow Template" msgstr "Plantilla de flujo de trabajo" -#: awx/api/serializers.py:701 awx/api/serializers.py:759 awx/api/views.py:4365 -#, python-format +#: awx/api/serializers.py:302 +msgid "Job Template" +msgstr "Plantilla de trabajo" + +#: awx/api/serializers.py:697 msgid "" -"Standard Output too large to display (%(text_size)d bytes), only download " -"supported for sizes over %(supported_size)d bytes" +"Indicates whether all of the events generated by this unified job have been " +"saved to the database." msgstr "" -"La salida estándar es muy larga para ser mostrada (%(text_size)d bytes), " -"sólo está soportada la descarga para tamaños por encima de " -"%(supported_size)d bytes" +"Indica si todos los eventos generados por esta tarea unificada se guardaron " +"en la base de datos." -#: awx/api/serializers.py:774 +#: awx/api/serializers.py:854 msgid "Write-only field used to change the password." -msgstr "Campo de sólo escritura utilizado para cambiar la contraseña." +msgstr "Campo de solo escritura utilizado para cambiar la contraseña." -#: awx/api/serializers.py:776 +#: awx/api/serializers.py:856 msgid "Set if the account is managed by an external service" msgstr "Establecer si la cuenta es administrada por un servicio externo" -#: awx/api/serializers.py:800 +#: awx/api/serializers.py:880 msgid "Password required for new User." msgstr "Contraseña requerida para un usuario nuevo." -#: awx/api/serializers.py:886 +#: awx/api/serializers.py:971 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "Incapaz de cambiar %s en usuario gestionado por LDAP." -#: awx/api/serializers.py:1050 -msgid "Organization is missing" -msgstr "Organización no encontrada." +#: awx/api/serializers.py:1057 +msgid "Must be a simple space-separated string with allowed scopes {}." +msgstr "" +"Debe ser una cadena simple separada por espacios con alcances permitidos {}." -#: awx/api/serializers.py:1054 +#: awx/api/serializers.py:1151 +msgid "Authorization Grant Type" +msgstr "Tipo de autorización" + +#: awx/api/serializers.py:1153 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "Secreto del cliente" + +#: awx/api/serializers.py:1156 +msgid "Client Type" +msgstr "Tipo de cliente" + +#: awx/api/serializers.py:1159 +msgid "Redirect URIs" +msgstr "Redirigir URI" + +#: awx/api/serializers.py:1162 +msgid "Skip Authorization" +msgstr "Omitir autorización" + +#: awx/api/serializers.py:1264 +msgid "This path is already being used by another manual project." +msgstr "Esta ruta ya está siendo usada por otro proyecto manual." + +#: awx/api/serializers.py:1290 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "Este campo ya no se utiliza y será retirado en un futuro lanzamiento." + +#: awx/api/serializers.py:1349 +msgid "Organization is missing" +msgstr "Organización no encontrada" + +#: awx/api/serializers.py:1353 msgid "Update options must be set to false for manual projects." msgstr "" -"Opciones de actualización deben ser establecidas a false para proyectos " +"Las opciones de actualización se deben establecer en false para proyectos " "manuales." -#: awx/api/serializers.py:1060 +#: awx/api/serializers.py:1359 msgid "Array of playbooks available within this project." msgstr "Colección de playbooks disponibles dentro de este proyecto." -#: awx/api/serializers.py:1079 +#: awx/api/serializers.py:1378 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." @@ -243,71 +306,74 @@ msgstr "" "Colección de archivos de inventario y directorios disponibles dentro de este" " proyecto, no global." -#: awx/api/serializers.py:1201 -msgid "Smart inventories must specify host_filter" -msgstr "" +#: awx/api/serializers.py:1426 awx/api/serializers.py:3194 +msgid "A count of hosts uniquely assigned to each status." +msgstr "Un número de hosts asignados de manera única a cada estado." -#: awx/api/serializers.py:1303 +#: awx/api/serializers.py:1429 awx/api/serializers.py:3197 +msgid "A count of all plays and tasks for the job run." +msgstr "La cantidad de reproducciones y tareas para la ejecución del trabajo." + +#: awx/api/serializers.py:1554 +msgid "Smart inventories must specify host_filter" +msgstr "Los inventarios inteligentes deben especificar host_filter" + +#: awx/api/serializers.py:1658 #, python-format msgid "Invalid port specification: %s" -msgstr "Especificación de puerto inválido: %s" +msgstr "Especificación de puerto no válida: %s" -#: awx/api/serializers.py:1314 +#: awx/api/serializers.py:1669 msgid "Cannot create Host for Smart Inventory" msgstr "No es posible crear un host para el Inventario inteligente" -#: awx/api/serializers.py:1336 awx/api/serializers.py:3321 -#: awx/api/serializers.py:3406 awx/main/validators.py:198 -msgid "Must be valid JSON or YAML." -msgstr "Debe ser un válido JSON o YAML." - -#: awx/api/serializers.py:1432 +#: awx/api/serializers.py:1781 msgid "Invalid group name." -msgstr "Nombre de grupo inválido." +msgstr "Nombre de grupo no válido." -#: awx/api/serializers.py:1437 +#: awx/api/serializers.py:1786 msgid "Cannot create Group for Smart Inventory" msgstr "No es posible crear un grupo para el Inventario inteligente" -#: awx/api/serializers.py:1509 +#: awx/api/serializers.py:1861 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" -"El script debe empezar con una secuencia hashbang, p.e.... #!/usr/bin/env " -"python" +"El script debe empezar con una secuencia hashbang, es decir.... " +"#!/usr/bin/env python" -#: awx/api/serializers.py:1555 +#: awx/api/serializers.py:1910 msgid "`{}` is a prohibited environment variable" msgstr "`{}` es una variable de entorno prohibida" -#: awx/api/serializers.py:1566 +#: awx/api/serializers.py:1921 msgid "If 'source' is 'custom', 'source_script' must be provided." -msgstr "Si 'source' es 'custom', 'source_script' debe ser especificado." +msgstr "Si 'source' es 'custom', se debe especificar 'source_script'." -#: awx/api/serializers.py:1572 +#: awx/api/serializers.py:1927 msgid "Must provide an inventory." msgstr "Debe proporcionar un inventario." -#: awx/api/serializers.py:1576 +#: awx/api/serializers.py:1931 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" "El 'source_script' no pertenece a la misma organización que el inventario." -#: awx/api/serializers.py:1578 +#: awx/api/serializers.py:1933 msgid "'source_script' doesn't exist." msgstr "'source_script' no existe." -#: awx/api/serializers.py:1602 +#: awx/api/serializers.py:1967 msgid "Automatic group relationship, will be removed in 3.3" msgstr "Relación de grupo automática; se eliminará en 3.3" -#: awx/api/serializers.py:1679 +#: awx/api/serializers.py:2053 msgid "Cannot use manual project for SCM-based inventory." msgstr "No se puede usar el proyecto manual para el inventario basado en SCM." -#: awx/api/serializers.py:1685 +#: awx/api/serializers.py:2059 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." @@ -315,75 +381,77 @@ msgstr "" "Las fuentes de inventario manuales se crean automáticamente cuando se crea " "un grupo en la API v1." -#: awx/api/serializers.py:1690 +#: awx/api/serializers.py:2064 msgid "Setting not compatible with existing schedules." msgstr "Configuración no compatible con programaciones existentes." -#: awx/api/serializers.py:1695 +#: awx/api/serializers.py:2069 msgid "Cannot create Inventory Source for Smart Inventory" msgstr "" "No es posible crear una fuente de inventarios para el Inventario inteligente" -#: awx/api/serializers.py:1709 +#: awx/api/serializers.py:2120 #, python-format msgid "Cannot set %s if not SCM type." msgstr "No es posible definir %s si no es de tipo SCM." -#: awx/api/serializers.py:1950 +#: awx/api/serializers.py:2388 msgid "Modifications not allowed for managed credential types" msgstr "" "Modificaciones no permitidas para los tipos de credenciales administradas" -#: awx/api/serializers.py:1955 +#: awx/api/serializers.py:2393 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "" "No se permiten las modificaciones a entradas para los tipos de credenciales " "que están en uso" -#: awx/api/serializers.py:1961 +#: awx/api/serializers.py:2399 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "Debe ser 'cloud' o 'net', no %s" -#: awx/api/serializers.py:1967 +#: awx/api/serializers.py:2405 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "" "'ask_at_runtime' no es compatible con las credenciales personalizadas." -#: awx/api/serializers.py:2140 +#: awx/api/serializers.py:2585 #, python-format msgid "\"%s\" is not a valid choice" msgstr "\"%s\" no es una opción válida" -#: awx/api/serializers.py:2159 -#, python-format -msgid "'%s' is not a valid field for %s" -msgstr "'%s' no es un campo válido para %s" +#: awx/api/serializers.py:2604 +#, python-brace-format +msgid "'{field_name}' is not a valid field for {credential_type_name}" +msgstr "'{field_name}' no es un campo válido para {credential_type_name}" -#: awx/api/serializers.py:2180 +#: awx/api/serializers.py:2625 msgid "" "You cannot change the credential type of the credential, as it may break the" " functionality of the resources using it." msgstr "" +"No puede cambiar el tipo de credencial, ya que puede interrumpir la " +"funcionalidad de los recursos que la usan." -#: awx/api/serializers.py:2191 +#: awx/api/serializers.py:2637 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." msgstr "" -"Campo de sólo escritura utilizado para añadir usuario a rol de propietario. " -"Si se indica, no otorgar equipo u organización. Sólo válido para creación." +"Campo de solo escritura utilizado para añadir usuario a rol de propietario. " +"Si se indica, no otorgar equipo u organización. Solo válido para creación." -#: awx/api/serializers.py:2196 +#: awx/api/serializers.py:2642 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." msgstr "" -"Campo de sólo escritura para añadir equipo a un rol propietario.Si se " -"indica, no otorgar usuario u organización. Sólo válido para creación." +"Campo de solo escritura para añadir equipo a un rol propietario. Si se " +"indica, no otorgar usuario u organización. Solo válido para creación." -#: awx/api/serializers.py:2201 +#: awx/api/serializers.py:2647 msgid "" "Inherit permissions from organization roles. If provided on creation, do not" " give either user or team." @@ -391,698 +459,958 @@ msgstr "" "Permisos heredados desde roles de organización. Si se indica, no otorgar " "usuario o equipo." -#: awx/api/serializers.py:2217 +#: awx/api/serializers.py:2663 msgid "Missing 'user', 'team', or 'organization'." -msgstr "No encontrado 'user', 'team' u 'organization'" +msgstr "'User', 'team' u 'organization' no encontrados." -#: awx/api/serializers.py:2257 +#: awx/api/serializers.py:2703 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" -"Credenciales de organización deben ser establecidas y coincidir antes de ser" -" asignadas a un equipo" +"Se debe establecer y corresponder la organización de credenciales antes de " +"asignarlas a un equipo" -#: awx/api/serializers.py:2424 +#: awx/api/serializers.py:2904 msgid "You must provide a cloud credential." msgstr "Debe proporcionar una credencial de nube." -#: awx/api/serializers.py:2425 +#: awx/api/serializers.py:2905 msgid "You must provide a network credential." msgstr "Debe indicar un credencial de red." -#: awx/api/serializers.py:2441 +#: awx/api/serializers.py:2906 awx/main/models/jobs.py:155 +msgid "You must provide an SSH credential." +msgstr "Debe proporcionar una credencial SSH." + +#: awx/api/serializers.py:2907 +msgid "You must provide a vault credential." +msgstr "Debe proporcionar una credencial de Vault." + +#: awx/api/serializers.py:2926 msgid "This field is required." msgstr "Este campo es obligatorio." -#: awx/api/serializers.py:2443 awx/api/serializers.py:2445 +#: awx/api/serializers.py:2928 awx/api/serializers.py:2930 msgid "Playbook not found for project." msgstr "Playbook no encontrado para el proyecto." -#: awx/api/serializers.py:2447 +#: awx/api/serializers.py:2932 msgid "Must select playbook for project." msgstr "Debe seleccionar un playbook para el proyecto." -#: awx/api/serializers.py:2522 +#: awx/api/serializers.py:3013 +msgid "Cannot enable provisioning callback without an inventory set." +msgstr "" +"No puede habilitar la callback de aprovisionamiento sin un conjunto de " +"inventario." + +#: awx/api/serializers.py:3016 msgid "Must either set a default value or ask to prompt on launch." msgstr "" "Debe establecer un valor por defecto o preguntar por valor al ejecutar." -#: awx/api/serializers.py:2524 awx/main/models/jobs.py:326 +#: awx/api/serializers.py:3018 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "Tipos de trabajo 'run' y 'check' deben tener asignado un proyecto." -#: awx/api/serializers.py:2611 +#: awx/api/serializers.py:3134 msgid "Invalid job template." -msgstr "Plantilla de trabajo inválida" +msgstr "Plantilla de trabajo no válida." -#: awx/api/serializers.py:2708 -msgid "Neither credential nor vault credential provided." +#: awx/api/serializers.py:3249 +msgid "No change to job limit" +msgstr "Sin cambios en el límite de tareas" + +#: awx/api/serializers.py:3250 +msgid "All failed and unreachable hosts" +msgstr "Todos los hosts fallidos y sin comunicación" + +#: awx/api/serializers.py:3265 +msgid "Missing passwords needed to start: {}" +msgstr "Se necesitan las contraseñas faltantes para iniciar: {}" + +#: awx/api/serializers.py:3284 +msgid "Relaunch by host status not available until job finishes running." msgstr "" +"Relanzamiento por estado de host no disponible hasta que la tarea termine de" +" ejecutarse." -#: awx/api/serializers.py:2711 +#: awx/api/serializers.py:3298 msgid "Job Template Project is missing or undefined." msgstr "Proyecto en la plantilla de trabajo no encontrado o no definido." -#: awx/api/serializers.py:2713 +#: awx/api/serializers.py:3300 msgid "Job Template Inventory is missing or undefined." msgstr "Inventario en la plantilla de trabajo no encontrado o no definido." -#: awx/api/serializers.py:2782 awx/main/tasks.py:2186 +#: awx/api/serializers.py:3338 +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:3405 awx/main/tasks.py:2268 msgid "{} are prohibited from use in ad hoc commands." -msgstr "" +msgstr "{} tienen uso prohibido en comandos ad hoc." -#: awx/api/serializers.py:3008 -#, python-format -msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." +#: awx/api/serializers.py:3474 awx/api/views.py:4843 +#, python-brace-format +msgid "" +"Standard Output too large to display ({text_size} bytes), only download " +"supported for sizes over {supported_size} bytes." msgstr "" -"j%(job_type)s no es un tipo de trabajo válido. Las opciones son %(choices)s." +"La salida estándar es demasiado larga para visualizarse ({text_size} bytes);" +" la descarga solo se admite para tamaños por encima de {supported_size} " +"bytes." -#: awx/api/serializers.py:3013 -msgid "Workflow job template is missing during creation." +#: awx/api/serializers.py:3671 +msgid "Provided variable {} has no database value to replace with." msgstr "" -"Plantilla de tarea en el flujo de trabajo no encontrada durante la creación." +"La variable {} provista no tiene un valor de base de datos con qué " +"reemplazarla." -#: awx/api/serializers.py:3018 +#: awx/api/serializers.py:3747 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "No es posible anidar un %s dentro de un WorkflowJobTemplate" -#: awx/api/serializers.py:3291 -#, python-format -msgid "Job Template '%s' is missing or undefined." -msgstr "Plantilla de trabajo '%s' no encontrada o no definida." +#: awx/api/serializers.py:3754 awx/api/views.py:783 +msgid "Related template is not configured to accept credentials on launch." +msgstr "" +"La plantilla relacionada no está configurada para aceptar credenciales " +"durante el lanzamiento." -#: awx/api/serializers.py:3294 +#: awx/api/serializers.py:4211 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:3335 awx/api/views.py:3023 -#, python-format -msgid "Cannot assign multiple %s credentials." -msgstr "No se pueden asignar múltiples credenciales %s." +#: awx/api/serializers.py:4213 +msgid "The provided inventory is being deleted." +msgstr "El inventario provisto se está eliminando." -#: awx/api/serializers.py:3337 awx/api/views.py:3026 -msgid "Extra credentials must be network or cloud." -msgstr "Las credenciales adicionales deben ser red o nube." +#: awx/api/serializers.py:4221 +msgid "Cannot assign multiple {} credentials." +msgstr "No se pueden asignar múltiples credenciales {}." -#: awx/api/serializers.py:3474 +#: awx/api/serializers.py:4234 +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:4360 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:3497 +#: awx/api/serializers.py:4383 msgid "No values specified for field '{}'" msgstr "Ningún valor especificado para el campo '{}'" -#: awx/api/serializers.py:3502 +#: awx/api/serializers.py:4388 msgid "Missing required fields for Notification Configuration: {}." -msgstr "Campos no definidos para la configuración de notificación: {}." +msgstr "" +"Campos obligatorios no definidos para la configuración de notificación: {}." -#: awx/api/serializers.py:3505 +#: awx/api/serializers.py:4391 msgid "Configuration field '{}' incorrect type, expected {}." -msgstr "Tipo incorrecto en la configuración del campo '{} ', esperado {}." +msgstr "Tipo incorrecto en el campo de configuración '{}'; esperado {}." -#: awx/api/serializers.py:3558 -msgid "Inventory Source must be a cloud resource." -msgstr "Fuente del inventario debe ser un recurso cloud." - -#: awx/api/serializers.py:3560 -msgid "Manual Project cannot have a schedule set." -msgstr "El proyecto manual no puede tener una programación establecida." - -#: awx/api/serializers.py:3563 +#: awx/api/serializers.py:4453 msgid "" -"Inventory sources with `update_on_project_update` cannot be scheduled. " -"Schedule its source project `{}` instead." +"Valid DTSTART required in rrule. Value should start with: " +"DTSTART:YYYYMMDDTHHMMSSZ" 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:3582 -msgid "Projects and inventory updates cannot accept extra variables." -msgstr "" -"Las actualizaciones de inventarios y proyectos no pueden aceptar variables " -"adicionales." - -#: awx/api/serializers.py:3604 -msgid "" -"DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" -msgstr "" -"DTSTART necesario en 'rrule'. El valor debe coincidir: " +"DTSTART válido necesario en rrule. El valor debe empezar con: " "DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:3606 +#: awx/api/serializers.py:4455 +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:4457 msgid "Multiple DTSTART is not supported." msgstr "Múltiple DTSTART no está soportado." -#: awx/api/serializers.py:3608 -msgid "RRULE require in rrule." -msgstr "RRULE requerido en rrule" +#: awx/api/serializers.py:4459 +msgid "RRULE required in rrule." +msgstr "RRULE requerido en rrule." -#: awx/api/serializers.py:3610 +#: awx/api/serializers.py:4461 msgid "Multiple RRULE is not supported." msgstr "Múltiple RRULE no está soportado." -#: awx/api/serializers.py:3612 +#: awx/api/serializers.py:4463 msgid "INTERVAL required in rrule." -msgstr "INTERVAL requerido en 'rrule'." +msgstr "INTERVAL requerido en rrule." -#: awx/api/serializers.py:3614 -msgid "TZID is not supported." -msgstr "TZID no está soportado." - -#: awx/api/serializers.py:3616 +#: awx/api/serializers.py:4465 msgid "SECONDLY is not supported." msgstr "SECONDLY no está soportado." -#: awx/api/serializers.py:3618 +#: awx/api/serializers.py:4467 msgid "Multiple BYMONTHDAYs not supported." msgstr "Multiple BYMONTHDAYs no soportado." -#: awx/api/serializers.py:3620 +#: awx/api/serializers.py:4469 msgid "Multiple BYMONTHs not supported." msgstr "Multiple BYMONTHs no soportado." -#: awx/api/serializers.py:3622 +#: awx/api/serializers.py:4471 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY con prefijo numérico no soportado." -#: awx/api/serializers.py:3624 +#: awx/api/serializers.py:4473 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY no soportado." -#: awx/api/serializers.py:3626 +#: awx/api/serializers.py:4475 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO no soportado." -#: awx/api/serializers.py:3630 +#: awx/api/serializers.py:4477 +msgid "RRULE may not contain both COUNT and UNTIL" +msgstr "RRULE no puede contener ambos COUNT y UNTIL" + +#: awx/api/serializers.py:4481 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 no está soportada." -#: awx/api/serializers.py:3634 -msgid "rrule parsing failed validation." -msgstr "Validación fallida analizando 'rrule'" +#: awx/api/serializers.py:4485 +msgid "rrule parsing failed validation: {}" +msgstr "validación fallida analizando rrule: {}" -#: awx/api/serializers.py:3760 +#: awx/api/serializers.py:4526 +msgid "Inventory Source must be a cloud resource." +msgstr "Fuente del inventario debe ser un recurso cloud." + +#: awx/api/serializers.py:4528 +msgid "Manual Project cannot have a schedule set." +msgstr "El proyecto manual no puede tener una programación establecida." + +#: awx/api/serializers.py:4541 +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:4546 +msgid "Count of all jobs that target this instance" +msgstr "Todos los trabajos que abordan esta instancia" + +#: awx/api/serializers.py:4579 +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:4584 +msgid "Count of all jobs that target this instance group" +msgstr "Todos los trabajos que abordan este grupo de instancias" + +#: awx/api/serializers.py:4592 +msgid "Policy Instance Percentage" +msgstr "Porcentaje de instancias de políticas" + +#: awx/api/serializers.py:4593 +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:4598 +msgid "Policy Instance Minimum" +msgstr "Mínimo de instancias de políticas" + +#: awx/api/serializers.py:4599 +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:4604 +msgid "Policy Instance List" +msgstr "Lista de instancias de políticas" + +#: awx/api/serializers.py:4605 +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:4627 +msgid "Duplicate entry {}." +msgstr "Entrada por duplicado {}." + +#: awx/api/serializers.py:4629 +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:4634 +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:4704 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." +"Un resumen de los valores nuevos y cambiados cuando se crea, se actualiza o " +"se elimina un objeto." -#: awx/api/serializers.py:3762 +#: awx/api/serializers.py:4706 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 " +"Para crear, actualizar y eliminar eventos, este es el tipo de objeto que fue" +" afectado. Para asociar o desasociar eventos, este es el tipo de objeto " "asociado o desasociado con object2." -#: awx/api/serializers.py:3765 +#: awx/api/serializers.py:4709 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." +"eventos, este es el tipo de objetos con el que object1 está asociado." -#: awx/api/serializers.py:3768 +#: awx/api/serializers.py:4712 msgid "The action taken with respect to the given object(s)." -msgstr "La acción tomada al respeto al/los especificado(s) objeto(s)." +msgstr "La acción tomada con respeto al/a los objeto(s) especificado(s)." -#: awx/api/serializers.py:3885 -msgid "Unable to login with provided credentials." -msgstr "Incapaz de iniciar sesión con los credenciales indicados." - -#: awx/api/serializers.py:3887 -msgid "Must include \"username\" and \"password\"." -msgstr "Debe incluir \"usuario\" y \"contraseña\"." - -#: awx/api/views.py:108 +#: awx/api/views.py:118 msgid "Your license does not allow use of the activity stream." msgstr "Su licencia no permite el uso de flujo de actividad." -#: awx/api/views.py:118 +#: awx/api/views.py:128 msgid "Your license does not permit use of system tracking." msgstr "Su licencia no permite el uso de sistema de rastreo." -#: awx/api/views.py:128 +#: awx/api/views.py:138 msgid "Your license does not allow use of workflows." msgstr "Su licencia no permite el uso de flujos de trabajo." -#: awx/api/views.py:142 +#: awx/api/views.py:152 msgid "Cannot delete job resource when associated workflow job is running." msgstr "" "No es posible eliminar un recurso de trabajo cuando la tarea del flujo de " "trabajo está en ejecución." -#: awx/api/views.py:146 +#: awx/api/views.py:157 msgid "Cannot delete running job resource." msgstr "No es posible eliminar el recurso de trabajo en ejecución." -#: awx/api/views.py:155 awx/templates/rest_framework/api.html:28 -msgid "REST API" -msgstr "REST API" +#: awx/api/views.py:162 +msgid "Job has not finished processing events." +msgstr "La tarea no terminó de procesar eventos." -#: awx/api/views.py:164 awx/templates/rest_framework/api.html:4 +#: awx/api/views.py:221 +msgid "Related job {} is still processing events." +msgstr "La tarea {} relacionada aún está procesando eventos." + +#: awx/api/views.py:228 awx/templates/rest_framework/api.html:28 +msgid "REST API" +msgstr "API REST" + +#: awx/api/views.py:238 awx/templates/rest_framework/api.html:4 msgid "AWX REST API" msgstr "API REST de AWX" -#: awx/api/views.py:226 +#: awx/api/views.py:252 +msgid "API OAuth 2 Authorization Root" +msgstr "Raíz de autorización de API OAuth 2" + +#: awx/api/views.py:317 msgid "Version 1" msgstr "Version 1" -#: awx/api/views.py:230 +#: awx/api/views.py:321 msgid "Version 2" msgstr "Versión 2" -#: awx/api/views.py:241 +#: awx/api/views.py:330 msgid "Ping" msgstr "Ping" -#: awx/api/views.py:272 awx/conf/apps.py:12 +#: awx/api/views.py:361 awx/conf/apps.py:10 msgid "Configuration" msgstr "Configuración" -#: awx/api/views.py:325 +#: awx/api/views.py:418 msgid "Invalid license data" -msgstr "Datos de licencia inválidos." +msgstr "Datos de licencia no válidos" -#: awx/api/views.py:327 +#: awx/api/views.py:420 msgid "Missing 'eula_accepted' property" msgstr "Propiedad 'eula_accepted' no encontrada" -#: awx/api/views.py:331 +#: awx/api/views.py:424 msgid "'eula_accepted' value is invalid" -msgstr "Valor 'eula_accepted' no es válido" +msgstr "Valor 'eula_accepted' no válido" -#: awx/api/views.py:334 +#: awx/api/views.py:427 msgid "'eula_accepted' must be True" msgstr "'eula_accepted' debe ser True" -#: awx/api/views.py:341 +#: awx/api/views.py:434 msgid "Invalid JSON" -msgstr "JSON inválido" +msgstr "JSON no válido" -#: awx/api/views.py:349 +#: awx/api/views.py:442 msgid "Invalid License" -msgstr "Licencia inválida" +msgstr "Licencia no válida" -#: awx/api/views.py:359 +#: awx/api/views.py:452 msgid "Invalid license" -msgstr "Licencia inválida" +msgstr "Licencia no válida" -#: awx/api/views.py:367 +#: awx/api/views.py:460 #, python-format msgid "Failed to remove license (%s)" msgstr "Error al eliminar licencia (%s)" -#: awx/api/views.py:372 +#: awx/api/views.py:465 msgid "Dashboard" msgstr "Panel de control" -#: awx/api/views.py:471 +#: awx/api/views.py:564 msgid "Dashboard Jobs Graphs" msgstr "Panel de control de gráficas de trabajo" -#: awx/api/views.py:507 +#: awx/api/views.py:600 #, python-format msgid "Unknown period \"%s\"" -msgstr "Periodo desconocido \"%s\"" +msgstr "Período desconocido \"%s\"" -#: awx/api/views.py:521 +#: awx/api/views.py:614 msgid "Instances" msgstr "Instancias" -#: awx/api/views.py:529 +#: awx/api/views.py:622 msgid "Instance Detail" msgstr "Detalle de la instancia" -#: awx/api/views.py:537 -msgid "Instance Running Jobs" -msgstr "Tareas en ejecución de la instancia" +#: awx/api/views.py:643 +msgid "Instance Jobs" +msgstr "Tareas de instancia" -#: awx/api/views.py:552 +#: awx/api/views.py:657 msgid "Instance's Instance Groups" msgstr "Grupos de instancias de la instancia" -#: awx/api/views.py:562 +#: awx/api/views.py:666 msgid "Instance Groups" msgstr "Grupos de instancias" -#: awx/api/views.py:570 +#: awx/api/views.py:674 msgid "Instance Group Detail" msgstr "Detalle del grupo de instancias" -#: awx/api/views.py:578 +#: awx/api/views.py:682 +msgid "Isolated Groups can not be removed from the API" +msgstr "Grupos aislados no se pueden remover de la API" + +#: awx/api/views.py:684 +msgid "" +"Instance Groups acting as a controller for an Isolated Group can not be " +"removed from the API" +msgstr "" +"Los Grupos de instancias que actúan como un controlador para un Grupo " +"aislado no se pueden eliminar de la API" + +#: awx/api/views.py:690 msgid "Instance Group Running Jobs" msgstr "Tareas en ejecución del grupo de instancias" -#: awx/api/views.py:588 +#: awx/api/views.py:699 msgid "Instance Group's Instances" msgstr "Instancias del grupo de instancias" -#: awx/api/views.py:598 +#: awx/api/views.py:709 msgid "Schedules" msgstr "Programaciones" -#: awx/api/views.py:617 +#: awx/api/views.py:723 +msgid "Schedule Recurrence Rule Preview" +msgstr "Programe la vista previa de la regla de recurrencia" + +#: awx/api/views.py:770 +msgid "Cannot assign credential when related template is null." +msgstr "" +"No se puede asignar la credencial cuando la plantilla relacionada es nula." + +#: awx/api/views.py:775 +msgid "Related template cannot accept {} on launch." +msgstr "La plantilla relacionada no puede aceptar {} durante el lanzamiento." + +#: awx/api/views.py:777 +msgid "" +"Credential that requires user input on launch cannot be used in saved launch" +" configuration." +msgstr "" +"Una credencial que requiere ingreso de datos del usuario durante el " +"lanzamiento no se puede utilizar en una configuración de lanzamiento " +"guardada." + +#: awx/api/views.py:785 +#, python-brace-format +msgid "" +"This launch configuration already provides a {credential_type} credential." +msgstr "" +"Esta configuración de lanzamiento ya proporciona una credencial " +"{credential_type}." + +#: awx/api/views.py:788 +#, python-brace-format +msgid "Related template already uses {credential_type} credential." +msgstr "La plantilla relacionada ya usa la credencial {credential_type}." + +#: awx/api/views.py:806 msgid "Schedule Jobs List" msgstr "Lista de trabajos programados" -#: awx/api/views.py:843 +#: awx/api/views.py:961 msgid "Your license only permits a single organization to exist." msgstr "Su licencia solo permite que exista una organización." -#: awx/api/views.py:1079 awx/api/views.py:4666 +#: awx/api/views.py:1193 awx/api/views.py:5056 msgid "You cannot assign an Organization role as a child role for a Team." msgstr "No puede asignar un rol de organización como rol hijo para un equipo." -#: awx/api/views.py:1083 awx/api/views.py:4680 +#: awx/api/views.py:1197 awx/api/views.py:5070 msgid "You cannot grant system-level permissions to a team." msgstr "No puede asignar permisos de nivel de sistema a un equipo." -#: awx/api/views.py:1090 awx/api/views.py:4672 +#: awx/api/views.py:1204 awx/api/views.py:5062 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" msgstr "" -"No puede asignar credenciales de acceso a un equipo cuando el campo de " -"organización no está establecido o pertenezca a una organización diferente." +"No puede asignar acceso con credencial a un equipo cuando el campo " +"Organización no está establecido o pertenece a una organización diferente." -#: awx/api/views.py:1180 -msgid "Cannot delete project." -msgstr "No se puede eliminar el proyecto." - -#: awx/api/views.py:1215 +#: awx/api/views.py:1318 msgid "Project Schedules" msgstr "Programación del proyecto" -#: awx/api/views.py:1227 +#: awx/api/views.py:1329 msgid "Project SCM Inventory Sources" msgstr "Fuentes de inventario SCM del proyecto" -#: awx/api/views.py:1354 +#: awx/api/views.py:1430 +msgid "Project Update Events List" +msgstr "Lista de eventos de actualización de proyectos" + +#: awx/api/views.py:1444 +msgid "System Job Events List" +msgstr "Lista de eventos de tareas del sistema" + +#: awx/api/views.py:1458 +msgid "Inventory Update Events List" +msgstr "Lista de eventos de actualización de inventarios" + +#: awx/api/views.py:1492 msgid "Project Update SCM Inventory Updates" msgstr "Actualizaciones de inventario SCM de la actualización del proyecto" -#: awx/api/views.py:1409 +#: awx/api/views.py:1551 msgid "Me" msgstr "Yo" -#: awx/api/views.py:1453 awx/api/views.py:4623 -msgid "You may not perform any action with your own admin_role." -msgstr "No puede realizar ninguna acción con su admin_role." +#: awx/api/views.py:1559 +msgid "OAuth 2 Applications" +msgstr "Aplicaciones OAuth 2" -#: awx/api/views.py:1459 awx/api/views.py:4627 -msgid "You may not change the membership of a users admin_role" -msgstr "No puede cambiar la pertenencia a usuarios admin_role" +#: awx/api/views.py:1568 +msgid "OAuth 2 Application Detail" +msgstr "Detalle de aplicaciones OAuth 2" -#: awx/api/views.py:1464 awx/api/views.py:4632 +#: awx/api/views.py:1577 +msgid "OAuth 2 Application Tokens" +msgstr "Tokens de aplicaciones OAuth 2" + +#: awx/api/views.py:1599 +msgid "OAuth2 Tokens" +msgstr "Tokens OAuth2" + +#: awx/api/views.py:1608 +msgid "OAuth2 User Tokens" +msgstr "Tokens de usuario OAuth2" + +#: awx/api/views.py:1620 +msgid "OAuth2 User Authorized Access Tokens" +msgstr "Tokens de acceso autorizado de usuario OAuth2" + +#: awx/api/views.py:1635 +msgid "Organization OAuth2 Applications" +msgstr "Aplicaciones OAuth2 de la organización " + +#: awx/api/views.py:1647 +msgid "OAuth2 Personal Access Tokens" +msgstr "Tokens de acceso personal OAuth2" + +#: awx/api/views.py:1662 +msgid "OAuth Token Detail" +msgstr "Detalle del token OAuth" + +#: awx/api/views.py:1722 awx/api/views.py:5023 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" msgstr "" -"No puede conceder credenciales de acceso a un usuario que no está en la " -"organización del credencial." +"No puede conceder acceso con credencial a un usuario que no está en la " +"organización de credenciales" -#: awx/api/views.py:1468 awx/api/views.py:4636 +#: awx/api/views.py:1726 awx/api/views.py:5027 msgid "You cannot grant private credential access to another user" -msgstr "No puede conceder acceso a un credencial privado a otro usuario" +msgstr "No puede conceder acceso con credencial privado a otro usuario" -#: awx/api/views.py:1566 +#: awx/api/views.py:1824 #, python-format msgid "Cannot change %s." msgstr "No se puede cambiar %s." -#: awx/api/views.py:1572 +#: awx/api/views.py:1830 msgid "Cannot delete user." -msgstr "No se puede eliminar usuario." +msgstr "No se puede eliminar el usuario." -#: awx/api/views.py:1601 +#: awx/api/views.py:1854 msgid "Deletion not allowed for managed credential types" msgstr "" "No se permite la eliminación para los tipos de credenciales administradas" -#: awx/api/views.py:1603 +#: awx/api/views.py:1856 msgid "Credential types that are in use cannot be deleted" msgstr "No se pueden eliminar los tipos de credenciales en uso" -#: awx/api/views.py:1781 +#: awx/api/views.py:2031 msgid "Cannot delete inventory script." -msgstr "No se puede eliminar script de inventario." +msgstr "No se puede eliminar el script de inventario." -#: awx/api/views.py:1866 +#: awx/api/views.py:2122 #, python-brace-format msgid "{0}" msgstr "{0}" -#: awx/api/views.py:2101 +#: awx/api/views.py:2353 msgid "Fact not found." -msgstr "Fact no encontrado." +msgstr "Hecho no encontrado." -#: awx/api/views.py:2125 +#: awx/api/views.py:2375 msgid "SSLError while trying to connect to {}" msgstr "SSLError al intentar conectarse a {}" -#: awx/api/views.py:2127 +#: awx/api/views.py:2377 msgid "Request to {} timed out." -msgstr "El tiempo de solicitud {} caducó." +msgstr "Caducó el tiempo de solicitud para {}." -#: awx/api/views.py:2129 -msgid "Unkown exception {} while trying to GET {}" -msgstr "Excepción desconocida {} al intentar OBTENER {}" +#: awx/api/views.py:2379 +msgid "Unknown exception {} while trying to GET {}" +msgstr "Excepción desconocida {} al intentar usar GET {}" -#: awx/api/views.py:2132 +#: awx/api/views.py:2382 msgid "" "Unauthorized access. Please check your Insights Credential username and " "password." msgstr "" "Acceso no autorizado. Verifique su nombre de usuario y contraseña de " -"Insights." +"credencial de Insights." -#: awx/api/views.py:2134 +#: awx/api/views.py:2385 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" msgstr "" "No se pudieron recopilar los informes y planes de mantenimiento desde la API" -" de Insights en la dirección URL {}. El servidor respondió con el código de " -"estado {} y el mensaje {}" +" de Insights en la URL {}. El servidor respondió con el código de estado {} " +"y el mensaje {}" -#: awx/api/views.py:2140 +#: awx/api/views.py:2392 msgid "Expected JSON response from Insights but instead got {}" msgstr "Respuesta JSON esperada de Insights; en cambio, se recibió {}" -#: awx/api/views.py:2147 +#: awx/api/views.py:2399 msgid "This host is not recognized as an Insights host." msgstr "Este host no se reconoce como un host de Insights." -#: awx/api/views.py:2152 +#: awx/api/views.py:2404 msgid "The Insights Credential for \"{}\" was not found." msgstr "No se encontró la credencial de Insights para \"{}\"." -#: awx/api/views.py:2221 +#: awx/api/views.py:2472 msgid "Cyclical Group association." msgstr "Asociación de grupos cíclica." -#: awx/api/views.py:2499 +#: awx/api/views.py:2686 msgid "Inventory Source List" msgstr "Listado de fuentes del inventario" -#: awx/api/views.py:2512 +#: awx/api/views.py:2698 msgid "Inventory Sources Update" msgstr "Actualización de fuentes de inventario" -#: awx/api/views.py:2542 +#: awx/api/views.py:2731 msgid "Could not start because `can_update` returned False" msgstr "No se pudo iniciar porque `can_update` devolvió False" -#: awx/api/views.py:2550 +#: awx/api/views.py:2739 msgid "No inventory sources to update." msgstr "No hay fuentes de inventario para actualizar." -#: awx/api/views.py:2582 -msgid "Cannot delete inventory source." -msgstr "No se puede eliminar la fuente del inventario." - -#: awx/api/views.py:2590 +#: awx/api/views.py:2768 msgid "Inventory Source Schedules" -msgstr "Programaciones de la fuente del inventario" +msgstr "Programaciones de fuente de inventario" -#: awx/api/views.py:2620 +#: awx/api/views.py:2796 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" -"Plantillas de notificación pueden ser sólo asignadas cuando la fuente es una" -" de estas {}." +"Plantillas de notificación solo se pueden asignar cuando la fuente es una de" +" {}." #: awx/api/views.py:2851 -msgid "Job Template Schedules" -msgstr "Programación plantilla de trabajo" +msgid "Vault credentials are not yet supported for inventory sources." +msgstr "Las credenciales Vault aún no se admiten para fuentes de inventario." -#: awx/api/views.py:2871 awx/api/views.py:2882 +#: awx/api/views.py:2856 +msgid "Source already has cloud credential assigned." +msgstr "La fuente ya tiene asignada la credencial de nube." + +#: awx/api/views.py:3016 +msgid "" +"'credentials' cannot be used in combination with 'credential', " +"'vault_credential', or 'extra_credentials'." +msgstr "" +"'credentials' no se puede utilizar en combinación con 'credential', " +"'vault_credential' o 'extra_credentials'." + +#: awx/api/views.py:3128 +msgid "Job Template Schedules" +msgstr "Programaciones de plantilla de trabajo" + +#: awx/api/views.py:3146 awx/api/views.py:3157 msgid "Your license does not allow adding surveys." msgstr "Su licencia no permite añadir cuestionarios." -#: awx/api/views.py:2889 -msgid "'name' missing from survey spec." -msgstr "'name' no encontrado en el especificado cuestionario." +#: awx/api/views.py:3176 +msgid "Field '{}' is missing from survey spec." +msgstr "El campo '{}' no se encuentra en el cuestionario identificado." -#: awx/api/views.py:2891 -msgid "'description' missing from survey spec." -msgstr "'description' no encontrado en el especificado cuestionario." +#: awx/api/views.py:3178 +msgid "Expected {} for field '{}', received {} type." +msgstr "{} esperado para el campo '{}'; tipo {} recibido." -#: awx/api/views.py:2893 -msgid "'spec' missing from survey spec." -msgstr "'spec' no encontrado en el especificado cuestionario." - -#: awx/api/views.py:2895 -msgid "'spec' must be a list of items." -msgstr "'spec' debe ser una lista de elementos." - -#: awx/api/views.py:2897 +#: awx/api/views.py:3182 msgid "'spec' doesn't contain any items." msgstr "'spec' no contiene ningún elemento." -#: awx/api/views.py:2903 +#: awx/api/views.py:3191 #, python-format msgid "Survey question %s is not a json object." msgstr "Pregunta de cuestionario %s no es un objeto JSON." -#: awx/api/views.py:2905 +#: awx/api/views.py:3193 #, python-format msgid "'type' missing from survey question %s." -msgstr "'type' no encontrado en la pregunta de cuestionario %s." +msgstr "'type' no encontrado en la pregunta de cuestionario %s." -#: awx/api/views.py:2907 +#: awx/api/views.py:3195 #, python-format msgid "'question_name' missing from survey question %s." -msgstr "'question_name' no aparece en la pregunta de cuestionario %s." +msgstr "'question_name' no encontrado en la pregunta de cuestionario %s." -#: awx/api/views.py:2909 +#: awx/api/views.py:3197 #, python-format msgid "'variable' missing from survey question %s." -msgstr "'variable' no encontrado en la pregunta de cuestionario %s." +msgstr "'variable' no encontrada en la pregunta de cuestionario %s." -#: awx/api/views.py:2911 +#: awx/api/views.py:3199 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "" "'variable' '%(item)s' repetida en la pregunta de cuestionario %(survey)s." -#: awx/api/views.py:2916 +#: awx/api/views.py:3204 #, python-format msgid "'required' missing from survey question %s." msgstr "'required' no encontrado en la pregunta de cuestionario %s." -#: awx/api/views.py:2921 +#: awx/api/views.py:3209 #, python-brace-format msgid "" "Value {question_default} for '{variable_name}' expected to be a string." msgstr "" +"Se espera que el valor {question_default} para '{variable_name}' sea una " +"cadena." -#: awx/api/views.py:2928 +#: awx/api/views.py:3219 #, python-brace-format msgid "" -"$encrypted$ is reserved keyword for password questions and may not be used " -"as a default for '{variable_name}' in survey question {question_position}." +"$encrypted$ is a reserved keyword for password question defaults, survey " +"question {question_position} is type {question_type}." msgstr "" +"$encrypted$ es una palabra clave reservada para valores predeterminados de " +"la pregunta de la contraseña; la pregunta del cuestionario " +"{question_position} es de tipo {question_type}." -#: awx/api/views.py:3049 +#: awx/api/views.py:3235 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword, may not be used for new default in " +"position {question_position}." +msgstr "" +"$encrypted$ es una palabra clave reservada y no puede utilizarse como un " +"nuevo valor predeterminado en la posición {question_position}." + +#: awx/api/views.py:3309 +#, python-brace-format +msgid "Cannot assign multiple {credential_type} credentials." +msgstr "No se pueden asignar múltiples credenciales {credential_type}." + +#: awx/api/views.py:3327 +msgid "Extra credentials must be network or cloud." +msgstr "Las credenciales adicionales deben ser red o nube." + +#: awx/api/views.py:3349 msgid "Maximum number of labels for {} reached." msgstr "Número máximo de etiquetas para {} alcanzado." -#: awx/api/views.py:3170 +#: awx/api/views.py:3472 msgid "No matching host could be found!" -msgstr "¡Ningún servidor indicado pudo ser encontrado!" +msgstr "No se encontró ningún host coincidente." -#: awx/api/views.py:3173 +#: awx/api/views.py:3475 msgid "Multiple hosts matched the request!" -msgstr "¡Varios servidores corresponden a la petición!" +msgstr "Varios hosts corresponden a la petición." -#: awx/api/views.py:3178 +#: awx/api/views.py:3480 msgid "Cannot start automatically, user input required!" msgstr "" -"No se puede iniciar automáticamente, !Entrada de datos de usuario necesaria!" +"No se puede iniciar automáticamente; entrada de datos de usuario necesaria." -#: awx/api/views.py:3185 +#: awx/api/views.py:3487 msgid "Host callback job already pending." -msgstr "Trabajo de callback para el servidor ya está pendiente." +msgstr "Trabajo de callback para el host ya está pendiente." -#: awx/api/views.py:3199 +#: awx/api/views.py:3502 awx/api/views.py:4284 msgid "Error starting job!" -msgstr "¡Error iniciando trabajo!" +msgstr "Error al iniciar trabajo." -#: awx/api/views.py:3306 +#: awx/api/views.py:3622 #, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr "No se puede asociar {0} cuando se ha asociado {1}." -#: awx/api/views.py:3331 +#: awx/api/views.py:3647 msgid "Multiple parent relationship not allowed." msgstr "No se permiten múltiples relaciones primarias." -#: awx/api/views.py:3336 +#: awx/api/views.py:3652 msgid "Cycle detected." msgstr "Ciclo detectado." -#: awx/api/views.py:3540 +#: awx/api/views.py:3850 msgid "Workflow Job Template Schedules" msgstr "Programaciones de plantilla de tareas de flujo de trabajo" -#: awx/api/views.py:3685 awx/api/views.py:4268 +#: awx/api/views.py:3986 awx/api/views.py:4690 msgid "Superuser privileges needed." msgstr "Privilegios de superusuario necesarios." -#: awx/api/views.py:3717 +#: awx/api/views.py:4018 msgid "System Job Template Schedules" -msgstr "Programación de plantilla de trabajos de sistema." +msgstr "Programaciones de plantilla de trabajos de sistema" -#: awx/api/views.py:3780 +#: awx/api/views.py:4076 msgid "POST not allowed for Job launching in version 2 of the api" msgstr "" +"POST no permitido para una tarea que se lanza en la versión 2 de la API" -#: awx/api/views.py:3942 +#: awx/api/views.py:4100 awx/api/views.py:4106 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "PUT no permitido para Detalles de la tarea en la versión 2 de la API" + +#: awx/api/views.py:4267 +#, python-brace-format +msgid "Wait until job finishes before retrying on {status_value} hosts." +msgstr "" +"Espere a que termine el trabajo antes de intentar nuevamente en hosts " +"{status_value}." + +#: awx/api/views.py:4272 +#, python-brace-format +msgid "Cannot retry on {status_value} hosts, playbook stats not available." +msgstr "" +"No se puede volver a intentar en hosts {status_value}; las estadísticas de " +"playbook no están disponibles." + +#: awx/api/views.py:4277 +#, python-brace-format +msgid "Cannot relaunch because previous job had 0 {status_value} hosts." +msgstr "" +"No se puede volver a lanzar porque la tarea anterior tuvo 0 hosts " +"{status_value}." + +#: awx/api/views.py:4306 +msgid "Cannot create schedule because job requires credential passwords." +msgstr "" +"No se puede crear la programación porque la tarea requiere contraseñas de " +"credenciales." + +#: awx/api/views.py:4311 +msgid "Cannot create schedule because job was launched by legacy method." +msgstr "" +"No se puede crear una programación porque la tarea se lanzó por un método de" +" legado." + +#: awx/api/views.py:4313 +msgid "Cannot create schedule because a related resource is missing." +msgstr "" +"No se puede crear la programación porque falta un recurso relacionado." + +#: awx/api/views.py:4368 msgid "Job Host Summaries List" -msgstr "Lista resumida de trabajos de servidor" +msgstr "Lista resumida de hosts de trabajo" -#: awx/api/views.py:3989 +#: awx/api/views.py:4417 msgid "Job Event Children List" -msgstr "LIsta de hijos de eventos de trabajo" +msgstr "Lista de hijos de eventos de trabajo" -#: awx/api/views.py:3998 +#: awx/api/views.py:4427 msgid "Job Event Hosts List" -msgstr "Lista de eventos de trabajos de servidor." +msgstr "Lista de hosts de eventos de trabajo" -#: awx/api/views.py:4008 +#: awx/api/views.py:4436 msgid "Job Events List" msgstr "Lista de eventos de trabajo" -#: awx/api/views.py:4222 +#: awx/api/views.py:4647 msgid "Ad Hoc Command Events List" msgstr "Lista de eventos para comando Ad Hoc" -#: awx/api/views.py:4437 -msgid "Error generating stdout download file: {}" -msgstr "Error generando la descarga del fichero de salida estándar: {}" - -#: awx/api/views.py:4450 -#, python-format -msgid "Error generating stdout download file: %s" -msgstr "Error generando la descarga del fichero de salida estándar: %s" - -#: awx/api/views.py:4495 +#: awx/api/views.py:4889 msgid "Delete not allowed while there are pending notifications" msgstr "Eliminar no está permitido mientras existan notificaciones pendientes" -#: awx/api/views.py:4502 +#: awx/api/views.py:4897 msgid "Notification Template Test" msgstr "Prueba de plantilla de notificación" @@ -1222,11 +1550,11 @@ msgstr "Cows" #: awx/conf/conf.py:73 msgid "Example Read-Only Setting" -msgstr "Ejemplo de ajuste de sólo lectura.f" +msgstr "Ejemplo de ajuste de solo lectura" #: awx/conf/conf.py:74 msgid "Example setting that cannot be changed." -msgstr "Ejemplo de ajuste que no puede ser cambiado." +msgstr "Ejemplo de ajuste que no se puede cambiar" #: awx/conf/conf.py:93 msgid "Example Setting" @@ -1234,46 +1562,63 @@ msgstr "Ejemplo de ajuste" #: awx/conf/conf.py:94 msgid "Example setting which can be different for each user." -msgstr "Ejemplo de configuración que puede ser diferente para cada usuario." +msgstr "Ejemplo de ajuste que puede ser diferente para cada usuario." -#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:56 +#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:55 msgid "User" msgstr "Usuario" -#: awx/conf/fields.py:63 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 +#, python-brace-format +msgid "" +"Expected None, True, False, a string or list of strings but got {input_type}" +" instead." +msgstr "" +"Se esperaba None, True, False, una cadena de texto o una lista de cadenas de" +" texto; pero, en cambio, se obtuvo {input_type}." + +#: awx/conf/fields.py:104 msgid "Enter a valid URL" msgstr "Introduzca una URL válida" -#: awx/conf/fields.py:95 +#: awx/conf/fields.py:136 #, python-brace-format msgid "\"{input}\" is not a valid string." msgstr "\"{input}\" no es una cadena válida." +#: awx/conf/fields.py:151 +#, python-brace-format +msgid "" +"Expected a list of tuples of max length 2 but got {input_type} instead." +msgstr "" +"Se esperaba una lista de tuplas de longitud máxima 2, pero, en cambio, se " +"obtuvo {input_type}." + #: awx/conf/license.py:22 msgid "Your Tower license does not allow that." msgstr "Su licencia Tower no permite eso." #: awx/conf/management/commands/migrate_to_database_settings.py:41 msgid "Only show which settings would be commented/migrated." -msgstr "Sólo mostrar los ajustes que serán comentados/migrados." +msgstr "Solo mostrar los ajustes que serán comentados/migrados." #: awx/conf/management/commands/migrate_to_database_settings.py:48 msgid "" "Skip over settings that would raise an error when commenting/migrating." -msgstr "Omitir los ajustes que causarán un error cuando comentando/migrando." +msgstr "Omitir los ajustes que causarán un error al comentar/migrar." #: awx/conf/management/commands/migrate_to_database_settings.py:55 msgid "Skip commenting out settings in files." -msgstr "Omitir la entrada de comentarios para ajustes en ficheros." +msgstr "Omitir los comentarios sobre ajustes en archivos." #: awx/conf/management/commands/migrate_to_database_settings.py:62 msgid "Skip migrating and only comment out settings in files." -msgstr "" +msgstr "Omita la migración y solo comente sobre ajustes en archivos." #: awx/conf/management/commands/migrate_to_database_settings.py:68 msgid "Backup existing settings files with this suffix." msgstr "" -"Hacer copia de seguridad de todos los ficheros de configuración existentes " +"Hacer copia de seguridad de todos los archivos de configuración existentes " "con este sufijo." #: awx/conf/registry.py:73 awx/conf/tests/unit/test_registry.py:169 @@ -1294,12 +1639,12 @@ msgstr "Cambiado" #: awx/conf/registry.py:86 msgid "User-Defaults" -msgstr "Parámetros de usuario por defecto" +msgstr "Valores predeterminados del usuario" #: awx/conf/registry.py:154 msgid "This value has been set manually in a settings file." msgstr "" -"Este valor ha sido establecido manualmente en el fichero de configuración." +"Este valor se ha establecido manualmente en el archivo de configuración." #: awx/conf/tests/unit/test_registry.py:46 #: awx/conf/tests/unit/test_registry.py:56 @@ -1343,9 +1688,9 @@ msgstr "" #: awx/conf/tests/unit/test_settings.py:411 #: awx/conf/tests/unit/test_settings.py:430 #: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:51 -#: awx/main/conf.py:63 awx/main/conf.py:81 awx/main/conf.py:96 -#: awx/main/conf.py:121 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "Sistema" @@ -1357,104 +1702,108 @@ msgstr "Sistema" msgid "OtherSystem" msgstr "Otro sistema" -#: awx/conf/views.py:48 +#: awx/conf/views.py:47 msgid "Setting Categories" msgstr "Categorías de ajustes" -#: awx/conf/views.py:73 +#: awx/conf/views.py:71 msgid "Setting Detail" msgstr "Detalles del ajuste" -#: awx/conf/views.py:168 +#: awx/conf/views.py:166 msgid "Logging Connectivity Test" msgstr "Registrando prueba de conectividad" -#: awx/main/access.py:44 -msgid "Resource is being used by running jobs." -msgstr "" +#: awx/main/access.py:57 +#, python-format +msgid "Required related field %s for permission check." +msgstr "Campo relacionado %s requerido para verificación de permisos." -#: awx/main/access.py:237 +#: awx/main/access.py:73 #, python-format msgid "Bad data found in related field %s." msgstr "Dato incorrecto encontrado en el campo relacionado %s." -#: awx/main/access.py:281 +#: awx/main/access.py:293 msgid "License is missing." msgstr "Licencia no encontrada." -#: awx/main/access.py:283 +#: awx/main/access.py:295 msgid "License has expired." msgstr "La licencia ha expirado." -#: awx/main/access.py:291 +#: awx/main/access.py:303 #, python-format msgid "License count of %s instances has been reached." -msgstr "El número de licencias de instancias %s ha sido alcanzado." +msgstr "Se alcanzó el número de licencias de instancias %s." -#: awx/main/access.py:293 +#: awx/main/access.py:305 #, python-format msgid "License count of %s instances has been exceeded." -msgstr "El número de licencias de instancias %s ha sido excedido." +msgstr "Se superó el número de licencias de instancias %s." -#: awx/main/access.py:295 +#: awx/main/access.py:307 msgid "Host count exceeds available instances." -msgstr "El número de servidores excede las instancias disponibles." +msgstr "El número de hosts excede las instancias disponibles." -#: awx/main/access.py:299 +#: awx/main/access.py:311 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "Funcionalidad %s no está habilitada en la licencia activa." -#: awx/main/access.py:301 +#: awx/main/access.py:313 msgid "Features not found in active license." msgstr "Funcionalidades no encontradas en la licencia activa." -#: awx/main/access.py:707 +#: awx/main/access.py:818 msgid "Unable to change inventory on a host." msgstr "Imposible modificar el inventario en un servidor." -#: awx/main/access.py:724 awx/main/access.py:769 +#: awx/main/access.py:835 awx/main/access.py:880 msgid "Cannot associate two items from different inventories." msgstr "No es posible asociar dos elementos de diferentes inventarios." -#: awx/main/access.py:757 +#: awx/main/access.py:868 msgid "Unable to change inventory on a group." msgstr "Imposible cambiar el inventario en un grupo." -#: awx/main/access.py:1017 +#: awx/main/access.py:1129 msgid "Unable to change organization on a team." msgstr "Imposible cambiar la organización en un equipo." -#: awx/main/access.py:1030 +#: awx/main/access.py:1146 msgid "The {} role cannot be assigned to a team" -msgstr "El rol {} no puede ser asignado a un equipo." +msgstr "El rol {} no se puede asignar a un equipo" -#: awx/main/access.py:1032 +#: awx/main/access.py:1148 msgid "The admin_role for a User cannot be assigned to a team" -msgstr "El admin_role para un usuario no puede ser asignado a un equipo" +msgstr "El admin_role para un usuario no se puede asignar a un equipo" -#: awx/main/access.py:1479 +#: awx/main/access.py:1502 awx/main/access.py:1936 +msgid "Job was launched with prompts provided by another user." +msgstr "La tarea se inició con avisos provistos por otro usuario." + +#: awx/main/access.py:1522 msgid "Job has been orphaned from its job template." -msgstr "Se ha eliminado la plantilla de trabajo de la tarea." +msgstr "Se eliminó la plantilla de trabajo de la tarea." -#: awx/main/access.py:1481 -msgid "You do not have execute permission to related job template." -msgstr "" -"No tiene permiso de ejecución para la plantilla de trabajo relacionada." +#: awx/main/access.py:1524 +msgid "Job was launched with unknown prompted fields." +msgstr "La tarea se inició con campos completados desconocidos." -#: awx/main/access.py:1484 +#: awx/main/access.py:1526 msgid "Job was launched with prompted fields." msgstr "La tarea se ejecutó con campos completados." -#: awx/main/access.py:1486 +#: awx/main/access.py:1528 msgid " Organization level permissions required." msgstr "Se requieren permisos de nivel de organización." -#: awx/main/access.py:1488 +#: awx/main/access.py:1530 msgid " You do not have permission to related resources." msgstr "No tiene permisos para los recursos relacionados." -#: awx/main/access.py:1833 +#: awx/main/access.py:1950 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1476,8 +1825,7 @@ msgstr "Habilite la captura de actividades para el flujo de actividad." #: awx/main/conf.py:30 msgid "Enable Activity Stream for Inventory Sync" -msgstr "" -"Habilitar el flujo de actividad para la sincronización de inventarios." +msgstr "Habilitar el flujo de actividad para la sincronización de inventarios" #: awx/main/conf.py:31 msgid "" @@ -1490,83 +1838,94 @@ msgstr "" #: awx/main/conf.py:40 msgid "All Users Visible to Organization Admins" msgstr "" -"Todos los usuarios visibles para los administradores de la organización." +"Todos los usuarios visibles para los administradores de la organización" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." msgstr "" "Controla si cualquier administrador de organización puede ver todos los " -"usuarios, incluso aquellos que no están asociados a su organización." +"usuarios y equipos, incluso aquellos no están asociados a su organización." -#: awx/main/conf.py:49 +#: awx/main/conf.py:50 +msgid "Organization Admins Can Manage Users and Teams" +msgstr "" +"Los administradores de la organización pueden gestionar usuarios y equipos" + +#: awx/main/conf.py:51 +msgid "" +"Controls whether any Organization Admin has the privileges to create and " +"manage users and teams. You may want to disable this ability if you are " +"using an LDAP or SAML integration." +msgstr "" +"Controla si algún Administrador de la organización tiene los privilegios " +"para crear y gestionar usuarios y equipos. Recomendamos deshabilitar esta " +"capacidad si está usando una integración de LDAP o SAML." + +#: awx/main/conf.py:60 msgid "Enable Administrator Alerts" msgstr "Habilitar alertas de administrador" -#: awx/main/conf.py:50 +#: awx/main/conf.py:61 msgid "Email Admin users for system events that may require attention." msgstr "" "Enviar correo electrónico a los usuarios administradores para los eventos " "del sistema que requieren atención." -#: awx/main/conf.py:60 +#: awx/main/conf.py:71 msgid "Base URL of the Tower host" msgstr "Dirección base URL para el servidor Tower" -#: awx/main/conf.py:61 +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to" " the Tower host." msgstr "" -"Este configuración es utilizada por servicios cono notificaciones para " -"mostrar una url válida al servidor Tower." +"Esta configuración es utilizada por servicios cono notificaciones para " +"mostrar una URL válida al host Tower." -#: awx/main/conf.py:70 +#: awx/main/conf.py:81 msgid "Remote Host Headers" -msgstr "Cabeceras de servidor remoto" +msgstr "Encabezados de host remoto" -#: awx/main/conf.py:71 +#: awx/main/conf.py:82 msgid "" -"HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy.\n" -"\n" -"Note: The headers will be searched in order and the first found remote host name or IP will be used.\n" -"\n" -"In the below example 8.8.8.7 would be the chosen IP address.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"HTTP headers and meta keys to search to determine remote host name or IP. " +"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " +"behind a reverse proxy. See the \"Proxy Support\" section of the " +"Adminstrator guide for more details." msgstr "" -"Cabeceras HTTP y claves-meta para buscar determinado nombre de servidor o IP. Añadir elementos adicionales a la lista, como \"HTTP_X_FORWARDED_FOR\", si se está detrás de un proxy inverso.\n" -"\n" -"Note: Las cabeceras serán buscadas en el orden y el primer servidor remoto encontrado a través del nombre de servidor o IP, será utilizado.\n" -"\n" -"En el siguiente ejemplo, la dirección IP 8.8.8.7 debería ser escogida.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"Los encabezados HTTP y las llaves de activación para buscar y determinar el " +"nombre de host remoto o IP. Añada elementos adicionales a esta lista, como " +"\"HTTP_X_FORWARDED_FOR\", si está detrás de un proxy inverso. Consulte la " +"sección \"Soporte de proxy\" de la guía del adminstrador para obtener más " +"información." -#: awx/main/conf.py:88 +#: awx/main/conf.py:94 msgid "Proxy IP Whitelist" msgstr "Lista blanca de IP de proxy" -#: awx/main/conf.py:89 +#: awx/main/conf.py:95 msgid "" -"If Tower is behind a reverse proxy/load balancer, use this setting to whitelist the proxy IP addresses from which Tower should trust custom REMOTE_HOST_HEADERS header values\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"If this setting is an empty list (the default), the headers specified by REMOTE_HOST_HEADERS will be trusted unconditionally')" +"If Tower is behind a reverse proxy/load balancer, use this setting to " +"whitelist the proxy IP addresses from which Tower should trust custom " +"REMOTE_HOST_HEADERS header values. If this setting is an empty list (the " +"default), the headers specified by REMOTE_HOST_HEADERS will be trusted " +"unconditionally')" msgstr "" -"Si Tower está detrás de un equilibrador de carga/proxy inverso, use este ajuste para colocar las direcciones IP del proxy en la lista blanca desde la cual Tower debe confiar en los valores personalizados del encabezado REMOTE_HOST_HEADERS\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"Si este ajuste es una lista vacía (valor predeterminado), se confiará en los encabezados especificados por REMOTE_HOST_HEADERS de manera incondicional')" +"Si Tower está detrás de un equilibrador de carga/proxy inverso, use este " +"ajuste para colocar las direcciones IP del proxy en la lista blanca desde la" +" cual Tower debe confiar en los valores personalizados del encabezado " +"REMOTE_HOST_HEADERS. Si este ajuste es una lista vacía (valor " +"predeterminado), se confiará en los encabezados especificados por " +"REMOTE_HOST_HEADERS de manera incondicional')" -#: awx/main/conf.py:117 +#: awx/main/conf.py:121 msgid "License" msgstr "Licencia" -#: awx/main/conf.py:118 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use " "/api/v1/config/ to update or change the license." @@ -1574,29 +1933,60 @@ msgstr "" "Los controles de licencia cuyas funciones y funcionalidades están " "habilitadas. Use /api/v1/config/ para actualizar o cambiar la licencia." -#: awx/main/conf.py:128 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" -msgstr "Módulos Ansible autorizados para ejecutar trabajos Ad Hoc" +msgstr "Módulos Ansible autorizados para trabajos Ad Hoc" -#: awx/main/conf.py:129 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "Lista de módulos permitidos para su uso con trabajos ad-hoc." -#: awx/main/conf.py:130 awx/main/conf.py:140 awx/main/conf.py:151 -#: awx/main/conf.py:161 awx/main/conf.py:171 awx/main/conf.py:181 -#: awx/main/conf.py:192 awx/main/conf.py:204 awx/main/conf.py:216 -#: awx/main/conf.py:229 awx/main/conf.py:241 awx/main/conf.py:251 -#: awx/main/conf.py:262 awx/main/conf.py:272 awx/main/conf.py:282 -#: awx/main/conf.py:292 awx/main/conf.py:304 awx/main/conf.py:316 -#: awx/main/conf.py:328 awx/main/conf.py:341 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "Trabajos" -#: awx/main/conf.py:138 +#: awx/main/conf.py:143 +msgid "Always" +msgstr "Siempre" + +#: awx/main/conf.py:144 +msgid "Never" +msgstr "Nunca" + +#: awx/main/conf.py:145 +msgid "Only On Job Template Definitions" +msgstr "Solo en definiciones de plantilla de tareas" + +#: awx/main/conf.py:148 +msgid "When can extra variables contain Jinja templates?" +msgstr "¿Cuándo pueden las variables adicionales contener plantillas Jinja?" + +#: awx/main/conf.py:150 +msgid "" +"Ansible allows variable substitution via the Jinja2 templating language for " +"--extra-vars. This poses a potential security risk where Tower users with " +"the ability to specify extra vars at job launch time can use Jinja2 " +"templates to run arbitrary Python. It is recommended that this value be set" +" to \"template\" or \"never\"." +msgstr "" +"Ansible permite la sustitución de variables a través del lenguaje de " +"plantillas Jinja2 para --extra-vars. Esto presenta un potencial de riesgo a " +"la seguridad, en donde los usuarios de Tower con la capacidad de especificar" +" vars adicionales en el momento de lanzamiento de la tarea pueden usar las " +"plantillas Jinja2 para ejecutar Python arbitrario. Se recomienda que este " +"valor se establezca como \"plantilla\" o \"nunca\"." + +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "Habilitar aislamiento de trabajo" -#: awx/main/conf.py:139 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." @@ -1604,11 +1994,11 @@ msgstr "" "Aísla un trabajo Ansible de partes protegidas del sistema para prevenir la " "exposición de información confidencial." -#: awx/main/conf.py:147 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "Ruta de ejecución de una tarea" -#: awx/main/conf.py:148 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " @@ -1618,22 +2008,22 @@ msgstr "" "ejecución y el aislamiento de tareas (como archivos de credenciales y " "scripts de inventario personalizados)." -#: awx/main/conf.py:159 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" msgstr "Rutas a ocultar para los trabajos aislados" -#: awx/main/conf.py:160 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." msgstr "" "Rutas adicionales para ocultar de los procesos aislados. Ingrese una ruta " "por línea." -#: awx/main/conf.py:169 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" -msgstr "Rutas a exponer para los trabajos aislados" +msgstr "Rutas por exponer para los trabajos aislados" -#: awx/main/conf.py:170 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." @@ -1641,11 +2031,11 @@ msgstr "" "Lista blanca de rutas que de otra manera se esconderían para exponer a " "trabajos aislados. Ingrese una ruta por línea." -#: awx/main/conf.py:179 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "Intervalo de verificación de estado aislado" -#: awx/main/conf.py:180 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." @@ -1653,11 +2043,11 @@ msgstr "" "La cantidad de segundos de inactividad entre las verificaciones de estado " "para las tareas que se ejecutan en instancias aisladas." -#: awx/main/conf.py:189 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "Tiempo de espera para la ejecución aislada" -#: awx/main/conf.py:190 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " @@ -1667,11 +2057,11 @@ msgstr "" "aisladas. Esto incluye el tiempo que se necesita para copiar archivos de " "control de código fuente (playbooks) en la instancia aislada." -#: awx/main/conf.py:201 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "Tiempo de espera para la conexión aislada" -#: awx/main/conf.py:202 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " @@ -1681,11 +2071,11 @@ msgstr "" "cuando se comunica con instancias aisladas. El valor debe ser " "considerablemente superior a la latencia de red esperada." -#: awx/main/conf.py:212 +#: awx/main/conf.py:237 msgid "Generate RSA keys for isolated instances" -msgstr "Genere claves de RSA para instancias aisladas" +msgstr "Generar claves de RSA para instancias aisladas" -#: awx/main/conf.py:213 +#: awx/main/conf.py:238 msgid "" "If set, a random RSA key will be generated and distributed to isolated " "instances. To disable this behavior and manage authentication for isolated " @@ -1696,19 +2086,19 @@ msgstr "" "autenticación para instancias aisladas fuera de Tower, desactive esta " "configuración." -#: awx/main/conf.py:227 awx/main/conf.py:228 +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "La clave privada RSA para el tráfico SSH en instancias aisladas" -#: awx/main/conf.py:239 awx/main/conf.py:240 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "La clave pública RSA para el tráfico SSH en instancias aisladas" -#: awx/main/conf.py:249 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "Variables de entorno adicionales" -#: awx/main/conf.py:250 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." @@ -1717,11 +2107,11 @@ msgstr "" "playbook, actualizaciones de inventarios, actualizaciones de proyectos y " "envío de notificaciones." -#: awx/main/conf.py:260 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" -msgstr "Tamaño máximo a mostrar para la salida estándar." +msgstr "Tamaño de visualización máximo para la salida estándar" -#: awx/main/conf.py:261 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." @@ -1729,38 +2119,38 @@ msgstr "" "Tamaño máximo de la salida estándar en bytes para mostrar antes de obligar a" " que la salida sea descargada." -#: awx/main/conf.py:270 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "" -"Tamaño máximo de la salida estándar para mostrar del evento del trabajo." +"Tamaño de visualización máximo de la salida estándar del evento del trabajo" -#: awx/main/conf.py:271 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." msgstr "" -"Tamaño máximo de la salida estándar en bytes a mostrar para un único trabajo" -" o evento del comando ad hoc. `stdout` terminará con `...` cuando sea " -"truncado." +"Tamaño máximo de la salida estándar en bytes por mostrar para un único " +"trabajo o evento del comando ad hoc. `stdout` terminará con `...` cuando se " +"trunque." -#: awx/main/conf.py:280 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" -msgstr "Máximo número de trabajos programados" +msgstr "Número máximo de trabajos programados" -#: awx/main/conf.py:281 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." msgstr "" -"Número máximo de la misma plantilla de trabajo que pueden estar esperando " -"para ser ejecutado cuando se lanzan desde una programación antes de que no " -"se creen más." +"Número máximo de la misma plantilla de trabajo que puede estar esperando " +"para ejecutarse cuando se lanza desde una programación antes de que no se " +"creen más." -#: awx/main/conf.py:290 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "Plugins de Ansible callback" -#: awx/main/conf.py:291 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." @@ -1768,11 +2158,11 @@ msgstr "" "Lista de rutas para buscar complementos adicionales de retorno de llamada " "que se utilizan cuando se ejecutan tareas. Ingrese una ruta por línea." -#: awx/main/conf.py:301 +#: awx/main/conf.py:327 msgid "Default Job Timeout" -msgstr "Tiempo de espera por defecto para el trabajo" +msgstr "Tiempo de espera predeterminado para el trabajo" -#: awx/main/conf.py:302 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " @@ -1783,11 +2173,11 @@ msgstr "" "de espera establecido en una plantilla de trabajo individual reemplazará " "este." -#: awx/main/conf.py:313 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "Tiempo de espera por defecto para la actualización del inventario" -#: awx/main/conf.py:314 +#: awx/main/conf.py:340 msgid "" "Maximum time in seconds to allow inventory updates to run. Use value of 0 to" " indicate that no timeout should be imposed. A timeout set on an individual " @@ -1798,11 +2188,11 @@ msgstr "" "tipo de tiempo de expiración. El tiempo de expiración definido para una " "fuente de inventario individual debería anularlo." -#: awx/main/conf.py:325 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" -msgstr "Tiempo de espera por defecto para la actualización de un proyecto" +msgstr "Tiempo de espera predeterminado para la actualización de un proyecto" -#: awx/main/conf.py:326 +#: awx/main/conf.py:352 msgid "" "Maximum time in seconds to allow project updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " @@ -1813,84 +2203,86 @@ msgstr "" "definirse ningún tipo de tiempo de expiración. El tiempo de expiración " "definido para una fuente de inventario individual debería anularlo." -#: awx/main/conf.py:337 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "Tiempo de espera de la caché de eventos Ansible por host" -#: awx/main/conf.py:338 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be" " accessible by a playbook. Note, this does not influence the deletion of " -"ansible_facts from the database." +"ansible_facts from the database. Use a value of 0 to indicate that no " +"timeout should be imposed." msgstr "" -"Tiempo máximo, en segundos, que los eventos Ansible almacenados son " -"considerados válidos desde la última vez que fueron modificados. Solo se " -"podrá acceder a los eventos válidos y no obsoletos a través de un playbook. " -"Tenga en cuenta que esto no influye en la eliminación de ansible_facts de la" -" base de datos." +"Tiempo máximo, en segundos, en que los datos almacenados de Ansible se " +"consideran válidos desde la última vez que fueron modificados. Solo se podrá" +" acceder a datos válidos y actualizados mediante la playbook. Observe que " +"esto no influye en la eliminación de datos de Ansible de la base de datos. " +"Use un valor de 0 para indicar que no se debe imponer ningún tiempo de " +"expiración." -#: awx/main/conf.py:350 +#: awx/main/conf.py:377 msgid "Logging Aggregator" -msgstr "Agregación de registros" +msgstr "Agregador de registros" -#: awx/main/conf.py:351 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." -msgstr "Hostname/IP donde los logs externos serán enviados." +msgstr "Nombre de host/IP donde se enviarán los registros externos." -#: awx/main/conf.py:352 awx/main/conf.py:363 awx/main/conf.py:375 -#: awx/main/conf.py:385 awx/main/conf.py:397 awx/main/conf.py:412 -#: awx/main/conf.py:424 awx/main/conf.py:433 awx/main/conf.py:443 -#: awx/main/conf.py:453 awx/main/conf.py:464 awx/main/conf.py:476 -#: awx/main/conf.py:489 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:480 awx/main/conf.py:491 awx/main/conf.py:503 +#: awx/main/conf.py:516 msgid "Logging" msgstr "Registros" -#: awx/main/conf.py:360 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "Puerto de agregación de registros" -#: awx/main/conf.py:361 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." msgstr "" -"El puerto del Agregador de Logs al cual enviar logs (si es requerido y no " -"está definido en el Agregador de Logs)." +"El puerto del Agregador de registros al cual enviar registros (si se " +"requiere y no está definido en el Agregador de registros)." -#: awx/main/conf.py:373 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" -msgstr "Tipo de agregación de registros." +msgstr "Tipo de agregador de registros" -#: awx/main/conf.py:374 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." -msgstr "Formato de mensajes para el agregador de registros escogidos." +msgstr "Mensajes de formato para el agregador de registros escogido." -#: awx/main/conf.py:383 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "Usuario del agregador de registros" -#: awx/main/conf.py:384 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "Usuario para el agregador de registros externo (si es necesario)." -#: awx/main/conf.py:395 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "Contraseña/Token del agregador de registros" -#: awx/main/conf.py:396 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "" -"Contraseña o token de autentificación para el agregador de registros externo" -" (si es necesario)." +"Contraseña o token de autenticación para el agregador de registros externo " +"(si es necesario)." -#: awx/main/conf.py:405 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "" -"Registradores que envían datos al formulario de agregadores de registros" +"Registradores que envían datos al formulario del agregador de registros" -#: awx/main/conf.py:406 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include any or all of: \n" "awx - service logs\n" @@ -1904,52 +2296,52 @@ msgstr "" "job_events - datos de retorno de llamada de eventos de tareas Ansible\n" "system_tracking - información obtenida de tareas de análisis" -#: awx/main/conf.py:419 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" -msgstr "Sistema de registros tratará los facts individualmente." +msgstr "Sistema de registros rastrea los hechos individualmente" -#: awx/main/conf.py:420 +#: awx/main/conf.py:447 msgid "" -"If set, system tracking facts will be sent for each package, service, " -"orother item found in a scan, allowing for greater search query granularity." -" If unset, facts will be sent as a single dictionary, allowing for greater " +"If set, system tracking facts will be sent for each package, service, or " +"other item found in a scan, allowing for greater search query granularity. " +"If unset, facts will be sent as a single dictionary, allowing for greater " "efficiency in fact processing." msgstr "" -"Si se establece, los facts del sistema de rastreo serán enviados por cada " -"paquete, servicio u otro elemento encontrado en el escaneo, permitiendo " -"mayor granularidad en la consulta de búsqueda. Si no se establece, lo facts " -"serán enviados como un único diccionario, permitiendo mayor eficiencia en el" -" proceso de facts." +"Si se establece, los datos del sistema de rastreo se enviarán para cada " +"paquete, servicio u otro elemento encontrado en el escaneo, lo que permite " +"mayor granularidad en la consulta de búsqueda. Si no se establece, los datos" +" se enviarán como un único diccionario, lo que permite mayor eficiencia en " +"el proceso de datos." -#: awx/main/conf.py:431 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "Habilitar registro externo" -#: awx/main/conf.py:432 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "Habilitar el envío de registros a un agregador de registros externo." -#: awx/main/conf.py:441 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "Indentificador de Torre único a través del cluster." -#: awx/main/conf.py:442 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." -msgstr "Útil para identificar instancias de Torre." +msgstr "Útil para identificar instancias de Torre de manera única." -#: awx/main/conf.py:451 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "Registrando protocolo de agregador" -#: awx/main/conf.py:452 +#: awx/main/conf.py:479 msgid "Protocol used to communicate with log aggregator." msgstr "Protocolo utilizado para comunicarse con el agregador de registros." -#: awx/main/conf.py:460 +#: awx/main/conf.py:487 msgid "TCP Connection Timeout" msgstr "Tiempo de espera para la conexión TCP" -#: awx/main/conf.py:461 +#: awx/main/conf.py:488 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." @@ -1958,11 +2350,11 @@ msgstr "" "externo caduque. Aplica a protocolos de agregadores de registros HTTPS y " "TCP." -#: awx/main/conf.py:471 +#: awx/main/conf.py:498 msgid "Enable/disable HTTPS certificate verification" msgstr "Habilitar/deshabilitar verificación de certificado HTTPS" -#: awx/main/conf.py:472 +#: awx/main/conf.py:499 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -1975,11 +2367,11 @@ msgstr "" " de registros externo haya enviado el certificado antes de establecer la " "conexión." -#: awx/main/conf.py:484 +#: awx/main/conf.py:511 msgid "Logging Aggregator Level Threshold" msgstr "Umbral de nivel del agregador de registros" -#: awx/main/conf.py:485 +#: awx/main/conf.py:512 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -1991,89 +2383,150 @@ msgstr "" "CRITICAL. El controlador de registros ignorará los mensajes menos graves que" " el umbral (los mensajes de la categoría awx.anlytics omiten este ajuste)." -#: awx/main/conf.py:508 awx/sso/conf.py:1105 +#: awx/main/conf.py:535 awx/sso/conf.py:1262 msgid "\n" msgstr "\n" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Sudo" msgstr "Sudo" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Su" msgstr "Su" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pbrun" msgstr "Pbrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pfexec" msgstr "Pfexec" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "DZDO" msgstr "DZDO" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Pmrun" msgstr "Pmrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Runas" msgstr "Runas" -#: awx/main/fields.py:57 -#, python-format -msgid "'%s' is not one of ['%s']" -msgstr "'%s' no es uno de ['%s']" +#: awx/main/constants.py:19 +msgid "Enable" +msgstr "Habilitar" -#: awx/main/fields.py:533 +#: awx/main/constants.py:19 +msgid "Doas" +msgstr "Doas" + +#: awx/main/constants.py:21 +msgid "None" +msgstr "Ninguno" + +#: awx/main/fields.py:62 +#, python-brace-format +msgid "'{value}' is not one of ['{allowed_values}']" +msgstr "'{value}' no es uno de ['{allowed_values}']" + +#: awx/main/fields.py:421 +#, python-brace-format +msgid "{type} provided in relative path {path}, expected {expected_type}" +msgstr "{type} provisto en la ruta relativa {path}; se espera {expected_type}" + +#: awx/main/fields.py:426 +#, python-brace-format +msgid "{type} provided, expected {expected_type}" +msgstr "{type} provisto; se espera {expected_type}" + +#: awx/main/fields.py:431 +#, python-brace-format +msgid "Schema validation error in relative path {path} ({error})" +msgstr "Error de validación del esquema en ruta relativa {path} ({error})" + +#: awx/main/fields.py:552 +msgid "secret values must be of type string, not {}" +msgstr "los valores secretos deben ser de tipo cadena, no {}" + +#: awx/main/fields.py:587 #, python-format msgid "cannot be set unless \"%s\" is set" -msgstr "no puede establecerse excepto que se defina \"%s\"" +msgstr "no puede establecerse, a menos que se defina \"%s\"" -#: awx/main/fields.py:549 +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "requerido para %s" -#: awx/main/fields.py:573 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "se debe establecer cuando la clave SSH está cifrada." -#: awx/main/fields.py:579 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "no se debe establecer cuando la clave SSH no está cifrada." -#: awx/main/fields.py:637 +#: awx/main/fields.py:691 msgid "'dependencies' is not supported for custom credentials." msgstr "'dependencias' no es compatible con las credenciales personalizadas." -#: awx/main/fields.py:651 +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "\"tower\" es un nombre de campo reservado" -#: awx/main/fields.py:658 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "los ID de campo deben ser únicos (%s)" -#: awx/main/fields.py:671 -#, python-format -msgid "%s not allowed for %s type (%s)" -msgstr "%s no está permitido para el tipo %s (%s)" +#: awx/main/fields.py:725 +msgid "become_method is a reserved type name" +msgstr "become_method es un nombre de tipo reservado" -#: awx/main/fields.py:755 -#, python-format -msgid "%s uses an undefined field (%s)" -msgstr "%s usa un campo indefinido (%s)" +#: awx/main/fields.py:736 +#, python-brace-format +msgid "{sub_key} not allowed for {element_type} type ({element_id})" +msgstr "{sub_key} no permitido para el tipo {element_type} ({element_id})" -#: awx/main/middleware.py:157 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "" +"Se debe definir el inyector del archivo sin nombre para hacer referencia a " +"`tower.filename`." + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "" +"No se puede hacer referencia directa al contenedor del espacio de nombres " +"`tower`." + +#: awx/main/fields.py:827 +msgid "Must use multi-file syntax when injecting multiple files" +msgstr "" +"Debe usar una sintaxis de archivos múltiples al inyectar archivos múltiples" + +#: awx/main/fields.py:844 +#, python-brace-format +msgid "{sub_key} uses an undefined field ({error_msg})" +msgstr "{sub_key} usa un campo indefinido ({error_msg})" + +#: awx/main/fields.py:851 +#, python-brace-format +msgid "" +"Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" +msgstr "" +"Plantilla que arroja un error de sintaxis para {sub_key} dentro de {type} " +"({error_msg})" + +#: awx/main/middleware.py:146 msgid "Formats of all available named urls" msgstr "Formatos de todas las URL con nombre disponibles" -#: awx/main/middleware.py:158 +#: awx/main/middleware.py:147 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." @@ -2081,15 +2534,15 @@ msgstr "" "Lista de solo lectura de los pares clave-valor que muestra el formato " "estándar de todas las URL con nombre disponibles." -#: awx/main/middleware.py:160 awx/main/middleware.py:170 +#: awx/main/middleware.py:149 awx/main/middleware.py:159 msgid "Named URL" msgstr "URL con nombre" -#: awx/main/middleware.py:167 +#: awx/main/middleware.py:156 msgid "List of all named url graph nodes." msgstr "Lista de todos los nodos gráficos de URL con nombre." -#: awx/main/middleware.py:168 +#: awx/main/middleware.py:157 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use" " this list to programmatically generate named URLs for resources" @@ -2098,31 +2551,35 @@ msgstr "" "gráfica de URL con nombre. Use esta lista para generar URL con nombre para " "recursos mediante programación." -#: awx/main/migrations/_reencrypt.py:25 awx/main/models/notifications.py:33 +#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:35 msgid "Email" msgstr "Correo electrónico" -#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:34 +#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:36 msgid "Slack" msgstr "Slack" -#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:35 +#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:37 msgid "Twilio" msgstr "Twilio" -#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:36 +#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:38 msgid "Pagerduty" msgstr "Pagerduty" -#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:37 +#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:39 msgid "HipChat" msgstr "HipChat" -#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:38 +#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:41 +msgid "Mattermost" +msgstr "Mattermost" + +#: awx/main/migrations/_reencrypt.py:32 awx/main/models/notifications.py:40 msgid "Webhook" msgstr "Webhook" -#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:39 +#: awx/main/migrations/_reencrypt.py:33 awx/main/models/notifications.py:43 msgid "IRC" msgstr "IRC" @@ -2146,200 +2603,176 @@ msgstr "Entidad asociada con otra entidad" msgid "Entity was Disassociated with another Entity" msgstr "La entidad fue desasociada de otra entidad" -#: awx/main/models/ad_hoc_commands.py:100 +#: awx/main/models/ad_hoc_commands.py:95 msgid "No valid inventory." -msgstr "Inventario no válido" +msgstr "Inventario no válido." -#: awx/main/models/ad_hoc_commands.py:107 +#: awx/main/models/ad_hoc_commands.py:102 msgid "You must provide a machine / SSH credential." -msgstr "Debe proporcionar un credencial de máquina / SSH." - -#: awx/main/models/ad_hoc_commands.py:118 -#: awx/main/models/ad_hoc_commands.py:126 -msgid "Invalid type for ad hoc command" -msgstr "Tipo inválido para comando ad hoc" +msgstr "Debe proporcionar una credencial de máquina/SSH." +#: awx/main/models/ad_hoc_commands.py:113 #: awx/main/models/ad_hoc_commands.py:121 +msgid "Invalid type for ad hoc command" +msgstr "Tipo no válido para comando ad hoc" + +#: awx/main/models/ad_hoc_commands.py:116 msgid "Unsupported module for ad hoc commands." msgstr "Módulo no soportado para comandos ad hoc." -#: awx/main/models/ad_hoc_commands.py:129 +#: awx/main/models/ad_hoc_commands.py:124 #, python-format msgid "No argument passed to %s module." msgstr "Ningún argumento pasado al módulo %s." -#: awx/main/models/ad_hoc_commands.py:245 awx/main/models/jobs.py:911 -msgid "Host Failed" -msgstr "Servidor fallido" - -#: awx/main/models/ad_hoc_commands.py:246 awx/main/models/jobs.py:912 -msgid "Host OK" -msgstr "Servidor OK" - -#: awx/main/models/ad_hoc_commands.py:247 awx/main/models/jobs.py:915 -msgid "Host Unreachable" -msgstr "Servidor no alcanzable" - -#: awx/main/models/ad_hoc_commands.py:252 awx/main/models/jobs.py:914 -msgid "Host Skipped" -msgstr "Servidor omitido" - -#: awx/main/models/ad_hoc_commands.py:262 awx/main/models/jobs.py:942 -msgid "Debug" -msgstr "Debug" - -#: awx/main/models/ad_hoc_commands.py:263 awx/main/models/jobs.py:943 -msgid "Verbose" -msgstr "Nivel de detalle" - -#: awx/main/models/ad_hoc_commands.py:264 awx/main/models/jobs.py:944 -msgid "Deprecated" -msgstr "Obsoleto" - -#: awx/main/models/ad_hoc_commands.py:265 awx/main/models/jobs.py:945 -msgid "Warning" -msgstr "Advertencia" - -#: awx/main/models/ad_hoc_commands.py:266 awx/main/models/jobs.py:946 -msgid "System Warning" -msgstr "Advertencia del sistema" - -#: awx/main/models/ad_hoc_commands.py:267 awx/main/models/jobs.py:947 -#: awx/main/models/unified_jobs.py:64 -msgid "Error" -msgstr "Error" - -#: awx/main/models/base.py:40 awx/main/models/base.py:46 -#: awx/main/models/base.py:51 +#: awx/main/models/base.py:33 awx/main/models/base.py:39 +#: awx/main/models/base.py:44 awx/main/models/base.py:49 msgid "Run" msgstr "Ejecutar" -#: awx/main/models/base.py:41 awx/main/models/base.py:47 -#: awx/main/models/base.py:52 +#: awx/main/models/base.py:34 awx/main/models/base.py:40 +#: awx/main/models/base.py:45 awx/main/models/base.py:50 msgid "Check" msgstr "Comprobar" -#: awx/main/models/base.py:42 +#: awx/main/models/base.py:35 msgid "Scan" msgstr "Escanear" -#: awx/main/models/credential.py:86 +#: awx/main/models/credential/__init__.py:110 msgid "Host" -msgstr "Servidor" +msgstr "Host" -#: awx/main/models/credential.py:87 +#: awx/main/models/credential/__init__.py:111 msgid "The hostname or IP address to use." -msgstr "El hostname o dirección IP a utilizar." +msgstr "El nombre de host o la dirección IP por utilizar." -#: awx/main/models/credential.py:93 +#: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "Usuario" -#: awx/main/models/credential.py:94 +#: awx/main/models/credential/__init__.py:118 msgid "Username for this credential." -msgstr "Usuario para este credencial" +msgstr "Usuario para esta credencial." -#: awx/main/models/credential.py:100 +#: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "Contraseña" -#: awx/main/models/credential.py:101 +#: awx/main/models/credential/__init__.py:125 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." msgstr "" -"Contraseña para este credencial (o \"ASK\" para solicitar al usuario por " +"Contraseña para esta credencial (o \"ASK\" para solicitar al usuario las " "credenciales de máquina)." -#: awx/main/models/credential.py:108 +#: awx/main/models/credential/__init__.py:132 msgid "Security Token" msgstr "Token de seguridad" -#: awx/main/models/credential.py:109 +#: awx/main/models/credential/__init__.py:133 msgid "Security Token for this credential" -msgstr "Token de seguridad para este credencial" +msgstr "Token de seguridad para esta credencial" -#: awx/main/models/credential.py:115 +#: awx/main/models/credential/__init__.py:139 msgid "Project" msgstr "Proyecto" -#: awx/main/models/credential.py:116 +#: awx/main/models/credential/__init__.py:140 msgid "The identifier for the project." -msgstr "El identificador para el proyecto" +msgstr "El identificador para el proyecto." -#: awx/main/models/credential.py:122 +#: awx/main/models/credential/__init__.py:146 msgid "Domain" msgstr "Dominio" -#: awx/main/models/credential.py:123 +#: awx/main/models/credential/__init__.py:147 msgid "The identifier for the domain." msgstr "El identificador para el dominio." -#: awx/main/models/credential.py:128 +#: awx/main/models/credential/__init__.py:152 msgid "SSH private key" msgstr "Clave privada SSH" -#: awx/main/models/credential.py:129 +#: awx/main/models/credential/__init__.py:153 msgid "RSA or DSA private key to be used instead of password." -msgstr "Clave privada RSA o DSA a ser utilizada en vez de una contraseña." +msgstr "Clave privada RSA o DSA para utilizar en lugar de una contraseña." -#: awx/main/models/credential.py:135 +#: awx/main/models/credential/__init__.py:159 msgid "SSH key unlock" -msgstr "Desbloquear clave SSH" +msgstr "Desbloqueo de clave SSH" -#: awx/main/models/credential.py:136 +#: awx/main/models/credential/__init__.py:160 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." msgstr "" "Frase de contraseña para desbloquear la clave privada SSH si está cifrada (o" -" \"ASK\" para solicitar al usuario por credenciales de máquina)." +" \"ASK\" para solicitar al usuario las credenciales de máquina)." -#: awx/main/models/credential.py:143 -msgid "None" -msgstr "Ninguno" - -#: awx/main/models/credential.py:144 +#: awx/main/models/credential/__init__.py:168 msgid "Privilege escalation method." msgstr "Método de elevación de privilegios." -#: awx/main/models/credential.py:150 +#: awx/main/models/credential/__init__.py:174 msgid "Privilege escalation username." -msgstr "Usuario para la elevación de privilegios." +msgstr "Nombre de usuario para la elevación de privilegios." -#: awx/main/models/credential.py:156 +#: awx/main/models/credential/__init__.py:180 msgid "Password for privilege escalation method." -msgstr "Contraseña para el método de elevación de privilegios" +msgstr "Contraseña para el método de elevación de privilegios." -#: awx/main/models/credential.py:162 +#: awx/main/models/credential/__init__.py:186 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "Contraseña de Vault (o \"ASK\" para solicitar al usuario)." -#: awx/main/models/credential.py:166 +#: awx/main/models/credential/__init__.py:190 msgid "Whether to use the authorize mechanism." -msgstr "Si se utilizará el mecanismo de autentificación." +msgstr "Si se utilizará el mecanismo de autenticación." -#: awx/main/models/credential.py:172 +#: awx/main/models/credential/__init__.py:196 msgid "Password used by the authorize mechanism." -msgstr "Contraseña utilizada para el mecanismo de autentificación." +msgstr "Contraseña utilizada por el mecanismo de autenticación." -#: awx/main/models/credential.py:178 +#: awx/main/models/credential/__init__.py:202 msgid "Client Id or Application Id for the credential" msgstr "Id del cliente o Id de aplicación para el credencial" -#: awx/main/models/credential.py:184 +#: awx/main/models/credential/__init__.py:208 msgid "Secret Token for this credential" -msgstr "Token secreto para este credencial" +msgstr "Token secreto para esta credencial" -#: awx/main/models/credential.py:190 +#: awx/main/models/credential/__init__.py:214 msgid "Subscription identifier for this credential" -msgstr "Identificador de suscripción para este credencial" +msgstr "Identificador de suscripción para esta credencial" -#: awx/main/models/credential.py:196 +#: awx/main/models/credential/__init__.py:220 msgid "Tenant identifier for this credential" -msgstr "Identificador de inquilino [Tenant] para este credencial" +msgstr "Identificador de inquilino [Tenant] para esta credencial" -#: awx/main/models/credential.py:220 +#: awx/main/models/credential/__init__.py:244 msgid "" "Specify the type of credential you want to create. Refer to the Ansible " "Tower documentation for details on each type." @@ -2347,7 +2780,8 @@ msgstr "" "Especifique el tipo de credencial que desea crear. Consulte la documentación" " de Ansible Tower para obtener información sobre cada tipo." -#: awx/main/models/credential.py:234 awx/main/models/credential.py:420 +#: awx/main/models/credential/__init__.py:258 +#: awx/main/models/credential/__init__.py:476 msgid "" "Enter inputs using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2357,31 +2791,36 @@ msgstr "" "selección para alternar entre las dos opciones. Consulte la documentación de" " Ansible Tower para ver sintaxis de ejemplo." -#: awx/main/models/credential.py:401 +#: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "Máquina" -#: awx/main/models/credential.py:402 +#: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "Vault" -#: awx/main/models/credential.py:403 +#: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "Red" -#: awx/main/models/credential.py:404 +#: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "Fuente de control" -#: awx/main/models/credential.py:405 +#: awx/main/models/credential/__init__.py:461 msgid "Cloud" msgstr "Nube" -#: awx/main/models/credential.py:406 +#: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "Insights" -#: awx/main/models/credential.py:427 +#: awx/main/models/credential/__init__.py:483 msgid "" "Enter injectors using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2391,106 +2830,531 @@ msgstr "" "selección para alternar entre las dos opciones. Consulte la documentación de" " Ansible Tower para ver sintaxis de ejemplo." -#: awx/main/models/credential.py:478 +#: awx/main/models/credential/__init__.py:534 #, python-format msgid "adding %s credential type" +msgstr "añadir el tipo de credencial %s" + +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "Clave privada SSH" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "Frase de contraseña para clave privada" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "Método de escalación de privilegios" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying" +" the --become-method Ansible parameter." msgstr "" +"Especifique un método para operaciones \"become\". Esto equivale a " +"especificar el parámetro --become-method de Ansible." + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "Nombre de usuario de escalación de privilegios" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "Contraseña de escalación de privilegios" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "Clave privada SCM" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "Contraseña Vault" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "Identificador de Vault" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the " +"--vault-id Ansible parameter for providing multiple Vault passwords. Note: " +"this feature only works in Ansible 2.4+." +msgstr "" +"Especifique una ID de Vault (opcional). Esto es equivalente a especificar el" +" parámetro --vault-id de Ansible para ofrecer múltiples contraseñas Vault. " +"Observe: esta función solo funciona en Ansible 2.4+." + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "Autorizar" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "Contraseña de autorización" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "Amazon Web Services" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "Clave de acceso" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "Clave secreta" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "Token STS" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"Management (IAM) users." +msgstr "" +"Security Token Service (STS) es un servicio web que le permite solicitar " +"credenciales temporales, con privilegio limitado, para usuarios de AWS " +"Identity y Access Management (IAM)." + +#: awx/main/models/credential/__init__.py:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "OpenStack" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "Contraseña (clave API)" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "Host (URL de autenticación)" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, " +"https://openstack.business.com/v2.0/" +msgstr "" +"El host con el cual autenticar. Por ejemplo, " +"https://openstack.business.com/v2.0/" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "Proyecto (Nombre del inquilino [Tenant])" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "Nombre de dominio" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" +"Los dominios OpenStack definen los límites administrativos. Solo es " +"necesario para las URL de autenticación para KeyStone v3. Consulte la " +"documentación de Ansible Tower para conocer los escenarios comunes." + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "VMware vCenter" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "Host de vCenter" + +#: awx/main/models/credential/__init__.py:926 +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:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "Red Hat Satellite 6" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "URL Satellite 6" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"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:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "Red Hat CloudForms" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "URL CloudForms" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"instance. For example, https://cloudforms.example.org" +msgstr "" +"Introduzca la URL para la máquina virtual que corresponda a su instancia " +"CloudForm. Por ejemplo, https://cloudforms.example.org" + +#: awx/main/models/credential/__init__.py:1004 +#: awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "Google Compute Engine" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "Dirección de correo electrónico de cuenta de servicio" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "" +"La dirección de correo electrónico asignada a la cuenta de servicio de " +"Google Compute Engine." + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" +"La ID de proyecto es la identificación asignada por GCE. Por lo general, " +"está formada por dos o tres palabras seguidas por un número de tres dígitos." +" Ejemplos: project-id-000 y another-project-id" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "Clave privada RSA" + +#: awx/main/models/credential/__init__.py:1028 +msgid "" +"Paste the contents of the PEM file associated with the service account " +"email." +msgstr "" +"Pegue el contenido del archivo PEM asociado al correo electrónico de la " +"cuenta de servicio." + +#: awx/main/models/credential/__init__.py:1040 +#: awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "Microsoft Azure Resource Manager" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "ID de suscripción" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "" +"La ID de suscripción es un elemento de Azure, que está asignado a un nombre " +"de usuario." + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "ID de cliente" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "ID de inquilino" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "Entorno de nube de Azure" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "" +"Variable AZURE_CLOUD_ENVIRONMENT del entorno al usar Azure GovCloud o la " +"pila de Azure." + +#: awx/main/models/credential/__init__.py:1115 +#: awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "Virtualización de Red Hat" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "El host con el cual autenticarse." + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "Archivo CA" + +#: awx/main/models/credential/__init__.py:1136 +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:1167 +#: awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "Ansible Tower" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "Nombre de host de Ansible Tower" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "La URL de base de Ansible Tower con la cual autenticarse." + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "Verificar SSL " + +#: awx/main/models/events.py:89 awx/main/models/events.py:608 +msgid "Host Failed" +msgstr "Host fallido" + +#: awx/main/models/events.py:90 awx/main/models/events.py:609 +msgid "Host OK" +msgstr "Host OK" + +#: awx/main/models/events.py:91 +msgid "Host Failure" +msgstr "Error del host" + +#: awx/main/models/events.py:92 awx/main/models/events.py:615 +msgid "Host Skipped" +msgstr "Host omitido" + +#: awx/main/models/events.py:93 awx/main/models/events.py:610 +msgid "Host Unreachable" +msgstr "Host no alcanzable" + +#: awx/main/models/events.py:94 awx/main/models/events.py:108 +msgid "No Hosts Remaining" +msgstr "No más hosts" + +#: awx/main/models/events.py:95 +msgid "Host Polling" +msgstr "Sondeo al host" + +#: awx/main/models/events.py:96 +msgid "Host Async OK" +msgstr "Host Async OK" + +#: awx/main/models/events.py:97 +msgid "Host Async Failure" +msgstr "Host Async fallido" + +#: awx/main/models/events.py:98 +msgid "Item OK" +msgstr "Elemento OK" + +#: awx/main/models/events.py:99 +msgid "Item Failed" +msgstr "Elemento fallido" + +#: awx/main/models/events.py:100 +msgid "Item Skipped" +msgstr "Elemento omitido" + +#: awx/main/models/events.py:101 +msgid "Host Retry" +msgstr "Reintentar host" + +#: awx/main/models/events.py:103 +msgid "File Difference" +msgstr "Diferencia entre archivos" + +#: awx/main/models/events.py:104 +msgid "Playbook Started" +msgstr "Playbook iniciado" + +#: awx/main/models/events.py:105 +msgid "Running Handlers" +msgstr "Handlers ejecutándose" + +#: awx/main/models/events.py:106 +msgid "Including File" +msgstr "Incluyendo archivo" + +#: awx/main/models/events.py:107 +msgid "No Hosts Matched" +msgstr "Ningún host corresponde" + +#: awx/main/models/events.py:109 +msgid "Task Started" +msgstr "Tarea iniciada" + +#: awx/main/models/events.py:111 +msgid "Variables Prompted" +msgstr "Variables solicitadas" + +#: awx/main/models/events.py:112 +msgid "Gathering Facts" +msgstr "Obteniendo hechos" + +#: awx/main/models/events.py:113 +msgid "internal: on Import for Host" +msgstr "interno: en la importación para el host" + +#: awx/main/models/events.py:114 +msgid "internal: on Not Import for Host" +msgstr "interno: en la no importación para el host" + +#: awx/main/models/events.py:115 +msgid "Play Started" +msgstr "Jugada iniciada" + +#: awx/main/models/events.py:116 +msgid "Playbook Complete" +msgstr "Playbook terminado" + +#: awx/main/models/events.py:120 awx/main/models/events.py:625 +msgid "Debug" +msgstr "Depurar" + +#: awx/main/models/events.py:121 awx/main/models/events.py:626 +msgid "Verbose" +msgstr "Nivel de detalle" + +#: awx/main/models/events.py:122 awx/main/models/events.py:627 +msgid "Deprecated" +msgstr "Obsoleto" + +#: awx/main/models/events.py:123 awx/main/models/events.py:628 +msgid "Warning" +msgstr "Advertencia" + +#: awx/main/models/events.py:124 awx/main/models/events.py:629 +msgid "System Warning" +msgstr "Advertencia del sistema" + +#: awx/main/models/events.py:125 awx/main/models/events.py:630 +#: awx/main/models/unified_jobs.py:67 +msgid "Error" +msgstr "Error" #: awx/main/models/fact.py:25 msgid "Host for the facts that the fact scan captured." -msgstr "Servidor para los facts que el escaneo de facts capture." +msgstr "Host para los hechos que el escaneo de hechos capture." #: awx/main/models/fact.py:30 msgid "Date and time of the corresponding fact scan gathering time." msgstr "" -"Fecha y hora que corresponden al escaneo de facts en el tiempo que fueron " -"obtenido." +"Fecha y hora que corresponden al escaneo de hechos en el tiempo que fueron " +"obtenidos." #: awx/main/models/fact.py:33 msgid "" "Arbitrary JSON structure of module facts captured at timestamp for a single " "host." msgstr "" -"Estructura de JSON arbitraria de módulos facts capturados en la fecha y hora" -" para un único servidor." +"Estructura de JSON arbitraria de hechos de módulos capturados en la fecha y " +"hora para un único host." -#: awx/main/models/ha.py:78 +#: awx/main/models/ha.py:153 msgid "Instances that are members of this InstanceGroup" msgstr "Las instancias que son miembros de este grupo de instancias" -#: awx/main/models/ha.py:83 +#: awx/main/models/ha.py:158 msgid "Instance Group to remotely control this group." msgstr "Grupo de instancias para controlar remotamente este grupo." -#: awx/main/models/inventory.py:52 +#: awx/main/models/ha.py:165 +msgid "Percentage of Instances to automatically assign to this group" +msgstr "" +"Porcentaje de instancias que se asignarán automáticamente a este grupo" + +#: awx/main/models/ha.py:169 +msgid "" +"Static minimum number of Instances to automatically assign to this group" +msgstr "" +"Número mínimo estático de instancias que se asignarán automáticamente a este" +" grupo" + +#: awx/main/models/ha.py:174 +msgid "" +"List of exact-match Instances that will always be automatically assigned to " +"this group" +msgstr "" +"Lista de instancias con coincidencia exacta que se asignarán siempre " +"automáticamente a este grupo" + +#: awx/main/models/inventory.py:61 msgid "Hosts have a direct link to this inventory." msgstr "Los hosts tienen un enlace directo a este inventario." -#: awx/main/models/inventory.py:53 +#: awx/main/models/inventory.py:62 msgid "Hosts for inventory generated using the host_filter property." -msgstr "Hosts para inventario generado a través de la propiedad host_filter." +msgstr "Hosts para inventario generados a través de la propiedad host_filter." -#: awx/main/models/inventory.py:58 +#: awx/main/models/inventory.py:67 msgid "inventories" msgstr "inventarios" -#: awx/main/models/inventory.py:65 +#: awx/main/models/inventory.py:74 msgid "Organization containing this inventory." msgstr "Organización que contiene este inventario." -#: awx/main/models/inventory.py:72 +#: awx/main/models/inventory.py:81 msgid "Inventory variables in JSON or YAML format." msgstr "Variables de inventario en formato JSON o YAML." -#: awx/main/models/inventory.py:77 +#: awx/main/models/inventory.py:86 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "" "Indicador que establece si algún servidor en este inventario ha fallado." -#: awx/main/models/inventory.py:82 +#: awx/main/models/inventory.py:91 msgid "Total number of hosts in this inventory." -msgstr "Número total de servidores en este inventario." +msgstr "Número total de hosts en este inventario." -#: awx/main/models/inventory.py:87 +#: awx/main/models/inventory.py:96 msgid "Number of hosts in this inventory with active failures." -msgstr "Número de servidores en este inventario con fallos actuales." +msgstr "Número de hosts en este inventario con errores activos." -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:101 msgid "Total number of groups in this inventory." msgstr "Número total de grupos en este inventario." -#: awx/main/models/inventory.py:97 +#: awx/main/models/inventory.py:106 msgid "Number of groups in this inventory with active failures." -msgstr "Número de grupos en este inventario con fallos actuales." +msgstr "Número de grupos en este inventario con errores activos." -#: awx/main/models/inventory.py:102 +#: awx/main/models/inventory.py:111 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "" -"Indicador que establece si el inventario tiene alguna fuente de externa de " -"inventario." +"Indicador que establece si este inventario tiene alguna fuente de externa " +"de inventario." -#: awx/main/models/inventory.py:107 +#: awx/main/models/inventory.py:116 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:112 +#: awx/main/models/inventory.py:121 msgid "Number of external inventory sources in this inventory with failures." msgstr "" -"Número de inventarios de origen externo en este inventario con errores." +"Número de inventarios de origen externo en este inventario con errores." -#: awx/main/models/inventory.py:119 +#: awx/main/models/inventory.py:128 msgid "Kind of inventory being represented." msgstr "Tipo de inventario que se representa." -#: awx/main/models/inventory.py:125 +#: awx/main/models/inventory.py:134 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:152 +#: awx/main/models/inventory.py:161 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." @@ -2498,290 +3362,261 @@ 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:161 +#: awx/main/models/inventory.py:170 msgid "Flag indicating the inventory is being deleted." msgstr "Indicador que muestra que el inventario se eliminará." -#: awx/main/models/inventory.py:374 +#: awx/main/models/inventory.py:459 msgid "Assignment not allowed for Smart Inventory" msgstr "Tarea no permitida para el inventario inteligente" -#: awx/main/models/inventory.py:376 awx/main/models/projects.py:148 +#: awx/main/models/inventory.py:461 awx/main/models/projects.py:159 msgid "Credential kind must be 'insights'." msgstr "Tipo de credencial debe ser 'insights'." -#: awx/main/models/inventory.py:443 +#: awx/main/models/inventory.py:546 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:449 +#: awx/main/models/inventory.py:552 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" +"única el host" -#: awx/main/models/inventory.py:454 +#: awx/main/models/inventory.py:557 msgid "Host variables in JSON or YAML format." -msgstr "Variables del servidor en formato JSON o YAML." +msgstr "Variables de host en formato JSON o YAML." -#: awx/main/models/inventory.py:476 +#: awx/main/models/inventory.py:579 msgid "Flag indicating whether the last job failed for this host." -msgstr "" -"Indicador que establece si el último trabajo ha fallado para este servidor." +msgstr "Indicador que establece si el último trabajo falló para este host." -#: awx/main/models/inventory.py:481 +#: awx/main/models/inventory.py:584 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." msgstr "" -"Indicador que establece si este servidor fue creado/actualizado desde algún " +"Indicador que establece si este host se creó/se actualizó desde algún " "inventario de fuente externa." -#: awx/main/models/inventory.py:487 +#: awx/main/models/inventory.py:590 msgid "Inventory source(s) that created or modified this host." -msgstr "Fuente(s) del inventario que crearon o modificaron este servidor." +msgstr "Fuente(s) del inventario que crearon o modificaron este host." -#: awx/main/models/inventory.py:492 +#: awx/main/models/inventory.py:595 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." -msgstr "Estructura de JSON arbitraria de ansible_facts por host más reciente." +msgstr "Estructura de JSON arbitraria de ansible_facts más reciente por host." -#: awx/main/models/inventory.py:498 +#: awx/main/models/inventory.py:601 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:505 +#: awx/main/models/inventory.py:608 msgid "Red Hat Insights host unique identifier." msgstr "Identificador único de host de Red Hat Insights." -#: awx/main/models/inventory.py:633 +#: awx/main/models/inventory.py:743 msgid "Group variables in JSON or YAML format." msgstr "Grupo de variables en formato JSON o YAML." -#: awx/main/models/inventory.py:639 +#: awx/main/models/inventory.py:749 msgid "Hosts associated directly with this group." -msgstr "Hosts associated directly with this group." +msgstr "Hosts asociados directamente con este grupo." -#: awx/main/models/inventory.py:644 +#: awx/main/models/inventory.py:754 msgid "Total number of hosts directly or indirectly in this group." -msgstr "" -"Número total de servidores directamente o indirectamente en este grupo." +msgstr "Número total de hosts directamente o indirectamente en este grupo." -#: awx/main/models/inventory.py:649 +#: awx/main/models/inventory.py:759 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "" -"Indicador que establece si este grupo tiene algunos servidores con fallos " -"actuales." +"Indicador que establece si este grupo tiene algunos hosts con errores " +"activos." -#: awx/main/models/inventory.py:654 +#: awx/main/models/inventory.py:764 msgid "Number of hosts in this group with active failures." -msgstr "Número de servidores en este grupo con fallos actuales." +msgstr "Número de hosts en este grupo con errores activos." -#: awx/main/models/inventory.py:659 +#: awx/main/models/inventory.py:769 msgid "Total number of child groups contained within this group." msgstr "Número total de grupos hijo dentro de este grupo." -#: awx/main/models/inventory.py:664 +#: awx/main/models/inventory.py:774 msgid "Number of child groups within this group that have active failures." msgstr "" -"Número de grupos hijo dentro de este grupo que tienen fallos actuales." +"Número de grupos hijo dentro de este grupo que tienen errores activos." -#: awx/main/models/inventory.py:669 +#: awx/main/models/inventory.py:779 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." msgstr "" -"Indicador que establece si este grupo fue creado/actualizado desde un " +"Indicador que establece si este grupo se creó/se actualizó desde un " "inventario de fuente externa." -#: awx/main/models/inventory.py:675 +#: awx/main/models/inventory.py:785 msgid "Inventory source(s) that created or modified this group." -msgstr "Fuente(s) de inventario que crearon o modificaron este grupo." +msgstr "Fuentes de inventario que crearon o modificaron este grupo." -#: awx/main/models/inventory.py:865 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:428 +#: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "Manual" -#: awx/main/models/inventory.py:866 +#: awx/main/models/inventory.py:982 msgid "File, Directory or Script" msgstr "Archivo, directorio o script" -#: awx/main/models/inventory.py:867 +#: awx/main/models/inventory.py:983 msgid "Sourced from a Project" msgstr "Extraído de un proyecto" -#: awx/main/models/inventory.py:868 +#: awx/main/models/inventory.py:984 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:869 -msgid "Google Compute Engine" -msgstr "Google Compute Engine" - -#: awx/main/models/inventory.py:870 -msgid "Microsoft Azure Resource Manager" -msgstr "Microsoft Azure Resource Manager" - -#: awx/main/models/inventory.py:871 -msgid "VMware vCenter" -msgstr "VMware vCenter" - -#: awx/main/models/inventory.py:872 -msgid "Red Hat Satellite 6" -msgstr "Red Hat Satellite 6" - -#: awx/main/models/inventory.py:873 -msgid "Red Hat CloudForms" -msgstr "Red Hat CloudForms" - -#: awx/main/models/inventory.py:874 -msgid "OpenStack" -msgstr "OpenStack" - -#: awx/main/models/inventory.py:875 -msgid "oVirt4" -msgstr "" - -#: awx/main/models/inventory.py:876 -msgid "Ansible Tower" -msgstr "Ansible Tower" - -#: awx/main/models/inventory.py:877 +#: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "Script personalizado" -#: awx/main/models/inventory.py:994 +#: awx/main/models/inventory.py:1110 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:1013 +#: awx/main/models/inventory.py:1121 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." +"Lista de expresiones de filtrado separadas por comas (solo EC2). Los hosts " +"se importan cuando ALGUNO de los filtros coincide." -#: awx/main/models/inventory.py:1019 +#: awx/main/models/inventory.py:1127 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "" -"Limitar grupos creados automáticamente desde la fuente del inventario (sólo " -"EC2)" +"Limitar grupos creados automáticamente desde la fuente del inventario (solo " +"EC2)." -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1131 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "" -"Sobrescribir grupos locales y servidores desde una fuente remota del " -"inventario." +"Sobrescribir grupos y hosts locales desde una fuente de inventario remota." -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1135 msgid "Overwrite local variables from remote inventory source." msgstr "" -"Sobrescribir las variables locales desde una fuente remota del inventario." +"Sobrescribir las variables locales desde una fuente de inventario remota." -#: awx/main/models/inventory.py:1032 awx/main/models/jobs.py:160 -#: awx/main/models/projects.py:117 +#: awx/main/models/inventory.py:1140 awx/main/models/jobs.py:140 +#: awx/main/models/projects.py:128 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:1065 +#: awx/main/models/inventory.py:1173 msgid "Image ID" -msgstr "Id de imagen" +msgstr "ID de imagen" -#: awx/main/models/inventory.py:1066 +#: awx/main/models/inventory.py:1174 msgid "Availability Zone" msgstr "Zona de disponibilidad" -#: awx/main/models/inventory.py:1067 +#: awx/main/models/inventory.py:1175 msgid "Account" msgstr "Cuenta" -#: awx/main/models/inventory.py:1068 +#: awx/main/models/inventory.py:1176 msgid "Instance ID" msgstr "ID de instancia" -#: awx/main/models/inventory.py:1069 +#: awx/main/models/inventory.py:1177 msgid "Instance State" -msgstr "Estado de la instancia" +msgstr "Estado de instancia" -#: awx/main/models/inventory.py:1070 +#: awx/main/models/inventory.py:1178 +msgid "Platform" +msgstr "Plataforma" + +#: awx/main/models/inventory.py:1179 msgid "Instance Type" msgstr "Tipo de instancia" -#: awx/main/models/inventory.py:1071 +#: awx/main/models/inventory.py:1180 msgid "Key Name" msgstr "Nombre clave" -#: awx/main/models/inventory.py:1072 +#: awx/main/models/inventory.py:1181 msgid "Region" msgstr "Región" -#: awx/main/models/inventory.py:1073 +#: awx/main/models/inventory.py:1182 msgid "Security Group" msgstr "Grupo de seguridad" -#: awx/main/models/inventory.py:1074 +#: awx/main/models/inventory.py:1183 msgid "Tags" msgstr "Etiquetas" -#: awx/main/models/inventory.py:1075 +#: awx/main/models/inventory.py:1184 msgid "Tag None" msgstr "Etiqueta ninguna" -#: awx/main/models/inventory.py:1076 +#: awx/main/models/inventory.py:1185 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1145 +#: awx/main/models/inventory.py:1253 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "" "Fuentes de inventario basados en Cloud (como %s) requieren credenciales para" -" identificar los correspondientes servicios cloud." +" identificar el servicio cloud correspondiente." -#: awx/main/models/inventory.py:1152 +#: awx/main/models/inventory.py:1259 msgid "Credential is required for a cloud source." -msgstr "Un credencial es necesario para una fuente cloud." +msgstr "Se requiere una credencial para una fuente cloud." -#: awx/main/models/inventory.py:1155 +#: awx/main/models/inventory.py:1262 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:1179 +#: awx/main/models/inventory.py:1314 #, python-format msgid "Invalid %(source)s region: %(region)s" -msgstr "Región %(source)s inválida: %(region)s" +msgstr "Región %(source)s no válida: %(region)s" -#: awx/main/models/inventory.py:1203 +#: awx/main/models/inventory.py:1338 #, python-format msgid "Invalid filter expression: %(filter)s" -msgstr "Expresión de filtro inválida: %(filter)s" +msgstr "Expresión de filtro no válida: %(filter)s" -#: awx/main/models/inventory.py:1224 +#: awx/main/models/inventory.py:1359 #, python-format msgid "Invalid group by choice: %(choice)s" -msgstr "Grupo escogido inválido: %(choice)s" +msgstr "Grupo escogido no válido: %(choice)s" -#: awx/main/models/inventory.py:1259 +#: awx/main/models/inventory.py:1394 msgid "Project containing inventory file used as source." msgstr "Proyecto que contiene el archivo de inventario usado como fuente." -#: awx/main/models/inventory.py:1407 +#: awx/main/models/inventory.py:1555 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." msgstr "" -"Imposible configurar este elemento para sincronización cloud. Está " -"administrado actualmente por %s." +"Imposible configurar este elemento para sincronización cloud. Ya es " +"administrado por %s." -#: awx/main/models/inventory.py:1417 +#: awx/main/models/inventory.py:1565 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." @@ -2789,7 +3624,7 @@ 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:1424 +#: awx/main/models/inventory.py:1572 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " @@ -2800,28 +3635,31 @@ msgstr "" "su lugar, configure el proyecto de fuente correspondiente para actualizar en" " la ejecución." -#: awx/main/models/inventory.py:1430 -msgid "SCM type sources must set `overwrite_vars` to `true`." -msgstr "Las fuentes de tipo SCM deben configurar `overwrite_vars` en `true`." +#: awx/main/models/inventory.py:1579 +msgid "" +"SCM type sources must set `overwrite_vars` to `true` until Ansible 2.5." +msgstr "" +"Las fuentes de tipo SCM deben configurar `overwrite_vars` como `true` hasta " +"Ansible 2.5." -#: awx/main/models/inventory.py:1435 +#: awx/main/models/inventory.py:1584 msgid "Cannot set source_path if not SCM type." -msgstr "No se puede configurar source_path si no es del tipo SCM." +msgstr "No se puede configurar source_path si no es de tipo SCM." -#: awx/main/models/inventory.py:1460 +#: awx/main/models/inventory.py:1615 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:1573 +#: awx/main/models/inventory.py:1725 msgid "Inventory script contents" msgstr "Contenido del script de inventario" -#: awx/main/models/inventory.py:1578 +#: awx/main/models/inventory.py:1730 msgid "Organization owning this inventory script" -msgstr "Organización propietario de este script de inventario" +msgstr "Organización propietaria de este script de inventario" #: awx/main/models/jobs.py:66 msgid "" @@ -2831,7 +3669,7 @@ msgstr "" "Si se habilita, los cambios de texto realizados en cualquier archivo de " "plantilla en el host se muestran en la salida estándar" -#: awx/main/models/jobs.py:164 +#: awx/main/models/jobs.py:145 msgid "" "If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts" " at the end of a playbook run to the database and caching facts for use by " @@ -2842,304 +3680,314 @@ msgstr "" "base de datos y almacenará en caché los eventos que son utilizados por " "Ansible." -#: awx/main/models/jobs.py:173 -msgid "You must provide an SSH credential." -msgstr "Debe proporcionar una credencial SSH." - -#: awx/main/models/jobs.py:181 +#: awx/main/models/jobs.py:163 msgid "You must provide a Vault credential." msgstr "Debe proporcionar una credencial de Vault." -#: awx/main/models/jobs.py:317 +#: awx/main/models/jobs.py:308 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "" "La plantilla de trabajo debe proporcionar 'inventory' o permitir " "solicitarlo." -#: awx/main/models/jobs.py:321 -msgid "Job Template must provide 'credential' or allow prompting for it." +#: awx/main/models/jobs.py:403 +msgid "Field is not configured to prompt on launch." msgstr "" -"La plantilla de trabajo debe proporcionar 'inventory' o permitir " -"solicitarlo." +"El campo no está configurado para emitir avisos durante el lanzamiento." -#: awx/main/models/jobs.py:427 -msgid "Cannot override job_type to or from a scan job." -msgstr "No se puede sustituir job_type a o desde un trabajo de escaneo." +#: awx/main/models/jobs.py:409 +msgid "Saved launch configurations cannot provide passwords needed to start." +msgstr "" +"Las opciones de configuración de lanzamiento guardadas no pueden brindar las" +" contraseñas necesarias para el inicio." -#: awx/main/models/jobs.py:493 awx/main/models/projects.py:263 +#: awx/main/models/jobs.py:417 +msgid "Job Template {} is missing or undefined." +msgstr "Plantilla de tareas {} no encontrada o no definida." + +#: awx/main/models/jobs.py:498 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "Revisión SCM" -#: awx/main/models/jobs.py:494 +#: awx/main/models/jobs.py:499 msgid "The SCM Revision from the Project used for this job, if available" msgstr "" "La revisión SCM desde el proyecto usado para este trabajo, si está " "disponible" -#: awx/main/models/jobs.py:502 +#: awx/main/models/jobs.py:507 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" msgstr "" -"La tarea de actualización de SCM utilizado para asegurarse que los playbooks" -" estaban disponibles para la ejecución del trabajo" +"La tarea de actualización de SCM utilizada para asegurarse de que los " +"playbooks estaban disponibles para la ejecución del trabajo" -#: awx/main/models/jobs.py:809 +#: awx/main/models/jobs.py:634 +#, python-brace-format +msgid "{status_value} is not a valid status option." +msgstr "{status_value} no es una opción de estado válida." + +#: awx/main/models/jobs.py:999 msgid "job host summaries" -msgstr "Resumen de trabajos de servidor" +msgstr "resumen de hosts de trabajo" -#: awx/main/models/jobs.py:913 -msgid "Host Failure" -msgstr "Fallo del servidor" - -#: awx/main/models/jobs.py:916 awx/main/models/jobs.py:930 -msgid "No Hosts Remaining" -msgstr "No más servidores" - -#: awx/main/models/jobs.py:917 -msgid "Host Polling" -msgstr "Sondeo al servidor" - -#: awx/main/models/jobs.py:918 -msgid "Host Async OK" -msgstr "Servidor Async OK" - -#: awx/main/models/jobs.py:919 -msgid "Host Async Failure" -msgstr "Servidor Async fallido" - -#: awx/main/models/jobs.py:920 -msgid "Item OK" -msgstr "Elemento OK" - -#: awx/main/models/jobs.py:921 -msgid "Item Failed" -msgstr "Elemento fallido" - -#: awx/main/models/jobs.py:922 -msgid "Item Skipped" -msgstr "Elemento omitido" - -#: awx/main/models/jobs.py:923 -msgid "Host Retry" -msgstr "Reintentar servidor" - -#: awx/main/models/jobs.py:925 -msgid "File Difference" -msgstr "Diferencias del fichero" - -#: awx/main/models/jobs.py:926 -msgid "Playbook Started" -msgstr "Playbook iniciado" - -#: awx/main/models/jobs.py:927 -msgid "Running Handlers" -msgstr "Handlers ejecutándose" - -#: awx/main/models/jobs.py:928 -msgid "Including File" -msgstr "Incluyendo fichero" - -#: awx/main/models/jobs.py:929 -msgid "No Hosts Matched" -msgstr "Ningún servidor corresponde" - -#: awx/main/models/jobs.py:931 -msgid "Task Started" -msgstr "Tarea iniciada" - -#: awx/main/models/jobs.py:933 -msgid "Variables Prompted" -msgstr "Variables solicitadas" - -#: awx/main/models/jobs.py:934 -msgid "Gathering Facts" -msgstr "Obteniendo facts" - -#: awx/main/models/jobs.py:935 -msgid "internal: on Import for Host" -msgstr "internal: en la importación para el servidor" - -#: awx/main/models/jobs.py:936 -msgid "internal: on Not Import for Host" -msgstr "internal: en la no importación para el servidor" - -#: awx/main/models/jobs.py:937 -msgid "Play Started" -msgstr "Jugada iniciada" - -#: awx/main/models/jobs.py:938 -msgid "Playbook Complete" -msgstr "Playbook terminado" - -#: awx/main/models/jobs.py:1351 +#: awx/main/models/jobs.py:1070 msgid "Remove jobs older than a certain number of days" -msgstr "Eliminar trabajos más antiguos que el ńumero de días especificado" +msgstr "Eliminar trabajos más antiguos que el número de días especificado" -#: awx/main/models/jobs.py:1352 +#: awx/main/models/jobs.py:1071 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:1353 +#: awx/main/models/jobs.py:1072 msgid "Purge and/or reduce the granularity of system tracking data" +msgstr "Limpiar o reducir la granularidad de los datos del sistema de rastreo" + +#: awx/main/models/jobs.py:1142 +#, python-brace-format +msgid "Variables {list_of_keys} are not allowed for system jobs." msgstr "" -"Limpiar y/o reducir la granularidad de los datos del sistema de rastreo" +"Las variables {list_of_keys} no están permitidas para tareas del sistema." + +#: awx/main/models/jobs.py:1157 +msgid "days must be a positive integer." +msgstr "días debe ser un número entero." #: awx/main/models/label.py:29 msgid "Organization this label belongs to." msgstr "Organización a la que esta etiqueta pertenece." -#: awx/main/models/notifications.py:138 awx/main/models/unified_jobs.py:59 +#: awx/main/models/mixins.py:309 +#, python-brace-format +msgid "" +"Variables {list_of_keys} are not allowed on launch. Check the Prompt on " +"Launch setting on the Job Template to include Extra Variables." +msgstr "" +"Las variables {list_of_keys} no están permitidas durante el lanzamiento. " +"Verifique la configuración de Aviso durante el lanzamiento en la Plantilla " +"de tareas para incluir las Variables adicionales." + +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "" +"La ruta de archivos absoluta local que contiene un Python virtualenv para " +"uso" + +#: awx/main/models/mixins.py:447 +msgid "{} is not a valid virtualenv in {}" +msgstr "{} no es un virtualenv válido en {}" + +#: awx/main/models/notifications.py:42 +msgid "Rocket.Chat" +msgstr "Rocket.Chat" + +#: awx/main/models/notifications.py:142 awx/main/models/unified_jobs.py:62 msgid "Pending" msgstr "Pendiente" -#: awx/main/models/notifications.py:139 awx/main/models/unified_jobs.py:62 +#: awx/main/models/notifications.py:143 awx/main/models/unified_jobs.py:65 msgid "Successful" msgstr "Correctamente" -#: awx/main/models/notifications.py:140 awx/main/models/unified_jobs.py:63 +#: awx/main/models/notifications.py:144 awx/main/models/unified_jobs.py:66 msgid "Failed" msgstr "Fallido" -#: awx/main/models/organization.py:132 -msgid "Token not invalidated" -msgstr "Token no invalidado" +#: awx/main/models/notifications.py:218 +msgid "status_str must be either succeeded or failed" +msgstr "status_str debe ser 'succeeded' o 'failed'" -#: awx/main/models/organization.py:133 -msgid "Token is expired" -msgstr "Token está expirado" +#: awx/main/models/oauth.py:27 +msgid "application" +msgstr "aplicación" -#: awx/main/models/organization.py:134 +#: awx/main/models/oauth.py:32 +msgid "Confidential" +msgstr "Confidencial" + +#: awx/main/models/oauth.py:33 +msgid "Public" +msgstr "Público" + +#: awx/main/models/oauth.py:41 +msgid "Authorization code" +msgstr "Código de autorización" + +#: awx/main/models/oauth.py:42 +msgid "Implicit" +msgstr "Implícito" + +#: awx/main/models/oauth.py:43 +msgid "Resource owner password-based" +msgstr "Basado en contraseña del propietario de recursos" + +#: awx/main/models/oauth.py:44 +msgid "Client credentials" +msgstr "Credenciales del cliente" + +#: awx/main/models/oauth.py:59 +msgid "Organization containing this application." +msgstr "Organización que contiene esta aplicación." + +#: awx/main/models/oauth.py:68 msgid "" -"The maximum number of allowed sessions for this user has been exceeded." +"Used for more stringent verification of access to an application when " +"creating a token." msgstr "" -"El número máximo de sesiones permitidas para este usuario ha sido excedido." +"Utilizado para una verificación más estricta de acceso a una aplicación al " +"crear un token." -#: awx/main/models/organization.py:137 -msgid "Invalid token" -msgstr "Token inválido" +#: awx/main/models/oauth.py:73 +msgid "" +"Set to Public or Confidential depending on how secure the client device is." +msgstr "" +"Establecer como Público o Confidencial según cuán seguro sea el dispositivo " +"del cliente." -#: awx/main/models/organization.py:155 -msgid "Reason the auth token was invalidated." -msgstr "Razón por la que el token fue invalidado." +#: awx/main/models/oauth.py:77 +msgid "" +"Set True to skip authorization step for completely trusted applications." +msgstr "" +"Se debe establecer como True para omitir el paso de autorización para " +"aplicaciones completamente confiables." -#: awx/main/models/organization.py:194 -msgid "Invalid reason specified" -msgstr "Razón especificada inválida" +#: awx/main/models/oauth.py:82 +msgid "" +"The Grant type the user must use for acquire tokens for this application." +msgstr "" +"El tipo de Permiso que debe usar el usuario para adquirir tokens para esta " +"aplicación." -#: awx/main/models/projects.py:43 +#: awx/main/models/oauth.py:90 +msgid "access token" +msgstr "Token de acceso" + +#: awx/main/models/oauth.py:98 +msgid "The user representing the token owner" +msgstr "El usuario que representa al propietario del token" + +#: awx/main/models/oauth.py:113 +msgid "" +"Allowed scopes, further restricts user's permissions. Must be a simple " +"space-separated string with allowed scopes ['read', 'write']." +msgstr "" +"Los alcances permitidos limitan aún más los permisos de los usuarios. Debe " +"ser una cadena simple y separada por espacios, con alcances permitidos " +"['lectura', 'escritura']." + +#: awx/main/models/projects.py:54 msgid "Git" msgstr "Git" -#: awx/main/models/projects.py:44 +#: awx/main/models/projects.py:55 msgid "Mercurial" msgstr "Mercurial" -#: awx/main/models/projects.py:45 +#: awx/main/models/projects.py:56 msgid "Subversion" msgstr "Subversion" -#: awx/main/models/projects.py:46 +#: awx/main/models/projects.py:57 msgid "Red Hat Insights" msgstr "Red Hat Insights" -#: awx/main/models/projects.py:72 +#: awx/main/models/projects.py:83 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." msgstr "" -"Ruta local (relativa a PROJECTS_ROOT) que contiene playbooks y ficheros " +"Ruta local (relativa a PROJECTS_ROOT) que contiene playbooks y archivos " "relacionados para este proyecto." -#: awx/main/models/projects.py:81 +#: awx/main/models/projects.py:92 msgid "SCM Type" msgstr "Tipo SCM" -#: awx/main/models/projects.py:82 +#: awx/main/models/projects.py:93 msgid "Specifies the source control system used to store the project." msgstr "" "Especifica el sistema de control de fuentes utilizado para almacenar el " "proyecto." -#: awx/main/models/projects.py:88 +#: awx/main/models/projects.py:99 msgid "SCM URL" -msgstr "SCM URL" +msgstr "URL de SCM" -#: awx/main/models/projects.py:89 +#: awx/main/models/projects.py:100 msgid "The location where the project is stored." -msgstr "La ubicación donde el proyecto está alojado." +msgstr "La ubicación donde está alojado el proyecto." -#: awx/main/models/projects.py:95 +#: awx/main/models/projects.py:106 msgid "SCM Branch" msgstr "Rama SCM" -#: awx/main/models/projects.py:96 +#: awx/main/models/projects.py:107 msgid "Specific branch, tag or commit to checkout." msgstr "Especificar rama, etiqueta o commit para checkout." -#: awx/main/models/projects.py:100 +#: awx/main/models/projects.py:111 msgid "Discard any local changes before syncing the project." msgstr "" "Descartar cualquier cambio local antes de la sincronización del proyecto." -#: awx/main/models/projects.py:104 +#: awx/main/models/projects.py:115 msgid "Delete the project before syncing." msgstr "Eliminar el proyecto antes de la sincronización." -#: awx/main/models/projects.py:133 -msgid "Invalid SCM URL." -msgstr "SCM URL inválida." - -#: awx/main/models/projects.py:136 -msgid "SCM URL is required." -msgstr "SCM URL es obligatoria." - #: awx/main/models/projects.py:144 +msgid "Invalid SCM URL." +msgstr "URL de SCM no válida." + +#: awx/main/models/projects.py:147 +msgid "SCM URL is required." +msgstr "URL de SCM es obligatoria." + +#: awx/main/models/projects.py:155 msgid "Insights Credential is required for an Insights Project." msgstr "Se requiere una credencial de Insights para un proyecto de Insights." -#: awx/main/models/projects.py:150 +#: awx/main/models/projects.py:161 msgid "Credential kind must be 'scm'." -msgstr "TIpo de credenciales deben ser 'scm'." +msgstr "Tipo de credenciales debe ser 'scm'." -#: awx/main/models/projects.py:167 +#: awx/main/models/projects.py:178 msgid "Invalid credential." -msgstr "Credencial inválida." +msgstr "Credencial no válida." -#: awx/main/models/projects.py:249 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "" -"Actualizar el proyecto mientras un trabajo es ejecutado que usa este " -"proyecto." +"Actualizar el proyecto mientras se ejecuta un trabajo que usa este proyecto." -#: awx/main/models/projects.py:254 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." msgstr "" -"El número de segundos desde de que la última actualización del proyecto se " -"ejecutó para que la nueva actualización del proyecto será ejecutada como un " +"El número de segundos desde que se ejecutó la última actualización del " +"proyecto para que la nueva actualización del proyecto se ejecute como un " "trabajo dependiente." -#: awx/main/models/projects.py:264 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "La última revisión obtenida por una actualización del proyecto." -#: awx/main/models/projects.py:271 +#: awx/main/models/projects.py:285 msgid "Playbook Files" -msgstr "Ficheros Playbook" +msgstr "Archivos Playbook" -#: awx/main/models/projects.py:272 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "Lista de playbooks encontrados en este proyecto" -#: awx/main/models/projects.py:279 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "Archivos de inventario" -#: awx/main/models/projects.py:280 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "" @@ -3163,97 +4011,145 @@ msgid "Admin" msgstr "Admin" #: awx/main/models/rbac.py:40 +msgid "Project Admin" +msgstr "Administrador de proyectos" + +#: awx/main/models/rbac.py:41 +msgid "Inventory Admin" +msgstr "Administrador de inventarios" + +#: awx/main/models/rbac.py:42 +msgid "Credential Admin" +msgstr "Administrador de credenciales" + +#: awx/main/models/rbac.py:43 +msgid "Workflow Admin" +msgstr "Administrador de flujos de trabajo" + +#: awx/main/models/rbac.py:44 +msgid "Notification Admin" +msgstr "Administrador de notificaciones" + +#: awx/main/models/rbac.py:45 msgid "Auditor" msgstr "Auditor" -#: awx/main/models/rbac.py:41 +#: awx/main/models/rbac.py:46 msgid "Execute" msgstr "Ejecutar" -#: awx/main/models/rbac.py:42 +#: awx/main/models/rbac.py:47 msgid "Member" msgstr "Miembro" -#: awx/main/models/rbac.py:43 +#: awx/main/models/rbac.py:48 msgid "Read" msgstr "Lectura" -#: awx/main/models/rbac.py:44 +#: awx/main/models/rbac.py:49 msgid "Update" msgstr "Actualización" -#: awx/main/models/rbac.py:45 +#: awx/main/models/rbac.py:50 msgid "Use" msgstr "Uso" -#: awx/main/models/rbac.py:49 +#: awx/main/models/rbac.py:54 msgid "Can manage all aspects of the system" msgstr "Puede gestionar todos los aspectos del sistema" -#: awx/main/models/rbac.py:50 +#: awx/main/models/rbac.py:55 msgid "Can view all settings on the system" -msgstr "Puede ver todas los ajustes del sistema" +msgstr "Puede ver todos los ajustes del sistema" -#: awx/main/models/rbac.py:51 +#: awx/main/models/rbac.py:56 msgid "May run ad hoc commands on an inventory" msgstr "Puede ejecutar comandos ad-hoc en un inventario" -#: awx/main/models/rbac.py:52 +#: awx/main/models/rbac.py:57 #, python-format msgid "Can manage all aspects of the %s" -msgstr "Puede gestionar todos los aspectos del %s" +msgstr "Puede gestionar todos los aspectos de %s" -#: awx/main/models/rbac.py:53 +#: awx/main/models/rbac.py:58 +#, python-format +msgid "Can manage all projects of the %s" +msgstr "Puede gestionar todos los proyectos de %s" + +#: awx/main/models/rbac.py:59 +#, python-format +msgid "Can manage all inventories of the %s" +msgstr "Puede gestionar todos los inventarios de %s" + +#: awx/main/models/rbac.py:60 +#, python-format +msgid "Can manage all credentials of the %s" +msgstr "Puede gestionar todas las credenciales de %s" + +#: awx/main/models/rbac.py:61 +#, python-format +msgid "Can manage all workflows of the %s" +msgstr "Puede gestionar todos los flujos de trabajo de %s" + +#: awx/main/models/rbac.py:62 +#, python-format +msgid "Can manage all notifications of the %s" +msgstr "Puede gestionar todas las notificaciones de %s" + +#: awx/main/models/rbac.py:63 #, python-format msgid "Can view all settings for the %s" -msgstr "Puede ver todas los ajustes para el %s" +msgstr "Puede ver todos los ajustes de %s" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:65 +msgid "May run any executable resources in the organization" +msgstr "Puede ejecutar cualquier recurso ejecutable en la organización" + +#: awx/main/models/rbac.py:66 #, python-format msgid "May run the %s" -msgstr "Puede ejecutar el %s" +msgstr "Puede ejecutar %s" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:68 #, python-format msgid "User is a member of the %s" -msgstr "Usuario es miembro del %s" +msgstr "Usuario es miembro de %s" -#: awx/main/models/rbac.py:56 +#: awx/main/models/rbac.py:69 #, python-format msgid "May view settings for the %s" -msgstr "Podría ver ajustes para el %s" +msgstr "Podría ver ajustes para %s" -#: awx/main/models/rbac.py:57 +#: awx/main/models/rbac.py:70 msgid "" "May update project or inventory or group using the configured source update " "system" msgstr "" -"Podría actualizar el proyecto o el inventario o el grupo utilizando el " -"sistema de actualización configurado en la fuente." +"Podría actualizar el proyecto o el inventario, así como el grupo utilizando " +"el sistema de actualización configurado en la fuente" -#: awx/main/models/rbac.py:58 +#: awx/main/models/rbac.py:71 #, python-format msgid "Can use the %s in a job template" -msgstr "Puede usar el %s en una plantilla de trabajo" +msgstr "Puede usar %s en una plantilla de trabajo" -#: awx/main/models/rbac.py:122 +#: awx/main/models/rbac.py:135 msgid "roles" msgstr "roles" -#: awx/main/models/rbac.py:434 +#: awx/main/models/rbac.py:441 msgid "role_ancestors" msgstr "role_ancestors" -#: awx/main/models/schedules.py:71 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "Habilita el procesamiento de esta programación." -#: awx/main/models/schedules.py:77 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." -msgstr "" -"La primera ocurrencia del programador sucede en o después de esta fecha." +msgstr "La primera ocurrencia del programador sucede en esta fecha o después." -#: awx/main/models/schedules.py:83 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." @@ -3261,111 +4157,133 @@ msgstr "" "La última ocurrencia del planificador sucede antes de esta fecha, justo " "después de que la planificación expire." -#: awx/main/models/schedules.py:87 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "Un valor representando la regla de programación recurrente iCal." -#: awx/main/models/schedules.py:93 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." -msgstr "La siguiente vez que la acción programa se ejecutará." +msgstr "La próxima vez que se ejecutará la acción programa." -#: awx/main/models/schedules.py:109 -msgid "Expected JSON" -msgstr "JSON esperado" - -#: awx/main/models/schedules.py:121 -msgid "days must be a positive integer." -msgstr "días debe ser un número entero." - -#: awx/main/models/unified_jobs.py:58 +#: awx/main/models/unified_jobs.py:61 msgid "New" msgstr "Nuevo" -#: awx/main/models/unified_jobs.py:60 +#: awx/main/models/unified_jobs.py:63 msgid "Waiting" msgstr "Esperando" -#: awx/main/models/unified_jobs.py:61 +#: awx/main/models/unified_jobs.py:64 msgid "Running" msgstr "Ejecutándose" -#: awx/main/models/unified_jobs.py:65 +#: awx/main/models/unified_jobs.py:68 msgid "Canceled" msgstr "Cancelado" -#: awx/main/models/unified_jobs.py:69 +#: awx/main/models/unified_jobs.py:72 msgid "Never Updated" msgstr "Nunca actualizado" -#: awx/main/models/unified_jobs.py:73 awx/ui/templates/ui/index.html:67 -#: awx/ui/templates/ui/index.html.py:86 +#: awx/main/models/unified_jobs.py:76 msgid "OK" -msgstr "OK" +msgstr "Correcto" -#: awx/main/models/unified_jobs.py:74 +#: awx/main/models/unified_jobs.py:77 msgid "Missing" msgstr "No encontrado" -#: awx/main/models/unified_jobs.py:78 +#: awx/main/models/unified_jobs.py:81 msgid "No External Source" msgstr "Sin fuente externa" -#: awx/main/models/unified_jobs.py:85 +#: awx/main/models/unified_jobs.py:88 msgid "Updating" msgstr "Actualizando" -#: awx/main/models/unified_jobs.py:429 +#: awx/main/models/unified_jobs.py:427 +msgid "Field is not allowed on launch." +msgstr "El campo no está permitido durante el lanzamiento." + +#: awx/main/models/unified_jobs.py:455 +#, python-brace-format +msgid "" +"Variables {list_of_keys} provided, but this template cannot accept " +"variables." +msgstr "" +"Variables {list_of_keys} provistas, aunque esta plantilla no puede aceptar " +"variables." + +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "Relanzar" -#: awx/main/models/unified_jobs.py:430 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "Callback" -#: awx/main/models/unified_jobs.py:431 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "Programado" -#: awx/main/models/unified_jobs.py:432 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "Dependencia" -#: awx/main/models/unified_jobs.py:433 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "Flujo de trabajo" -#: awx/main/models/unified_jobs.py:434 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "Sincronizar" -#: awx/main/models/unified_jobs.py:481 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "El nodo en el que se ejecutó la tarea." -#: awx/main/models/unified_jobs.py:507 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "La instancia que gestionó el entorno de ejecución aislado." + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." -msgstr "La fecha y hora que el trabajo fue puesto en la cola para iniciarse." +msgstr "" +"La fecha y hora en que el trabajo se colocó en la cola para iniciarse." -#: awx/main/models/unified_jobs.py:513 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." -msgstr "La fecha y hora en la que el trabajo finalizó su ejecución." +msgstr "La fecha y hora en que el trabajo finalizó su ejecución." -#: awx/main/models/unified_jobs.py:519 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." -msgstr "Tiempo transcurrido en segundos que el trabajo se ejecutó. " +msgstr "Tiempo transcurrido en segundos en que se ejecutó el trabajo." -#: awx/main/models/unified_jobs.py:541 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and" " capture stdout" msgstr "" -"Un campo de estado que indica el estado del trabajo si éste no fue capaz de " +"Un campo de estado que indica el estado del trabajo si este no fue capaz de " "ejecutarse y obtener la salida estándar." -#: awx/main/models/unified_jobs.py:580 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "El grupo Rampart/Instancia en el que se ejecutó la tarea" +#: awx/main/models/workflow.py:203 +#, python-brace-format +msgid "" +"Bad launch configuration starting template {template_pk} as part of workflow {workflow_pk}. Errors:\n" +"{error_text}" +msgstr "" +"Plantilla de inicio de la configuración de mal lanzamiento {template_pk} como parte del flujo de trabajo {workflow_pk}. Errores:\n" +"{error_text}" + +#: awx/main/models/workflow.py:388 +msgid "Field is not allowed for use in workflows." +msgstr "El campo no se permite para el uso en flujos de trabajo." + #: awx/main/notifications/base.py:17 #: awx/main/notifications/email_backend.py:28 msgid "" @@ -3375,196 +4293,222 @@ msgstr "" "{} #{} tenía el estado {}; ver detalles en {}\n" "\n" -#: awx/main/notifications/hipchat_backend.py:47 +#: awx/main/notifications/hipchat_backend.py:48 msgid "Error sending messages: {}" -msgstr "Error enviando mensajes: {}" +msgstr "Error al enviar mensajes: {}" -#: awx/main/notifications/hipchat_backend.py:49 +#: awx/main/notifications/hipchat_backend.py:50 msgid "Error sending message to hipchat: {}" -msgstr "Error enviando mensaje a hipchat: {}" +msgstr "Error al enviar mensaje a hipchat: {}" #: awx/main/notifications/irc_backend.py:54 msgid "Exception connecting to irc server: {}" -msgstr "Excepción conectando al servidor de ir: {}" +msgstr "Excepción al conectarse al servidor irc: {}" + +#: awx/main/notifications/mattermost_backend.py:48 +#: awx/main/notifications/mattermost_backend.py:50 +msgid "Error sending notification mattermost: {}" +msgstr "Error al enviar la notificación mattermost: {}" #: awx/main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" -msgstr "Excepción conectando a PagerDuty: {}" +msgstr "Excepción al conectarse a PagerDuty: {}" #: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 +#: awx/main/notifications/slack_backend.py:82 +#: awx/main/notifications/slack_backend.py:99 #: awx/main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" -msgstr "Excepción enviando mensajes: {}" +msgstr "Excepción al enviar mensajes: {}" + +#: awx/main/notifications/rocketchat_backend.py:46 +#: awx/main/notifications/rocketchat_backend.py:49 +msgid "Error sending notification rocket.chat: {}" +msgstr "Error al enviar la notificación rocket.chat: {}" #: awx/main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" -msgstr "Excepción conectando a Twilio: {}" +msgstr "Excepción al conectarse a Twilio: {}" #: awx/main/notifications/webhook_backend.py:38 #: awx/main/notifications/webhook_backend.py:40 msgid "Error sending notification webhook: {}" -msgstr "Error enviando notificación weebhook: {}" +msgstr "Error al enviar la notificación weebhook: {}" -#: awx/main/scheduler/task_manager.py:197 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" msgstr "" -"Trabajo generado desde un flujo de trabajo no pudo ser iniciado porque no " -"tenía el estado correcto o credenciales manuales eran solicitados." +"No se pudo iniciar el trabajo generado desde un flujo de trabajo porque no " +"tenía el estado correcto o se requerían credenciales manuales." -#: awx/main/scheduler/task_manager.py:201 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" msgstr "" -"Trabajo generado desde un flujo de trabajo no pudo ser iniciado porque no se" -" encontraron los recursos relacionados como un proyecto o un inventario." +"No se pudo iniciar un trabajo generado desde un flujo de trabajo porque no " +"se encontraron los recursos relacionados como un proyecto o un inventario." -#: awx/main/tasks.py:184 +#: awx/main/signals.py:616 +msgid "limit_reached" +msgstr "limit_reached" + +#: awx/main/tasks.py:282 msgid "Ansible Tower host usage over 90%" -msgstr "Ansible Tower uso de servidores por encima de 90%" +msgstr "Uso de hosts de Ansible Tower por encima de 90 %" -#: awx/main/tasks.py:189 +#: awx/main/tasks.py:287 msgid "Ansible Tower license will expire soon" -msgstr "Licencia de Ansible Tower expirará pronto" +msgstr "La licencia de Ansible Tower expirará pronto" -#: awx/main/tasks.py:318 -msgid "status_str must be either succeeded or failed" -msgstr "status_str debe ser 'succeeded' o 'failed'" +#: awx/main/tasks.py:1335 +msgid "Job could not start because it does not have a valid inventory." +msgstr "La tarea no se pudo iniciar por no tener un inventario válido." -#: awx/main/tasks.py:1549 -msgid "Dependent inventory update {} was canceled." -msgstr "Se canceló la actualización del inventario dependiente {}." - -#: awx/main/utils/common.py:89 +#: awx/main/utils/common.py:97 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "Imposible convertir \"%s\" a booleano" -#: awx/main/utils/common.py:235 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Tipo de SCM no soportado \"%s\"" -#: awx/main/utils/common.py:242 awx/main/utils/common.py:254 -#: awx/main/utils/common.py:273 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" -msgstr "URL %s inválida" +msgstr "URL %s no válida" -#: awx/main/utils/common.py:244 awx/main/utils/common.py:283 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "URL %s no soportada" -#: awx/main/utils/common.py:285 +#: awx/main/utils/common.py:304 #, python-format msgid "Unsupported host \"%s\" for file:// URL" -msgstr "Servidor \"%s\" no soportado para URL file://" +msgstr "Host \"%s\" no soportado para URL file://" -#: awx/main/utils/common.py:287 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "Servidor es obligatorio para URL %s" -#: awx/main/utils/common.py:305 +#: awx/main/utils/common.py:324 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "Usuario debe ser \"git\" para acceso SSH a %s." -#: awx/main/utils/common.py:311 +#: awx/main/utils/common.py:330 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "Usuario debe ser \"hg\" para acceso SSH a %s." -#: awx/main/validators.py:60 +#: awx/main/utils/common.py:611 +#, 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:644 +#, python-brace-format +msgid "Variables not compatible with JSON standard (error: {json_error})" +msgstr "Variables no compatibles con el estándar JSON (error: {json_error})" + +#: awx/main/utils/common.py:650 +#, python-brace-format +msgid "" +"Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." +msgstr "" +"No se puede analizar como JSON (error: {json_error}) o YAML (error: " +"{yaml_error})." + +#: awx/main/validators.py:67 #, python-format msgid "Invalid certificate or key: %s..." msgstr "Clave o certificado no válido: %s…" -#: awx/main/validators.py:74 +#: awx/main/validators.py:83 #, python-format msgid "Invalid private key: unsupported type \"%s\"" -msgstr "Clave privada inválida: tipo no soportado \"%s\"" +msgstr "Clave privada no válida: tipo no soportado \"%s\"" -#: awx/main/validators.py:78 +#: awx/main/validators.py:87 #, python-format msgid "Unsupported PEM object type: \"%s\"" -msgstr "TIpo de objeto PEM no soportado: \"%s\"" +msgstr "Tipo de objeto PEM no soportado: \"%s\"" -#: awx/main/validators.py:103 +#: awx/main/validators.py:112 msgid "Invalid base64-encoded data" -msgstr "Datos codificados en base64 inválidos" +msgstr "Datos codificados en base64 no válidos" -#: awx/main/validators.py:122 +#: awx/main/validators.py:131 msgid "Exactly one private key is required." -msgstr "Exactamente una clave privada es necesaria." +msgstr "Se requiere exactamente una clave privada." -#: awx/main/validators.py:124 +#: awx/main/validators.py:133 msgid "At least one private key is required." -msgstr "Al menos una clave privada es necesaria." +msgstr "Se requiere al menos una clave privada." -#: awx/main/validators.py:126 +#: awx/main/validators.py:135 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d " "provided." msgstr "" -"Al menos %(min_keys)d claves privadas son necesarias, sólo %(key_count)d han" -" sido proporcionadas." +"Se requieren al menos %(min_keys)d claves privadas; solo se proporcionaron " +"%(key_count)d." -#: awx/main/validators.py:129 +#: awx/main/validators.py:138 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." -msgstr "" -"Sólo una clave privada está permitida, %(key_count)d han sido " -"proporcionadas." +msgstr "Solo se permite una clave privada; se proporcionaron %(key_count)d." -#: awx/main/validators.py:131 +#: awx/main/validators.py:140 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." msgstr "" -"No más de %(max_keys)d claves privadas son permitidas, %(key_count)d han " -"sido proporcionadas." +"No se permiten más de %(max_keys)d claves privadas; se proporcionaron " +"%(key_count)d." -#: awx/main/validators.py:136 +#: awx/main/validators.py:145 msgid "Exactly one certificate is required." -msgstr "Exactamente un certificado es necesario." +msgstr "Se requiere exactamente un certificado." -#: awx/main/validators.py:138 +#: awx/main/validators.py:147 msgid "At least one certificate is required." -msgstr "Al menos un certificado es necesario." +msgstr "Se requiere al menos un certificado." -#: awx/main/validators.py:140 +#: awx/main/validators.py:149 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " "provided." msgstr "" -"Al menos %(min_certs)d certificados son necesarios, sólo %(cert_count)d han " -"sido proporcionados." +"Se requieren al menos %(min_certs)d certificados; solo se proporcionaron " +"%(cert_count)d." -#: awx/main/validators.py:143 +#: awx/main/validators.py:152 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." -msgstr "" -"Sólo un certificado está permitido, %(cert_count)d han sido proporcionados." +msgstr "Solo se permite un certificado; se proporcionaron %(cert_count)d." -#: awx/main/validators.py:145 +#: awx/main/validators.py:154 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d " "provided." msgstr "" -"No más de %(max_certs)d certificados están permitidos, %(cert_count)d han " -"sido proporcionados." +"No se permiten más de %(max_certs)d certificados; se proporcionaron " +"%(cert_count)d." #: awx/main/views.py:23 msgid "API Error" -msgstr "API Error" +msgstr "Error de API" #: awx/main/views.py:61 msgid "Bad Request" @@ -3572,7 +4516,7 @@ msgstr "Solicitud incorrecta" #: awx/main/views.py:62 msgid "The request could not be understood by the server." -msgstr "La petición no puede ser entendida por el servidor." +msgstr "El servidor no puede entender la petición." #: awx/main/views.py:69 msgid "Forbidden" @@ -3588,7 +4532,7 @@ msgstr "No encontrado" #: awx/main/views.py:78 msgid "The requested resource could not be found." -msgstr "El recurso solicitado no pudo ser encontrado." +msgstr "No se pudo encontrar el recurso solicitado." #: awx/main/views.py:85 msgid "Server Error" @@ -3596,289 +4540,289 @@ msgstr "Error de servidor" #: awx/main/views.py:86 msgid "A server error has occurred." -msgstr "Un error en el servidor ha ocurrido." +msgstr "Se produjo un error de servidor." -#: awx/settings/defaults.py:665 +#: awx/settings/defaults.py:721 msgid "US East (Northern Virginia)" -msgstr "Este de EE.UU. (Virginia del norte)" +msgstr "Este de EE. UU. (Virginia del norte)" -#: awx/settings/defaults.py:666 +#: awx/settings/defaults.py:722 msgid "US East (Ohio)" -msgstr "Este de EE.UU. (Ohio)" +msgstr "Este de EE. UU. (Ohio)" -#: awx/settings/defaults.py:667 +#: awx/settings/defaults.py:723 msgid "US West (Oregon)" -msgstr "Oeste de EE.UU. (Oregón)" +msgstr "Oeste de EE. UU. (Oregón)" -#: awx/settings/defaults.py:668 +#: awx/settings/defaults.py:724 msgid "US West (Northern California)" -msgstr "Oeste de EE.UU (California del norte)" +msgstr "Oeste de EE. UU (California del norte)" -#: awx/settings/defaults.py:669 +#: awx/settings/defaults.py:725 msgid "Canada (Central)" -msgstr "Canada (Central)" +msgstr "Canadá (Central)" -#: awx/settings/defaults.py:670 +#: awx/settings/defaults.py:726 msgid "EU (Frankfurt)" msgstr "UE (Fráncfort)" -#: awx/settings/defaults.py:671 +#: awx/settings/defaults.py:727 msgid "EU (Ireland)" msgstr "UE (Irlanda)" -#: awx/settings/defaults.py:672 +#: awx/settings/defaults.py:728 msgid "EU (London)" msgstr "UE (Londres)" -#: awx/settings/defaults.py:673 +#: awx/settings/defaults.py:729 msgid "Asia Pacific (Singapore)" msgstr "Asia Pacífico (Singapur)" -#: awx/settings/defaults.py:674 +#: awx/settings/defaults.py:730 msgid "Asia Pacific (Sydney)" msgstr "Asia Pacífico (Sídney)" -#: awx/settings/defaults.py:675 +#: awx/settings/defaults.py:731 msgid "Asia Pacific (Tokyo)" msgstr "Asia Pacífico (Tokio)" -#: awx/settings/defaults.py:676 +#: awx/settings/defaults.py:732 msgid "Asia Pacific (Seoul)" msgstr "Asia Pacífico (Seúl)" -#: awx/settings/defaults.py:677 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Mumbai)" msgstr "Asia Pacífico (Bombay)" -#: awx/settings/defaults.py:678 +#: awx/settings/defaults.py:734 msgid "South America (Sao Paulo)" msgstr "América del sur (São Paulo)" -#: awx/settings/defaults.py:679 +#: awx/settings/defaults.py:735 msgid "US West (GovCloud)" -msgstr "Oeste de EE.UU. (GovCloud)" +msgstr "Oeste de EE. UU. (GovCloud)" -#: awx/settings/defaults.py:680 +#: awx/settings/defaults.py:736 msgid "China (Beijing)" msgstr "China (Pekín)" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:785 msgid "US East 1 (B)" msgstr "Este de EE. UU. 1 (B)" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:786 msgid "US East 1 (C)" msgstr "Este de EE. UU. 1 (C)" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:787 msgid "US East 1 (D)" msgstr "Este de EE. UU. 1 (D)" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:788 msgid "US East 4 (A)" msgstr "Este de EE. UU. 4 (A)" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:789 msgid "US East 4 (B)" msgstr "Este de EE. UU. 4 (B)" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:790 msgid "US East 4 (C)" msgstr "Este de EE. UU. 4 (C)" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:791 msgid "US Central (A)" -msgstr "EE.UU. Central (A)" +msgstr "EE. UU. Central (A)" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:792 msgid "US Central (B)" -msgstr "EE.UU. Central (B)" +msgstr "EE. UU. Central (B)" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:793 msgid "US Central (C)" -msgstr "EE.UU. Central (C)" +msgstr "EE. UU. Central (C)" -#: awx/settings/defaults.py:738 +#: awx/settings/defaults.py:794 msgid "US Central (F)" -msgstr "EE.UU. Central (F)" +msgstr "EE. UU. Central (F)" -#: awx/settings/defaults.py:739 +#: awx/settings/defaults.py:795 msgid "US West (A)" msgstr "Oeste de EE. UU. (A)" -#: awx/settings/defaults.py:740 +#: awx/settings/defaults.py:796 msgid "US West (B)" msgstr "Oeste de EE. UU. (B)" -#: awx/settings/defaults.py:741 +#: awx/settings/defaults.py:797 msgid "US West (C)" msgstr "Oeste de EE. UU. (C)" -#: awx/settings/defaults.py:742 +#: awx/settings/defaults.py:798 msgid "Europe West 1 (B)" msgstr "Oeste de Europa 1 (B)" -#: awx/settings/defaults.py:743 +#: awx/settings/defaults.py:799 msgid "Europe West 1 (C)" msgstr "Oeste de Europa 1 (C)" -#: awx/settings/defaults.py:744 +#: awx/settings/defaults.py:800 msgid "Europe West 1 (D)" msgstr "Oeste de Europa 1 (D)" -#: awx/settings/defaults.py:745 +#: awx/settings/defaults.py:801 msgid "Europe West 2 (A)" msgstr "Oeste de Europa 2 (A)" -#: awx/settings/defaults.py:746 +#: awx/settings/defaults.py:802 msgid "Europe West 2 (B)" msgstr "Oeste de Europa 2 (B)" -#: awx/settings/defaults.py:747 +#: awx/settings/defaults.py:803 msgid "Europe West 2 (C)" msgstr "Oeste de Europa 2 (C)" -#: awx/settings/defaults.py:748 +#: awx/settings/defaults.py:804 msgid "Asia East (A)" msgstr "Este de Asia (A)" -#: awx/settings/defaults.py:749 +#: awx/settings/defaults.py:805 msgid "Asia East (B)" msgstr "Este de Asia (B)" -#: awx/settings/defaults.py:750 +#: awx/settings/defaults.py:806 msgid "Asia East (C)" msgstr "Este de Asia (C)" -#: awx/settings/defaults.py:751 +#: awx/settings/defaults.py:807 msgid "Asia Southeast (A)" msgstr "Sudeste de Asia (A)" -#: awx/settings/defaults.py:752 +#: awx/settings/defaults.py:808 msgid "Asia Southeast (B)" msgstr "Sudeste de Asia (B)" -#: awx/settings/defaults.py:753 +#: awx/settings/defaults.py:809 msgid "Asia Northeast (A)" msgstr "Noreste de Asia (A)" -#: awx/settings/defaults.py:754 +#: awx/settings/defaults.py:810 msgid "Asia Northeast (B)" msgstr "Noreste de Asia (B)" -#: awx/settings/defaults.py:755 +#: awx/settings/defaults.py:811 msgid "Asia Northeast (C)" msgstr "Noreste de Asia (C)" -#: awx/settings/defaults.py:756 +#: awx/settings/defaults.py:812 msgid "Australia Southeast (A)" msgstr "Sudeste de Australia (A)" -#: awx/settings/defaults.py:757 +#: awx/settings/defaults.py:813 msgid "Australia Southeast (B)" msgstr "Sudeste de Australia (B)" -#: awx/settings/defaults.py:758 +#: awx/settings/defaults.py:814 msgid "Australia Southeast (C)" msgstr "Sudeste de Australia (C)" -#: awx/settings/defaults.py:780 +#: awx/settings/defaults.py:836 msgid "US East" -msgstr "Este de EE.UU." +msgstr "Este de EE. UU." -#: awx/settings/defaults.py:781 +#: awx/settings/defaults.py:837 msgid "US East 2" -msgstr "Este de EE.UU. 2" +msgstr "Este de EE. UU. 2" -#: awx/settings/defaults.py:782 +#: awx/settings/defaults.py:838 msgid "US Central" -msgstr "EE.UU. Central" +msgstr "EE. UU. Central" -#: awx/settings/defaults.py:783 +#: awx/settings/defaults.py:839 msgid "US North Central" -msgstr "Norte-centro de EE.UU." +msgstr "Norte-Centro de EE. UU." -#: awx/settings/defaults.py:784 +#: awx/settings/defaults.py:840 msgid "US South Central" -msgstr "Sur-Centro de EE.UU." +msgstr "Sur-Centro de EE. UU." -#: awx/settings/defaults.py:785 +#: awx/settings/defaults.py:841 msgid "US West Central" msgstr "Oeste central de EE. UU." -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:842 msgid "US West" -msgstr "Oeste de EE.UU." +msgstr "Oeste de EE. UU." -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:843 msgid "US West 2" msgstr "Oeste de EE. UU. 2" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:844 msgid "Canada East" msgstr "Este de Canadá" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:845 msgid "Canada Central" msgstr "Canadá Central" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:846 msgid "Brazil South" msgstr "Sur de Brasil" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:847 msgid "Europe North" msgstr "Norte de Europa" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:848 msgid "Europe West" msgstr "Oeste de Europa" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:849 msgid "UK West" msgstr "Oeste del Reino Unido" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:850 msgid "UK South" msgstr "Sur del Reino Unido" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:851 msgid "Asia East" msgstr "Este de Asia" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:852 msgid "Asia Southeast" msgstr "Sudeste de Asia" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:853 msgid "Australia East" msgstr "Este de Australia" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:854 msgid "Australia Southeast" msgstr "Sudeste de Australia" -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:855 msgid "India West" msgstr "Oeste de India" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:856 msgid "India South" msgstr "Sur de India" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:857 msgid "Japan East" msgstr "Este de Japón" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:858 msgid "Japan West" msgstr "Oeste de Japón" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:859 msgid "Korea Central" msgstr "Corea central" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:860 msgid "Korea South" msgstr "Sur de Corea" @@ -3891,9 +4835,9 @@ msgid "" "Mapping to organization admins/users from social auth accounts. This setting\n" "controls which users are placed into which Tower organizations based on their\n" "username and email address. Configuration details are available in the Ansible\n" -"Tower documentation.'" +"Tower documentation." msgstr "" -"Mapeo de administradores o usuarios de la organización desde cuentas de " +"Asignación a administradores o usuarios de la organización desde cuentas de " "autorización social. Esta configuración controla qué usuarios se ubican en " "qué organizaciones de Tower en función de su nombre de usuario y dirección " "de correo electrónico. Detalles de configuración disponibles en la " @@ -3909,27 +4853,27 @@ msgstr "" #: awx/sso/conf.py:80 msgid "Authentication Backends" -msgstr "Backends para autentificación" +msgstr "Backends para autenticación" #: awx/sso/conf.py:81 msgid "" "List of authentication backends that are enabled based on license features " "and other authentication settings." msgstr "" -"Listado de backends de autentificación que están habilitados basados en " -"funcionalidades de la licencia y otros ajustes de autentificación." +"Listado de backends de autenticación que están habilitados basados en " +"funcionalidades de la licencia y otros ajustes de autenticación." #: awx/sso/conf.py:94 msgid "Social Auth Organization Map" -msgstr "Autentificación social - Mapa de organización" +msgstr "Autenticación social - Mapa de organización" #: awx/sso/conf.py:106 msgid "Social Auth Team Map" -msgstr "Autentificación social - Mapa de equipos" +msgstr "Autenticación social - Mapa de equipos" #: awx/sso/conf.py:118 msgid "Social Auth User Fields" -msgstr "Autentificación social - Campos usuario" +msgstr "Autenticación social - Campos de usuario" #: awx/sso/conf.py:119 msgid "" @@ -3937,41 +4881,43 @@ msgid "" " being created. Only users who have previously logged in using social auth " "or have a user account with a matching email address will be able to login." msgstr "" -"Cuando se establece una lista vacía `[]`, esta configuración previene que " -"nuevos usuarios puedan ser creados. Sólo usuarios que previamente han " -"iniciado sesión usando autentificación social o tengan una cuenta de usuario" -" que corresponda con la dirección de correcto podrán iniciar sesión." +"Cuando se establece una lista vacía `[]`, esta configuración impide que se " +"puedan crear nuevos usuarios. Solo los usuarios que han iniciado sesión " +"previamente usando autenticación social o que tengan una cuenta de usuario " +"que corresponda con la dirección de correo electrónico podrán iniciar " +"sesión." -#: awx/sso/conf.py:137 +#: awx/sso/conf.py:141 msgid "LDAP Server URI" -msgstr "URI servidor LDAP" +msgstr "URI de servidor LDAP" -#: awx/sso/conf.py:138 +#: awx/sso/conf.py:142 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be" " specified by separating with spaces or commas. LDAP authentication is " "disabled if this parameter is empty." msgstr "" -"URI para conectar a un servidor LDAP. Por ejemplo " -"\"ldap://ldap.ejemplo.com:389\" (no SSL) or \"ldaps://ldap.ejemplo.com:636\"" -" (SSL). Varios servidores LDAP pueden ser especificados separados por " -"espacios o comandos. La autentificación LDAP está deshabilitado si este " -"parámetro está vacío." +"URI para conectarse a un servidor LDAP. Por ejemplo, " +"\"ldap://ldap.ejemplo.com:389\" (no SSL) o \"ldaps://ldap.ejemplo.com:636\" " +"(SSL). Se pueden especificar varios servidores LDAP separados por espacios o" +" comas. La autenticación LDAP está deshabilitada si este parámetro está " +"vacío." -#: awx/sso/conf.py:142 awx/sso/conf.py:158 awx/sso/conf.py:170 -#: awx/sso/conf.py:182 awx/sso/conf.py:198 awx/sso/conf.py:218 -#: awx/sso/conf.py:240 awx/sso/conf.py:255 awx/sso/conf.py:273 -#: awx/sso/conf.py:290 awx/sso/conf.py:307 awx/sso/conf.py:323 -#: awx/sso/conf.py:337 awx/sso/conf.py:354 awx/sso/conf.py:380 +#: awx/sso/conf.py:146 awx/sso/conf.py:162 awx/sso/conf.py:174 +#: awx/sso/conf.py:186 awx/sso/conf.py:202 awx/sso/conf.py:222 +#: awx/sso/conf.py:244 awx/sso/conf.py:259 awx/sso/conf.py:277 +#: awx/sso/conf.py:294 awx/sso/conf.py:306 awx/sso/conf.py:332 +#: awx/sso/conf.py:348 awx/sso/conf.py:362 awx/sso/conf.py:380 +#: awx/sso/conf.py:406 msgid "LDAP" msgstr "LDAP" -#: awx/sso/conf.py:154 +#: awx/sso/conf.py:158 msgid "LDAP Bind DN" msgstr "DN para enlazar con LDAP" -#: awx/sso/conf.py:155 +#: awx/sso/conf.py:159 msgid "" "DN (Distinguished Name) of user to bind for all search queries. This is the " "system user account we will use to login to query LDAP for other user " @@ -3983,27 +4929,27 @@ msgstr "" "Consulte la documentación de Ansible Tower para acceder a ejemplos de " "sintaxis." -#: awx/sso/conf.py:168 +#: awx/sso/conf.py:172 msgid "LDAP Bind Password" msgstr "Contraseña para enlazar con LDAP" -#: awx/sso/conf.py:169 +#: awx/sso/conf.py:173 msgid "Password used to bind LDAP user account." msgstr "Contraseña usada para enlazar a LDAP con la cuenta de usuario." -#: awx/sso/conf.py:180 +#: awx/sso/conf.py:184 msgid "LDAP Start TLS" msgstr "LDAP - Iniciar TLS" -#: awx/sso/conf.py:181 +#: awx/sso/conf.py:185 msgid "Whether to enable TLS when the LDAP connection is not using SSL." -msgstr "Para activar o no TLS cuando la conexión LDAP no utilice SSL" +msgstr "Para activar o no TLS cuando la conexión LDAP no utilice SSL." -#: awx/sso/conf.py:191 +#: awx/sso/conf.py:195 msgid "LDAP Connection Options" msgstr "Opciones de conexión a LDAP" -#: awx/sso/conf.py:192 +#: awx/sso/conf.py:196 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -4011,18 +4957,18 @@ msgid "" "https://www.python-ldap.org/doc/html/ldap.html#options for possible options " "and values that can be set." msgstr "" -"Opciones adicionales a establecer para la conexión LDAP. Referenciadores " -"LDAP están deshabilitados por defecto (para prevenir que ciertas consultas " +"Opciones adicionales por establecer para la conexión LDAP. Las referencias " +"LDAP están deshabilitadas por defecto (para prevenir que ciertas consultas " "LDAP se bloqueen con AD). Los nombres de las opciones deben ser cadenas de " -"texto (p.e. \"OPT_REFERRALS\"). Acuda a https://www.python-" -"ldap.org/doc/html/ldap.html#options para posibles opciones y valores que " -"pueden ser establecidos." +"texto (p. ej., \"OPT_REFERRALS\"). Consulte https://www.python-" +"ldap.org/doc/html/ldap.html#options para conocer posibles opciones y valores" +" que se pueden establecer." -#: awx/sso/conf.py:211 +#: awx/sso/conf.py:215 msgid "LDAP User Search" msgstr "Búsqueda de usuarios LDAP" -#: awx/sso/conf.py:212 +#: awx/sso/conf.py:216 msgid "" "LDAP search query to find users. Any user that matches the given pattern " "will be able to login to Tower. The user should also be mapped into a Tower" @@ -4032,16 +4978,16 @@ msgid "" msgstr "" "Búsqueda en LDAP para encontrar usuarios. Cualquier usuario que se ajuste a " "un patrón determinado podrá iniciar sesión en Tower. El usuario también " -"debería mapearse en una organización de Tower (conforme se define en la " +"debería asignarse en una organización de Tower (conforme se define en la " "configuración AUTH_LDAP_ORGANIZATION_MAP). Si es necesario respaldar varias " "búsquedas, es posible utilizar \"LDAPUnion\". Consulte la documentación de " "Tower para acceder a información detallada." -#: awx/sso/conf.py:234 +#: awx/sso/conf.py:238 msgid "LDAP User DN Template" msgstr "Plantilla de DN para el usuario LDAP" -#: awx/sso/conf.py:235 +#: awx/sso/conf.py:239 msgid "" "Alternative to user search, if user DNs are all of the same format. This " "approach is more efficient for user lookups than searching if it is usable " @@ -4051,89 +4997,99 @@ msgstr "" "Alternativa a la búsqueda de usuarios, en caso de que los DN de los usuarios" " tengan todos el mismo formato. Este enfoque es más efectivo para buscar " "usuarios que las búsquedas, en caso de poder utilizarse en el entorno de su " -"organización. Si esta configuración tiene un valor, se utilizará en vez de " -"AUTH_LDAP_USER_SEARCH." +"organización. Si esta configuración tiene un valor, se utilizará en lugar de" +" AUTH_LDAP_USER_SEARCH." -#: awx/sso/conf.py:250 +#: awx/sso/conf.py:254 msgid "LDAP User Attribute Map" msgstr "Mapa de atributos de usuario LDAP" -#: awx/sso/conf.py:251 +#: awx/sso/conf.py:255 msgid "" "Mapping of LDAP user schema to Tower API user attributes. The default " "setting is valid for ActiveDirectory but users with other LDAP " "configurations may need to change the values. Refer to the Ansible Tower " -"documentation for additonal details." +"documentation for additional details." msgstr "" -"Mapeo de esquemas de usuarios de LDAP con los atributos de usuario API de " -"Tower. La configuración predeterminada es válida para ActiveDirectory. Sin " -"embargo, los usuarios con otras configuraciones de LDAP tal vez necesiten " -"modificar los valores. Consulte la documentación de Ansible Tower para " -"acceder a información detallada." +"Asignación de esquemas de usuarios de LDAP con los atributos de usuario API " +"de Tower. La configuración predeterminada es válida para ActiveDirectory. " +"Sin embargo, los usuarios con otras configuraciones de LDAP tal vez " +"necesiten modificar los valores. Consulte la documentación de Ansible Tower " +"para acceder a información detallada." -#: awx/sso/conf.py:269 +#: awx/sso/conf.py:273 msgid "LDAP Group Search" msgstr "Búsqueda de grupos LDAP" -#: awx/sso/conf.py:270 +#: awx/sso/conf.py:274 msgid "" "Users are mapped to organizations based on their membership in LDAP groups. " "This setting defines the LDAP search query to find groups. Unlike the user " "search, group search does not support LDAPSearchUnion." msgstr "" -"Se mapea a los usuarios en las organizaciones en función de su membresía en " -"los grupos LDAP. Esta configuración define la búsqueda de LDAP para " +"Se asigna a los usuarios en las organizaciones en función de su membresía en" +" los grupos LDAP. Esta configuración define la búsqueda de LDAP para " "encontrar grupos. A diferencia de la búsqueda de usuarios, la búsqueda de " "grupos no es compatible con LDAPSearchUnion." -#: awx/sso/conf.py:286 +#: awx/sso/conf.py:290 msgid "LDAP Group Type" msgstr "Tipo de grupo LDAP" -#: awx/sso/conf.py:287 +#: awx/sso/conf.py:291 msgid "" "The group type may need to be changed based on the type of the LDAP server." -" Values are listed at: http://pythonhosted.org/django-auth-ldap/groups.html" -"#types-of-groups" +" Values are listed at: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" msgstr "" -"El tipo de grupo es necesario ser cambiado basado en el tipo del servidor de" -" LDAP. Valores posibles listados en: http://pythonhosted.org/django-auth-" -"ldap/groups.html#types-of-groups" +"Puede tener que cambiarse el tipo de grupo en función del tipo de servidor " +"de LDAP. Los valores se enumeran en: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" -#: awx/sso/conf.py:302 +#: awx/sso/conf.py:304 +msgid "LDAP Group Type Parameters" +msgstr "Parámetros del tipo de grupo LDAP" + +#: awx/sso/conf.py:305 +msgid "Key value parameters to send the chosen group type init method." +msgstr "" +"Parámetros de valor clave para enviar al método de inicio del tipo de grupo " +"elegido." + +#: awx/sso/conf.py:327 msgid "LDAP Require Group" msgstr "Grupo LDAP requerido" -#: awx/sso/conf.py:303 +#: awx/sso/conf.py:328 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " "search will be able to login via Tower. Only one require group is supported." msgstr "" "Grupo DN necesario para iniciar sesión. Si se especifica, el usuario debe " -"ser miembro de este grupo para iniciar sesión usando LDAP. SI no se " +"ser miembro de este grupo para iniciar sesión mediante LDAP. Si no se " "establece, cualquiera en LDAP que corresponda con la búsqueda de usuario " "será capaz de iniciar sesión en Tower. No es posible especificar varios " "grupos." -#: awx/sso/conf.py:319 +#: awx/sso/conf.py:344 msgid "LDAP Deny Group" msgstr "Grupo LDAP no permitido" -#: awx/sso/conf.py:320 +#: awx/sso/conf.py:345 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." msgstr "" -"Grupo DN no permitido para iniciar sesión. SI se especifica, el usuario no " -"podrá iniciar sesión si es miembro de este grupo. Sólo un grupo no permitido" +"Grupo DN no permitido para iniciar sesión. Si se especifica, el usuario no " +"podrá iniciar sesión si es miembro de este grupo. Solo un grupo no permitido" " está soportado." -#: awx/sso/conf.py:333 +#: awx/sso/conf.py:358 msgid "LDAP User Flags By Group" msgstr "Indicadores de usuario LDAP por grupo" -#: awx/sso/conf.py:334 +#: awx/sso/conf.py:359 msgid "" "Retrieve users from a given group. At this time, superuser and system " "auditors are the only groups supported. Refer to the Ansible Tower " @@ -4143,11 +5099,11 @@ msgstr "" " y los auditores del sistema son los únicos grupos que se admiten. Consulte " "la documentación de Ansible Tower para obtener información detallada." -#: awx/sso/conf.py:349 +#: awx/sso/conf.py:375 msgid "LDAP Organization Map" msgstr "Mapa de organización LDAP" -#: awx/sso/conf.py:350 +#: awx/sso/conf.py:376 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " "which users are placed into which Tower organizations relative to their LDAP" @@ -4159,11 +5115,11 @@ msgstr "" "organizaciones de Tower en función de sus membresías en grupos de LDAP. " "Detalles de configuración disponibles en la documentación de Ansible Tower." -#: awx/sso/conf.py:377 +#: awx/sso/conf.py:403 msgid "LDAP Team Map" msgstr "Mapa de equipos LDAP" -#: awx/sso/conf.py:378 +#: awx/sso/conf.py:404 msgid "" "Mapping between team members (users) and LDAP groups. Configuration details " "are available in the Ansible Tower documentation." @@ -4171,11 +5127,11 @@ msgstr "" "Mapear entre los miembros de equipos (usuarios) y grupos de LDAP. Detalles " "de configuración disponibles en la documentación de Ansible Tower." -#: awx/sso/conf.py:406 +#: awx/sso/conf.py:440 msgid "RADIUS Server" msgstr "Servidor RADIUS" -#: awx/sso/conf.py:407 +#: awx/sso/conf.py:441 msgid "" "Hostname/IP of RADIUS server. RADIUS authentication is disabled if this " "setting is empty." @@ -4183,80 +5139,80 @@ msgstr "" "Hostname/IP del servidor RADIUS. La autenticación de RADIUS se desactiva si " "esta configuración está vacía." -#: awx/sso/conf.py:409 awx/sso/conf.py:423 awx/sso/conf.py:435 +#: awx/sso/conf.py:443 awx/sso/conf.py:457 awx/sso/conf.py:469 #: awx/sso/models.py:14 msgid "RADIUS" msgstr "RADIUS" -#: awx/sso/conf.py:421 +#: awx/sso/conf.py:455 msgid "RADIUS Port" msgstr "Puerto RADIUS" -#: awx/sso/conf.py:422 +#: awx/sso/conf.py:456 msgid "Port of RADIUS server." msgstr "Puerto del servidor RADIUS" -#: awx/sso/conf.py:433 +#: awx/sso/conf.py:467 msgid "RADIUS Secret" msgstr "Clave secreta RADIUS" -#: awx/sso/conf.py:434 +#: awx/sso/conf.py:468 msgid "Shared secret for authenticating to RADIUS server." -msgstr "Clave secreta compartida para autentificación a RADIUS." +msgstr "Clave secreta compartida para autenticación a RADIUS." -#: awx/sso/conf.py:450 +#: awx/sso/conf.py:484 msgid "TACACS+ Server" msgstr "Servidor TACACS+" -#: awx/sso/conf.py:451 +#: awx/sso/conf.py:485 msgid "Hostname of TACACS+ server." msgstr "Nombre de host del servidor TACACS+." -#: awx/sso/conf.py:452 awx/sso/conf.py:465 awx/sso/conf.py:478 -#: awx/sso/conf.py:491 awx/sso/conf.py:503 awx/sso/models.py:15 +#: awx/sso/conf.py:486 awx/sso/conf.py:499 awx/sso/conf.py:512 +#: awx/sso/conf.py:525 awx/sso/conf.py:537 awx/sso/models.py:15 msgid "TACACS+" msgstr "TACACS+" -#: awx/sso/conf.py:463 +#: awx/sso/conf.py:497 msgid "TACACS+ Port" msgstr "Puerto TACACS+" -#: awx/sso/conf.py:464 +#: awx/sso/conf.py:498 msgid "Port number of TACACS+ server." msgstr "Número de puerto del servidor TACACS+." -#: awx/sso/conf.py:476 +#: awx/sso/conf.py:510 msgid "TACACS+ Secret" msgstr "Clave secreta TACACS+" -#: awx/sso/conf.py:477 +#: awx/sso/conf.py:511 msgid "Shared secret for authenticating to TACACS+ server." msgstr "" -"Clave secreta compartida para la autentificación en el servidor TACACS+." +"Clave secreta compartida para la autenticación en el servidor TACACS+." -#: awx/sso/conf.py:489 +#: awx/sso/conf.py:523 msgid "TACACS+ Auth Session Timeout" msgstr "Tiempo de espera para la sesión de autenticación de TACACS+" -#: awx/sso/conf.py:490 +#: awx/sso/conf.py:524 msgid "TACACS+ session timeout value in seconds, 0 disables timeout." msgstr "" "Valor de tiempo de espera para la sesión TACACS+ en segundos. El valor 0 " "deshabilita el tiempo de espera." -#: awx/sso/conf.py:501 +#: awx/sso/conf.py:535 msgid "TACACS+ Authentication Protocol" msgstr "Protocolo de autenticación de TACACS+" -#: awx/sso/conf.py:502 +#: awx/sso/conf.py:536 msgid "Choose the authentication protocol used by TACACS+ client." msgstr "Elija el protocolo de autenticación utilizado por el cliente TACACS+." -#: awx/sso/conf.py:517 +#: awx/sso/conf.py:551 msgid "Google OAuth2 Callback URL" msgstr "Google OAuth2 Callback URL" -#: awx/sso/conf.py:518 awx/sso/conf.py:611 awx/sso/conf.py:676 +#: awx/sso/conf.py:552 awx/sso/conf.py:645 awx/sso/conf.py:710 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4266,33 +5222,33 @@ msgstr "" "proceso de registro. Consulte la documentación de Ansible Tower para obtener" " información detallada." -#: awx/sso/conf.py:521 awx/sso/conf.py:533 awx/sso/conf.py:545 -#: awx/sso/conf.py:558 awx/sso/conf.py:572 awx/sso/conf.py:584 -#: awx/sso/conf.py:596 +#: awx/sso/conf.py:555 awx/sso/conf.py:567 awx/sso/conf.py:579 +#: awx/sso/conf.py:592 awx/sso/conf.py:606 awx/sso/conf.py:618 +#: awx/sso/conf.py:630 msgid "Google OAuth2" msgstr "Google OAuth2" -#: awx/sso/conf.py:531 +#: awx/sso/conf.py:565 msgid "Google OAuth2 Key" msgstr "Clave Google OAuth2" -#: awx/sso/conf.py:532 +#: awx/sso/conf.py:566 msgid "The OAuth2 key from your web application." msgstr "Clave OAuth2 de su aplicación web." -#: awx/sso/conf.py:543 +#: awx/sso/conf.py:577 msgid "Google OAuth2 Secret" msgstr "Clave secreta para Google OAuth2" -#: awx/sso/conf.py:544 +#: awx/sso/conf.py:578 msgid "The OAuth2 secret from your web application." msgstr "Secreto OAuth2 de su aplicación web." -#: awx/sso/conf.py:555 +#: awx/sso/conf.py:589 msgid "Google OAuth2 Whitelisted Domains" msgstr "Lista blanca de dominios para Google OAuth2" -#: awx/sso/conf.py:556 +#: awx/sso/conf.py:590 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." @@ -4300,11 +5256,11 @@ msgstr "" "Actualizar esta configuración para restringir los dominios que están " "permitidos para iniciar sesión utilizando Google OAuth2." -#: awx/sso/conf.py:567 +#: awx/sso/conf.py:601 msgid "Google OAuth2 Extra Arguments" msgstr "Argumentos adicionales para Google OAuth2" -#: awx/sso/conf.py:568 +#: awx/sso/conf.py:602 msgid "" "Extra arguments for Google OAuth2 login. You can restrict it to only allow a" " single domain to authenticate, even if the user is logged in with multple " @@ -4315,85 +5271,85 @@ msgstr "" "usuario ha iniciado sesión con varias cuentas de Google. Consulte la " "documentación de Ansible Tower para obtener información detallada." -#: awx/sso/conf.py:582 +#: awx/sso/conf.py:616 msgid "Google OAuth2 Organization Map" msgstr "Mapa de organización para Google OAuth2" -#: awx/sso/conf.py:594 +#: awx/sso/conf.py:628 msgid "Google OAuth2 Team Map" msgstr "Mapa de equipo para Google OAuth2" -#: awx/sso/conf.py:610 +#: awx/sso/conf.py:644 msgid "GitHub OAuth2 Callback URL" msgstr "GitHub OAuth2 Callback URL" -#: awx/sso/conf.py:614 awx/sso/conf.py:626 awx/sso/conf.py:637 -#: awx/sso/conf.py:649 awx/sso/conf.py:661 +#: awx/sso/conf.py:648 awx/sso/conf.py:660 awx/sso/conf.py:671 +#: awx/sso/conf.py:683 awx/sso/conf.py:695 msgid "GitHub OAuth2" msgstr "GitHub OAuth2" -#: awx/sso/conf.py:624 +#: awx/sso/conf.py:658 msgid "GitHub OAuth2 Key" msgstr "Clave para Github OAuth2" -#: awx/sso/conf.py:625 +#: awx/sso/conf.py:659 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "" "La clave OAuth2 (ID del cliente) de su aplicación de desarrollo GitHub." -#: awx/sso/conf.py:635 +#: awx/sso/conf.py:669 msgid "GitHub OAuth2 Secret" msgstr "Clave secreta para GitHub OAuth2" -#: awx/sso/conf.py:636 +#: awx/sso/conf.py:670 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "" "La clave secreta OAuth2 (Clave secreta del cliente) de su aplicación de " "desarrollo GitHub." -#: awx/sso/conf.py:647 +#: awx/sso/conf.py:681 msgid "GitHub OAuth2 Organization Map" msgstr "Mapa de organización para GitHub OAuth2" -#: awx/sso/conf.py:659 +#: awx/sso/conf.py:693 msgid "GitHub OAuth2 Team Map" msgstr "Mapa de equipo para GitHub OAuth2" -#: awx/sso/conf.py:675 +#: awx/sso/conf.py:709 msgid "GitHub Organization OAuth2 Callback URL" msgstr "OAuth2 URL Callback para organización Github" -#: awx/sso/conf.py:679 awx/sso/conf.py:691 awx/sso/conf.py:702 -#: awx/sso/conf.py:715 awx/sso/conf.py:726 awx/sso/conf.py:738 +#: awx/sso/conf.py:713 awx/sso/conf.py:725 awx/sso/conf.py:736 +#: awx/sso/conf.py:749 awx/sso/conf.py:760 awx/sso/conf.py:772 msgid "GitHub Organization OAuth2" msgstr "OAuth2 para la organización Github" -#: awx/sso/conf.py:689 +#: awx/sso/conf.py:723 msgid "GitHub Organization OAuth2 Key" msgstr "Clave OAuth2 para la organización Github" -#: awx/sso/conf.py:690 awx/sso/conf.py:768 +#: awx/sso/conf.py:724 awx/sso/conf.py:802 msgid "The OAuth2 key (Client ID) from your GitHub organization application." msgstr "" "La clave OAuth2 (ID del cliente) de su aplicación de organización GitHub." -#: awx/sso/conf.py:700 +#: awx/sso/conf.py:734 msgid "GitHub Organization OAuth2 Secret" msgstr "Clave secreta OAuth2 para la organización GitHub" -#: awx/sso/conf.py:701 awx/sso/conf.py:779 +#: awx/sso/conf.py:735 awx/sso/conf.py:813 msgid "" "The OAuth2 secret (Client Secret) from your GitHub organization application." msgstr "" "La clave secreta OAuth2 (Clave secreta del cliente) from your GitHub " "organization application." -#: awx/sso/conf.py:712 +#: awx/sso/conf.py:746 msgid "GitHub Organization Name" msgstr "Nombre para la organización GitHub" -#: awx/sso/conf.py:713 +#: awx/sso/conf.py:747 msgid "" "The name of your GitHub organization, as used in your organization's URL: " "https://github.com//." @@ -4401,19 +5357,19 @@ msgstr "" "El nombre de su organización de su GitHub, el utilizado en su URL de " "organización: https://github.com//." -#: awx/sso/conf.py:724 +#: awx/sso/conf.py:758 msgid "GitHub Organization OAuth2 Organization Map" msgstr "Mapa de organización OAuth2 para organizaciones GitHub" -#: awx/sso/conf.py:736 +#: awx/sso/conf.py:770 msgid "GitHub Organization OAuth2 Team Map" msgstr "Mapa de equipos OAuth2 para equipos GitHub" -#: awx/sso/conf.py:752 +#: awx/sso/conf.py:786 msgid "GitHub Team OAuth2 Callback URL" msgstr "URL callback OAuth2 para los equipos GitHub" -#: awx/sso/conf.py:753 +#: awx/sso/conf.py:787 msgid "" "Create an organization-owned application at " "https://github.com/organizations//settings/applications and obtain " @@ -4425,24 +5381,24 @@ msgstr "" "obtener una clave OAuth2 (ID del cliente ) y clave secreta (Clave secreta " "del cliente). Proporcione esta URL como URL callback para su aplicación." -#: awx/sso/conf.py:757 awx/sso/conf.py:769 awx/sso/conf.py:780 -#: awx/sso/conf.py:793 awx/sso/conf.py:804 awx/sso/conf.py:816 +#: awx/sso/conf.py:791 awx/sso/conf.py:803 awx/sso/conf.py:814 +#: awx/sso/conf.py:827 awx/sso/conf.py:838 awx/sso/conf.py:850 msgid "GitHub Team OAuth2" msgstr "OAuth2 para equipos GitHub" -#: awx/sso/conf.py:767 +#: awx/sso/conf.py:801 msgid "GitHub Team OAuth2 Key" msgstr "Clave OAuth2 para equipos GitHub" -#: awx/sso/conf.py:778 +#: awx/sso/conf.py:812 msgid "GitHub Team OAuth2 Secret" msgstr "Clave secreta OAuth2 para equipos GitHub" -#: awx/sso/conf.py:790 +#: awx/sso/conf.py:824 msgid "GitHub Team ID" msgstr "ID de equipo GitHub" -#: awx/sso/conf.py:791 +#: awx/sso/conf.py:825 msgid "" "Find the numeric team ID using the Github API: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." @@ -4450,19 +5406,19 @@ msgstr "" "Encuentre su identificador numérico de equipo utilizando la API de GitHub: " "http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." -#: awx/sso/conf.py:802 +#: awx/sso/conf.py:836 msgid "GitHub Team OAuth2 Organization Map" msgstr "Mapa de organizaciones OAuth2 para los equipos GitHub" -#: awx/sso/conf.py:814 +#: awx/sso/conf.py:848 msgid "GitHub Team OAuth2 Team Map" msgstr "Mapa de equipos OAuth2 para equipos GitHub" -#: awx/sso/conf.py:830 +#: awx/sso/conf.py:864 msgid "Azure AD OAuth2 Callback URL" msgstr "URL callback OAuth2 para Azure AD" -#: awx/sso/conf.py:831 +#: awx/sso/conf.py:865 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4472,42 +5428,42 @@ msgstr "" "proceso de registro. Consulte la documentación de Ansible Tower para obtener" " información detallada." -#: awx/sso/conf.py:834 awx/sso/conf.py:846 awx/sso/conf.py:857 -#: awx/sso/conf.py:869 awx/sso/conf.py:881 +#: awx/sso/conf.py:868 awx/sso/conf.py:880 awx/sso/conf.py:891 +#: awx/sso/conf.py:903 awx/sso/conf.py:915 msgid "Azure AD OAuth2" msgstr "Azure AD OAuth2" -#: awx/sso/conf.py:844 +#: awx/sso/conf.py:878 msgid "Azure AD OAuth2 Key" msgstr "Clave OAuth2 para Azure AD" -#: awx/sso/conf.py:845 +#: awx/sso/conf.py:879 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "La clave OAuth2 (ID del cliente) de su aplicación en Azure AD." -#: awx/sso/conf.py:855 +#: awx/sso/conf.py:889 msgid "Azure AD OAuth2 Secret" msgstr "Clave secreta OAuth2 para Azure AD" -#: awx/sso/conf.py:856 +#: awx/sso/conf.py:890 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "" "La clave secreta OAuth2 (Clave secreta del cliente) de su aplicación Azure " "AD." -#: awx/sso/conf.py:867 +#: awx/sso/conf.py:901 msgid "Azure AD OAuth2 Organization Map" msgstr "Mapa de organizaciones OAuth2 para Azure AD" -#: awx/sso/conf.py:879 +#: awx/sso/conf.py:913 msgid "Azure AD OAuth2 Team Map" msgstr "Mapa de equipos OAuth2 para Azure AD" -#: awx/sso/conf.py:904 +#: awx/sso/conf.py:938 msgid "SAML Assertion Consumer Service (ACS) URL" msgstr "URL del Servicio de consumidor de aserciones (ACS) SAML" -#: awx/sso/conf.py:905 +#: awx/sso/conf.py:939 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this ACS URL for your " @@ -4517,18 +5473,20 @@ msgstr "" "identidad (IdP) que ha configurado. Proporcione su ID de entidad SP y esta " "dirección URL ACS para su aplicación." -#: awx/sso/conf.py:908 awx/sso/conf.py:922 awx/sso/conf.py:936 -#: awx/sso/conf.py:951 awx/sso/conf.py:965 awx/sso/conf.py:978 -#: awx/sso/conf.py:999 awx/sso/conf.py:1017 awx/sso/conf.py:1036 -#: awx/sso/conf.py:1070 awx/sso/conf.py:1083 awx/sso/models.py:16 +#: awx/sso/conf.py:942 awx/sso/conf.py:956 awx/sso/conf.py:970 +#: awx/sso/conf.py:985 awx/sso/conf.py:999 awx/sso/conf.py:1012 +#: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 +#: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 +#: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 +#: awx/sso/conf.py:1211 awx/sso/models.py:16 msgid "SAML" msgstr "SAML" -#: awx/sso/conf.py:919 +#: awx/sso/conf.py:953 msgid "SAML Service Provider Metadata URL" msgstr "URL de metadatos para el proveedor de servicios SAML" -#: awx/sso/conf.py:920 +#: awx/sso/conf.py:954 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." @@ -4536,11 +5494,11 @@ msgstr "" "Si su proveedor de identidad (IdP) permite subir ficheros de metadatos en " "XML, puede descargarlo desde esta dirección." -#: awx/sso/conf.py:932 +#: awx/sso/conf.py:966 msgid "SAML Service Provider Entity ID" msgstr "ID de la entidad del proveedor de servicio SAML" -#: awx/sso/conf.py:933 +#: awx/sso/conf.py:967 msgid "" "The application-defined unique identifier used as the audience of the SAML " "service provider (SP) configuration. This is usually the URL for Tower." @@ -4549,11 +5507,11 @@ msgstr "" "configuración para la audiencia del proveedor de servicio (SP) SAML. Por lo " "general, es la URL para Tower." -#: awx/sso/conf.py:948 +#: awx/sso/conf.py:982 msgid "SAML Service Provider Public Certificate" msgstr "Certificado público del proveedor de servicio SAML" -#: awx/sso/conf.py:949 +#: awx/sso/conf.py:983 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " certificate content here." @@ -4561,11 +5519,11 @@ msgstr "" "Crear par de claves para Tower a usar como proveedor de servicio (SP) e " "incluir el contenido del certificado aquí." -#: awx/sso/conf.py:962 +#: awx/sso/conf.py:996 msgid "SAML Service Provider Private Key" msgstr "Clave privada del proveedor de servicio SAML" -#: awx/sso/conf.py:963 +#: awx/sso/conf.py:997 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " private key content here." @@ -4573,11 +5531,11 @@ msgstr "" "Crear par de claves para Tower a usar como proveedor de servicio (SP) e " "incluir el contenido de la clave privada aquí." -#: awx/sso/conf.py:975 +#: awx/sso/conf.py:1009 msgid "SAML Service Provider Organization Info" msgstr "Información organizacional del proveedor de servicio SAML" -#: awx/sso/conf.py:976 +#: awx/sso/conf.py:1010 msgid "" "Provide the URL, display name, and the name of your app. Refer to the " "Ansible Tower documentation for example syntax." @@ -4586,11 +5544,11 @@ msgstr "" "Consulte la documentación de Ansible Tower para acceder a ejemplos de " "sintaxis." -#: awx/sso/conf.py:995 +#: awx/sso/conf.py:1029 msgid "SAML Service Provider Technical Contact" msgstr "Contacto técnico del proveedor de servicio SAML" -#: awx/sso/conf.py:996 +#: awx/sso/conf.py:1030 msgid "" "Provide the name and email address of the technical contact for your service" " provider. Refer to the Ansible Tower documentation for example syntax." @@ -4599,11 +5557,11 @@ msgstr "" " de su proveedor de servicios. Consulte la documentación de Ansible Tower " "para obtener ejemplos de sintaxis." -#: awx/sso/conf.py:1013 +#: awx/sso/conf.py:1047 msgid "SAML Service Provider Support Contact" msgstr "Contacto de soporte del proveedor de servicio SAML" -#: awx/sso/conf.py:1014 +#: awx/sso/conf.py:1048 msgid "" "Provide the name and email address of the support contact for your service " "provider. Refer to the Ansible Tower documentation for example syntax." @@ -4612,11 +5570,11 @@ msgstr "" "soporte técnico de su proveedor de servicios. Consulte la documentación de " "Ansible Tower para obtener ejemplos de sintaxis." -#: awx/sso/conf.py:1030 +#: awx/sso/conf.py:1064 msgid "SAML Enabled Identity Providers" msgstr "Proveedores de identidad habilitados SAML" -#: awx/sso/conf.py:1031 +#: awx/sso/conf.py:1065 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " @@ -4632,45 +5590,99 @@ msgstr "" "documentación de Ansible Tower para obtener información detallada adicional " "y ejemplos de sintaxis." -#: awx/sso/conf.py:1068 +#: awx/sso/conf.py:1102 +msgid "SAML Security Config" +msgstr "Configuración de seguridad SAML" + +#: awx/sso/conf.py:1103 +msgid "" +"A dict of key value pairs that are passed to the underlying python-saml " +"security setting https://github.com/onelogin/python-saml#settings" +msgstr "" +"Un diccionario de pares de valores clave que se envían a la configuración de" +" seguridad python-saml subyacente https://github.com/onelogin/python-" +"saml#settings" + +#: awx/sso/conf.py:1135 +msgid "SAML Service Provider extra configuration data" +msgstr "Datos de configuración adicionales del proveedor de servicio SAML" + +#: awx/sso/conf.py:1136 +msgid "" +"A dict of key value pairs to be passed to the underlying python-saml Service" +" Provider configuration setting." +msgstr "" +"Un diccionario de pares de valores clave que se envían a los valores de " +"configuración del proveedor de servicio python-saml subyacente" + +#: awx/sso/conf.py:1149 +msgid "SAML IDP to extra_data attribute mapping" +msgstr "Asignación de atributos de SAML IDP a extra_data" + +#: awx/sso/conf.py:1150 +msgid "" +"A list of tuples that maps IDP attributes to extra_attributes. Each " +"attribute will be a list of values, even if only 1 value." +msgstr "" +"Una lista de tuplas que asigna atributos IDP a extra_attributes. Cada " +"atributo será una lista de valores, aunque sea un solo valor." + +#: awx/sso/conf.py:1167 msgid "SAML Organization Map" msgstr "Mapa de organización SAML" -#: awx/sso/conf.py:1081 +#: awx/sso/conf.py:1180 msgid "SAML Team Map" msgstr "Mapa de equipo SAML" -#: awx/sso/fields.py:123 +#: awx/sso/conf.py:1193 +msgid "SAML Organization Attribute Mapping" +msgstr "Asignación de atributos de la Organización SAML" + +#: awx/sso/conf.py:1194 +msgid "Used to translate user organization membership into Tower." +msgstr "" +"Utilizado para traducir la membresía a Tower de la organización del usuario." + +#: awx/sso/conf.py:1209 +msgid "SAML Team Attribute Mapping" +msgstr "Asignación de atributos del Equipo SAML" + +#: awx/sso/conf.py:1210 +msgid "Used to translate user team membership into Tower." +msgstr "Utilizado para traducir la membresía a Tower del equipo del usuario." + +#: awx/sso/fields.py:183 #, python-brace-format msgid "Invalid connection option(s): {invalid_options}." msgstr "Opción(es) de conexión inválida(s): {invalid_options}." -#: awx/sso/fields.py:194 +#: awx/sso/fields.py:266 msgid "Base" msgstr "Base" -#: awx/sso/fields.py:195 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "Un nivel" -#: awx/sso/fields.py:196 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "Árbol hijo" -#: awx/sso/fields.py:214 +#: awx/sso/fields.py:286 #, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "" "Esperado una lista de tres elementos pero en cambio se proporcionaron " "{length}" -#: awx/sso/fields.py:215 +#: awx/sso/fields.py:287 #, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" "Esperado una instancia de LDAPSearch pero en cambio se obtuvo {input_type}" -#: awx/sso/fields.py:251 +#: awx/sso/fields.py:323 #, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " @@ -4679,92 +5691,83 @@ msgstr "" "Esperado una instancia de LDAPSearch o LDAPSearchUnion pero en cambio se " "obtuvo {input_type}" -#: awx/sso/fields.py:289 +#: awx/sso/fields.py:361 #, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "Atributo(s) de usuario inválido(s): {invalid_attrs}." -#: awx/sso/fields.py:306 +#: awx/sso/fields.py:378 #, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" "Se esperaba una instancia de LDAPGroupType pero en cambio se obtuvo " "{input_type}." -#: awx/sso/fields.py:334 -#, python-brace-format -msgid "Invalid user flag: \"{invalid_flag}\"." -msgstr "Indicador de usuario inválido: \"{invalid_flag}\"." - -#: awx/sso/fields.py:350 awx/sso/fields.py:517 -#, python-brace-format -msgid "" -"Expected None, True, False, a string or list of strings but got {input_type}" -" instead." -msgstr "" -"Esperado None, True, False, una cadena de texto o una lista de cadenas de " -"texto pero en cambio se obtuvo {input_type}" - -#: awx/sso/fields.py:386 -#, python-brace-format -msgid "Missing key(s): {missing_keys}." -msgstr "Clave(s) no encontradas: {missing_keys}." - -#: awx/sso/fields.py:387 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 #, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "Clave(s) inválida(s): {invalid_keys}." -#: awx/sso/fields.py:436 awx/sso/fields.py:553 +#: awx/sso/fields.py:443 +#, python-brace-format +msgid "Invalid user flag: \"{invalid_flag}\"." +msgstr "Indicador de usuario inválido: \"{invalid_flag}\"." + +#: awx/sso/fields.py:464 +#, python-brace-format +msgid "Missing key(s): {missing_keys}." +msgstr "Clave(s) no encontradas: {missing_keys}." + +#: awx/sso/fields.py:514 awx/sso/fields.py:631 #, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "Clave(s) inválida(s) para map de organización: {invalid_keys}." -#: awx/sso/fields.py:454 +#: awx/sso/fields.py:532 #, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "Clave necesario no encontrada para mapa de equipo: {invalid_keys}." -#: awx/sso/fields.py:455 awx/sso/fields.py:572 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 #, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "Clave(s) inválida(s) para mapa de equipo: {invalid_keys}." -#: awx/sso/fields.py:571 +#: awx/sso/fields.py:649 #, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "Clave necesaria no encontrada para mapa de equipo: {missing_keys}." -#: awx/sso/fields.py:589 +#: awx/sso/fields.py:667 #, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "" "Clave(s) necesaria(s) no encontrada(s) para el registro informacional de la " "organización: {missing_keys}." -#: awx/sso/fields.py:602 +#: awx/sso/fields.py:680 #, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "" "Código(s) de lenguaje(s) inválido(s) para información organizacional: " "{invalid_lang_codes}." -#: awx/sso/fields.py:621 +#: awx/sso/fields.py:699 #, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "Clave(s) necesaria(s) no encontrada(s) para contacto: {missing_keys}." -#: awx/sso/fields.py:633 +#: awx/sso/fields.py:711 #, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "Clave(s) necesaria(s) no encontrada(s) para IdP: {missing_keys}." -#: awx/sso/pipeline.py:24 +#: awx/sso/pipeline.py:31 #, python-brace-format msgid "An account cannot be found for {0}" msgstr "Una cuenta no puede ser encontrada para {0}" -#: awx/sso/pipeline.py:30 +#: awx/sso/pipeline.py:37 msgid "Your account is inactive" msgstr "Su cuenta está inactiva" @@ -4791,70 +5794,48 @@ msgstr "La clave secreta TACACS+ no permite caracteres no ascii" msgid "AWX" msgstr "AWX" -#: awx/templates/rest_framework/api.html:39 +#: awx/templates/rest_framework/api.html:42 msgid "Ansible Tower API Guide" msgstr "Guía de la API de Ansible Tower" -#: awx/templates/rest_framework/api.html:40 +#: awx/templates/rest_framework/api.html:43 msgid "Back to Ansible Tower" msgstr "Volver a Ansible Tower" -#: awx/templates/rest_framework/api.html:41 +#: awx/templates/rest_framework/api.html:44 msgid "Resize" msgstr "Redimensionar" +#: awx/templates/rest_framework/base.html:37 +msgid "navbar" +msgstr "navbar" + +#: awx/templates/rest_framework/base.html:75 +msgid "content" +msgstr "contenido" + #: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 -#, python-format -msgid "Make a GET request on the %(name)s resource" -msgstr "Realizar una petición GET en el recurso %(name)s " +msgid "request form" +msgstr "solicitar formulario" -#: awx/templates/rest_framework/base.html:80 -msgid "Specify a format for the GET request" -msgstr "Especificar un formato para una petición GET" - -#: awx/templates/rest_framework/base.html:86 -#, python-format -msgid "" -"Make a GET request on the %(name)s resource with the format set to " -"`%(format)s`" -msgstr "" -"Realizar una petición GET en el recurso %(name)s con el formato establecido" -" a `%(format)s`" - -#: awx/templates/rest_framework/base.html:100 -#, python-format -msgid "Make an OPTIONS request on the %(name)s resource" -msgstr "Realizar una petición OPTIONS en el recurso %(name)s" - -#: awx/templates/rest_framework/base.html:106 -#, python-format -msgid "Make a DELETE request on the %(name)s resource" -msgstr "Realizar una petición DELETE en el recurso %(name)s" - -#: awx/templates/rest_framework/base.html:113 +#: awx/templates/rest_framework/base.html:134 msgid "Filters" msgstr "Filtros" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 -#, python-format -msgid "Make a POST request on the %(name)s resource" -msgstr "Realizar una petición POST en el recurso %(name)s" +#: awx/templates/rest_framework/base.html:139 +msgid "main content" +msgstr "contenido principal" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 -#, python-format -msgid "Make a PUT request on the %(name)s resource" -msgstr "Realizar una petición PUT en el recurso %(name)s" +#: awx/templates/rest_framework/base.html:155 +msgid "request info" +msgstr "solicitar información" -#: awx/templates/rest_framework/base.html:233 -#, python-format -msgid "Make a PATCH request on the %(name)s resource" -msgstr "Realizar una petición PATCH en el recurso %(name)s" +#: awx/templates/rest_framework/base.html:159 +msgid "response info" +msgstr "información de respuesta" #: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:36 awx/ui/conf.py:51 -#: awx/ui/conf.py:63 +#: awx/ui/conf.py:63 awx/ui/conf.py:73 msgid "UI" msgstr "UI" @@ -4910,13 +5891,27 @@ msgstr "" "con fondo transparente. Formatos GIF, PNG y JPEG están soportados." #: awx/ui/conf.py:60 -msgid "Max Job Events Retreived by UI" -msgstr "" +msgid "Max Job Events Retrieved by UI" +msgstr "Máxima cantidad de eventos de tareas recuperados por UI" #: awx/ui/conf.py:61 msgid "" -"Maximum number of job events for the UI to retreive within a single request." +"Maximum number of job events for the UI to retrieve within a single request." msgstr "" +"Máxima cantidad de eventos de tareas para que la UI recupere dentro de una " +"sola solicitud." + +#: awx/ui/conf.py:70 +msgid "Enable Live Updates in the UI" +msgstr "Habilite las actualizaciones en directo en la UI" + +#: awx/ui/conf.py:71 +msgid "" +"If disabled, the page will not refresh when events are received. Reloading " +"the page will be required to get the latest details." +msgstr "" +"Si está deshabilitada, la página no se actualizará al recibir eventos. Se " +"deberá volver a cargar la página para obtener la información más reciente." #: awx/ui/fields.py:29 msgid "" @@ -4929,99 +5924,3 @@ msgstr "" #: awx/ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "Dato codificado en base64 inválido en la URL de datos" - -#: awx/ui/templates/ui/index.html:31 -msgid "" -"Your session will expire in 60 seconds, would you like to " -"continue?" -msgstr "" -"Su sesión caducará en 60 segundos, ¿Le gustaría continuar?" - -#: awx/ui/templates/ui/index.html:46 -msgid "CANCEL" -msgstr "CANCELAR" - -#: awx/ui/templates/ui/index.html:98 -msgid "Set how many days of data should be retained." -msgstr "Establecer cuántos días de datos debería ser retenidos." - -#: awx/ui/templates/ui/index.html:104 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Por favor introduzca un entero que no sea " -"negativo que sea menor de " -"9999." - -#: awx/ui/templates/ui/index.html:109 -msgid "" -"For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.\n" -"
\n" -"
CAUTION: Setting both numerical variables to \"0\" will delete all facts.\n" -"
\n" -"
" -msgstr "" -"Para facts obtenidos más antiguos que el periodo especificado, guarda un escanéo de facts (instantánea [snapshot]) por una ventana de tiempo (frecuencia). Por ejemplo, facts más antiguos de 30 días son eliminados, mientras que escaneo de facts de una semana son mantenidos.\n" -"
\n" -"
CUIDADO: Ajustar ambas variables numéricas a \"0\" eliminará todos los facts.\n" -"
\n" -"
" - -#: awx/ui/templates/ui/index.html:118 -msgid "Select a time period after which to remove old facts" -msgstr "" -"Seleccione un periodo en el que después de él se eliminará los facts " -"antiguos." - -#: awx/ui/templates/ui/index.html:132 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Por favor introduzca un entero que no sea " -"negativo que sea menor " -"9999." - -#: awx/ui/templates/ui/index.html:137 -msgid "Select a frequency for snapshot retention" -msgstr "" -"Selecciona las frecuencia para la retención de instantáneas (snapshots)" - -#: awx/ui/templates/ui/index.html:151 -msgid "" -"Please enter an integer that is not" -" negative that is " -"lower than 9999." -msgstr "" -"Por favor introduzca un entero que no sea " -"negativo que sea " -"menor de 9999." - -#: awx/ui/templates/ui/index.html:157 -msgid "working..." -msgstr "en funcionamiento..." diff --git a/awx/locale/fr/LC_MESSAGES/django.po b/awx/locale/fr/LC_MESSAGES/django.po index f009e410f4..4f206e4334 100644 --- a/awx/locale/fr/LC_MESSAGES/django.po +++ b/awx/locale/fr/LC_MESSAGES/django.po @@ -1,12 +1,13 @@ # aude_stoquart , 2017. #zanata # croe , 2017. #zanata # mkim , 2017. #zanata +# croe , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-30 20:23+0000\n" -"PO-Revision-Date: 2017-12-04 03:58+0000\n" +"POT-Creation-Date: 2018-06-14 18:30+0000\n" +"PO-Revision-Date: 2018-06-20 10:37+0000\n" "Last-Translator: croe \n" "Language-Team: French\n" "MIME-Version: 1.0\n" @@ -14,31 +15,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" -"X-Generator: Zanata 4.3.2\n" +"X-Generator: Zanata 4.6.0\n" -#: awx/api/authentication.py:67 -msgid "Invalid token header. No credentials provided." -msgstr "" -"En-tête de token non valide. Aucune information d'identification fournie." - -#: awx/api/authentication.py:70 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "" -"En-tête de token non valide. La chaîne token ne doit pas contenir d'espaces." - -#: awx/api/authentication.py:105 -msgid "User inactive or deleted" -msgstr "Utilisateur inactif ou supprimé" - -#: awx/api/authentication.py:161 -msgid "Invalid task token" -msgstr "Token de tâche non valide" - -#: awx/api/conf.py:12 +#: awx/api/conf.py:15 msgid "Idle Time Force Log Out" msgstr "Temps d'inactivité - Forcer la déconnexion" -#: awx/api/conf.py:13 +#: awx/api/conf.py:16 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." @@ -46,67 +29,101 @@ msgstr "" "Délai en secondes pendant lequel un utilisateur peut rester inactif avant de" " devoir se reconnecter." -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:85 -#: awx/sso/conf.py:96 awx/sso/conf.py:108 awx/sso/conf.py:123 +#: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 +#: awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/sso/conf.py:123 msgid "Authentication" msgstr "Authentification" -#: awx/api/conf.py:22 -msgid "Maximum number of simultaneous logins" -msgstr "Nombre maximal de connexions simultanées" +#: awx/api/conf.py:24 +msgid "Maximum number of simultaneous logged in sessions" +msgstr "Le nombre maximum de sessions actives en simultané" -#: awx/api/conf.py:23 +#: awx/api/conf.py:25 msgid "" -"Maximum number of simultaneous logins a user may have. To disable enter -1." +"Maximum number of simultaneous logged in sessions a user may have. To " +"disable enter -1." msgstr "" -"Nombre maximal de connexions simultanées dont un utilisateur peut disposer. " -"Pour désactiver cette option, entrez -1." +"Nombre maximal de connexions actives simultanées dont un utilisateur peut " +"disposer. Pour désactiver cette option, entrez -1." -#: awx/api/conf.py:31 +#: awx/api/conf.py:32 msgid "Enable HTTP Basic Auth" msgstr "Activer l'authentification HTTP de base" -#: awx/api/conf.py:32 +#: awx/api/conf.py:33 msgid "Enable HTTP Basic Auth for the API Browser." msgstr "Activer l'authentification HTTP de base pour le navigateur d'API." -#: awx/api/filters.py:129 +#: awx/api/conf.py:42 +msgid "OAuth 2 Timeout Settings" +msgstr "OAuth 2 Config Timeout" + +#: awx/api/conf.py:43 +msgid "" +"Dictionary for customizing OAuth 2 timeouts, available items are " +"`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number " +"of seconds, and `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of " +"authorization grants in the number of seconds." +msgstr "" +"Dictionnaire pour personnaliser les timeouts OAuth 2; les éléments " +"disponibles sont `ACCESS_TOKEN_EXPIRE_SECONDS`, la durée des jetons d'accès " +"en nombre de secondes, et`AUTHORIZATION_CODE_EXPIRE_SECONDS`, la durée des " +"autorisations données en nombre de secondes." + +#: awx/api/exceptions.py:20 +msgid "Resource is being used by running jobs." +msgstr "Ressource utilisée par les tâches en cours." + +#: awx/api/fields.py:81 +#, python-brace-format +msgid "Invalid key names: {invalid_key_names}" +msgstr "Noms de clés non valides : {invalid_key_names}" + +#: awx/api/fields.py:107 +msgid "Credential {} does not exist" +msgstr "L'identifiant {} n'existe pas" + +#: awx/api/filters.py:96 +msgid "No related model for field {}." +msgstr "Aucun modèle qui y soit lié pour le champ '{}'" + +#: awx/api/filters.py:113 msgid "Filtering on password fields is not allowed." msgstr "Le filtrage des champs de mot de passe n'est pas autorisé." -#: awx/api/filters.py:141 awx/api/filters.py:143 +#: awx/api/filters.py:125 awx/api/filters.py:127 #, python-format msgid "Filtering on %s is not allowed." msgstr "Le filtrage de %s n'est pas autorisé." -#: awx/api/filters.py:146 +#: awx/api/filters.py:130 msgid "Loops not allowed in filters, detected on field {}." msgstr "Boucles non autorisés dans les filtres, détectées sur le champ {}." -#: awx/api/filters.py:171 +#: awx/api/filters.py:159 +msgid "Query string field name not provided." +msgstr "Nom de champ du string de requête non fourni." + +#: awx/api/filters.py:186 #, python-brace-format msgid "Invalid {field_name} id: {field_id}" msgstr "Id {field_name} non valide : {field_id}" -#: awx/api/filters.py:302 +#: awx/api/filters.py:319 #, python-format msgid "cannot filter on kind %s" msgstr "impossible de filtrer sur le genre %s" -#: awx/api/filters.py:409 -#, python-format -msgid "cannot order by field %s" -msgstr "impossible de trier par champ %s" - -#: awx/api/generics.py:550 awx/api/generics.py:612 +#: awx/api/generics.py:620 awx/api/generics.py:682 msgid "\"id\" field must be an integer." msgstr "Le champ \"id\" doit être un nombre entier." -#: awx/api/generics.py:609 +#: awx/api/generics.py:679 msgid "\"id\" is required to disassociate" msgstr "\"id\" est nécessaire pour dissocier" -#: awx/api/generics.py:660 +#: awx/api/generics.py:730 msgid "{} 'id' field is missing." msgstr "Le champ {} 'id' est manquant." @@ -146,11 +163,11 @@ msgstr "Horodatage lors de la création de ce {}." msgid "Timestamp when this {} was last modified." msgstr "Horodatage lors de la modification de ce {}." -#: awx/api/parsers.py:64 +#: awx/api/parsers.py:33 msgid "JSON parse error - not a JSON object" msgstr "Erreur d'analyse JSON - pas un objet JSON" -#: awx/api/parsers.py:67 +#: awx/api/parsers.py:36 #, python-format msgid "" "JSON parse error - %s\n" @@ -159,76 +176,120 @@ msgstr "" "Erreur d'analyse JSON - %s\n" "Cause possible : virgule de fin." -#: awx/api/serializers.py:268 +#: awx/api/serializers.py:153 +msgid "" +"The original object is already named {}, a copy from it cannot have the same" +" name." +msgstr "" +"L'objet d'origine s'appelle déjà {}, une copie de cet objet ne peut pas " +"avoir le même nom." + +#: awx/api/serializers.py:295 msgid "Playbook Run" msgstr "Exécution du playbook" -#: awx/api/serializers.py:269 +#: awx/api/serializers.py:296 msgid "Command" msgstr "Commande" -#: awx/api/serializers.py:270 awx/main/models/unified_jobs.py:435 +#: awx/api/serializers.py:297 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "Mise à jour SCM" -#: awx/api/serializers.py:271 +#: awx/api/serializers.py:298 msgid "Inventory Sync" msgstr "Synchronisation des inventaires" -#: awx/api/serializers.py:272 +#: awx/api/serializers.py:299 msgid "Management Job" msgstr "Tâche de gestion" -#: awx/api/serializers.py:273 +#: awx/api/serializers.py:300 msgid "Workflow Job" msgstr "Tâche de workflow" -#: awx/api/serializers.py:274 +#: awx/api/serializers.py:301 msgid "Workflow Template" msgstr "Modèle de workflow" -#: awx/api/serializers.py:701 awx/api/serializers.py:759 awx/api/views.py:4365 -#, python-format -msgid "" -"Standard Output too large to display (%(text_size)d bytes), only download " -"supported for sizes over %(supported_size)d bytes" -msgstr "" -"Sortie standard trop grande pour pouvoir s'afficher (%(text_size)d octets). " -"Le téléchargement est pris en charge seulement pour une taille supérieure à " -"%(supported_size)d octets" +#: awx/api/serializers.py:302 +msgid "Job Template" +msgstr "Modèle de job" -#: awx/api/serializers.py:774 +#: awx/api/serializers.py:697 +msgid "" +"Indicates whether all of the events generated by this unified job have been " +"saved to the database." +msgstr "" +"Indique si tous les événements générés par ce job unifié ont été enregistrés" +" dans la base de données." + +#: awx/api/serializers.py:854 msgid "Write-only field used to change the password." msgstr "Champ en écriture seule servant à modifier le mot de passe." -#: awx/api/serializers.py:776 +#: awx/api/serializers.py:856 msgid "Set if the account is managed by an external service" msgstr "À définir si le compte est géré par un service externe" -#: awx/api/serializers.py:800 +#: awx/api/serializers.py:880 msgid "Password required for new User." msgstr "Mot de passe requis pour le nouvel utilisateur." -#: awx/api/serializers.py:886 +#: awx/api/serializers.py:971 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "Impossible de redéfinir %s sur un utilisateur géré par LDAP." -#: awx/api/serializers.py:1050 +#: awx/api/serializers.py:1057 +msgid "Must be a simple space-separated string with allowed scopes {}." +msgstr "" +"Doit correspondre à une simple chaîne de caractères séparés par des espaces " +"avec dans les limites autorisées {}." + +#: awx/api/serializers.py:1151 +msgid "Authorization Grant Type" +msgstr "Type d'autorisation" + +#: awx/api/serializers.py:1153 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "Question secrète du client" + +#: awx/api/serializers.py:1156 +msgid "Client Type" +msgstr "Type de client" + +#: awx/api/serializers.py:1159 +msgid "Redirect URIs" +msgstr "Redirection d'URIs." + +#: awx/api/serializers.py:1162 +msgid "Skip Authorization" +msgstr "Sauter l'étape d'autorisation" + +#: awx/api/serializers.py:1264 +msgid "This path is already being used by another manual project." +msgstr "Ce chemin est déjà utilisé par un autre projet manuel." + +#: awx/api/serializers.py:1290 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "Ce champ obsolète et sera supprimé dans une prochaine version." + +#: awx/api/serializers.py:1349 msgid "Organization is missing" msgstr "L'organisation est manquante" -#: awx/api/serializers.py:1054 +#: awx/api/serializers.py:1353 msgid "Update options must be set to false for manual projects." msgstr "" "La Mise à jour des options doit être définie à false pour les projets " "manuels." -#: awx/api/serializers.py:1060 +#: awx/api/serializers.py:1359 msgid "Array of playbooks available within this project." msgstr "Tableau des playbooks disponibles dans ce projet." -#: awx/api/serializers.py:1079 +#: awx/api/serializers.py:1378 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." @@ -236,71 +297,74 @@ msgstr "" "Tableau des fichiers d'inventaires et des répertoires disponibles dans ce " "projet : incomplet." -#: awx/api/serializers.py:1201 +#: awx/api/serializers.py:1426 awx/api/serializers.py:3194 +msgid "A count of hosts uniquely assigned to each status." +msgstr "Un nombre d'hôtes assignés exclusivement à chaque statut." + +#: awx/api/serializers.py:1429 awx/api/serializers.py:3197 +msgid "A count of all plays and tasks for the job run." +msgstr "Le nombre de lectures et de tâches liées à l'exécution d'un job." + +#: awx/api/serializers.py:1554 msgid "Smart inventories must specify host_filter" msgstr "Les inventaires smart doivent spécifier le host_filter" -#: awx/api/serializers.py:1303 +#: awx/api/serializers.py:1658 #, python-format msgid "Invalid port specification: %s" msgstr "Spécification de port non valide : %s" -#: awx/api/serializers.py:1314 +#: awx/api/serializers.py:1669 msgid "Cannot create Host for Smart Inventory" msgstr "Impossible de créer un Hôte pour l' Inventaire Smart" -#: awx/api/serializers.py:1336 awx/api/serializers.py:3321 -#: awx/api/serializers.py:3406 awx/main/validators.py:198 -msgid "Must be valid JSON or YAML." -msgstr "Syntaxe JSON ou YAML valide exigée." - -#: awx/api/serializers.py:1432 +#: awx/api/serializers.py:1781 msgid "Invalid group name." msgstr "Nom de groupe incorrect." -#: awx/api/serializers.py:1437 +#: awx/api/serializers.py:1786 msgid "Cannot create Group for Smart Inventory" msgstr "Impossible de créer de Groupe pour l' Inventaire Smart" -#: awx/api/serializers.py:1509 +#: awx/api/serializers.py:1861 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" "Le script doit commencer par une séquence hashbang : c.-à-d. ... " "#!/usr/bin/env python" -#: awx/api/serializers.py:1555 +#: awx/api/serializers.py:1910 msgid "`{}` is a prohibited environment variable" msgstr "`{}`est une variable d'environnement interdite" -#: awx/api/serializers.py:1566 +#: awx/api/serializers.py:1921 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "Si la valeur 'source' est 'custom', 'source_script' doit être défini." -#: awx/api/serializers.py:1572 +#: awx/api/serializers.py:1927 msgid "Must provide an inventory." msgstr "Vous devez fournir un inventaire." -#: awx/api/serializers.py:1576 +#: awx/api/serializers.py:1931 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" "Le 'source_script' n'appartient pas à la même organisation que l'inventaire." -#: awx/api/serializers.py:1578 +#: awx/api/serializers.py:1933 msgid "'source_script' doesn't exist." msgstr "'source_script' n'existe pas." -#: awx/api/serializers.py:1602 +#: awx/api/serializers.py:1967 msgid "Automatic group relationship, will be removed in 3.3" msgstr "Relation de groupe automatique, sera supprimée dans la version 3.3" -#: awx/api/serializers.py:1679 +#: awx/api/serializers.py:2053 msgid "Cannot use manual project for SCM-based inventory." msgstr "Impossible d'utiliser un projet manuel pour un inventaire SCM." -#: awx/api/serializers.py:1685 +#: awx/api/serializers.py:2059 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." @@ -308,54 +372,54 @@ msgstr "" "Les sources d'inventaire manuel sont créées automatiquement lorsqu'un groupe" " est créé dans l'API v1." -#: awx/api/serializers.py:1690 +#: awx/api/serializers.py:2064 msgid "Setting not compatible with existing schedules." msgstr "Paramètre incompatible avec les planifications existantes." -#: awx/api/serializers.py:1695 +#: awx/api/serializers.py:2069 msgid "Cannot create Inventory Source for Smart Inventory" msgstr "Impossible de créer une Source d'inventaire pour l' Inventaire Smart" -#: awx/api/serializers.py:1709 +#: awx/api/serializers.py:2120 #, python-format msgid "Cannot set %s if not SCM type." msgstr "Impossible de définir %s si pas du type SCM ." -#: awx/api/serializers.py:1950 +#: awx/api/serializers.py:2388 msgid "Modifications not allowed for managed credential types" msgstr "" "Modifications non autorisées pour les types d'informations d'identification " "gérés" -#: awx/api/serializers.py:1955 +#: awx/api/serializers.py:2393 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "" "Modifications apportées aux entrées non autorisées pour les types " "d'informations d'identification en cours d'utilisation" -#: awx/api/serializers.py:1961 +#: awx/api/serializers.py:2399 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "Doit être 'cloud' ou 'net', pas %s" -#: awx/api/serializers.py:1967 +#: awx/api/serializers.py:2405 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "" "'ask_at_runtime' n'est pas pris en charge pour les informations " "d'identification personnalisées." -#: awx/api/serializers.py:2140 +#: awx/api/serializers.py:2585 #, python-format msgid "\"%s\" is not a valid choice" msgstr "\"%s\" n'est pas un choix valide." -#: awx/api/serializers.py:2159 -#, python-format -msgid "'%s' is not a valid field for %s" -msgstr "'%s' n'est pas un champ valide pour %s" +#: awx/api/serializers.py:2604 +#, python-brace-format +msgid "'{field_name}' is not a valid field for {credential_type_name}" +msgstr "'{field_name}' n'est pas un champ valide pour {credential_type_name}" -#: awx/api/serializers.py:2180 +#: awx/api/serializers.py:2625 msgid "" "You cannot change the credential type of the credential, as it may break the" " functionality of the resources using it." @@ -363,7 +427,7 @@ msgstr "" "Vous ne pouvez pas modifier le type d'identifiants, car cela peut " "compromettre la fonctionnalité de la ressource les utilisant." -#: awx/api/serializers.py:2191 +#: awx/api/serializers.py:2637 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." @@ -372,7 +436,7 @@ msgstr "" "propriétaire. Si vous le définissez, n'entrez ni équipe ni organisation. " "Seulement valable pour la création." -#: awx/api/serializers.py:2196 +#: awx/api/serializers.py:2642 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." @@ -381,7 +445,7 @@ msgstr "" "propriétaire. Si vous le définissez, n'entrez ni utilisateur ni " "organisation. Seulement valable pour la création." -#: awx/api/serializers.py:2201 +#: awx/api/serializers.py:2647 msgid "" "Inherit permissions from organization roles. If provided on creation, do not" " give either user or team." @@ -389,205 +453,313 @@ msgstr "" "Hériter des permissions à partir des rôles d'organisation. Si vous le " "définissez lors de la création, n'entrez ni utilisateur ni équipe." -#: awx/api/serializers.py:2217 +#: awx/api/serializers.py:2663 msgid "Missing 'user', 'team', or 'organization'." msgstr "Valeur 'utilisateur', 'équipe' ou 'organisation' manquante." -#: awx/api/serializers.py:2257 +#: awx/api/serializers.py:2703 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" "L'organisation des informations d'identification doit être définie et mise " "en correspondance avant de l'attribuer à une équipe" -#: awx/api/serializers.py:2424 +#: awx/api/serializers.py:2904 msgid "You must provide a cloud credential." msgstr "Vous devez fournir une information d'identification cloud." -#: awx/api/serializers.py:2425 +#: awx/api/serializers.py:2905 msgid "You must provide a network credential." msgstr "Vous devez fournir des informations d'identification réseau." -#: awx/api/serializers.py:2441 +#: awx/api/serializers.py:2906 awx/main/models/jobs.py:155 +msgid "You must provide an SSH credential." +msgstr "Vous devez fournir des informations d'identification SSH." + +#: awx/api/serializers.py:2907 +msgid "You must provide a vault credential." +msgstr "Vous devez fournir un archivage sécurisé." + +#: awx/api/serializers.py:2926 msgid "This field is required." msgstr "Ce champ est obligatoire." -#: awx/api/serializers.py:2443 awx/api/serializers.py:2445 +#: awx/api/serializers.py:2928 awx/api/serializers.py:2930 msgid "Playbook not found for project." msgstr "Playbook introuvable pour le projet." -#: awx/api/serializers.py:2447 +#: awx/api/serializers.py:2932 msgid "Must select playbook for project." msgstr "Un playbook doit être sélectionné pour le project." -#: awx/api/serializers.py:2522 +#: awx/api/serializers.py:3013 +msgid "Cannot enable provisioning callback without an inventory set." +msgstr "" +"Impossible d'activer le rappel de provisioning sans inventaire défini." + +#: awx/api/serializers.py:3016 msgid "Must either set a default value or ask to prompt on launch." msgstr "" "Une valeur par défaut doit être définie ou bien demander une invite au " "moment du lancement." -#: awx/api/serializers.py:2524 awx/main/models/jobs.py:326 +#: awx/api/serializers.py:3018 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "Un projet doit être assigné aux types de tâche 'run' et 'check'." -#: awx/api/serializers.py:2611 +#: awx/api/serializers.py:3134 msgid "Invalid job template." msgstr "Modèle de tâche non valide." -#: awx/api/serializers.py:2708 -msgid "Neither credential nor vault credential provided." -msgstr "Indentifiants et identifiants de l'archivage sécurisé non fournis" +#: awx/api/serializers.py:3249 +msgid "No change to job limit" +msgstr "Aucun changement à la limite du job" -#: awx/api/serializers.py:2711 +#: awx/api/serializers.py:3250 +msgid "All failed and unreachable hosts" +msgstr "Tous les hôtes inaccessibles ou ayant échoué" + +#: awx/api/serializers.py:3265 +msgid "Missing passwords needed to start: {}" +msgstr "Mots de passe manquants indispensables pour commencer : {}" + +#: awx/api/serializers.py:3284 +msgid "Relaunch by host status not available until job finishes running." +msgstr "" +"Relance par statut d'hôte non disponible jusqu'à la fin de l'exécution du " +"job en cours." + +#: awx/api/serializers.py:3298 msgid "Job Template Project is missing or undefined." msgstr "Le projet de modèle de tâche est manquant ou non défini." -#: awx/api/serializers.py:2713 +#: awx/api/serializers.py:3300 msgid "Job Template Inventory is missing or undefined." msgstr "Le projet de modèle d'inventaire est manquant ou non défini." -#: awx/api/serializers.py:2782 awx/main/tasks.py:2186 +#: awx/api/serializers.py:3338 +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:3405 awx/main/tasks.py:2268 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} ne sont pas autorisés à utiliser les commandes ad hoc." -#: awx/api/serializers.py:3008 -#, python-format -msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." +#: awx/api/serializers.py:3474 awx/api/views.py:4843 +#, python-brace-format +msgid "" +"Standard Output too large to display ({text_size} bytes), only download " +"supported for sizes over {supported_size} bytes." msgstr "" -"%(job_type)s n'est pas un type de tâche valide. Les choix sont %(choices)s." +"Sortie standard trop grande pour pouvoir s'afficher ({text_size} octets). Le" +" téléchargement est pris en charge seulement pour une taille supérieure à " +"{supported_size} octets." -#: awx/api/serializers.py:3013 -msgid "Workflow job template is missing during creation." -msgstr "Le modèle de tâche Workflow est manquant lors de la création." +#: awx/api/serializers.py:3671 +msgid "Provided variable {} has no database value to replace with." +msgstr "" +"La variable fournie {} n'a pas de valeur de base de données pour remplacer." -#: awx/api/serializers.py:3018 +#: awx/api/serializers.py:3747 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "Impossible d'imbriquer %s dans un modèle de tâche Workflow." -#: awx/api/serializers.py:3291 -#, python-format -msgid "Job Template '%s' is missing or undefined." -msgstr "Le modèle de tâche '%s' est manquant ou non défini." +#: awx/api/serializers.py:3754 awx/api/views.py:783 +msgid "Related template is not configured to accept credentials on launch." +msgstr "" +"Le modèle associé n'est pas configuré pour pouvoir accepter les identifiants" +" au lancement." -#: awx/api/serializers.py:3294 +#: awx/api/serializers.py:4211 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:3335 awx/api/views.py:3023 -#, python-format -msgid "Cannot assign multiple %s credentials." -msgstr "Impossible d'attribuer plusieurs informations d'identification %s ." +#: awx/api/serializers.py:4213 +msgid "The provided inventory is being deleted." +msgstr "L'inventaire fourni est en cours de suppression." -#: awx/api/serializers.py:3337 awx/api/views.py:3026 -msgid "Extra credentials must be network or cloud." +#: awx/api/serializers.py:4221 +msgid "Cannot assign multiple {} credentials." +msgstr "Ne peut pas attribuer plusieurs identifiants {}." + +#: awx/api/serializers.py:4234 +msgid "" +"Removing {} credential at launch time without replacement is not supported. " +"Provided list lacked credential(s): {}." msgstr "" -"Les informations d'identification supplémentaires doivent être des " -"identifiants réseau ou cloud." +"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:3474 +#: awx/api/serializers.py:4360 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "" "Champs obligatoires manquants pour la configuration des notifications : " "notification_type" -#: awx/api/serializers.py:3497 +#: awx/api/serializers.py:4383 msgid "No values specified for field '{}'" msgstr "Aucune valeur spécifiée pour le champ '{}'" -#: awx/api/serializers.py:3502 +#: awx/api/serializers.py:4388 msgid "Missing required fields for Notification Configuration: {}." msgstr "" "Champs obligatoires manquants pour la configuration des notifications : {}." -#: awx/api/serializers.py:3505 +#: awx/api/serializers.py:4391 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "Type de champ de configuration '{}' incorrect, {} attendu." -#: awx/api/serializers.py:3558 -msgid "Inventory Source must be a cloud resource." -msgstr "La source d'inventaire doit être une ressource cloud." - -#: awx/api/serializers.py:3560 -msgid "Manual Project cannot have a schedule set." -msgstr "Le projet manuel ne peut pas avoir de calendrier défini." - -#: awx/api/serializers.py:3563 +#: awx/api/serializers.py:4453 msgid "" -"Inventory sources with `update_on_project_update` cannot be scheduled. " -"Schedule its source project `{}` instead." +"Valid DTSTART required in rrule. Value should start with: " +"DTSTART:YYYYMMDDTHHMMSSZ" msgstr "" -"Impossible de planifier les sources d'inventaire avec " -"`update_on_project_update`. Planifiez plutôt son projet source`{}`." - -#: awx/api/serializers.py:3582 -msgid "Projects and inventory updates cannot accept extra variables." -msgstr "" -"Les projets et mises à jour d'inventaire ne peuvent pas accepter de " -"variables supplémentaires." - -#: awx/api/serializers.py:3604 -msgid "" -"DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" -msgstr "" -"DTSTART obligatoire dans rrule. La valeur doit correspondre à : " +"DTSTART valide obligatoire dans rrule. La valeur doit commencer par : " "DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:3606 +#: awx/api/serializers.py:4455 +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:4457 msgid "Multiple DTSTART is not supported." msgstr "Une seule valeur DTSTART est prise en charge." -#: awx/api/serializers.py:3608 -msgid "RRULE require in rrule." +#: awx/api/serializers.py:4459 +msgid "RRULE required in rrule." msgstr "RRULE obligatoire dans rrule." -#: awx/api/serializers.py:3610 +#: awx/api/serializers.py:4461 msgid "Multiple RRULE is not supported." msgstr "Une seule valeur RRULE est prise en charge." -#: awx/api/serializers.py:3612 +#: awx/api/serializers.py:4463 msgid "INTERVAL required in rrule." msgstr "INTERVAL obligatoire dans rrule." -#: awx/api/serializers.py:3614 -msgid "TZID is not supported." -msgstr "TZID n'est pas pris en charge." - -#: awx/api/serializers.py:3616 +#: awx/api/serializers.py:4465 msgid "SECONDLY is not supported." msgstr "SECONDLY n'est pas pris en charge." -#: awx/api/serializers.py:3618 +#: awx/api/serializers.py:4467 msgid "Multiple BYMONTHDAYs not supported." msgstr "Une seule valeur BYMONTHDAY est prise en charge." -#: awx/api/serializers.py:3620 +#: awx/api/serializers.py:4469 msgid "Multiple BYMONTHs not supported." msgstr "Une seule valeur BYMONTH est prise en charge." -#: awx/api/serializers.py:3622 +#: awx/api/serializers.py:4471 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY avec un préfixe numérique non pris en charge." -#: awx/api/serializers.py:3624 +#: awx/api/serializers.py:4473 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY non pris en charge." -#: awx/api/serializers.py:3626 +#: awx/api/serializers.py:4475 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO non pris en charge." -#: awx/api/serializers.py:3630 +#: awx/api/serializers.py:4477 +msgid "RRULE may not contain both COUNT and UNTIL" +msgstr "RRULE peut contenir à la fois COUNT et UNTIL" + +#: awx/api/serializers.py:4481 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 non pris en charge." -#: awx/api/serializers.py:3634 -msgid "rrule parsing failed validation." -msgstr "L'analyse rrule n'a pas pu être validée." +#: awx/api/serializers.py:4485 +msgid "rrule parsing failed validation: {}" +msgstr "L'analyse rrule n'a pas pu être validée : {}" -#: awx/api/serializers.py:3760 +#: awx/api/serializers.py:4526 +msgid "Inventory Source must be a cloud resource." +msgstr "La source d'inventaire doit être une ressource cloud." + +#: awx/api/serializers.py:4528 +msgid "Manual Project cannot have a schedule set." +msgstr "Le projet manuel ne peut pas avoir de calendrier défini." + +#: awx/api/serializers.py:4541 +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:4546 +msgid "Count of all jobs that target this instance" +msgstr "Le nombre de jobs qui ciblent cette instance." + +#: awx/api/serializers.py:4579 +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:4584 +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:4592 +msgid "Policy Instance Percentage" +msgstr "Pourcentage d'instances de stratégie" + +#: awx/api/serializers.py:4593 +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:4598 +msgid "Policy Instance Minimum" +msgstr "Instances de stratégies minimum" + +#: awx/api/serializers.py:4599 +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:4604 +msgid "Policy Instance List" +msgstr "Listes d'instances de stratégie" + +#: awx/api/serializers.py:4605 +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:4627 +msgid "Duplicate entry {}." +msgstr "Entrée dupliquée {}." + +#: awx/api/serializers.py:4629 +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:4634 +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:4704 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" @@ -595,7 +767,7 @@ msgstr "" "Un récapitulatif des valeurs nouvelles et modifiées lorsqu'un objet est " "créé, mis à jour ou supprimé" -#: awx/api/serializers.py:3762 +#: awx/api/serializers.py:4706 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 " @@ -605,7 +777,7 @@ msgstr "" "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:3765 +#: awx/api/serializers.py:4709 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated" @@ -615,166 +787,216 @@ msgstr "" "associer et dissocier des événements, il s'agit du type d'objet auquel " "object1 est associé." -#: awx/api/serializers.py:3768 +#: awx/api/serializers.py:4712 msgid "The action taken with respect to the given object(s)." msgstr "Action appliquée par rapport à l'objet ou aux objets donnés." -#: awx/api/serializers.py:3885 -msgid "Unable to login with provided credentials." -msgstr "Connexion impossible avec les informations d'identification fournies." - -#: awx/api/serializers.py:3887 -msgid "Must include \"username\" and \"password\"." -msgstr "Elles doivent inclure le nom d'utilisateur et le mot de passe." - -#: awx/api/views.py:108 +#: awx/api/views.py:118 msgid "Your license does not allow use of the activity stream." msgstr "Votre licence ne permet pas l'utilisation du flux d'activité." -#: awx/api/views.py:118 +#: awx/api/views.py:128 msgid "Your license does not permit use of system tracking." msgstr "Votre licence ne permet pas l'utilisation du suivi du système." -#: awx/api/views.py:128 +#: awx/api/views.py:138 msgid "Your license does not allow use of workflows." msgstr "Votre licence ne permet pas l'utilisation de workflows." -#: awx/api/views.py:142 +#: awx/api/views.py:152 msgid "Cannot delete job resource when associated workflow job is running." msgstr "" "Impossible de supprimer les ressources de tâche lorsqu'une tâche de workflow" " associée est en cours d'exécution." -#: awx/api/views.py:146 +#: awx/api/views.py:157 msgid "Cannot delete running job resource." msgstr "Impossible de supprimer la ressource de la tâche en cours." -#: awx/api/views.py:155 awx/templates/rest_framework/api.html:28 +#: awx/api/views.py:162 +msgid "Job has not finished processing events." +msgstr "Job n'ayant pas terminé de traiter les événements." + +#: awx/api/views.py:221 +msgid "Related job {} is still processing events." +msgstr "Le job associé {} est toujours en cours de traitement des événements." + +#: awx/api/views.py:228 awx/templates/rest_framework/api.html:28 msgid "REST API" msgstr "API REST" -#: awx/api/views.py:164 awx/templates/rest_framework/api.html:4 +#: awx/api/views.py:238 awx/templates/rest_framework/api.html:4 msgid "AWX REST API" msgstr "API REST AWX" -#: awx/api/views.py:226 +#: awx/api/views.py:252 +msgid "API OAuth 2 Authorization Root" +msgstr "API OAuth 2 Authorization Root" + +#: awx/api/views.py:317 msgid "Version 1" msgstr "Version 1" -#: awx/api/views.py:230 +#: awx/api/views.py:321 msgid "Version 2" msgstr "Version 2" -#: awx/api/views.py:241 +#: awx/api/views.py:330 msgid "Ping" msgstr "Ping" -#: awx/api/views.py:272 awx/conf/apps.py:12 +#: awx/api/views.py:361 awx/conf/apps.py:10 msgid "Configuration" msgstr "Configuration" -#: awx/api/views.py:325 +#: awx/api/views.py:418 msgid "Invalid license data" msgstr "Données de licence non valides" -#: awx/api/views.py:327 +#: awx/api/views.py:420 msgid "Missing 'eula_accepted' property" msgstr "Propriété 'eula_accepted' manquante" -#: awx/api/views.py:331 +#: awx/api/views.py:424 msgid "'eula_accepted' value is invalid" msgstr "La valeur 'eula_accepted' n'est pas valide" -#: awx/api/views.py:334 +#: awx/api/views.py:427 msgid "'eula_accepted' must be True" msgstr "La valeur 'eula_accepted' doit être True" -#: awx/api/views.py:341 +#: awx/api/views.py:434 msgid "Invalid JSON" msgstr "Syntaxe JSON non valide" -#: awx/api/views.py:349 +#: awx/api/views.py:442 msgid "Invalid License" msgstr "Licence non valide" -#: awx/api/views.py:359 +#: awx/api/views.py:452 msgid "Invalid license" msgstr "Licence non valide" -#: awx/api/views.py:367 +#: awx/api/views.py:460 #, python-format msgid "Failed to remove license (%s)" msgstr "Suppression de la licence (%s) impossible" -#: awx/api/views.py:372 +#: awx/api/views.py:465 msgid "Dashboard" msgstr "Tableau de bord" -#: awx/api/views.py:471 +#: awx/api/views.py:564 msgid "Dashboard Jobs Graphs" msgstr "Graphiques de tâches du tableau de bord" -#: awx/api/views.py:507 +#: awx/api/views.py:600 #, python-format msgid "Unknown period \"%s\"" msgstr "Période \"%s\" inconnue" -#: awx/api/views.py:521 +#: awx/api/views.py:614 msgid "Instances" msgstr "Instances" -#: awx/api/views.py:529 +#: awx/api/views.py:622 msgid "Instance Detail" msgstr "Détail de l'instance" -#: awx/api/views.py:537 -msgid "Instance Running Jobs" -msgstr "Instance exécutant les tâches" +#: awx/api/views.py:643 +msgid "Instance Jobs" +msgstr "Jobs d'instance" -#: awx/api/views.py:552 +#: awx/api/views.py:657 msgid "Instance's Instance Groups" msgstr "Groupes d'instances de l'instance" -#: awx/api/views.py:562 +#: awx/api/views.py:666 msgid "Instance Groups" msgstr "Groupes d'instances" -#: awx/api/views.py:570 +#: awx/api/views.py:674 msgid "Instance Group Detail" msgstr "Détail du groupe d'instances" -#: awx/api/views.py:578 +#: awx/api/views.py:682 +msgid "Isolated Groups can not be removed from the API" +msgstr "Les groupes isolés ne peuvent pas être supprimés des l'API" + +#: awx/api/views.py:684 +msgid "" +"Instance Groups acting as a controller for an Isolated Group can not be " +"removed from the API" +msgstr "" +"Les groupes d'instance agissant en tant que contrôleur d'un groupe isolé ne " +"peuvent pas être retirés de l'API" + +#: awx/api/views.py:690 msgid "Instance Group Running Jobs" msgstr "Groupe d'instances exécutant les tâches" -#: awx/api/views.py:588 +#: awx/api/views.py:699 msgid "Instance Group's Instances" msgstr "Instances du groupe d'instances" -#: awx/api/views.py:598 +#: awx/api/views.py:709 msgid "Schedules" msgstr "Calendriers" -#: awx/api/views.py:617 +#: awx/api/views.py:723 +msgid "Schedule Recurrence Rule Preview" +msgstr "Prévisualisation Règle récurrente de planning" + +#: awx/api/views.py:770 +msgid "Cannot assign credential when related template is null." +msgstr "" +"Impossible d'attribuer des identifiants lorsque le modèle associé est nul." + +#: awx/api/views.py:775 +msgid "Related template cannot accept {} on launch." +msgstr "Le modèle associé ne peut pas accepter {} au lancement." + +#: awx/api/views.py:777 +msgid "" +"Credential that requires user input on launch cannot be used in saved launch" +" configuration." +msgstr "" +"Les identifiants qui nécessitent l'entrée de l'utilisateur au lancement ne " +"peuvent pas être utilisés dans la configuration de lancement sauvegardée." + +#: awx/api/views.py:785 +#, python-brace-format +msgid "" +"This launch configuration already provides a {credential_type} credential." +msgstr "" +"Cette configuration de lancement fournit déjà un credential " +"{credential_type}." + +#: awx/api/views.py:788 +#, python-brace-format +msgid "Related template already uses {credential_type} credential." +msgstr "Le modèle associé utilise déjà l'identifiant {credential_type}." + +#: awx/api/views.py:806 msgid "Schedule Jobs List" msgstr "Listes des tâches de planification" -#: awx/api/views.py:843 +#: awx/api/views.py:961 msgid "Your license only permits a single organization to exist." msgstr "Votre licence permet uniquement l'existence d'une seule organisation." -#: awx/api/views.py:1079 awx/api/views.py:4666 +#: awx/api/views.py:1193 awx/api/views.py:5056 msgid "You cannot assign an Organization role as a child role for a Team." msgstr "" "Vous ne pouvez pas attribuer un rôle Organisation en tant que rôle enfant " "pour une équipe." -#: awx/api/views.py:1083 awx/api/views.py:4680 +#: awx/api/views.py:1197 awx/api/views.py:5070 msgid "You cannot grant system-level permissions to a team." msgstr "" "Vous ne pouvez pas accorder de permissions au niveau système à une équipe." -#: awx/api/views.py:1090 awx/api/views.py:4672 +#: awx/api/views.py:1204 awx/api/views.py:5062 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" @@ -783,36 +1005,71 @@ msgstr "" "équipe lorsque le champ Organisation n'est pas défini ou qu'elle appartient " "à une organisation différente" -#: awx/api/views.py:1180 -msgid "Cannot delete project." -msgstr "Suppression du projet impossible." - -#: awx/api/views.py:1215 +#: awx/api/views.py:1318 msgid "Project Schedules" msgstr "Calendriers des projets" -#: awx/api/views.py:1227 +#: awx/api/views.py:1329 msgid "Project SCM Inventory Sources" msgstr "Sources d'inventaire SCM du projet" -#: awx/api/views.py:1354 +#: awx/api/views.py:1430 +msgid "Project Update Events List" +msgstr "Liste des événements de mise à jour du projet" + +#: awx/api/views.py:1444 +msgid "System Job Events List" +msgstr "Liste des événements d'un job système" + +#: awx/api/views.py:1458 +msgid "Inventory Update Events List" +msgstr "Liste des événements de mise à jour de l'inventaire" + +#: awx/api/views.py:1492 msgid "Project Update SCM Inventory Updates" msgstr "Mises à jour de l'inventaire SCM de la mise à jour du projet" -#: awx/api/views.py:1409 +#: awx/api/views.py:1551 msgid "Me" msgstr "Moi-même" -#: awx/api/views.py:1453 awx/api/views.py:4623 -msgid "You may not perform any action with your own admin_role." -msgstr "Vous ne pouvez pas effectuer d'action avec votre propre admin_role." +#: awx/api/views.py:1559 +msgid "OAuth 2 Applications" +msgstr "OAuth 2 Applications" -#: awx/api/views.py:1459 awx/api/views.py:4627 -msgid "You may not change the membership of a users admin_role" -msgstr "" -"Vous ne pouvez pas modifier l'appartenance de l'admin_role d'un utilisateur" +#: awx/api/views.py:1568 +msgid "OAuth 2 Application Detail" +msgstr "OAuth 2 Détails Application" -#: awx/api/views.py:1464 awx/api/views.py:4632 +#: awx/api/views.py:1577 +msgid "OAuth 2 Application Tokens" +msgstr "OAuth 2 Jetons Application" + +#: awx/api/views.py:1599 +msgid "OAuth2 Tokens" +msgstr "OAuth2 Jetons" + +#: awx/api/views.py:1608 +msgid "OAuth2 User Tokens" +msgstr "OAuth2 Jetons Utilisateur" + +#: awx/api/views.py:1620 +msgid "OAuth2 User Authorized Access Tokens" +msgstr "OAuth2 Jetons d'accès Utilisateur autorisé" + +#: awx/api/views.py:1635 +msgid "Organization OAuth2 Applications" +msgstr "Organisation OAuth2 Applications" + +#: awx/api/views.py:1647 +msgid "OAuth2 Personal Access Tokens" +msgstr "OAuth2 Jetons d'accès personnels" + +#: awx/api/views.py:1662 +msgid "OAuth Token Detail" +msgstr "OAuth 2 Détails Jeton" + +#: awx/api/views.py:1722 awx/api/views.py:5023 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" @@ -821,65 +1078,65 @@ msgstr "" "utilisateur ne figurant pas dans l'organisation d'informations " "d'identification." -#: awx/api/views.py:1468 awx/api/views.py:4636 +#: awx/api/views.py:1726 awx/api/views.py:5027 msgid "You cannot grant private credential access to another user" msgstr "" "Vous ne pouvez pas accorder d'accès privé par informations d'identification " "à un autre utilisateur" -#: awx/api/views.py:1566 +#: awx/api/views.py:1824 #, python-format msgid "Cannot change %s." msgstr "Impossible de modifier %s." -#: awx/api/views.py:1572 +#: awx/api/views.py:1830 msgid "Cannot delete user." msgstr "Impossible de supprimer l'utilisateur." -#: awx/api/views.py:1601 +#: awx/api/views.py:1854 msgid "Deletion not allowed for managed credential types" msgstr "" "Suppression non autorisée pour les types d'informations d'identification " "gérés." -#: awx/api/views.py:1603 +#: awx/api/views.py:1856 msgid "Credential types that are in use cannot be deleted" msgstr "" "Les types d'informations d'identification utilisés ne peuvent pas être " "supprimés." -#: awx/api/views.py:1781 +#: awx/api/views.py:2031 msgid "Cannot delete inventory script." msgstr "Impossible de supprimer le script d'inventaire." -#: awx/api/views.py:1866 +#: awx/api/views.py:2122 #, python-brace-format msgid "{0}" msgstr "{0}" -#: awx/api/views.py:2101 +#: awx/api/views.py:2353 msgid "Fact not found." msgstr "Fait introuvable." -#: awx/api/views.py:2125 +#: awx/api/views.py:2375 msgid "SSLError while trying to connect to {}" msgstr "ErreurSSL lors de la tentative de connexion au {} " -#: awx/api/views.py:2127 +#: awx/api/views.py:2377 msgid "Request to {} timed out." msgstr "Délai de requête {} expiré." -#: awx/api/views.py:2129 -msgid "Unkown exception {} while trying to GET {}" +#: awx/api/views.py:2379 +msgid "Unknown exception {} while trying to GET {}" msgstr "Exception inconnue {} lors de la tentative GET {}" -#: awx/api/views.py:2132 +#: awx/api/views.py:2382 msgid "" "Unauthorized access. Please check your Insights Credential username and " "password." msgstr "Accès non autorisé. Veuillez vérifier vos identifiants pour Insights." -#: awx/api/views.py:2134 +#: awx/api/views.py:2385 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" @@ -888,216 +1145,274 @@ msgstr "" "sur l'URL {}. Le serveur a répondu avec un code de statut {} et un message " "{} " -#: awx/api/views.py:2140 +#: awx/api/views.py:2392 msgid "Expected JSON response from Insights but instead got {}" msgstr "" "Réponse JSON attendue de la part d'Insights, mais {} a été reçu à la place" -#: awx/api/views.py:2147 +#: awx/api/views.py:2399 msgid "This host is not recognized as an Insights host." msgstr "Cet hôte n'est pas reconnu comme hôte Insights." -#: awx/api/views.py:2152 +#: awx/api/views.py:2404 msgid "The Insights Credential for \"{}\" was not found." msgstr "Informations d'identification Insights pour \"{}\" introuvables." -#: awx/api/views.py:2221 +#: awx/api/views.py:2472 msgid "Cyclical Group association." msgstr "Association de groupe cyclique." -#: awx/api/views.py:2499 +#: awx/api/views.py:2686 msgid "Inventory Source List" msgstr "Liste des sources d'inventaire" -#: awx/api/views.py:2512 +#: awx/api/views.py:2698 msgid "Inventory Sources Update" msgstr "Mise à jour des sources d'inventaire" -#: awx/api/views.py:2542 +#: awx/api/views.py:2731 msgid "Could not start because `can_update` returned False" msgstr "Démarrage impossible car `can_update` a renvoyé False" -#: awx/api/views.py:2550 +#: awx/api/views.py:2739 msgid "No inventory sources to update." msgstr "Aucune source d'inventaire à actualiser." -#: awx/api/views.py:2582 -msgid "Cannot delete inventory source." -msgstr "Impossible de supprimer la source d'inventaire." - -#: awx/api/views.py:2590 +#: awx/api/views.py:2768 msgid "Inventory Source Schedules" msgstr "Calendriers des sources d'inventaire" -#: awx/api/views.py:2620 +#: awx/api/views.py:2796 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" "Les modèles de notification ne peuvent être attribués que lorsque la source " "est l'une des {}." #: awx/api/views.py:2851 +msgid "Vault credentials are not yet supported for inventory sources." +msgstr "" +"Les identifiants d'archivage sécurisé ne sont pas encore pris en charge pour" +" les sources d'inventaire." + +#: awx/api/views.py:2856 +msgid "Source already has cloud credential assigned." +msgstr "La source a déjà un identifiant cloud d'attribué." + +#: awx/api/views.py:3016 +msgid "" +"'credentials' cannot be used in combination with 'credential', " +"'vault_credential', or 'extra_credentials'." +msgstr "" +"Les 'credentials' (identifiants) ne peuvent pas être utilisés en combinaison" +" à des 'credential', 'vault_credential', ou 'extra_credentials'." + +#: awx/api/views.py:3128 msgid "Job Template Schedules" msgstr "Calendriers des modèles de tâche" -#: awx/api/views.py:2871 awx/api/views.py:2882 +#: awx/api/views.py:3146 awx/api/views.py:3157 msgid "Your license does not allow adding surveys." msgstr "Votre licence ne permet pas l'ajout de questionnaires." -#: awx/api/views.py:2889 -msgid "'name' missing from survey spec." -msgstr "'name' manquant dans la spécification du questionnaire." +#: awx/api/views.py:3176 +msgid "Field '{}' is missing from survey spec." +msgstr "Le champ '{}' manque dans la specification du questionnaire." -#: awx/api/views.py:2891 -msgid "'description' missing from survey spec." -msgstr "'description' manquante dans la spécification du questionnaire." +#: awx/api/views.py:3178 +msgid "Expected {} for field '{}', received {} type." +msgstr "{} attendu pour le champ '{}', type {} reçu." -#: awx/api/views.py:2893 -msgid "'spec' missing from survey spec." -msgstr "'spec' manquante dans la spécification du questionnaire." - -#: awx/api/views.py:2895 -msgid "'spec' must be a list of items." -msgstr "'spec' doit être une liste d'éléments" - -#: awx/api/views.py:2897 +#: awx/api/views.py:3182 msgid "'spec' doesn't contain any items." msgstr "'spec' ne contient aucun élément." -#: awx/api/views.py:2903 +#: awx/api/views.py:3191 #, python-format msgid "Survey question %s is not a json object." msgstr "La question %s n'est pas un objet json." -#: awx/api/views.py:2905 +#: awx/api/views.py:3193 #, python-format msgid "'type' missing from survey question %s." msgstr "'type' est manquant dans la question %s." -#: awx/api/views.py:2907 +#: awx/api/views.py:3195 #, python-format msgid "'question_name' missing from survey question %s." msgstr "'question_name' est manquant dans la question %s." -#: awx/api/views.py:2909 +#: awx/api/views.py:3197 #, python-format msgid "'variable' missing from survey question %s." msgstr "'variable' est manquant dans la question %s." -#: awx/api/views.py:2911 +#: awx/api/views.py:3199 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "'variable' '%(item)s' en double dans la question %(survey)s." -#: awx/api/views.py:2916 +#: awx/api/views.py:3204 #, python-format msgid "'required' missing from survey question %s." msgstr "'required' est manquant dans la question %s." -#: awx/api/views.py:2921 +#: awx/api/views.py:3209 #, python-brace-format msgid "" "Value {question_default} for '{variable_name}' expected to be a string." msgstr "Valeur {question_default} de '{variable_name}' de chaîne attendue. " -#: awx/api/views.py:2928 +#: awx/api/views.py:3219 #, python-brace-format msgid "" -"$encrypted$ is reserved keyword for password questions and may not be used " -"as a default for '{variable_name}' in survey question {question_position}." +"$encrypted$ is a reserved keyword for password question defaults, survey " +"question {question_position} is type {question_type}." msgstr "" -"$encrypted$ est un mot clé réservé pour les questions de mots de passe et ne" -" peuvent pas être utilisées par défaut pour '{variable_name}' dans les " -"questions d'enquêtes {question_position}." +"$encrypted$ est un mot clé réservé pour les questions de mots de passe par " +"défaut, la questions de questionnaire {question_position} est de type " +"{question_type}." -#: awx/api/views.py:3049 +#: awx/api/views.py:3235 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword, may not be used for new default in " +"position {question_position}." +msgstr "" +"$encrypted$ est un mot clé réservé, et ne peut pas être utilisé comme valeur" +" pour la position {question_position}." + +#: awx/api/views.py:3309 +#, python-brace-format +msgid "Cannot assign multiple {credential_type} credentials." +msgstr "Ne peut pas attribuer plusieurs identifiants {credential_type}." + +#: awx/api/views.py:3327 +msgid "Extra credentials must be network or cloud." +msgstr "" +"Les informations d'identification supplémentaires doivent être des " +"identifiants réseau ou cloud." + +#: awx/api/views.py:3349 msgid "Maximum number of labels for {} reached." msgstr "Nombre maximum d'étiquettes {} atteint." -#: awx/api/views.py:3170 +#: awx/api/views.py:3472 msgid "No matching host could be found!" msgstr "Aucun hôte correspondant n'a été trouvé." -#: awx/api/views.py:3173 +#: awx/api/views.py:3475 msgid "Multiple hosts matched the request!" msgstr "Plusieurs hôtes correspondent à la requête." -#: awx/api/views.py:3178 +#: awx/api/views.py:3480 msgid "Cannot start automatically, user input required!" msgstr "" "Impossible de démarrer automatiquement, saisie de l'utilisateur obligatoire." -#: awx/api/views.py:3185 +#: awx/api/views.py:3487 msgid "Host callback job already pending." msgstr "La tâche de rappel de l'hôte est déjà en attente." -#: awx/api/views.py:3199 +#: awx/api/views.py:3502 awx/api/views.py:4284 msgid "Error starting job!" msgstr "Erreur lors du démarrage de la tâche." -#: awx/api/views.py:3306 +#: awx/api/views.py:3622 #, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr " {0} ne peut pas être associé lorsque {1} est associé." -#: awx/api/views.py:3331 +#: awx/api/views.py:3647 msgid "Multiple parent relationship not allowed." msgstr "Relation de parents multiples non autorisée." -#: awx/api/views.py:3336 +#: awx/api/views.py:3652 msgid "Cycle detected." msgstr "Cycle détecté." -#: awx/api/views.py:3540 +#: awx/api/views.py:3850 msgid "Workflow Job Template Schedules" msgstr "Calendriers des modèles de tâche Workflow" -#: awx/api/views.py:3685 awx/api/views.py:4268 +#: awx/api/views.py:3986 awx/api/views.py:4690 msgid "Superuser privileges needed." msgstr "Privilèges de superutilisateur requis." -#: awx/api/views.py:3717 +#: awx/api/views.py:4018 msgid "System Job Template Schedules" msgstr "Calendriers des modèles de tâche Système" -#: awx/api/views.py:3780 +#: awx/api/views.py:4076 msgid "POST not allowed for Job launching in version 2 of the api" msgstr "" "POST non autorisé pour le lancement de tâches dans la version 2 de l'api" -#: awx/api/views.py:3942 +#: awx/api/views.py:4100 awx/api/views.py:4106 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "PUT non autorisé pour le détail des tâches dans la version 2 de l'API" + +#: awx/api/views.py:4267 +#, python-brace-format +msgid "Wait until job finishes before retrying on {status_value} hosts." +msgstr "" +"Patientez jusqu'à ce que le job se termine avant d'essayer à nouveau les " +"hôtes {status_value}." + +#: awx/api/views.py:4272 +#, python-brace-format +msgid "Cannot retry on {status_value} hosts, playbook stats not available." +msgstr "" +"Impossible d'essayer à nouveau sur les hôtes {status_value}, les stats de " +"playbook ne sont pas disponibles." + +#: awx/api/views.py:4277 +#, python-brace-format +msgid "Cannot relaunch because previous job had 0 {status_value} hosts." +msgstr "" +"Impossible de relancer car le job précédent possède 0 {status_value} hôtes." + +#: awx/api/views.py:4306 +msgid "Cannot create schedule because job requires credential passwords." +msgstr "" +"Impossible de créer un planning parce que le job exige des mots de passe " +"d'authentification." + +#: awx/api/views.py:4311 +msgid "Cannot create schedule because job was launched by legacy method." +msgstr "" +"Impossible de créer un planning parce que le travail a été lancé par la " +"méthode héritée." + +#: awx/api/views.py:4313 +msgid "Cannot create schedule because a related resource is missing." +msgstr "" +"Impossible de créer un planning car une ressource associée est manquante." + +#: awx/api/views.py:4368 msgid "Job Host Summaries List" msgstr "Liste récapitulative des hôtes de la tâche" -#: awx/api/views.py:3989 +#: awx/api/views.py:4417 msgid "Job Event Children List" msgstr "Liste des enfants d'événement de la tâche" -#: awx/api/views.py:3998 +#: awx/api/views.py:4427 msgid "Job Event Hosts List" msgstr "Liste des hôtes d'événement de la tâche" -#: awx/api/views.py:4008 +#: awx/api/views.py:4436 msgid "Job Events List" msgstr "Liste des événements de la tâche" -#: awx/api/views.py:4222 +#: awx/api/views.py:4647 msgid "Ad Hoc Command Events List" msgstr "Liste d'événements de la commande ad hoc" -#: awx/api/views.py:4437 -msgid "Error generating stdout download file: {}" -msgstr "Erreur lors de la génération du fichier de téléchargement stdout : {}" - -#: awx/api/views.py:4450 -#, python-format -msgid "Error generating stdout download file: %s" -msgstr "Erreur lors de la génération du fichier de téléchargement stdout : %s" - -#: awx/api/views.py:4495 +#: awx/api/views.py:4889 msgid "Delete not allowed while there are pending notifications" msgstr "Suppression non autorisée tant que des notifications sont en attente" -#: awx/api/views.py:4502 +#: awx/api/views.py:4897 msgid "Notification Template Test" msgstr "Test de modèle de notification" @@ -1250,19 +1565,36 @@ 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:95 awx/conf/registry.py:85 awx/conf/views.py:56 +#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:55 msgid "User" msgstr "Utilisateur" -#: awx/conf/fields.py:63 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 +#, python-brace-format +msgid "" +"Expected None, True, False, a string or list of strings but got {input_type}" +" instead." +msgstr "" +"Les valeurs None, True, False, une chaîne ou une liste de chaînes étaient " +"attendues, mais {input_type} a été obtenu à la place." + +#: awx/conf/fields.py:104 msgid "Enter a valid URL" msgstr "Entez une URL valide" -#: awx/conf/fields.py:95 +#: awx/conf/fields.py:136 #, python-brace-format msgid "\"{input}\" is not a valid string." msgstr "\"{input}\" n'est pas une chaîne valide." +#: awx/conf/fields.py:151 +#, python-brace-format +msgid "" +"Expected a list of tuples of max length 2 but got {input_type} instead." +msgstr "" +"Liste de tuples de longueur max 2 attendue mais a obtenu {input_type} à la " +"place." + #: awx/conf/license.py:22 msgid "Your Tower license does not allow that." msgstr "Votre licence Tower ne vous y autorise pas." @@ -1359,9 +1691,9 @@ msgstr "" #: awx/conf/tests/unit/test_settings.py:411 #: awx/conf/tests/unit/test_settings.py:430 #: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:51 -#: awx/main/conf.py:63 awx/main/conf.py:81 awx/main/conf.py:96 -#: awx/main/conf.py:121 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "Système" @@ -1373,104 +1705,109 @@ msgstr "Système" msgid "OtherSystem" msgstr "Autre Système" -#: awx/conf/views.py:48 +#: awx/conf/views.py:47 msgid "Setting Categories" msgstr "Catégories de paramètre" -#: awx/conf/views.py:73 +#: awx/conf/views.py:71 msgid "Setting Detail" msgstr "Détails du paramètre" -#: awx/conf/views.py:168 +#: awx/conf/views.py:166 msgid "Logging Connectivity Test" msgstr "Journalisation du test de connectivité" -#: awx/main/access.py:44 -msgid "Resource is being used by running jobs." -msgstr "Ressource utilisée par les tâches en cours." +#: awx/main/access.py:57 +#, python-format +msgid "Required related field %s for permission check." +msgstr "Champs associés % requis pour la vérification des permissions." -#: awx/main/access.py:237 +#: awx/main/access.py:73 #, python-format msgid "Bad data found in related field %s." msgstr "Données incorrectes trouvées dans le champ %s associé." -#: awx/main/access.py:281 +#: awx/main/access.py:293 msgid "License is missing." msgstr "La licence est manquante." -#: awx/main/access.py:283 +#: awx/main/access.py:295 msgid "License has expired." msgstr "La licence est arrivée à expiration." -#: awx/main/access.py:291 +#: awx/main/access.py:303 #, python-format msgid "License count of %s instances has been reached." msgstr "Le nombre de licences d'instances %s a été atteint." -#: awx/main/access.py:293 +#: awx/main/access.py:305 #, python-format msgid "License count of %s instances has been exceeded." msgstr "Le nombre de licences d'instances %s a été dépassé." -#: awx/main/access.py:295 +#: awx/main/access.py:307 msgid "Host count exceeds available instances." msgstr "Le nombre d'hôtes dépasse celui des instances disponibles." -#: awx/main/access.py:299 +#: awx/main/access.py:311 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "La fonctionnalité %s n'est pas activée dans la licence active." -#: awx/main/access.py:301 +#: awx/main/access.py:313 msgid "Features not found in active license." msgstr "Fonctionnalités introuvables dans la licence active." -#: awx/main/access.py:707 +#: awx/main/access.py:818 msgid "Unable to change inventory on a host." msgstr "Impossible de modifier l'inventaire sur un hôte." -#: awx/main/access.py:724 awx/main/access.py:769 +#: awx/main/access.py:835 awx/main/access.py:880 msgid "Cannot associate two items from different inventories." msgstr "Impossible d'associer deux éléments d'inventaires différents." -#: awx/main/access.py:757 +#: awx/main/access.py:868 msgid "Unable to change inventory on a group." msgstr "Impossible de modifier l'inventaire sur un groupe." -#: awx/main/access.py:1017 +#: awx/main/access.py:1129 msgid "Unable to change organization on a team." msgstr "Impossible de modifier l'organisation d'une équipe." -#: awx/main/access.py:1030 +#: awx/main/access.py:1146 msgid "The {} role cannot be assigned to a team" msgstr "Le rôle {} ne peut pas être attribué à une équipe" -#: awx/main/access.py:1032 +#: awx/main/access.py:1148 msgid "The admin_role for a User cannot be assigned to a team" msgstr "L'admin_role d'un utilisateur ne peut pas être attribué à une équipe" -#: awx/main/access.py:1479 +#: awx/main/access.py:1502 awx/main/access.py:1936 +msgid "Job was launched with prompts provided by another user." +msgstr "" +"Le job a été lancé par des champs d'invite provenant d'un autre utilisateur." + +#: awx/main/access.py:1522 msgid "Job has been orphaned from its job template." msgstr "La tâche est orpheline de son modèle de tâche." -#: awx/main/access.py:1481 -msgid "You do not have execute permission to related job template." -msgstr "" -"Vous n'avez pas de permission d'exécution pour le modèle de tâche lié." +#: awx/main/access.py:1524 +msgid "Job was launched with unknown prompted fields." +msgstr "Le job été lancé par des champs d'invite inconnus." -#: awx/main/access.py:1484 +#: awx/main/access.py:1526 msgid "Job was launched with prompted fields." msgstr "La tâche a été lancée avec les champs d'invite." -#: awx/main/access.py:1486 +#: awx/main/access.py:1528 msgid " Organization level permissions required." msgstr "Permissions au niveau de l'organisation requises." -#: awx/main/access.py:1488 +#: awx/main/access.py:1530 msgid " You do not have permission to related resources." msgstr "Vous n'avez pas de permission vers les ressources liées." -#: awx/main/access.py:1833 +#: awx/main/access.py:1950 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1509,27 +1846,45 @@ msgstr "" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." msgstr "" "Contrôle si un administrateur d'organisation peut ou non afficher tous les " -"utilisateurs, même ceux qui ne sont pas associés à son organisation." +"utilisateurs et les équipes, même ceux qui ne sont pas associés à son " +"organisation." -#: awx/main/conf.py:49 +#: awx/main/conf.py:50 +msgid "Organization Admins Can Manage Users and Teams" +msgstr "" +"Les administrateurs de l'organisation peuvent gérer les utilisateurs et les " +"équipes." + +#: awx/main/conf.py:51 +msgid "" +"Controls whether any Organization Admin has the privileges to create and " +"manage users and teams. You may want to disable this ability if you are " +"using an LDAP or SAML integration." +msgstr "" +"Contrôle si l'administrateur de l'organisation dispose des privilèges " +"nécessaires pour créer et gérer les utilisateurs et les équipes. Vous pouvez" +" désactiver cette fonctionnalité si vous utilisez une intégration LDAP ou " +"SAML." + +#: awx/main/conf.py:60 msgid "Enable Administrator Alerts" msgstr "Activer les alertes administrateur" -#: awx/main/conf.py:50 +#: awx/main/conf.py:61 msgid "Email Admin users for system events that may require attention." msgstr "" "Alerter les administrateurs par email concernant des événements système " "susceptibles de mériter leur attention." -#: awx/main/conf.py:60 +#: awx/main/conf.py:71 msgid "Base URL of the Tower host" msgstr "URL de base pour l'hôte Tower" -#: awx/main/conf.py:61 +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to" " the Tower host." @@ -1537,51 +1892,43 @@ msgstr "" "Ce paramètre est utilisé par des services sous la forme de notifications " "permettant de rendre valide une URL pour l'hôte Tower." -#: awx/main/conf.py:70 +#: awx/main/conf.py:81 msgid "Remote Host Headers" msgstr "En-têtes d'hôte distant" -#: awx/main/conf.py:71 +#: awx/main/conf.py:82 msgid "" -"HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy.\n" -"\n" -"Note: The headers will be searched in order and the first found remote host name or IP will be used.\n" -"\n" -"In the below example 8.8.8.7 would be the chosen IP address.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"HTTP headers and meta keys to search to determine remote host name or IP. " +"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " +"behind a reverse proxy. See the \"Proxy Support\" section of the " +"Adminstrator guide for more details." msgstr "" -"En-têtes HTTP et méta-clés à rechercher afin de déterminer le nom ou l'adresse IP d'un hôte distant. Ajoutez des éléments supplémentaires à cette liste, tels que \"HTTP_X_FORWARDED_FOR\", en présence d'un proxy inverse.\n" -"\n" -"Remarque : les en-têtes seront recherchés dans l'ordre, et le premier nom ou la première adresse IP d'hôte distant trouvé(e) sera utilisé(e).\n" -"\n" -"Dans l'exemple ci-dessous 8.8.8.7 est l'adresse IP choisie. \n" -"X-Forwarded-For : 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Hôte : 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"En-têtes HTTP et méta-clés à rechercher afin de déterminer le nom ou " +"l'adresse IP d'un hôte distant. Ajoutez des éléments supplémentaires à cette" +" liste, tels que \"HTTP_X_FORWARDED_FOR\", en présence d'un proxy inverse. " +"Voir la section \"Support Proxy\" du Guide de l'administrateur pour obtenir " +"des détails supplémentaires." -#: awx/main/conf.py:88 +#: awx/main/conf.py:94 msgid "Proxy IP Whitelist" msgstr "Liste blanche des IP proxy" -#: awx/main/conf.py:89 +#: awx/main/conf.py:95 msgid "" -"If Tower is behind a reverse proxy/load balancer, use this setting to whitelist the proxy IP addresses from which Tower should trust custom REMOTE_HOST_HEADERS header values\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"If this setting is an empty list (the default), the headers specified by REMOTE_HOST_HEADERS will be trusted unconditionally')" +"If Tower is behind a reverse proxy/load balancer, use this setting to " +"whitelist the proxy IP addresses from which Tower should trust custom " +"REMOTE_HOST_HEADERS header values. If this setting is an empty list (the " +"default), the headers specified by REMOTE_HOST_HEADERS will be trusted " +"unconditionally')" msgstr "" -"Si Tower se trouve derrière un proxy inverse/équilibreur de charge, utilisez ce paramètre pour mettre sur liste blanche les addresses IP du proxy pour lesquelles Tower doit avoir confiance valeurs d'en-tête REMOTE_HOST_HEADERS personnalisées\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"Si ce paramètre est une liste vide (liste par défaut), les en-têtes spécifiés par REMOTE_HOST_HEADERS seront approuvés sans condition')" +"Si Tower se trouve derrière un proxy inverse/équilibreur de charge, utilisez ce paramètre pour mettre sur liste blanche les adresses IP du proxy en provenance desquelles Tower devra approuver les valeurs d'en-tête REMOTE_HOST_HEADERS personnalisées. Si ce paramètre correspond à une liste vide (valeur par défaut), les en-têtes spécifiés par\n" +"REMOTE_HOST_HEADERS seront approuvés sans condition)" -#: awx/main/conf.py:117 +#: awx/main/conf.py:121 msgid "License" msgstr "Licence" -#: awx/main/conf.py:118 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use " "/api/v1/config/ to update or change the license." @@ -1589,29 +1936,62 @@ msgstr "" "La licence détermine les fonctionnalités et les fonctions activées. Utilisez" " /api/v1/config/ pour mettre à jour ou modifier la licence." -#: awx/main/conf.py:128 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "Modules Ansible autorisés pour des tâches ad hoc" -#: awx/main/conf.py:129 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "Liste des modules que des tâches ad hoc sont autorisées à utiliser." -#: awx/main/conf.py:130 awx/main/conf.py:140 awx/main/conf.py:151 -#: awx/main/conf.py:161 awx/main/conf.py:171 awx/main/conf.py:181 -#: awx/main/conf.py:192 awx/main/conf.py:204 awx/main/conf.py:216 -#: awx/main/conf.py:229 awx/main/conf.py:241 awx/main/conf.py:251 -#: awx/main/conf.py:262 awx/main/conf.py:272 awx/main/conf.py:282 -#: awx/main/conf.py:292 awx/main/conf.py:304 awx/main/conf.py:316 -#: awx/main/conf.py:328 awx/main/conf.py:341 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "Tâches" -#: awx/main/conf.py:138 +#: awx/main/conf.py:143 +msgid "Always" +msgstr "Toujours" + +#: awx/main/conf.py:144 +msgid "Never" +msgstr "Jamais" + +#: awx/main/conf.py:145 +msgid "Only On Job Template Definitions" +msgstr "Définitions de Modèle de job uniquement" + +#: awx/main/conf.py:148 +msgid "When can extra variables contain Jinja templates?" +msgstr "" +"Quand des variables supplémentaires peuvent-elles contenir des modèles Jinja" +" ?" + +#: awx/main/conf.py:150 +msgid "" +"Ansible allows variable substitution via the Jinja2 templating language for " +"--extra-vars. This poses a potential security risk where Tower users with " +"the ability to specify extra vars at job launch time can use Jinja2 " +"templates to run arbitrary Python. It is recommended that this value be set" +" to \"template\" or \"never\"." +msgstr "" +"Ansible permet la substitution de variables via le langage de modèle Jinja2 " +"pour --extra-vars. Cela pose un risque potentiel de sécurité où les " +"utilisateurs de Tower ayant la possibilité de spécifier des vars " +"supplémentaires au moment du lancement du job peuvent utiliser les modèles " +"Jinja2 pour exécuter arbitrairement Python. Il est recommandé de définir " +"cette valeur à \"template\" (modèle) ou \"never\" (jamais)." + +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "Activer l'isolement des tâches" -#: awx/main/conf.py:139 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." @@ -1619,11 +1999,11 @@ msgstr "" "Permet d'isoler une tâche Ansible des parties protégées du système pour " "éviter l'exposition d'informations sensibles." -#: awx/main/conf.py:147 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "Chemin d'exécution de la tâche" -#: awx/main/conf.py:148 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " @@ -1633,22 +2013,22 @@ msgstr "" "l'exécution et l'isolation de la tâche (comme les fichiers des informations " "d'identification et les scripts d'inventaire personnalisés)." -#: awx/main/conf.py:159 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" msgstr "Chemins à dissimuler des tâches isolées" -#: awx/main/conf.py:160 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." msgstr "" "Chemins supplémentaires à dissimuler des processus isolés. Saisissez un " "chemin par ligne." -#: awx/main/conf.py:169 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" msgstr "Chemins à exposer aux tâches isolées" -#: awx/main/conf.py:170 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." @@ -1656,11 +2036,11 @@ msgstr "" "Liste blanche des chemins qui seraient autrement dissimulés de façon à ne " "pas être exposés aux tâches isolées. Saisissez un chemin par ligne." -#: awx/main/conf.py:179 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "Intervalle de vérification du statut isolé" -#: awx/main/conf.py:180 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." @@ -1668,11 +2048,11 @@ msgstr "" "Nombre de secondes de veille entre les vérifications de statut pour les " "tâches s'exécutant sur des instances isolées." -#: awx/main/conf.py:189 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "Délai d'attente du lancement isolé" -#: awx/main/conf.py:190 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " @@ -1682,11 +2062,11 @@ msgstr "" "isolées. Cela inclut la durée nécessaire pour copier les fichiers de " "contrôle de la source (playbook) sur l'instance isolée." -#: awx/main/conf.py:201 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "Délai d'attente de connexion isolée" -#: awx/main/conf.py:202 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " @@ -1696,11 +2076,11 @@ msgstr "" " communication avec des instances isolées. La valeur doit être nettement " "supérieure à la latence du réseau attendue." -#: awx/main/conf.py:212 +#: awx/main/conf.py:237 msgid "Generate RSA keys for isolated instances" msgstr "Génère des clés RSA pour les instances isolées" -#: awx/main/conf.py:213 +#: awx/main/conf.py:238 msgid "" "If set, a random RSA key will be generated and distributed to isolated " "instances. To disable this behavior and manage authentication for isolated " @@ -1711,19 +2091,19 @@ msgstr "" "l'authentification pour les instances isolées en dehors de Tower, désactiver" " ce paramètre." -#: awx/main/conf.py:227 awx/main/conf.py:228 +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "Clé privée RSA pour le trafic SSH vers des instances isolées" -#: awx/main/conf.py:239 awx/main/conf.py:240 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "Clé publique RSA pour le trafic SSH vers des instances isolées" -#: awx/main/conf.py:249 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "Variables d'environnement supplémentaires" -#: awx/main/conf.py:250 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." @@ -1731,11 +2111,11 @@ msgstr "" "Variables d'environnement supplémentaires définies pour les exécutions de " "Playbook, les mises à jour de projet et l'envoi de notifications." -#: awx/main/conf.py:260 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" msgstr "Taille d'affichage maximale pour une sortie standard" -#: awx/main/conf.py:261 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." @@ -1743,12 +2123,12 @@ msgstr "" "Taille maximale d'une sortie standard en octets à afficher avant de demander" " le téléchargement de la sortie." -#: awx/main/conf.py:270 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "" "Taille d'affichage maximale pour une sortie standard d'événement de tâche" -#: awx/main/conf.py:271 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." @@ -1757,11 +2137,11 @@ msgstr "" "tâche ou pour un seul événement de commande ad hoc. `stdout` se terminera " "par `...` quand il sera tronqué." -#: awx/main/conf.py:280 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" msgstr "Nombre max. de tâches planifiées" -#: awx/main/conf.py:281 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." @@ -1770,11 +2150,11 @@ msgstr "" "d'exécution lors de son lancement à partir d'un calendrier, avant que " "d'autres ne soient créés." -#: awx/main/conf.py:290 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "Plug-ins de rappel Ansible" -#: awx/main/conf.py:291 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." @@ -1782,11 +2162,11 @@ msgstr "" "Liste des chemins servant à rechercher d'autres plug-ins de rappel qui " "serviront lors de l'exécution de tâches. Saisissez un chemin par ligne." -#: awx/main/conf.py:301 +#: awx/main/conf.py:327 msgid "Default Job Timeout" msgstr "Délai d'attente par défaut des tâches" -#: awx/main/conf.py:302 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " @@ -1796,11 +2176,11 @@ msgstr "" "pour indiquer qu'aucun délai ne doit être imposé. Un délai d'attente défini " "sur celui d'un modèle de tâche précis écrasera cette valeur." -#: awx/main/conf.py:313 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "Délai d'attente par défaut pour la mise à jour d'inventaire" -#: awx/main/conf.py:314 +#: awx/main/conf.py:340 msgid "" "Maximum time in seconds to allow inventory updates to run. Use value of 0 to" " indicate that no timeout should be imposed. A timeout set on an individual " @@ -1811,11 +2191,11 @@ msgstr "" "délai d'attente défini sur celui d'une source d'inventaire précise écrasera " "cette valeur." -#: awx/main/conf.py:325 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" msgstr "Délai d'attente par défaut pour la mise à jour de projet" -#: awx/main/conf.py:326 +#: awx/main/conf.py:352 msgid "" "Maximum time in seconds to allow project updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " @@ -1825,44 +2205,46 @@ msgstr "" "la valeur 0 pour indiquer qu'aucun délai ne doit être imposé. Un délai " "d'attente défini sur celui d'un projet précis écrasera cette valeur." -#: awx/main/conf.py:337 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "Expiration du délai d’attente du cache Ansible Fact Cache par hôte" -#: awx/main/conf.py:338 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be" " accessible by a playbook. Note, this does not influence the deletion of " -"ansible_facts from the database." +"ansible_facts from the database. Use a value of 0 to indicate that no " +"timeout should be imposed." msgstr "" "Durée maximale, en secondes, pendant laquelle les faits Ansible sont " "considérés comme valides depuis leur dernière modification. Seuls les faits " "valides - non périmés - seront accessibles par un Playbook. Remarquez que " "cela n'a aucune incidence sur la suppression d'ansible_facts de la base de " -"données." +"données. Utiliser une valeur de 0 pour indiquer qu'aucun timeout ne soit " +"imposé." -#: awx/main/conf.py:350 +#: awx/main/conf.py:377 msgid "Logging Aggregator" msgstr "Agrégateur de journalisation" -#: awx/main/conf.py:351 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." msgstr "Nom d'hôte / IP où les journaux externes seront envoyés." -#: awx/main/conf.py:352 awx/main/conf.py:363 awx/main/conf.py:375 -#: awx/main/conf.py:385 awx/main/conf.py:397 awx/main/conf.py:412 -#: awx/main/conf.py:424 awx/main/conf.py:433 awx/main/conf.py:443 -#: awx/main/conf.py:453 awx/main/conf.py:464 awx/main/conf.py:476 -#: awx/main/conf.py:489 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:480 awx/main/conf.py:491 awx/main/conf.py:503 +#: awx/main/conf.py:516 msgid "Logging" msgstr "Journalisation" -#: awx/main/conf.py:360 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "Port d'agrégateur de journalisation" -#: awx/main/conf.py:361 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." @@ -1870,43 +2252,43 @@ msgstr "" "Port d'agrégateur de journalisation où envoyer les journaux (le cas échéant " "ou si non fourni dans l'agrégateur de journalisation)." -#: awx/main/conf.py:373 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" msgstr "Type d'agrégateur de journalisation" -#: awx/main/conf.py:374 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." msgstr "" "Formater les messages pour l'agrégateur de journalisation que vous aurez " "choisi." -#: awx/main/conf.py:383 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "Nom d'utilisateur de l'agrégateur de journalisation" -#: awx/main/conf.py:384 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "" "Nom d'utilisateur pour agrégateur de journalisation externe (le cas " "échéant)." -#: awx/main/conf.py:395 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "Mot de passe / Jeton d'agrégateur de journalisation" -#: awx/main/conf.py:396 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "" "Mot de passe ou jeton d'authentification d'agrégateur de journalisation " "externe (le cas échéant)." -#: awx/main/conf.py:405 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "" "Journaliseurs à l'origine des envois de données à l'agrégateur de journaux" -#: awx/main/conf.py:406 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include any or all of: \n" "awx - service logs\n" @@ -1920,15 +2302,15 @@ msgstr "" "job_events - données de rappel issues d'événements de tâche Ansible\n" "system_tracking - données générées par des tâches de scan." -#: awx/main/conf.py:419 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" msgstr "Système de journalisation traçant des facts individuellement" -#: awx/main/conf.py:420 +#: awx/main/conf.py:447 msgid "" -"If set, system tracking facts will be sent for each package, service, " -"orother item found in a scan, allowing for greater search query granularity." -" If unset, facts will be sent as a single dictionary, allowing for greater " +"If set, system tracking facts will be sent for each package, service, or " +"other item found in a scan, allowing for greater search query granularity. " +"If unset, facts will be sent as a single dictionary, allowing for greater " "efficiency in fact processing." msgstr "" "Si défini, les facts de traçage de système seront envoyés pour chaque " @@ -1937,36 +2319,36 @@ msgstr "" " sous forme de dictionnaire unique, ce qui permet une meilleure efficacité " "du processus pour les facts." -#: awx/main/conf.py:431 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "Activer la journalisation externe" -#: awx/main/conf.py:432 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "Activer l'envoi de journaux à un agrégateur de journaux externe." -#: awx/main/conf.py:441 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "Identificateur unique Tower" -#: awx/main/conf.py:442 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." msgstr "Utile pour identifier les instances Tower spécifiquement." -#: awx/main/conf.py:451 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "Protocole de l'agrégateur de journalisation" -#: awx/main/conf.py:452 +#: awx/main/conf.py:479 msgid "Protocol used to communicate with log aggregator." msgstr "" "Protocole utilisé pour communiquer avec l'agrégateur de journalisation." -#: awx/main/conf.py:460 +#: awx/main/conf.py:487 msgid "TCP Connection Timeout" msgstr "Expiration du délai d'attente de connexion TCP" -#: awx/main/conf.py:461 +#: awx/main/conf.py:488 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." @@ -1975,11 +2357,11 @@ msgstr "" "agrégateur de journalisation externe. S'applique aux protocoles des " "agrégateurs de journalisation HTTPS et TCP." -#: awx/main/conf.py:471 +#: awx/main/conf.py:498 msgid "Enable/disable HTTPS certificate verification" msgstr "Activer/désactiver la vérification de certificat HTTPS" -#: awx/main/conf.py:472 +#: awx/main/conf.py:499 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -1992,11 +2374,11 @@ msgstr "" " envoyé par l'agrégateur de journalisation externe avant d'établir la " "connexion." -#: awx/main/conf.py:484 +#: awx/main/conf.py:511 msgid "Logging Aggregator Level Threshold" msgstr "Seuil du niveau de l'agrégateur de journalisation" -#: awx/main/conf.py:485 +#: awx/main/conf.py:512 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -2010,91 +2392,154 @@ msgstr "" "gestionnaire de journal. (les messages sous la catégorie awx.anlytics " "ignorent ce paramètre)" -#: awx/main/conf.py:508 awx/sso/conf.py:1105 +#: awx/main/conf.py:535 awx/sso/conf.py:1262 msgid "\n" msgstr "\n" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Sudo" msgstr "Sudo" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Su" msgstr "Su" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pbrun" msgstr "Pbrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pfexec" msgstr "Pfexec" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "DZDO" msgstr "DZDO" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Pmrun" msgstr "Pmrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Runas" msgstr "Runas" -#: awx/main/fields.py:57 -#, python-format -msgid "'%s' is not one of ['%s']" -msgstr "'%s' ne fait pas partie des ['%s']" +#: awx/main/constants.py:19 +msgid "Enable" +msgstr "Activer" -#: awx/main/fields.py:533 +#: awx/main/constants.py:19 +msgid "Doas" +msgstr "Comme" + +#: awx/main/constants.py:21 +msgid "None" +msgstr "Aucun" + +#: awx/main/fields.py:62 +#, python-brace-format +msgid "'{value}' is not one of ['{allowed_values}']" +msgstr "'{value}' n'appartient pas aux valeurs ['{allowed_values}']" + +#: awx/main/fields.py:421 +#, python-brace-format +msgid "{type} provided in relative path {path}, expected {expected_type}" +msgstr "" +"{type} donné dans le chemin d'accès relatif {path}, {expected_type} attendu" + +#: awx/main/fields.py:426 +#, python-brace-format +msgid "{type} provided, expected {expected_type}" +msgstr "{type} donné, {expected_type} attendu" + +#: awx/main/fields.py:431 +#, python-brace-format +msgid "Schema validation error in relative path {path} ({error})" +msgstr "" +"Erreur de validation de schéma dans le chemin relatif {path} ({error})" + +#: awx/main/fields.py:552 +msgid "secret values must be of type string, not {}" +msgstr "les valeurs secrètes doivent être sous forme de string, et non pas {}" + +#: awx/main/fields.py:587 #, python-format msgid "cannot be set unless \"%s\" is set" msgstr "ne peut être défini à moins que \"%s\" soit défini" -#: awx/main/fields.py:549 +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "requis pour %s" -#: awx/main/fields.py:573 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "doit être défini lorsque la clé SSH est chiffrée." -#: awx/main/fields.py:579 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "ne doit pas être défini lorsque la clé SSH n'est pas chiffrée." -#: awx/main/fields.py:637 +#: awx/main/fields.py:691 msgid "'dependencies' is not supported for custom credentials." msgstr "" "les dépendances ne sont pas prises en charge pour les identifiants " "personnalisés." -#: awx/main/fields.py:651 +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "\"tower\" est un nom de champ réservé" -#: awx/main/fields.py:658 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "Les ID de champ doivent être uniques (%s)" -#: awx/main/fields.py:671 -#, python-format -msgid "%s not allowed for %s type (%s)" -msgstr "%s non autorisé pour le type %s (%s)" +#: awx/main/fields.py:725 +msgid "become_method is a reserved type name" +msgstr "become_method est un type de nom réservé" -#: awx/main/fields.py:755 -#, python-format -msgid "%s uses an undefined field (%s)" -msgstr "%s utilise un champ non défini (%s)" +#: awx/main/fields.py:736 +#, python-brace-format +msgid "{sub_key} not allowed for {element_type} type ({element_id})" +msgstr "{sub_key} non autorisé pour le type {element_type} ({element_id})" -#: awx/main/middleware.py:157 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "" +"Doit définir l'injecteur de fichier sans nom pour pouvoir référencer « " +"tower.filename »." + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "" +"Impossible de référencer directement le conteneur d'espace de nommage " +"réservé « Tower »." + +#: awx/main/fields.py:827 +msgid "Must use multi-file syntax when injecting multiple files" +msgstr "" +"Doit utiliser la syntaxe multi-fichier lors de l'injection de plusieurs " +"fichiers." + +#: awx/main/fields.py:844 +#, python-brace-format +msgid "{sub_key} uses an undefined field ({error_msg})" +msgstr "{sub_key} utilise un champ non défini ({error_msg})" + +#: awx/main/fields.py:851 +#, python-brace-format +msgid "" +"Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" +msgstr "" +"Il y a une erreur de rendu de modèle {sub_key} dans {type} ({error_msg})" + +#: awx/main/middleware.py:146 msgid "Formats of all available named urls" msgstr "Formats de toutes les URL nommées disponibles" -#: awx/main/middleware.py:158 +#: awx/main/middleware.py:147 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." @@ -2102,15 +2547,15 @@ msgstr "" "Liste en lecture seule de paires clé/valeur qui affiche le format standard " "de toutes les URL nommées disponibles." -#: awx/main/middleware.py:160 awx/main/middleware.py:170 +#: awx/main/middleware.py:149 awx/main/middleware.py:159 msgid "Named URL" msgstr "URL nommée" -#: awx/main/middleware.py:167 +#: awx/main/middleware.py:156 msgid "List of all named url graph nodes." msgstr "Liste de tous les noeuds de graphique des URL nommées." -#: awx/main/middleware.py:168 +#: awx/main/middleware.py:157 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use" " this list to programmatically generate named URLs for resources" @@ -2119,31 +2564,35 @@ msgstr "" "graphiques des URL nommées. Utilisez cette liste pour générer via un " "programme des URL nommées pour les ressources" -#: awx/main/migrations/_reencrypt.py:25 awx/main/models/notifications.py:33 +#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:35 msgid "Email" msgstr "Email" -#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:34 +#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:36 msgid "Slack" msgstr "Slack" -#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:35 +#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:37 msgid "Twilio" msgstr "Twilio" -#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:36 +#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:38 msgid "Pagerduty" msgstr "Pagerduty" -#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:37 +#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:39 msgid "HipChat" msgstr "HipChat" -#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:38 +#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:41 +msgid "Mattermost" +msgstr "Mattermost" + +#: awx/main/migrations/_reencrypt.py:32 awx/main/models/notifications.py:40 msgid "Webhook" msgstr "Webhook" -#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:39 +#: awx/main/migrations/_reencrypt.py:33 awx/main/models/notifications.py:43 msgid "IRC" msgstr "IRC" @@ -2167,104 +2616,84 @@ msgstr "Entité associée à une autre entité" msgid "Entity was Disassociated with another Entity" msgstr "Entité dissociée d'une autre entité" -#: awx/main/models/ad_hoc_commands.py:100 +#: awx/main/models/ad_hoc_commands.py:95 msgid "No valid inventory." msgstr "Aucun inventaire valide." -#: awx/main/models/ad_hoc_commands.py:107 +#: awx/main/models/ad_hoc_commands.py:102 msgid "You must provide a machine / SSH credential." msgstr "Vous devez fournir des informations d'identification machine / SSH." -#: awx/main/models/ad_hoc_commands.py:118 -#: awx/main/models/ad_hoc_commands.py:126 +#: awx/main/models/ad_hoc_commands.py:113 +#: awx/main/models/ad_hoc_commands.py:121 msgid "Invalid type for ad hoc command" msgstr "Type non valide pour la commande ad hoc" -#: awx/main/models/ad_hoc_commands.py:121 +#: awx/main/models/ad_hoc_commands.py:116 msgid "Unsupported module for ad hoc commands." msgstr "Module non pris en charge pour les commandes ad hoc." -#: awx/main/models/ad_hoc_commands.py:129 +#: awx/main/models/ad_hoc_commands.py:124 #, python-format msgid "No argument passed to %s module." msgstr "Aucun argument transmis au module %s." -#: awx/main/models/ad_hoc_commands.py:245 awx/main/models/jobs.py:911 -msgid "Host Failed" -msgstr "Échec de l'hôte" - -#: awx/main/models/ad_hoc_commands.py:246 awx/main/models/jobs.py:912 -msgid "Host OK" -msgstr "Hôte OK" - -#: awx/main/models/ad_hoc_commands.py:247 awx/main/models/jobs.py:915 -msgid "Host Unreachable" -msgstr "Hôte inaccessible" - -#: awx/main/models/ad_hoc_commands.py:252 awx/main/models/jobs.py:914 -msgid "Host Skipped" -msgstr "Hôte ignoré" - -#: awx/main/models/ad_hoc_commands.py:262 awx/main/models/jobs.py:942 -msgid "Debug" -msgstr "Déboguer" - -#: awx/main/models/ad_hoc_commands.py:263 awx/main/models/jobs.py:943 -msgid "Verbose" -msgstr "Verbeux" - -#: awx/main/models/ad_hoc_commands.py:264 awx/main/models/jobs.py:944 -msgid "Deprecated" -msgstr "Obsolète" - -#: awx/main/models/ad_hoc_commands.py:265 awx/main/models/jobs.py:945 -msgid "Warning" -msgstr "Avertissement" - -#: awx/main/models/ad_hoc_commands.py:266 awx/main/models/jobs.py:946 -msgid "System Warning" -msgstr "Avertissement système" - -#: awx/main/models/ad_hoc_commands.py:267 awx/main/models/jobs.py:947 -#: awx/main/models/unified_jobs.py:64 -msgid "Error" -msgstr "Erreur" - -#: awx/main/models/base.py:40 awx/main/models/base.py:46 -#: awx/main/models/base.py:51 +#: awx/main/models/base.py:33 awx/main/models/base.py:39 +#: awx/main/models/base.py:44 awx/main/models/base.py:49 msgid "Run" msgstr "Exécuter" -#: awx/main/models/base.py:41 awx/main/models/base.py:47 -#: awx/main/models/base.py:52 +#: awx/main/models/base.py:34 awx/main/models/base.py:40 +#: awx/main/models/base.py:45 awx/main/models/base.py:50 msgid "Check" msgstr "Vérifier" -#: awx/main/models/base.py:42 +#: awx/main/models/base.py:35 msgid "Scan" msgstr "Scanner" -#: awx/main/models/credential.py:86 +#: awx/main/models/credential/__init__.py:110 msgid "Host" msgstr "Hôte" -#: awx/main/models/credential.py:87 +#: awx/main/models/credential/__init__.py:111 msgid "The hostname or IP address to use." msgstr "Nom d'hôte ou adresse IP à utiliser." -#: awx/main/models/credential.py:93 +#: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "Nom d'utilisateur" -#: awx/main/models/credential.py:94 +#: awx/main/models/credential/__init__.py:118 msgid "Username for this credential." msgstr "Nom d'utilisateur pour ces informations d'identification." -#: awx/main/models/credential.py:100 +#: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "Mot de passe" -#: awx/main/models/credential.py:101 +#: awx/main/models/credential/__init__.py:125 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." @@ -2272,43 +2701,43 @@ msgstr "" "Mot de passe pour ces informations d'identification (ou \"ASK\" pour " "demander à l'utilisateur les informations d'identification de la machine)." -#: awx/main/models/credential.py:108 +#: awx/main/models/credential/__init__.py:132 msgid "Security Token" msgstr "Token de sécurité" -#: awx/main/models/credential.py:109 +#: awx/main/models/credential/__init__.py:133 msgid "Security Token for this credential" msgstr "Token de sécurité pour ces informations d'identification" -#: awx/main/models/credential.py:115 +#: awx/main/models/credential/__init__.py:139 msgid "Project" msgstr "Projet" -#: awx/main/models/credential.py:116 +#: awx/main/models/credential/__init__.py:140 msgid "The identifier for the project." msgstr "Identifiant du projet." -#: awx/main/models/credential.py:122 +#: awx/main/models/credential/__init__.py:146 msgid "Domain" msgstr "Domaine" -#: awx/main/models/credential.py:123 +#: awx/main/models/credential/__init__.py:147 msgid "The identifier for the domain." msgstr "Identifiant du domaine." -#: awx/main/models/credential.py:128 +#: awx/main/models/credential/__init__.py:152 msgid "SSH private key" msgstr "Clé privée SSH" -#: awx/main/models/credential.py:129 +#: awx/main/models/credential/__init__.py:153 msgid "RSA or DSA private key to be used instead of password." msgstr "Clé privée RSA ou DSA à utiliser au lieu du mot de passe." -#: awx/main/models/credential.py:135 +#: awx/main/models/credential/__init__.py:159 msgid "SSH key unlock" msgstr "Déverrouillage de la clé SSH" -#: awx/main/models/credential.py:136 +#: awx/main/models/credential/__init__.py:160 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." @@ -2317,52 +2746,48 @@ msgstr "" "chiffrée (ou \"ASK\" pour demander à l'utilisateur les informations " "d'identification de la machine)." -#: awx/main/models/credential.py:143 -msgid "None" -msgstr "Aucun" - -#: awx/main/models/credential.py:144 +#: awx/main/models/credential/__init__.py:168 msgid "Privilege escalation method." msgstr "Méthode d'élévation des privilèges." -#: awx/main/models/credential.py:150 +#: awx/main/models/credential/__init__.py:174 msgid "Privilege escalation username." msgstr "Nom d'utilisateur pour l'élévation des privilèges" -#: awx/main/models/credential.py:156 +#: awx/main/models/credential/__init__.py:180 msgid "Password for privilege escalation method." msgstr "Mot de passe pour la méthode d'élévation des privilèges." -#: awx/main/models/credential.py:162 +#: awx/main/models/credential/__init__.py:186 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "Mot de passe Vault (ou \"ASK\" pour le demander à l'utilisateur)." -#: awx/main/models/credential.py:166 +#: awx/main/models/credential/__init__.py:190 msgid "Whether to use the authorize mechanism." msgstr "Indique s'il faut ou non utiliser le mécanisme d'autorisation." -#: awx/main/models/credential.py:172 +#: awx/main/models/credential/__init__.py:196 msgid "Password used by the authorize mechanism." msgstr "Mot de passe utilisé par le mécanisme d'autorisation." -#: awx/main/models/credential.py:178 +#: awx/main/models/credential/__init__.py:202 msgid "Client Id or Application Id for the credential" msgstr "" "ID du client ou de l'application pour les informations d'identification" -#: awx/main/models/credential.py:184 +#: awx/main/models/credential/__init__.py:208 msgid "Secret Token for this credential" msgstr "Token secret pour ces informations d'identification" -#: awx/main/models/credential.py:190 +#: awx/main/models/credential/__init__.py:214 msgid "Subscription identifier for this credential" msgstr "ID d'abonnement pour ces informations d'identification" -#: awx/main/models/credential.py:196 +#: awx/main/models/credential/__init__.py:220 msgid "Tenant identifier for this credential" msgstr "ID de tenant pour ces informations d'identification" -#: awx/main/models/credential.py:220 +#: awx/main/models/credential/__init__.py:244 msgid "" "Specify the type of credential you want to create. Refer to the Ansible " "Tower documentation for details on each type." @@ -2370,7 +2795,8 @@ msgstr "" "Spécifiez le type d'information d'identification à créer. Consultez la " "documentation d’Ansible Tower pour en savoir plus sur chaque type." -#: awx/main/models/credential.py:234 awx/main/models/credential.py:420 +#: awx/main/models/credential/__init__.py:258 +#: awx/main/models/credential/__init__.py:476 msgid "" "Enter inputs using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2380,31 +2806,36 @@ msgstr "" "pour basculer entre les deux. Consultez la documentation d’Ansible Tower " "pour avoir un exemple de syntaxe." -#: awx/main/models/credential.py:401 +#: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "Machine" -#: awx/main/models/credential.py:402 +#: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "Coffre-fort" -#: awx/main/models/credential.py:403 +#: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "Réseau" -#: awx/main/models/credential.py:404 +#: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "Contrôle de la source" -#: awx/main/models/credential.py:405 +#: awx/main/models/credential/__init__.py:461 msgid "Cloud" msgstr "Cloud" -#: awx/main/models/credential.py:406 +#: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "Insights" -#: awx/main/models/credential.py:427 +#: awx/main/models/credential/__init__.py:483 msgid "" "Enter injectors using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2414,11 +2845,414 @@ msgstr "" " pour basculer entre les deux. Consultez la documentation Ansible Tower pour" " avoir un exemple de syntaxe." -#: awx/main/models/credential.py:478 +#: awx/main/models/credential/__init__.py:534 #, python-format msgid "adding %s credential type" msgstr "ajout type d'identifiants %s" +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "Clé privée SSH" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "Phrase de passe pour la clé privée" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "Méthode d'escalade privilégiée" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying" +" the --become-method Ansible parameter." +msgstr "" +"Spécifiez une méthode pour les opérations « become ». Cela équivaut à " +"définir le paramètre Ansible --become-method." + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "Nom d’utilisateur pour l’élévation des privilèges" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "Mot de passe pour l’élévation des privilèges" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "Clé privée SCM" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "Mot de passe de l'archivage sécurisé" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "Identifiant Archivage sécurisé" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the " +"--vault-id Ansible parameter for providing multiple Vault passwords. Note: " +"this feature only works in Ansible 2.4+." +msgstr "" +"Spécifiez un ID d'archivage sécurisé (facultatif). Ceci équivaut à spécifier" +" le paramètre --vault-id d'Ansible pour fournir plusieurs mots de passe " +"d'archivage sécurisé. Remarque : cette fonctionnalité ne fonctionne que " +"dans Ansible 2.4+." + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "Autoriser" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "Mot de passe d’autorisation" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "Amazon Web Services" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "Clé d’accès" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "Clé secrète" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "Jeton STS" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"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:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "OpenStack" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "Mot de passe (clé API)" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "Hôte (URL d’authentification)" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, " +"https://openstack.business.com/v2.0/" +msgstr "" +"Hôte avec lequel s’authentifier. Exemple,\n" +"https://openstack.business.com/v2.0/" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "Projet (nom du client)" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "Nom de domaine" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" +"Les domaines OpenStack définissent les limites administratives. Ils sont " +"nécessaires uniquement pour les URL d’authentification Keystone v3. Voir la " +"documentation Ansible Tower pour les scénarios courants." + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "VMware vCenter" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "Hôte vCenter" + +#: awx/main/models/credential/__init__.py:926 +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:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "Red Hat Satellite 6" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "URL Satellite 6" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"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:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "Red Hat CloudForms" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "URL CloudForms" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"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:1004 +#: awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "Google Compute Engine" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "Adresse électronique du compte de service" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "" +"Adresse électronique attribuée au compte de service Google Compute Engine." + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" +"L’ID du projet est l’identifiant attribué par GCE. Il se compose souvent de " +"deux ou trois mots suivis d’un nombre à trois chiffres. Exemples : project-" +"id-000 and another-project-id" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "Clé privée RSA" + +#: awx/main/models/credential/__init__.py:1028 +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:1040 +#: awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "Microsoft Azure Resource Manager" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "ID d’abonnement" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "" +"L’ID d’abonnement est une construction Azure mappée à un nom d’utilisateur." + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "ID du client" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "ID Tenant" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "Environnement Cloud Azure" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "" +"Variable d'environnement AZURE_CLOUD_ENVIRONMENT avec Azure GovCloud ou une " +"pile Azure." + +#: awx/main/models/credential/__init__.py:1115 +#: awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "Red Hat Virtualization" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "Hôte avec lequel s’authentifier." + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "Fichier CA" + +#: awx/main/models/credential/__init__.py:1136 +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:1167 +#: awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "Ansible Tower" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "Nom d'hôte Ansible Tower" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "L'URL basé Ansible Tower avec lequel s'authentifier." + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "Vérifier SSL" + +#: awx/main/models/events.py:89 awx/main/models/events.py:608 +msgid "Host Failed" +msgstr "Échec de l'hôte" + +#: awx/main/models/events.py:90 awx/main/models/events.py:609 +msgid "Host OK" +msgstr "Hôte OK" + +#: awx/main/models/events.py:91 +msgid "Host Failure" +msgstr "Échec de l'hôte" + +#: awx/main/models/events.py:92 awx/main/models/events.py:615 +msgid "Host Skipped" +msgstr "Hôte ignoré" + +#: awx/main/models/events.py:93 awx/main/models/events.py:610 +msgid "Host Unreachable" +msgstr "Hôte inaccessible" + +#: awx/main/models/events.py:94 awx/main/models/events.py:108 +msgid "No Hosts Remaining" +msgstr "Aucun hôte restant" + +#: awx/main/models/events.py:95 +msgid "Host Polling" +msgstr "Interrogation de l'hôte" + +#: awx/main/models/events.py:96 +msgid "Host Async OK" +msgstr "Désynchronisation des hôtes OK" + +#: awx/main/models/events.py:97 +msgid "Host Async Failure" +msgstr "Échec de désynchronisation des hôtes" + +#: awx/main/models/events.py:98 +msgid "Item OK" +msgstr "Élément OK" + +#: awx/main/models/events.py:99 +msgid "Item Failed" +msgstr "Échec de l'élément" + +#: awx/main/models/events.py:100 +msgid "Item Skipped" +msgstr "Élément ignoré" + +#: awx/main/models/events.py:101 +msgid "Host Retry" +msgstr "Nouvel essai de l'hôte" + +#: awx/main/models/events.py:103 +msgid "File Difference" +msgstr "Écart entre les fichiers" + +#: awx/main/models/events.py:104 +msgid "Playbook Started" +msgstr "Playbook démarré" + +#: awx/main/models/events.py:105 +msgid "Running Handlers" +msgstr "Descripteurs d'exécution" + +#: awx/main/models/events.py:106 +msgid "Including File" +msgstr "Ajout de fichier" + +#: awx/main/models/events.py:107 +msgid "No Hosts Matched" +msgstr "Aucun hôte correspondant" + +#: awx/main/models/events.py:109 +msgid "Task Started" +msgstr "Tâche démarrée" + +#: awx/main/models/events.py:111 +msgid "Variables Prompted" +msgstr "Variables demandées" + +#: awx/main/models/events.py:112 +msgid "Gathering Facts" +msgstr "Collecte des faits" + +#: awx/main/models/events.py:113 +msgid "internal: on Import for Host" +msgstr "interne : à l'importation pour l'hôte" + +#: awx/main/models/events.py:114 +msgid "internal: on Not Import for Host" +msgstr "interne : à la non-importation pour l'hôte" + +#: awx/main/models/events.py:115 +msgid "Play Started" +msgstr "Scène démarrée" + +#: awx/main/models/events.py:116 +msgid "Playbook Complete" +msgstr "Playbook terminé" + +#: awx/main/models/events.py:120 awx/main/models/events.py:625 +msgid "Debug" +msgstr "Déboguer" + +#: awx/main/models/events.py:121 awx/main/models/events.py:626 +msgid "Verbose" +msgstr "Verbeux" + +#: awx/main/models/events.py:122 awx/main/models/events.py:627 +msgid "Deprecated" +msgstr "Obsolète" + +#: awx/main/models/events.py:123 awx/main/models/events.py:628 +msgid "Warning" +msgstr "Avertissement" + +#: awx/main/models/events.py:124 awx/main/models/events.py:629 +msgid "System Warning" +msgstr "Avertissement système" + +#: awx/main/models/events.py:125 awx/main/models/events.py:630 +#: awx/main/models/unified_jobs.py:67 +msgid "Error" +msgstr "Erreur" + #: awx/main/models/fact.py:25 msgid "Host for the facts that the fact scan captured." msgstr "Hôte pour les faits que le scan de faits a capturés." @@ -2437,82 +3271,102 @@ msgstr "" "Structure JSON arbitraire des faits de module capturés au moment de " "l'horodatage pour un seul hôte." -#: awx/main/models/ha.py:78 +#: awx/main/models/ha.py:153 msgid "Instances that are members of this InstanceGroup" msgstr "Instances membres de ce GroupeInstances." -#: awx/main/models/ha.py:83 +#: awx/main/models/ha.py:158 msgid "Instance Group to remotely control this group." msgstr "Groupe d'instances pour contrôler ce groupe à distance." -#: awx/main/models/inventory.py:52 +#: awx/main/models/ha.py:165 +msgid "Percentage of Instances to automatically assign to this group" +msgstr "" +"Le pourcentage d'instances qui seront automatiquement assignées à ce groupe" + +#: awx/main/models/ha.py:169 +msgid "" +"Static minimum number of Instances to automatically assign to this group" +msgstr "" +"Nombre minimum statique d'instances qui seront automatiquement assignées à " +"ce groupe." + +#: awx/main/models/ha.py:174 +msgid "" +"List of exact-match Instances that will always be automatically assigned to " +"this group" +msgstr "" +"Liste des cas de concordance exacte qui seront toujours assignés " +"automatiquement à ce groupe." + +#: awx/main/models/inventory.py:61 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:53 +#: awx/main/models/inventory.py:62 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:58 +#: awx/main/models/inventory.py:67 msgid "inventories" msgstr "inventaires" -#: awx/main/models/inventory.py:65 +#: awx/main/models/inventory.py:74 msgid "Organization containing this inventory." msgstr "Organisation contenant cet inventaire." -#: awx/main/models/inventory.py:72 +#: awx/main/models/inventory.py:81 msgid "Inventory variables in JSON or YAML format." msgstr "Variables d'inventaire au format JSON ou YAML." -#: awx/main/models/inventory.py:77 +#: awx/main/models/inventory.py:86 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "Marqueur indiquant si les hôtes de cet inventaire ont échoué." -#: awx/main/models/inventory.py:82 +#: awx/main/models/inventory.py:91 msgid "Total number of hosts in this inventory." msgstr "Nombre total d'hôtes dans cet inventaire." -#: awx/main/models/inventory.py:87 +#: awx/main/models/inventory.py:96 msgid "Number of hosts in this inventory with active failures." msgstr "Nombre d'hôtes dans cet inventaire avec des échecs non résolus." -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:101 msgid "Total number of groups in this inventory." msgstr "Nombre total de groupes dans cet inventaire." -#: awx/main/models/inventory.py:97 +#: awx/main/models/inventory.py:106 msgid "Number of groups in this inventory with active failures." msgstr "Nombre de groupes dans cet inventaire avec des échecs non résolus." -#: awx/main/models/inventory.py:102 +#: awx/main/models/inventory.py:111 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "" "Marqueur indiquant si cet inventaire contient des sources d'inventaire " "externes." -#: awx/main/models/inventory.py:107 +#: awx/main/models/inventory.py:116 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:112 +#: awx/main/models/inventory.py:121 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:119 +#: awx/main/models/inventory.py:128 msgid "Kind of inventory being represented." msgstr "Genre d'inventaire représenté." -#: awx/main/models/inventory.py:125 +#: awx/main/models/inventory.py:134 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:152 +#: awx/main/models/inventory.py:161 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." @@ -2520,38 +3374,38 @@ 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:161 +#: awx/main/models/inventory.py:170 msgid "Flag indicating the inventory is being deleted." msgstr "Marqueur indiquant que cet inventaire est en cours de suppression." -#: awx/main/models/inventory.py:374 +#: awx/main/models/inventory.py:459 msgid "Assignment not allowed for Smart Inventory" msgstr "Attribution non autorisée pour un inventaire Smart" -#: awx/main/models/inventory.py:376 awx/main/models/projects.py:148 +#: awx/main/models/inventory.py:461 awx/main/models/projects.py:159 msgid "Credential kind must be 'insights'." msgstr "Le genre d'informations d'identification doit être 'insights'." -#: awx/main/models/inventory.py:443 +#: awx/main/models/inventory.py:546 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:449 +#: awx/main/models/inventory.py:552 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:454 +#: awx/main/models/inventory.py:557 msgid "Host variables in JSON or YAML format." msgstr "Variables d'hôte au format JSON ou YAML." -#: awx/main/models/inventory.py:476 +#: awx/main/models/inventory.py:579 msgid "Flag indicating whether the last job failed for this host." msgstr "Marqueur indiquant si la dernière tâche a échoué pour cet hôte." -#: awx/main/models/inventory.py:481 +#: awx/main/models/inventory.py:584 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." @@ -2559,55 +3413,55 @@ msgstr "" "Marqueur indiquant si cet hôte a été créé/mis à jour à partir de sources " "d'inventaire externes." -#: awx/main/models/inventory.py:487 +#: awx/main/models/inventory.py:590 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:492 +#: awx/main/models/inventory.py:595 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:498 +#: awx/main/models/inventory.py:601 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:505 +#: awx/main/models/inventory.py:608 msgid "Red Hat Insights host unique identifier." msgstr "Identifiant unique de l'hôte de Red Hat Insights." -#: awx/main/models/inventory.py:633 +#: awx/main/models/inventory.py:743 msgid "Group variables in JSON or YAML format." msgstr "Variables de groupe au format JSON ou YAML." -#: awx/main/models/inventory.py:639 +#: awx/main/models/inventory.py:749 msgid "Hosts associated directly with this group." msgstr "Hôtes associés directement à ce groupe." -#: awx/main/models/inventory.py:644 +#: awx/main/models/inventory.py:754 msgid "Total number of hosts directly or indirectly in this group." msgstr "" "Nombre total d'hôtes associés directement ou indirectement à ce groupe." -#: awx/main/models/inventory.py:649 +#: awx/main/models/inventory.py:759 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "" "Marqueur indiquant si ce groupe possède ou non des hôtes avec des échecs non" " résolus." -#: awx/main/models/inventory.py:654 +#: awx/main/models/inventory.py:764 msgid "Number of hosts in this group with active failures." msgstr "Nombre d'hôtes dans ce groupe avec des échecs non résolus." -#: awx/main/models/inventory.py:659 +#: awx/main/models/inventory.py:769 msgid "Total number of child groups contained within this group." msgstr "Nombre total de groupes enfants compris dans ce groupe." -#: awx/main/models/inventory.py:664 +#: awx/main/models/inventory.py:774 msgid "Number of child groups within this group that have active failures." msgstr "Nombre de groupes enfants dans ce groupe avec des échecs non résolus." -#: awx/main/models/inventory.py:669 +#: awx/main/models/inventory.py:779 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." @@ -2615,68 +3469,36 @@ msgstr "" "Marqueur indiquant si ce groupe a été créé/mis à jour à partir de sources " "d'inventaire externes." -#: awx/main/models/inventory.py:675 +#: awx/main/models/inventory.py:785 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:865 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:428 +#: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "Manuel" -#: awx/main/models/inventory.py:866 +#: awx/main/models/inventory.py:982 msgid "File, Directory or Script" msgstr "Fichier, répertoire ou script" -#: awx/main/models/inventory.py:867 +#: awx/main/models/inventory.py:983 msgid "Sourced from a Project" msgstr "Provenance d'un projet" -#: awx/main/models/inventory.py:868 +#: awx/main/models/inventory.py:984 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:869 -msgid "Google Compute Engine" -msgstr "Google Compute Engine" - -#: awx/main/models/inventory.py:870 -msgid "Microsoft Azure Resource Manager" -msgstr "Microsoft Azure Resource Manager" - -#: awx/main/models/inventory.py:871 -msgid "VMware vCenter" -msgstr "VMware vCenter" - -#: awx/main/models/inventory.py:872 -msgid "Red Hat Satellite 6" -msgstr "Red Hat Satellite 6" - -#: awx/main/models/inventory.py:873 -msgid "Red Hat CloudForms" -msgstr "Red Hat CloudForms" - -#: awx/main/models/inventory.py:874 -msgid "OpenStack" -msgstr "OpenStack" - -#: awx/main/models/inventory.py:875 -msgid "oVirt4" -msgstr "oVirt4" - -#: awx/main/models/inventory.py:876 -msgid "Ansible Tower" -msgstr "Ansible Tower" - -#: awx/main/models/inventory.py:877 +#: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "Script personnalisé" -#: awx/main/models/inventory.py:994 +#: awx/main/models/inventory.py:1110 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:1013 +#: awx/main/models/inventory.py:1121 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." @@ -2684,75 +3506,79 @@ 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:1019 +#: awx/main/models/inventory.py:1127 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:1023 +#: awx/main/models/inventory.py:1131 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:1027 +#: awx/main/models/inventory.py:1135 msgid "Overwrite local variables from remote inventory source." msgstr "Écraser les variables locales de la source d'inventaire distante." -#: awx/main/models/inventory.py:1032 awx/main/models/jobs.py:160 -#: awx/main/models/projects.py:117 +#: awx/main/models/inventory.py:1140 awx/main/models/jobs.py:140 +#: awx/main/models/projects.py:128 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:1065 +#: awx/main/models/inventory.py:1173 msgid "Image ID" msgstr "ID d'image" -#: awx/main/models/inventory.py:1066 +#: awx/main/models/inventory.py:1174 msgid "Availability Zone" msgstr "Zone de disponibilité" -#: awx/main/models/inventory.py:1067 +#: awx/main/models/inventory.py:1175 msgid "Account" msgstr "Compte" -#: awx/main/models/inventory.py:1068 +#: awx/main/models/inventory.py:1176 msgid "Instance ID" msgstr "ID d'instance" -#: awx/main/models/inventory.py:1069 +#: awx/main/models/inventory.py:1177 msgid "Instance State" msgstr "État de l'instance" -#: awx/main/models/inventory.py:1070 +#: awx/main/models/inventory.py:1178 +msgid "Platform" +msgstr "Plateforme " + +#: awx/main/models/inventory.py:1179 msgid "Instance Type" msgstr "Type d'instance" -#: awx/main/models/inventory.py:1071 +#: awx/main/models/inventory.py:1180 msgid "Key Name" msgstr "Nom de la clé" -#: awx/main/models/inventory.py:1072 +#: awx/main/models/inventory.py:1181 msgid "Region" msgstr "Région" -#: awx/main/models/inventory.py:1073 +#: awx/main/models/inventory.py:1182 msgid "Security Group" msgstr "Groupe de sécurité" -#: awx/main/models/inventory.py:1074 +#: awx/main/models/inventory.py:1183 msgid "Tags" msgstr "Balises" -#: awx/main/models/inventory.py:1075 +#: awx/main/models/inventory.py:1184 msgid "Tag None" msgstr "Ne rien baliser" -#: awx/main/models/inventory.py:1076 +#: awx/main/models/inventory.py:1185 msgid "VPC ID" msgstr "ID VPC" -#: awx/main/models/inventory.py:1145 +#: awx/main/models/inventory.py:1253 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " @@ -2761,12 +3587,12 @@ 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:1152 +#: awx/main/models/inventory.py:1259 msgid "Credential is required for a cloud source." msgstr "" "Les informations d'identification sont requises pour une source cloud." -#: awx/main/models/inventory.py:1155 +#: awx/main/models/inventory.py:1262 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." @@ -2775,26 +3601,26 @@ msgstr "" "archivage sécurisé ne sont pas autorisés par les sources d'inventaire " "personnalisées." -#: awx/main/models/inventory.py:1179 +#: awx/main/models/inventory.py:1314 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "Région %(source)s non valide : %(region)s" -#: awx/main/models/inventory.py:1203 +#: awx/main/models/inventory.py:1338 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "Expression de filtre non valide : %(filter)s" -#: awx/main/models/inventory.py:1224 +#: awx/main/models/inventory.py:1359 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "Choix de regroupement non valide : %(choice)s" -#: awx/main/models/inventory.py:1259 +#: awx/main/models/inventory.py:1394 msgid "Project containing inventory file used as source." msgstr "Projet contenant le fichier d'inventaire utilisé comme source." -#: awx/main/models/inventory.py:1407 +#: awx/main/models/inventory.py:1555 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." @@ -2802,7 +3628,7 @@ msgstr "" "Impossible de configurer cet élément pour la synchronisation dans le cloud. " "Il est déjà géré par %s." -#: awx/main/models/inventory.py:1417 +#: awx/main/models/inventory.py:1565 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." @@ -2810,7 +3636,7 @@ 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:1424 +#: awx/main/models/inventory.py:1572 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " @@ -2821,26 +3647,28 @@ msgstr "" " la place, configurez le projet source correspondant pour qu'il se mette à " "jour au moment du lancement." -#: awx/main/models/inventory.py:1430 -msgid "SCM type sources must set `overwrite_vars` to `true`." -msgstr "Les sources de type SCM doivent définir `overwrite_vars` sur`true`." +#: awx/main/models/inventory.py:1579 +msgid "" +"SCM type sources must set `overwrite_vars` to `true` until Ansible 2.5." +msgstr "" +"Les sources de type SCM doivent définir les « overwrite_vars » sur « true »." -#: awx/main/models/inventory.py:1435 +#: awx/main/models/inventory.py:1584 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:1460 +#: awx/main/models/inventory.py:1615 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:1573 +#: awx/main/models/inventory.py:1725 msgid "Inventory script contents" msgstr "Contenus des scripts d'inventaire" -#: awx/main/models/inventory.py:1578 +#: awx/main/models/inventory.py:1730 msgid "Organization owning this inventory script" msgstr "Organisation propriétaire de ce script d'inventaire." @@ -2853,7 +3681,7 @@ msgstr "" "fichiers basés sur un modèle de l'hôte sont affichées dans la sortie " "standard out" -#: awx/main/models/jobs.py:164 +#: awx/main/models/jobs.py:145 msgid "" "If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts" " at the end of a playbook run to the database and caching facts for use by " @@ -2863,38 +3691,38 @@ msgstr "" " ; les faits persistants à la fin d'un playbook s'exécutent vers la base de " "données et les faits mis en cache pour être utilisés par Ansible." -#: awx/main/models/jobs.py:173 -msgid "You must provide an SSH credential." -msgstr "Vous devez fournir des informations d'identification SSH." - -#: awx/main/models/jobs.py:181 +#: awx/main/models/jobs.py:163 msgid "You must provide a Vault credential." msgstr "Vous devez fournir des informations d'identification du coffre-fort." -#: awx/main/models/jobs.py:317 +#: awx/main/models/jobs.py:308 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "" "Le modèle de tâche doit fournir un inventaire ou permettre d'en demander un." -#: awx/main/models/jobs.py:321 -msgid "Job Template must provide 'credential' or allow prompting for it." +#: awx/main/models/jobs.py:403 +msgid "Field is not configured to prompt on launch." +msgstr "Le champ n'est pas configuré pour être invité au lancement." + +#: awx/main/models/jobs.py:409 +msgid "Saved launch configurations cannot provide passwords needed to start." msgstr "" -"Le modèle de tâche doit fournir des informations d'identification ou " -"permettre d'en demander." +"Les configurations de lancement sauvegardées ne peuvent pas fournir les mots" +" de passe nécessaires au démarrage." -#: awx/main/models/jobs.py:427 -msgid "Cannot override job_type to or from a scan job." -msgstr "Impossible de remplacer job_type vers ou depuis une tâche de scan." +#: awx/main/models/jobs.py:417 +msgid "Job Template {} is missing or undefined." +msgstr "Modèle de Job {} manquant ou non défini." -#: awx/main/models/jobs.py:493 awx/main/models/projects.py:263 +#: awx/main/models/jobs.py:498 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "Révision SCM" -#: awx/main/models/jobs.py:494 +#: awx/main/models/jobs.py:499 msgid "The SCM Revision from the Project used for this job, if available" msgstr "Révision SCM du projet utilisé pour cette tâche, le cas échéant" -#: awx/main/models/jobs.py:502 +#: awx/main/models/jobs.py:507 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" @@ -2902,168 +3730,178 @@ msgstr "" "Activité d'actualisation du SCM qui permet de s'assurer que les playbooks " "étaient disponibles pour l'exécution de la tâche" -#: awx/main/models/jobs.py:809 +#: awx/main/models/jobs.py:634 +#, python-brace-format +msgid "{status_value} is not a valid status option." +msgstr "{status_value} ne correspond pas à une option de statut valide." + +#: awx/main/models/jobs.py:999 msgid "job host summaries" msgstr "récapitulatifs des hôtes pour la tâche" -#: awx/main/models/jobs.py:913 -msgid "Host Failure" -msgstr "Échec de l'hôte" - -#: awx/main/models/jobs.py:916 awx/main/models/jobs.py:930 -msgid "No Hosts Remaining" -msgstr "Aucun hôte restant" - -#: awx/main/models/jobs.py:917 -msgid "Host Polling" -msgstr "Interrogation de l'hôte" - -#: awx/main/models/jobs.py:918 -msgid "Host Async OK" -msgstr "Désynchronisation des hôtes OK" - -#: awx/main/models/jobs.py:919 -msgid "Host Async Failure" -msgstr "Échec de désynchronisation des hôtes" - -#: awx/main/models/jobs.py:920 -msgid "Item OK" -msgstr "Élément OK" - -#: awx/main/models/jobs.py:921 -msgid "Item Failed" -msgstr "Échec de l'élément" - -#: awx/main/models/jobs.py:922 -msgid "Item Skipped" -msgstr "Élément ignoré" - -#: awx/main/models/jobs.py:923 -msgid "Host Retry" -msgstr "Nouvel essai de l'hôte" - -#: awx/main/models/jobs.py:925 -msgid "File Difference" -msgstr "Écart entre les fichiers" - -#: awx/main/models/jobs.py:926 -msgid "Playbook Started" -msgstr "Playbook démarré" - -#: awx/main/models/jobs.py:927 -msgid "Running Handlers" -msgstr "Descripteurs d'exécution" - -#: awx/main/models/jobs.py:928 -msgid "Including File" -msgstr "Ajout de fichier" - -#: awx/main/models/jobs.py:929 -msgid "No Hosts Matched" -msgstr "Aucun hôte correspondant" - -#: awx/main/models/jobs.py:931 -msgid "Task Started" -msgstr "Tâche démarrée" - -#: awx/main/models/jobs.py:933 -msgid "Variables Prompted" -msgstr "Variables demandées" - -#: awx/main/models/jobs.py:934 -msgid "Gathering Facts" -msgstr "Collecte des faits" - -#: awx/main/models/jobs.py:935 -msgid "internal: on Import for Host" -msgstr "interne : à l'importation pour l'hôte" - -#: awx/main/models/jobs.py:936 -msgid "internal: on Not Import for Host" -msgstr "interne : à la non-importation pour l'hôte" - -#: awx/main/models/jobs.py:937 -msgid "Play Started" -msgstr "Scène démarrée" - -#: awx/main/models/jobs.py:938 -msgid "Playbook Complete" -msgstr "Playbook terminé" - -#: awx/main/models/jobs.py:1351 +#: awx/main/models/jobs.py:1070 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:1352 +#: awx/main/models/jobs.py:1071 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:1353 +#: awx/main/models/jobs.py:1072 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "Purger et/ou réduire la granularité des données de suivi du système" +#: awx/main/models/jobs.py:1142 +#, 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 jobs système." + +#: awx/main/models/jobs.py:1157 +msgid "days must be a positive integer." +msgstr "jours doit être un entier positif." + #: awx/main/models/label.py:29 msgid "Organization this label belongs to." msgstr "Organisation à laquelle appartient ce libellé." -#: awx/main/models/notifications.py:138 awx/main/models/unified_jobs.py:59 +#: awx/main/models/mixins.py:309 +#, python-brace-format +msgid "" +"Variables {list_of_keys} are not allowed on launch. Check the Prompt on " +"Launch setting on the Job Template to include Extra Variables." +msgstr "" +"Les variables {list_of_keys} ne sont pas autorisées au lancement. Vérifiez " +"le paramètre Invite au lancement sur le Modèle de job pour inclure des " +"Variables supplémentaires." + +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "" +"Chemin d'accès au fichier local absolu contenant un virtualenv Python " +"personnalisé à utiliser" + +#: awx/main/models/mixins.py:447 +msgid "{} is not a valid virtualenv in {}" +msgstr "{} n'est pas un virtualenv dans {}" + +#: awx/main/models/notifications.py:42 +msgid "Rocket.Chat" +msgstr "Rocket.Chat" + +#: awx/main/models/notifications.py:142 awx/main/models/unified_jobs.py:62 msgid "Pending" msgstr "En attente" -#: awx/main/models/notifications.py:139 awx/main/models/unified_jobs.py:62 +#: awx/main/models/notifications.py:143 awx/main/models/unified_jobs.py:65 msgid "Successful" msgstr "Réussi" -#: awx/main/models/notifications.py:140 awx/main/models/unified_jobs.py:63 +#: awx/main/models/notifications.py:144 awx/main/models/unified_jobs.py:66 msgid "Failed" msgstr "Échec" -#: awx/main/models/organization.py:132 -msgid "Token not invalidated" -msgstr "Token non invalidé" +#: awx/main/models/notifications.py:218 +msgid "status_str must be either succeeded or failed" +msgstr "status_str doit être une réussite ou un échec" -#: awx/main/models/organization.py:133 -msgid "Token is expired" -msgstr "Token arrivé à expiration" +#: awx/main/models/oauth.py:27 +msgid "application" +msgstr "application" -#: awx/main/models/organization.py:134 +#: awx/main/models/oauth.py:32 +msgid "Confidential" +msgstr "Confidentiel" + +#: awx/main/models/oauth.py:33 +msgid "Public" +msgstr "Public" + +#: awx/main/models/oauth.py:41 +msgid "Authorization code" +msgstr "Code d'autorisation" + +#: awx/main/models/oauth.py:42 +msgid "Implicit" +msgstr "Implicite" + +#: awx/main/models/oauth.py:43 +msgid "Resource owner password-based" +msgstr "Ressource basée mot de passe de propriétaire" + +#: awx/main/models/oauth.py:44 +msgid "Client credentials" +msgstr "Informations d’identification du client" + +#: awx/main/models/oauth.py:59 +msgid "Organization containing this application." +msgstr "Organisation contenant cette application." + +#: awx/main/models/oauth.py:68 msgid "" -"The maximum number of allowed sessions for this user has been exceeded." +"Used for more stringent verification of access to an application when " +"creating a token." msgstr "" -"Le nombre maximum de sessions autorisées pour cet utilisateur a été dépassé." +"Utilisé pour une vérification plus rigoureuse de l'accès à une application " +"lors de la création d'un jeton." -#: awx/main/models/organization.py:137 -msgid "Invalid token" -msgstr "Token non valide" - -#: awx/main/models/organization.py:155 -msgid "Reason the auth token was invalidated." +#: awx/main/models/oauth.py:73 +msgid "" +"Set to Public or Confidential depending on how secure the client device is." msgstr "" -"Raison pour laquelle le token d'authentification a été rendu non valide." +"Défini sur sur Public ou Confidentiel selon le degré de sécurité du " +"périphérique client." -#: awx/main/models/organization.py:194 -msgid "Invalid reason specified" -msgstr "Raison de non validité spécifiée" +#: awx/main/models/oauth.py:77 +msgid "" +"Set True to skip authorization step for completely trusted applications." +msgstr "" +"Définir sur True pour sauter l'étape d'autorisation pour les applications " +"totalement fiables." -#: awx/main/models/projects.py:43 +#: awx/main/models/oauth.py:82 +msgid "" +"The Grant type the user must use for acquire tokens for this application." +msgstr "" +"Le type de permission que l'utilisateur doit utiliser pour acquérir des " +"jetons pour cette application." + +#: awx/main/models/oauth.py:90 +msgid "access token" +msgstr "jeton d'accès" + +#: awx/main/models/oauth.py:98 +msgid "The user representing the token owner" +msgstr "L'utilisateur représentant le propriétaire du jeton." + +#: awx/main/models/oauth.py:113 +msgid "" +"Allowed scopes, further restricts user's permissions. Must be a simple " +"space-separated string with allowed scopes ['read', 'write']." +msgstr "" +"Limites autorisées, restreint encore plus les permissions de l'utilisateur. " +"Doit correspondre à une simple chaîne de caractères séparés par des espaces " +"avec des champs d'application autorisés ('read','write')." + +#: awx/main/models/projects.py:54 msgid "Git" msgstr "Git" -#: awx/main/models/projects.py:44 +#: awx/main/models/projects.py:55 msgid "Mercurial" msgstr "Mercurial" -#: awx/main/models/projects.py:45 +#: awx/main/models/projects.py:56 msgid "Subversion" msgstr "Subversion" -#: awx/main/models/projects.py:46 +#: awx/main/models/projects.py:57 msgid "Red Hat Insights" msgstr "Red Hat Insights" -#: awx/main/models/projects.py:72 +#: awx/main/models/projects.py:83 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." @@ -3071,66 +3909,66 @@ msgstr "" "Chemin local (relatif à PROJECTS_ROOT) contenant des playbooks et des " "fichiers associés pour ce projet." -#: awx/main/models/projects.py:81 +#: awx/main/models/projects.py:92 msgid "SCM Type" msgstr "Type de SCM" -#: awx/main/models/projects.py:82 +#: awx/main/models/projects.py:93 msgid "Specifies the source control system used to store the project." msgstr "" "Spécifie le système de contrôle des sources utilisé pour stocker le projet." -#: awx/main/models/projects.py:88 +#: awx/main/models/projects.py:99 msgid "SCM URL" msgstr "URL du SCM" -#: awx/main/models/projects.py:89 +#: awx/main/models/projects.py:100 msgid "The location where the project is stored." msgstr "Emplacement où le projet est stocké." -#: awx/main/models/projects.py:95 +#: awx/main/models/projects.py:106 msgid "SCM Branch" msgstr "Branche SCM" -#: awx/main/models/projects.py:96 +#: awx/main/models/projects.py:107 msgid "Specific branch, tag or commit to checkout." msgstr "Branche, balise ou validation spécifique à valider." -#: awx/main/models/projects.py:100 +#: awx/main/models/projects.py:111 msgid "Discard any local changes before syncing the project." msgstr "Ignorez les modifications locales avant de synchroniser le projet." -#: awx/main/models/projects.py:104 +#: awx/main/models/projects.py:115 msgid "Delete the project before syncing." msgstr "Supprimez le projet avant la synchronisation." -#: awx/main/models/projects.py:133 +#: awx/main/models/projects.py:144 msgid "Invalid SCM URL." msgstr "URL du SCM incorrecte." -#: awx/main/models/projects.py:136 +#: awx/main/models/projects.py:147 msgid "SCM URL is required." msgstr "L'URL du SCM est requise." -#: awx/main/models/projects.py:144 +#: awx/main/models/projects.py:155 msgid "Insights Credential is required for an Insights Project." msgstr "" "Des informations d'identification Insights sont requises pour un projet " "Insights." -#: awx/main/models/projects.py:150 +#: awx/main/models/projects.py:161 msgid "Credential kind must be 'scm'." msgstr "Le type d'informations d'identification doit être 'scm'." -#: awx/main/models/projects.py:167 +#: awx/main/models/projects.py:178 msgid "Invalid credential." msgstr "Informations d'identification non valides." -#: awx/main/models/projects.py:249 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "Mettez à jour le projet lorsqu'une tâche qui l'utilise est lancée." -#: awx/main/models/projects.py:254 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." @@ -3138,23 +3976,23 @@ msgstr "" "Délai écoulé (en secondes) entre la dernière mise à jour du projet et le " "lancement d'une nouvelle mise à jour en tant que dépendance de la tâche." -#: awx/main/models/projects.py:264 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "Dernière révision récupérée par une mise à jour du projet" -#: awx/main/models/projects.py:271 +#: awx/main/models/projects.py:285 msgid "Playbook Files" msgstr "Fichiers de playbook" -#: awx/main/models/projects.py:272 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "Liste des playbooks trouvés dans le projet" -#: awx/main/models/projects.py:279 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "Fichiers d'inventaire" -#: awx/main/models/projects.py:280 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "" @@ -3178,67 +4016,117 @@ msgid "Admin" msgstr "Admin" #: awx/main/models/rbac.py:40 +msgid "Project Admin" +msgstr "Admin Projet" + +#: awx/main/models/rbac.py:41 +msgid "Inventory Admin" +msgstr "Admin Inventaire" + +#: awx/main/models/rbac.py:42 +msgid "Credential Admin" +msgstr "Admin Identifiants" + +#: awx/main/models/rbac.py:43 +msgid "Workflow Admin" +msgstr "Admin Workflow" + +#: awx/main/models/rbac.py:44 +msgid "Notification Admin" +msgstr "Admin Notification" + +#: awx/main/models/rbac.py:45 msgid "Auditor" msgstr "Auditeur" -#: awx/main/models/rbac.py:41 +#: awx/main/models/rbac.py:46 msgid "Execute" msgstr "Execution" -#: awx/main/models/rbac.py:42 +#: awx/main/models/rbac.py:47 msgid "Member" msgstr "Membre" -#: awx/main/models/rbac.py:43 +#: awx/main/models/rbac.py:48 msgid "Read" msgstr "Lecture" -#: awx/main/models/rbac.py:44 +#: awx/main/models/rbac.py:49 msgid "Update" msgstr "Mise à jour" -#: awx/main/models/rbac.py:45 +#: awx/main/models/rbac.py:50 msgid "Use" msgstr "Utilisation" -#: awx/main/models/rbac.py:49 +#: awx/main/models/rbac.py:54 msgid "Can manage all aspects of the system" msgstr "Peut gérer tous les aspects du système" -#: awx/main/models/rbac.py:50 +#: awx/main/models/rbac.py:55 msgid "Can view all settings on the system" msgstr "Peut afficher tous les paramètres de configuration du système" -#: awx/main/models/rbac.py:51 +#: awx/main/models/rbac.py:56 msgid "May run ad hoc commands on an inventory" msgstr "Peut exécuter des commandes ad hoc sur un inventaire" -#: awx/main/models/rbac.py:52 +#: awx/main/models/rbac.py:57 #, python-format msgid "Can manage all aspects of the %s" msgstr "Peut exécuter tous les aspects de %s" -#: awx/main/models/rbac.py:53 +#: awx/main/models/rbac.py:58 +#, python-format +msgid "Can manage all projects of the %s" +msgstr "Peut gérer tous les projets de %s" + +#: awx/main/models/rbac.py:59 +#, python-format +msgid "Can manage all inventories of the %s" +msgstr "Peut gérer tous les inventaires de %s" + +#: awx/main/models/rbac.py:60 +#, python-format +msgid "Can manage all credentials of the %s" +msgstr "Peut gérer tous les identifiants de %s" + +#: awx/main/models/rbac.py:61 +#, python-format +msgid "Can manage all workflows of the %s" +msgstr "Peut gérer tous les workflows de %s" + +#: awx/main/models/rbac.py:62 +#, python-format +msgid "Can manage all notifications of the %s" +msgstr "Peut gérer toutes les notifications de %s" + +#: awx/main/models/rbac.py:63 #, python-format msgid "Can view all settings for the %s" msgstr "Peut afficher tous les paramètres de configuration du %s" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:65 +msgid "May run any executable resources in the organization" +msgstr "" +"Peut exécuter n'importe quelle ressource exécutable dans l'organisation" + +#: awx/main/models/rbac.py:66 #, python-format msgid "May run the %s" msgstr "Peut exécuter %s" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:68 #, python-format msgid "User is a member of the %s" msgstr "L'utilisateur est un membre de %s" -#: awx/main/models/rbac.py:56 +#: awx/main/models/rbac.py:69 #, python-format msgid "May view settings for the %s" msgstr "Peut afficher les paramètres de configuration de %s" -#: awx/main/models/rbac.py:57 +#: awx/main/models/rbac.py:70 msgid "" "May update project or inventory or group using the configured source update " "system" @@ -3246,30 +4134,30 @@ msgstr "" "Peut mettre un projet, un inventaire, ou un groupe à jour en utilisant le " "système de mise à jour de la source configuré." -#: awx/main/models/rbac.py:58 +#: awx/main/models/rbac.py:71 #, python-format msgid "Can use the %s in a job template" msgstr "Peut utiliser %s dans un modèle de tâche" -#: awx/main/models/rbac.py:122 +#: awx/main/models/rbac.py:135 msgid "roles" msgstr "rôles" -#: awx/main/models/rbac.py:434 +#: awx/main/models/rbac.py:441 msgid "role_ancestors" msgstr "role_ancestors" -#: awx/main/models/schedules.py:71 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "Active le traitement de ce calendrier." -#: awx/main/models/schedules.py:77 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." msgstr "" "La première occurrence du calendrier se produit à ce moment précis ou " "ultérieurement." -#: awx/main/models/schedules.py:83 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." @@ -3277,102 +4165,110 @@ msgstr "" "La dernière occurrence du calendrier se produit avant ce moment précis. " "Passé ce délai, le calendrier arrive à expiration." -#: awx/main/models/schedules.py:87 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "Valeur représentant la règle de récurrence iCal des calendriers." -#: awx/main/models/schedules.py:93 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." msgstr "La prochaine fois que l'action planifiée s'exécutera." -#: awx/main/models/schedules.py:109 -msgid "Expected JSON" -msgstr "JSON attendu" - -#: awx/main/models/schedules.py:121 -msgid "days must be a positive integer." -msgstr "jours doit être un entier positif." - -#: awx/main/models/unified_jobs.py:58 +#: awx/main/models/unified_jobs.py:61 msgid "New" msgstr "Nouveau" -#: awx/main/models/unified_jobs.py:60 +#: awx/main/models/unified_jobs.py:63 msgid "Waiting" msgstr "En attente" -#: awx/main/models/unified_jobs.py:61 +#: awx/main/models/unified_jobs.py:64 msgid "Running" msgstr "En cours d'exécution" -#: awx/main/models/unified_jobs.py:65 +#: awx/main/models/unified_jobs.py:68 msgid "Canceled" msgstr "Annulé" -#: awx/main/models/unified_jobs.py:69 +#: awx/main/models/unified_jobs.py:72 msgid "Never Updated" msgstr "Jamais mis à jour" -#: awx/main/models/unified_jobs.py:73 awx/ui/templates/ui/index.html:67 -#: awx/ui/templates/ui/index.html.py:86 +#: awx/main/models/unified_jobs.py:76 msgid "OK" msgstr "OK" -#: awx/main/models/unified_jobs.py:74 +#: awx/main/models/unified_jobs.py:77 msgid "Missing" msgstr "Manquant" -#: awx/main/models/unified_jobs.py:78 +#: awx/main/models/unified_jobs.py:81 msgid "No External Source" msgstr "Aucune source externe" -#: awx/main/models/unified_jobs.py:85 +#: awx/main/models/unified_jobs.py:88 msgid "Updating" msgstr "Mise à jour en cours" -#: awx/main/models/unified_jobs.py:429 +#: awx/main/models/unified_jobs.py:427 +msgid "Field is not allowed on launch." +msgstr "Champ non autorisé au lancement." + +#: awx/main/models/unified_jobs.py:455 +#, python-brace-format +msgid "" +"Variables {list_of_keys} provided, but this template cannot accept " +"variables." +msgstr "" +"Variables {list_of_keys} fournies, mais ce modèle ne peut pas accepter de " +"variables." + +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "Relancer" -#: awx/main/models/unified_jobs.py:430 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "Rappeler" -#: awx/main/models/unified_jobs.py:431 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "Planifié" -#: awx/main/models/unified_jobs.py:432 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "Dépendance" -#: awx/main/models/unified_jobs.py:433 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "Workflow" -#: awx/main/models/unified_jobs.py:434 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "Sync" -#: awx/main/models/unified_jobs.py:481 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "Nœud sur lequel la tâche s'est exécutée." -#: awx/main/models/unified_jobs.py:507 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "L'instance qui gère l'environnement d'exécution isolé." + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." msgstr "" "Date et heure auxquelles la tâche a été mise en file d'attente pour le " "démarrage." -#: awx/main/models/unified_jobs.py:513 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." msgstr "Date et heure de fin d'exécution de la tâche." -#: awx/main/models/unified_jobs.py:519 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." msgstr "Délai écoulé (en secondes) pendant lequel la tâche s'est exécutée." -#: awx/main/models/unified_jobs.py:541 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and" " capture stdout" @@ -3380,10 +4276,23 @@ msgstr "" "Champ d'état indiquant l'état de la tâche si elle n'a pas pu s'exécuter et " "capturer stdout" -#: awx/main/models/unified_jobs.py:580 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "Groupe d'instance/Rampart sous lequel la tâche a été exécutée." +#: awx/main/models/workflow.py:203 +#, python-brace-format +msgid "" +"Bad launch configuration starting template {template_pk} as part of workflow {workflow_pk}. Errors:\n" +"{error_text}" +msgstr "" +"Mauvais modèle de départ de configuration du lancement {template_pk} dans le workflow {workflow_pk}. Erreurs :\n" +"{error_text}" + +#: awx/main/models/workflow.py:388 +msgid "Field is not allowed for use in workflows." +msgstr "Champ non autorisé dans les workflows." + #: awx/main/notifications/base.py:17 #: awx/main/notifications/email_backend.py:28 msgid "" @@ -3393,11 +4302,11 @@ msgstr "" "{} #{} était à l'état {}, voir les détails sur {}\n" "\n" -#: awx/main/notifications/hipchat_backend.py:47 +#: awx/main/notifications/hipchat_backend.py:48 msgid "Error sending messages: {}" msgstr "Erreur lors de l'envoi de messages : {}" -#: awx/main/notifications/hipchat_backend.py:49 +#: awx/main/notifications/hipchat_backend.py:50 msgid "Error sending message to hipchat: {}" msgstr "Erreur lors de l'envoi d'un message à hipchat : {}" @@ -3405,16 +4314,27 @@ msgstr "Erreur lors de l'envoi d'un message à hipchat : {}" msgid "Exception connecting to irc server: {}" msgstr "Exception lors de la connexion au serveur irc : {}" +#: awx/main/notifications/mattermost_backend.py:48 +#: awx/main/notifications/mattermost_backend.py:50 +msgid "Error sending notification mattermost: {}" +msgstr "Erreur d'envoi de notification mattermost: {}" + #: awx/main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" msgstr "Exception lors de la connexion à PagerDuty : {}" #: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 +#: awx/main/notifications/slack_backend.py:82 +#: awx/main/notifications/slack_backend.py:99 #: awx/main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" msgstr "Exception lors de l'envoi de messages : {}" +#: awx/main/notifications/rocketchat_backend.py:46 +#: awx/main/notifications/rocketchat_backend.py:49 +msgid "Error sending notification rocket.chat: {}" +msgstr "Erreur d'envoi de notification rocket.chat: {}" + #: awx/main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" msgstr "Exception lors de la connexion à Twilio : {}" @@ -3424,7 +4344,7 @@ msgstr "Exception lors de la connexion à Twilio : {}" msgid "Error sending notification webhook: {}" msgstr "Erreur lors de l'envoi d'un webhook de notification : {}" -#: awx/main/scheduler/task_manager.py:197 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" @@ -3433,7 +4353,7 @@ msgstr "" "dans l'état qui convient ou nécessitant des informations d'identification " "manuelles adéquates." -#: awx/main/scheduler/task_manager.py:201 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" @@ -3441,91 +4361,109 @@ msgstr "" "Tâche, lancée à partir du workflow, ne pouvant démarrer, pour cause de " "ressource manquante, tel un projet ou inventaire" -#: awx/main/tasks.py:184 +#: awx/main/signals.py:616 +msgid "limit_reached" +msgstr "limit_reached" + +#: awx/main/tasks.py:282 msgid "Ansible Tower host usage over 90%" msgstr "Utilisation d'hôtes Ansible Tower supérieure à 90 %" -#: awx/main/tasks.py:189 +#: awx/main/tasks.py:287 msgid "Ansible Tower license will expire soon" msgstr "La licence Ansible Tower expirera bientôt" -#: awx/main/tasks.py:318 -msgid "status_str must be either succeeded or failed" -msgstr "status_str doit être une réussite ou un échec" +#: awx/main/tasks.py:1335 +msgid "Job could not start because it does not have a valid inventory." +msgstr "Le job n'a pas pu commencer parce qu'il n'a pas d'inventaire valide." -#: awx/main/tasks.py:1549 -msgid "Dependent inventory update {} was canceled." -msgstr "La mise à jour d'inventaire dépendante {} a été annulée." - -#: awx/main/utils/common.py:89 +#: awx/main/utils/common.py:97 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "Impossible de convertir \"%s\" en booléen" -#: awx/main/utils/common.py:235 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Type de SCM \"%s\" non pris en charge" -#: awx/main/utils/common.py:242 awx/main/utils/common.py:254 -#: awx/main/utils/common.py:273 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" msgstr "URL %s non valide." -#: awx/main/utils/common.py:244 awx/main/utils/common.py:283 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "URL %s non prise en charge" -#: awx/main/utils/common.py:285 +#: awx/main/utils/common.py:304 #, 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:287 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "L'hôte est requis pour l'URL %s" -#: awx/main/utils/common.py:305 +#: awx/main/utils/common.py:324 #, 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:311 +#: awx/main/utils/common.py:330 #, 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/validators.py:60 +#: awx/main/utils/common.py:611 +#, python-brace-format +msgid "Input type `{data_type}` is not a dictionary" +msgstr "Le type d'entrée « {data_type} » ne n'est pas un dictionnaire" + +#: awx/main/utils/common.py:644 +#, python-brace-format +msgid "Variables not compatible with JSON standard (error: {json_error})" +msgstr "Variables non compatibles avec la syntaxe JSON (error: {json_error})" + +#: awx/main/utils/common.py:650 +#, python-brace-format +msgid "" +"Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." +msgstr "" +"Impossible d'analyser avec JSON (error: {json_error}) ou YAML (error: " +"{yaml_error})." + +#: awx/main/validators.py:67 #, python-format msgid "Invalid certificate or key: %s..." msgstr "Certificat ou clé non valide : %r..." -#: awx/main/validators.py:74 +#: awx/main/validators.py:83 #, python-format msgid "Invalid private key: unsupported type \"%s\"" msgstr "Clé privée non valide : type \"%s\" non pris en charge" -#: awx/main/validators.py:78 +#: awx/main/validators.py:87 #, python-format msgid "Unsupported PEM object type: \"%s\"" msgstr "Type d'objet PEM non pris en charge : \"%s\"" -#: awx/main/validators.py:103 +#: awx/main/validators.py:112 msgid "Invalid base64-encoded data" msgstr "Données codées en base64 non valides" -#: awx/main/validators.py:122 +#: awx/main/validators.py:131 msgid "Exactly one private key is required." msgstr "Une clé privée uniquement est nécessaire." -#: awx/main/validators.py:124 +#: awx/main/validators.py:133 msgid "At least one private key is required." msgstr "Une clé privée au moins est nécessaire." -#: awx/main/validators.py:126 +#: awx/main/validators.py:135 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d " @@ -3534,12 +4472,12 @@ msgstr "" "%(min_keys)d clés privées au moins sont requises, mais %(key_count)d " "uniquement ont été fournies." -#: awx/main/validators.py:129 +#: awx/main/validators.py:138 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." msgstr "Une seule clé privée est autorisée, %(key_count)d ont été fournies." -#: awx/main/validators.py:131 +#: awx/main/validators.py:140 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." @@ -3547,15 +4485,15 @@ msgstr "" "Pas plus de %(max_keys)d clés privées sont autorisées, %(key_count)d ont été" " fournies." -#: awx/main/validators.py:136 +#: awx/main/validators.py:145 msgid "Exactly one certificate is required." msgstr "Un certificat uniquement est nécessaire." -#: awx/main/validators.py:138 +#: awx/main/validators.py:147 msgid "At least one certificate is required." msgstr "Un certificat au moins est nécessaire." -#: awx/main/validators.py:140 +#: awx/main/validators.py:149 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " @@ -3564,12 +4502,12 @@ msgstr "" "%(min_certs)d certificats au moins sont requis, mais %(cert_count)d " "uniquement ont été fournis." -#: awx/main/validators.py:143 +#: awx/main/validators.py:152 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." msgstr "Un seul certificat est autorisé, %(cert_count)d ont été fournis." -#: awx/main/validators.py:145 +#: awx/main/validators.py:154 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d " @@ -3614,287 +4552,287 @@ msgstr "Erreur serveur" msgid "A server error has occurred." msgstr "Une erreur serveur s'est produite." -#: awx/settings/defaults.py:665 +#: awx/settings/defaults.py:721 msgid "US East (Northern Virginia)" msgstr "Est des États-Unis (Virginie du Nord)" -#: awx/settings/defaults.py:666 +#: awx/settings/defaults.py:722 msgid "US East (Ohio)" msgstr "Est des États-Unis (Ohio)" -#: awx/settings/defaults.py:667 +#: awx/settings/defaults.py:723 msgid "US West (Oregon)" msgstr "Ouest des États-Unis (Oregon)" -#: awx/settings/defaults.py:668 +#: awx/settings/defaults.py:724 msgid "US West (Northern California)" msgstr "Ouest des États-Unis (Nord de la Californie)" -#: awx/settings/defaults.py:669 +#: awx/settings/defaults.py:725 msgid "Canada (Central)" msgstr "Canada (Central)" -#: awx/settings/defaults.py:670 +#: awx/settings/defaults.py:726 msgid "EU (Frankfurt)" msgstr "UE (Francfort)" -#: awx/settings/defaults.py:671 +#: awx/settings/defaults.py:727 msgid "EU (Ireland)" msgstr "UE (Irlande)" -#: awx/settings/defaults.py:672 +#: awx/settings/defaults.py:728 msgid "EU (London)" msgstr "UE (Londres)" -#: awx/settings/defaults.py:673 +#: awx/settings/defaults.py:729 msgid "Asia Pacific (Singapore)" msgstr "Asie-Pacifique (Singapour)" -#: awx/settings/defaults.py:674 +#: awx/settings/defaults.py:730 msgid "Asia Pacific (Sydney)" msgstr "Asie-Pacifique (Sydney)" -#: awx/settings/defaults.py:675 +#: awx/settings/defaults.py:731 msgid "Asia Pacific (Tokyo)" msgstr "Asie-Pacifique (Tokyo)" -#: awx/settings/defaults.py:676 +#: awx/settings/defaults.py:732 msgid "Asia Pacific (Seoul)" msgstr "Asie-Pacifique (Séoul)" -#: awx/settings/defaults.py:677 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Mumbai)" msgstr "Asie-Pacifique (Mumbai)" -#: awx/settings/defaults.py:678 +#: awx/settings/defaults.py:734 msgid "South America (Sao Paulo)" msgstr "Amérique du Sud (Sao Paulo)" -#: awx/settings/defaults.py:679 +#: awx/settings/defaults.py:735 msgid "US West (GovCloud)" msgstr "Ouest des États-Unis (GovCloud)" -#: awx/settings/defaults.py:680 +#: awx/settings/defaults.py:736 msgid "China (Beijing)" msgstr "Chine (Pékin)" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:785 msgid "US East 1 (B)" msgstr "Est des États-Unis 1 (B)" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:786 msgid "US East 1 (C)" msgstr "Est des États-Unis 1 (C)" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:787 msgid "US East 1 (D)" msgstr "Est des États-Unis 1 (D)" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:788 msgid "US East 4 (A)" msgstr "Est des États-Unis 4 (A)" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:789 msgid "US East 4 (B)" msgstr "Est des États-Unis 4 (B)" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:790 msgid "US East 4 (C)" msgstr "Est des États-Unis 4 (C)" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:791 msgid "US Central (A)" msgstr "Centre des États-Unis (A)" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:792 msgid "US Central (B)" msgstr "Centre des États-Unis (B)" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:793 msgid "US Central (C)" msgstr "Centre des États-Unis (C)" -#: awx/settings/defaults.py:738 +#: awx/settings/defaults.py:794 msgid "US Central (F)" msgstr "Centre des États-Unis (F)" -#: awx/settings/defaults.py:739 +#: awx/settings/defaults.py:795 msgid "US West (A)" msgstr "Ouest des États-Unis (A)" -#: awx/settings/defaults.py:740 +#: awx/settings/defaults.py:796 msgid "US West (B)" msgstr "Ouest des États-Unis (B)" -#: awx/settings/defaults.py:741 +#: awx/settings/defaults.py:797 msgid "US West (C)" msgstr "Ouest des États-Unis (C)" -#: awx/settings/defaults.py:742 +#: awx/settings/defaults.py:798 msgid "Europe West 1 (B)" msgstr "Europe de l'Ouest 1 (B)" -#: awx/settings/defaults.py:743 +#: awx/settings/defaults.py:799 msgid "Europe West 1 (C)" msgstr "Europe de l'Ouest 1 (C)" -#: awx/settings/defaults.py:744 +#: awx/settings/defaults.py:800 msgid "Europe West 1 (D)" msgstr "Europe de l'Ouest 1 (D)" -#: awx/settings/defaults.py:745 +#: awx/settings/defaults.py:801 msgid "Europe West 2 (A)" msgstr "Europe de l'Ouest 2 (A)" -#: awx/settings/defaults.py:746 +#: awx/settings/defaults.py:802 msgid "Europe West 2 (B)" msgstr "Europe de l'Ouest 2 (B)" -#: awx/settings/defaults.py:747 +#: awx/settings/defaults.py:803 msgid "Europe West 2 (C)" msgstr "Europe de l'Ouest 2 (C)" -#: awx/settings/defaults.py:748 +#: awx/settings/defaults.py:804 msgid "Asia East (A)" msgstr "Asie de l'Est (A)" -#: awx/settings/defaults.py:749 +#: awx/settings/defaults.py:805 msgid "Asia East (B)" msgstr "Asie de l'Est (B)" -#: awx/settings/defaults.py:750 +#: awx/settings/defaults.py:806 msgid "Asia East (C)" msgstr "Asie de l'Est (C)" -#: awx/settings/defaults.py:751 +#: awx/settings/defaults.py:807 msgid "Asia Southeast (A)" msgstr "Asie du Sud-Est (A)" -#: awx/settings/defaults.py:752 +#: awx/settings/defaults.py:808 msgid "Asia Southeast (B)" msgstr "Asie du Sud-Est (B)" -#: awx/settings/defaults.py:753 +#: awx/settings/defaults.py:809 msgid "Asia Northeast (A)" msgstr "Asie du Nord-Est (A)" -#: awx/settings/defaults.py:754 +#: awx/settings/defaults.py:810 msgid "Asia Northeast (B)" msgstr "Asie du Nord-Est (B)" -#: awx/settings/defaults.py:755 +#: awx/settings/defaults.py:811 msgid "Asia Northeast (C)" msgstr "Asie du Nord-Est (C)" -#: awx/settings/defaults.py:756 +#: awx/settings/defaults.py:812 msgid "Australia Southeast (A)" msgstr "Sud-est de l'Australie (A)" -#: awx/settings/defaults.py:757 +#: awx/settings/defaults.py:813 msgid "Australia Southeast (B)" msgstr "Sud-est de l'Australie (B)" -#: awx/settings/defaults.py:758 +#: awx/settings/defaults.py:814 msgid "Australia Southeast (C)" msgstr "Sud-est de l'Australie (C)" -#: awx/settings/defaults.py:780 +#: awx/settings/defaults.py:836 msgid "US East" msgstr "Est des États-Unis" -#: awx/settings/defaults.py:781 +#: awx/settings/defaults.py:837 msgid "US East 2" msgstr "Est des États-Unis 2" -#: awx/settings/defaults.py:782 +#: awx/settings/defaults.py:838 msgid "US Central" msgstr "Centre des États-Unis" -#: awx/settings/defaults.py:783 +#: awx/settings/defaults.py:839 msgid "US North Central" msgstr "Centre-Nord des États-Unis" -#: awx/settings/defaults.py:784 +#: awx/settings/defaults.py:840 msgid "US South Central" msgstr "Centre-Sud des États-Unis" -#: awx/settings/defaults.py:785 +#: awx/settings/defaults.py:841 msgid "US West Central" msgstr "Centre-Ouest des États-Unis" -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:842 msgid "US West" msgstr "Ouest des États-Unis" -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:843 msgid "US West 2" msgstr "Ouest des États-Unis 2" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:844 msgid "Canada East" msgstr "Est du Canada" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:845 msgid "Canada Central" msgstr "Centre du Canada" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:846 msgid "Brazil South" msgstr "Sud du Brésil" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:847 msgid "Europe North" msgstr "Europe du Nord" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:848 msgid "Europe West" msgstr "Europe de l'Ouest" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:849 msgid "UK West" msgstr "Ouest du Royaume-Uni" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:850 msgid "UK South" msgstr "Sud du Royaume-Uni" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:851 msgid "Asia East" msgstr "Asie de l'Est" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:852 msgid "Asia Southeast" msgstr "Asie du Sud-Est" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:853 msgid "Australia East" msgstr "Est de l'Australie" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:854 msgid "Australia Southeast" msgstr "Sud-Est de l'Australie" -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:855 msgid "India West" msgstr "Ouest de l'Inde" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:856 msgid "India South" msgstr "Sud de l'Inde" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:857 msgid "Japan East" msgstr "Est du Japon" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:858 msgid "Japan West" msgstr "Ouest du Japon" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:859 msgid "Korea Central" msgstr "Centre de la Corée" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:860 msgid "Korea South" msgstr "Sud de la Corée" @@ -3907,7 +4845,7 @@ msgid "" "Mapping to organization admins/users from social auth accounts. This setting\n" "controls which users are placed into which Tower organizations based on their\n" "username and email address. Configuration details are available in the Ansible\n" -"Tower documentation.'" +"Tower documentation." msgstr "" "Mappage avec des administrateurs/utilisateurs d'organisation appartenant à des comptes d'authentification sociale. Ce paramètre\n" "contrôle les utilisateurs placés dans les organisations Tower en fonction de\n" @@ -3958,11 +4896,11 @@ msgstr "" "d'un compte utilisateur avec une adresse électronique correspondante " "pourront se connecter." -#: awx/sso/conf.py:137 +#: awx/sso/conf.py:141 msgid "LDAP Server URI" msgstr "URI du serveur LDAP" -#: awx/sso/conf.py:138 +#: awx/sso/conf.py:142 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be" @@ -3974,19 +4912,20 @@ msgstr "" " peuvent être définis en les séparant par des espaces ou des virgules. " "L'authentification LDAP est désactivée si ce paramètre est vide." -#: awx/sso/conf.py:142 awx/sso/conf.py:158 awx/sso/conf.py:170 -#: awx/sso/conf.py:182 awx/sso/conf.py:198 awx/sso/conf.py:218 -#: awx/sso/conf.py:240 awx/sso/conf.py:255 awx/sso/conf.py:273 -#: awx/sso/conf.py:290 awx/sso/conf.py:307 awx/sso/conf.py:323 -#: awx/sso/conf.py:337 awx/sso/conf.py:354 awx/sso/conf.py:380 +#: awx/sso/conf.py:146 awx/sso/conf.py:162 awx/sso/conf.py:174 +#: awx/sso/conf.py:186 awx/sso/conf.py:202 awx/sso/conf.py:222 +#: awx/sso/conf.py:244 awx/sso/conf.py:259 awx/sso/conf.py:277 +#: awx/sso/conf.py:294 awx/sso/conf.py:306 awx/sso/conf.py:332 +#: awx/sso/conf.py:348 awx/sso/conf.py:362 awx/sso/conf.py:380 +#: awx/sso/conf.py:406 msgid "LDAP" msgstr "LDAP" -#: awx/sso/conf.py:154 +#: awx/sso/conf.py:158 msgid "LDAP Bind DN" msgstr "ND de la liaison LDAP" -#: awx/sso/conf.py:155 +#: awx/sso/conf.py:159 msgid "" "DN (Distinguished Name) of user to bind for all search queries. This is the " "system user account we will use to login to query LDAP for other user " @@ -3998,27 +4937,27 @@ msgstr "" "utilisateur. Voir la documentation Ansible Tower pour obtenir des exemples " "de syntaxe." -#: awx/sso/conf.py:168 +#: awx/sso/conf.py:172 msgid "LDAP Bind Password" msgstr "Mot de passe de la liaison LDAP" -#: awx/sso/conf.py:169 +#: awx/sso/conf.py:173 msgid "Password used to bind LDAP user account." msgstr "Mot de passe utilisé pour lier le compte utilisateur LDAP." -#: awx/sso/conf.py:180 +#: awx/sso/conf.py:184 msgid "LDAP Start TLS" msgstr "LDAP - Lancer TLS" -#: awx/sso/conf.py:181 +#: awx/sso/conf.py:185 msgid "Whether to enable TLS when the LDAP connection is not using SSL." msgstr "Pour activer ou non TLS lorsque la connexion LDAP n'utilise pas SSL." -#: awx/sso/conf.py:191 +#: awx/sso/conf.py:195 msgid "LDAP Connection Options" msgstr "Options de connexion à LDAP" -#: awx/sso/conf.py:192 +#: awx/sso/conf.py:196 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -4033,11 +4972,11 @@ msgstr "" "ldap.org/doc/html/ldap.html#options afin de connaître les options possibles " "et les valeurs que vous pouvez définir." -#: awx/sso/conf.py:211 +#: awx/sso/conf.py:215 msgid "LDAP User Search" msgstr "Recherche d'utilisateurs LDAP" -#: awx/sso/conf.py:212 +#: awx/sso/conf.py:216 msgid "" "LDAP search query to find users. Any user that matches the given pattern " "will be able to login to Tower. The user should also be mapped into a Tower" @@ -4052,11 +4991,11 @@ msgstr "" " de recherche doivent être prises en charge, l'utilisation de \"LDAPUnion\" " "est possible. Se reporter à la documentation Tower pour plus d'informations." -#: awx/sso/conf.py:234 +#: awx/sso/conf.py:238 msgid "LDAP User DN Template" msgstr "Modèle de ND pour les utilisateurs LDAP" -#: awx/sso/conf.py:235 +#: awx/sso/conf.py:239 msgid "" "Alternative to user search, if user DNs are all of the same format. This " "approach is more efficient for user lookups than searching if it is usable " @@ -4069,16 +5008,16 @@ msgstr "" "organisationnel. Si ce paramètre est défini, sa valeur sera utilisée à la " "place de AUTH_LDAP_USER_SEARCH." -#: awx/sso/conf.py:250 +#: awx/sso/conf.py:254 msgid "LDAP User Attribute Map" msgstr "Mappe des attributs d'utilisateurs LDAP" -#: awx/sso/conf.py:251 +#: awx/sso/conf.py:255 msgid "" "Mapping of LDAP user schema to Tower API user attributes. The default " "setting is valid for ActiveDirectory but users with other LDAP " "configurations may need to change the values. Refer to the Ansible Tower " -"documentation for additonal details." +"documentation for additional details." msgstr "" "Mappage du schéma utilisateur LDAP avec les attributs utilisateur d'API " "Tower. Le paramètre par défaut est valide pour ActiveDirectory, mais les " @@ -4086,11 +5025,11 @@ msgstr "" "modifier les valeurs. Voir la documentation Ansible Tower pour obtenir des " "détails supplémentaires." -#: awx/sso/conf.py:269 +#: awx/sso/conf.py:273 msgid "LDAP Group Search" msgstr "Recherche de groupes LDAP" -#: awx/sso/conf.py:270 +#: awx/sso/conf.py:274 msgid "" "Users are mapped to organizations based on their membership in LDAP groups. " "This setting defines the LDAP search query to find groups. Unlike the user " @@ -4102,25 +5041,35 @@ msgstr "" "contrairement à la recherche d'utilisateurs LDAP, la recherche des groupes " "ne prend pas en charge LDAPSearchUnion." -#: awx/sso/conf.py:286 +#: awx/sso/conf.py:290 msgid "LDAP Group Type" msgstr "Type de groupe LDAP" -#: awx/sso/conf.py:287 +#: awx/sso/conf.py:291 msgid "" "The group type may need to be changed based on the type of the LDAP server." -" Values are listed at: http://pythonhosted.org/django-auth-ldap/groups.html" -"#types-of-groups" +" Values are listed at: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" msgstr "" "Il convient parfois de modifier le type de groupe en fonction du type de " -"serveur LDAP. Les valeurs sont répertoriées à l'adresse suivante : " -"http://pythonhosted.org/django-auth-ldap/groups.html#types-of-groups" +"serveur LDAP. Les valeurs sont répertoriées à l'adresse suivante : https" +"://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups" -#: awx/sso/conf.py:302 +#: awx/sso/conf.py:304 +msgid "LDAP Group Type Parameters" +msgstr "Paramètres de types de groupes LDAP" + +#: awx/sso/conf.py:305 +msgid "Key value parameters to send the chosen group type init method." +msgstr "" +"Paramètres de valeurs-clés pour envoyer la méthode init de type de groupe " +"sélectionné." + +#: awx/sso/conf.py:327 msgid "LDAP Require Group" msgstr "Groupe LDAP obligatoire" -#: awx/sso/conf.py:303 +#: awx/sso/conf.py:328 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " @@ -4132,11 +5081,11 @@ msgstr "" "recherche d'utilisateurs pourra se connecter via Tower. Un seul groupe est " "pris en charge." -#: awx/sso/conf.py:319 +#: awx/sso/conf.py:344 msgid "LDAP Deny Group" msgstr "Groupe LDAP refusé" -#: awx/sso/conf.py:320 +#: awx/sso/conf.py:345 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." @@ -4145,11 +5094,11 @@ msgstr "" " n'est pas autorisé à se connecter s'il est membre de ce groupe. Un seul " "groupe refusé est pris en charge." -#: awx/sso/conf.py:333 +#: awx/sso/conf.py:358 msgid "LDAP User Flags By Group" msgstr "Marqueurs d'utilisateur LDAP par groupe" -#: awx/sso/conf.py:334 +#: awx/sso/conf.py:359 msgid "" "Retrieve users from a given group. At this time, superuser and system " "auditors are the only groups supported. Refer to the Ansible Tower " @@ -4160,11 +5109,11 @@ msgstr "" " charge. Voir la documentation Ansible Tower pour obtenir plus " "d'informations." -#: awx/sso/conf.py:349 +#: awx/sso/conf.py:375 msgid "LDAP Organization Map" msgstr "Mappe d'organisations LDAP" -#: awx/sso/conf.py:350 +#: awx/sso/conf.py:376 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " "which users are placed into which Tower organizations relative to their LDAP" @@ -4177,11 +5126,11 @@ msgstr "" " Les informations de configuration sont disponibles dans la documentation " "Ansible Tower." -#: awx/sso/conf.py:377 +#: awx/sso/conf.py:403 msgid "LDAP Team Map" msgstr "Mappe d'équipes LDAP" -#: awx/sso/conf.py:378 +#: awx/sso/conf.py:404 msgid "" "Mapping between team members (users) and LDAP groups. Configuration details " "are available in the Ansible Tower documentation." @@ -4189,11 +5138,11 @@ msgstr "" "Mappage entre des membres de l'équipe (utilisateurs) et des groupes LDAP. Les informations\n" " de configuration sont disponibles dans la documentation Ansible Tower." -#: awx/sso/conf.py:406 +#: awx/sso/conf.py:440 msgid "RADIUS Server" msgstr "Serveur RADIUS" -#: awx/sso/conf.py:407 +#: awx/sso/conf.py:441 msgid "" "Hostname/IP of RADIUS server. RADIUS authentication is disabled if this " "setting is empty." @@ -4201,80 +5150,80 @@ msgstr "" "Nom d'hôte/IP du serveur RADIUS. L'authentification RADIUS est désactivée si" " ce paramètre est vide." -#: awx/sso/conf.py:409 awx/sso/conf.py:423 awx/sso/conf.py:435 +#: awx/sso/conf.py:443 awx/sso/conf.py:457 awx/sso/conf.py:469 #: awx/sso/models.py:14 msgid "RADIUS" msgstr "RADIUS" -#: awx/sso/conf.py:421 +#: awx/sso/conf.py:455 msgid "RADIUS Port" msgstr "Port RADIUS" -#: awx/sso/conf.py:422 +#: awx/sso/conf.py:456 msgid "Port of RADIUS server." msgstr "Port du serveur RADIUS." -#: awx/sso/conf.py:433 +#: awx/sso/conf.py:467 msgid "RADIUS Secret" msgstr "Secret RADIUS" -#: awx/sso/conf.py:434 +#: awx/sso/conf.py:468 msgid "Shared secret for authenticating to RADIUS server." msgstr "Secret partagé pour l'authentification sur le serveur RADIUS." -#: awx/sso/conf.py:450 +#: awx/sso/conf.py:484 msgid "TACACS+ Server" msgstr "Serveur TACACS+" -#: awx/sso/conf.py:451 +#: awx/sso/conf.py:485 msgid "Hostname of TACACS+ server." msgstr "Nom d'hôte du serveur TACACS+" -#: awx/sso/conf.py:452 awx/sso/conf.py:465 awx/sso/conf.py:478 -#: awx/sso/conf.py:491 awx/sso/conf.py:503 awx/sso/models.py:15 +#: awx/sso/conf.py:486 awx/sso/conf.py:499 awx/sso/conf.py:512 +#: awx/sso/conf.py:525 awx/sso/conf.py:537 awx/sso/models.py:15 msgid "TACACS+" msgstr "TACACS+" -#: awx/sso/conf.py:463 +#: awx/sso/conf.py:497 msgid "TACACS+ Port" msgstr "Port TACACS+" -#: awx/sso/conf.py:464 +#: awx/sso/conf.py:498 msgid "Port number of TACACS+ server." msgstr "Numéro de port du serveur TACACS+" -#: awx/sso/conf.py:476 +#: awx/sso/conf.py:510 msgid "TACACS+ Secret" msgstr "Secret TACACS+" -#: awx/sso/conf.py:477 +#: awx/sso/conf.py:511 msgid "Shared secret for authenticating to TACACS+ server." msgstr "Secret partagé pour l'authentification sur le serveur TACACS+." -#: awx/sso/conf.py:489 +#: awx/sso/conf.py:523 msgid "TACACS+ Auth Session Timeout" msgstr "Expiration du délai d'attente d'autorisation de la session TACACS+." -#: awx/sso/conf.py:490 +#: awx/sso/conf.py:524 msgid "TACACS+ session timeout value in seconds, 0 disables timeout." msgstr "" "Valeur du délai d'attente de la session TACACS+ en secondes, 0 désactive le " "délai d'attente." -#: awx/sso/conf.py:501 +#: awx/sso/conf.py:535 msgid "TACACS+ Authentication Protocol" msgstr "Protocole d'autorisation TACACS+" -#: awx/sso/conf.py:502 +#: awx/sso/conf.py:536 msgid "Choose the authentication protocol used by TACACS+ client." msgstr "" "Choisissez le protocole d'authentification utilisé par le client TACACS+." -#: awx/sso/conf.py:517 +#: awx/sso/conf.py:551 msgid "Google OAuth2 Callback URL" msgstr "URL de rappel OAuth2 pour Google" -#: awx/sso/conf.py:518 awx/sso/conf.py:611 awx/sso/conf.py:676 +#: awx/sso/conf.py:552 awx/sso/conf.py:645 awx/sso/conf.py:710 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4284,33 +5233,33 @@ msgstr "" "votre processus d'enregistrement. Voir la documentation Ansible Tower pour " "obtenir plus d'informations." -#: awx/sso/conf.py:521 awx/sso/conf.py:533 awx/sso/conf.py:545 -#: awx/sso/conf.py:558 awx/sso/conf.py:572 awx/sso/conf.py:584 -#: awx/sso/conf.py:596 +#: awx/sso/conf.py:555 awx/sso/conf.py:567 awx/sso/conf.py:579 +#: awx/sso/conf.py:592 awx/sso/conf.py:606 awx/sso/conf.py:618 +#: awx/sso/conf.py:630 msgid "Google OAuth2" msgstr "OAuth2 pour Google" -#: awx/sso/conf.py:531 +#: awx/sso/conf.py:565 msgid "Google OAuth2 Key" msgstr "Clé OAuth2 pour Google" -#: awx/sso/conf.py:532 +#: awx/sso/conf.py:566 msgid "The OAuth2 key from your web application." msgstr "Clé OAuth2 de votre application Web." -#: awx/sso/conf.py:543 +#: awx/sso/conf.py:577 msgid "Google OAuth2 Secret" msgstr "Secret OAuth2 pour Google" -#: awx/sso/conf.py:544 +#: awx/sso/conf.py:578 msgid "The OAuth2 secret from your web application." msgstr "Secret OAuth2 de votre application Web." -#: awx/sso/conf.py:555 +#: awx/sso/conf.py:589 msgid "Google OAuth2 Whitelisted Domains" msgstr "Domaines sur liste blanche OAuth2 pour Google" -#: awx/sso/conf.py:556 +#: awx/sso/conf.py:590 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." @@ -4318,11 +5267,11 @@ msgstr "" "Mettez à jour ce paramètre pour limiter les domaines qui sont autorisés à se" " connecter à l'aide de l'authentification OAuth2 avec un compte Google." -#: awx/sso/conf.py:567 +#: awx/sso/conf.py:601 msgid "Google OAuth2 Extra Arguments" msgstr "Arguments OAuth2 supplémentaires pour Google" -#: awx/sso/conf.py:568 +#: awx/sso/conf.py:602 msgid "" "Extra arguments for Google OAuth2 login. You can restrict it to only allow a" " single domain to authenticate, even if the user is logged in with multple " @@ -4333,81 +5282,81 @@ msgstr "" "connecté avec plusieurs comptes Google. Voir la documentation Ansible Tower " "pour obtenir plus d'informations." -#: awx/sso/conf.py:582 +#: awx/sso/conf.py:616 msgid "Google OAuth2 Organization Map" msgstr "Mappe d'organisations OAuth2 pour Google" -#: awx/sso/conf.py:594 +#: awx/sso/conf.py:628 msgid "Google OAuth2 Team Map" msgstr "Mappe d'équipes OAuth2 pour Google" -#: awx/sso/conf.py:610 +#: awx/sso/conf.py:644 msgid "GitHub OAuth2 Callback URL" msgstr "URL de rappel OAuth2 pour GitHub" -#: awx/sso/conf.py:614 awx/sso/conf.py:626 awx/sso/conf.py:637 -#: awx/sso/conf.py:649 awx/sso/conf.py:661 +#: awx/sso/conf.py:648 awx/sso/conf.py:660 awx/sso/conf.py:671 +#: awx/sso/conf.py:683 awx/sso/conf.py:695 msgid "GitHub OAuth2" msgstr "OAuth2 pour GitHub" -#: awx/sso/conf.py:624 +#: awx/sso/conf.py:658 msgid "GitHub OAuth2 Key" msgstr "Clé OAuth2 pour GitHub" -#: awx/sso/conf.py:625 +#: awx/sso/conf.py:659 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "Clé OAuth2 (ID client) de votre application de développeur GitHub." -#: awx/sso/conf.py:635 +#: awx/sso/conf.py:669 msgid "GitHub OAuth2 Secret" msgstr "Secret OAuth2 pour GitHub" -#: awx/sso/conf.py:636 +#: awx/sso/conf.py:670 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "" "Secret OAuth2 (secret client) de votre application de développeur GitHub." -#: awx/sso/conf.py:647 +#: awx/sso/conf.py:681 msgid "GitHub OAuth2 Organization Map" msgstr "Mappe d'organisations OAuth2 pour GitHub" -#: awx/sso/conf.py:659 +#: awx/sso/conf.py:693 msgid "GitHub OAuth2 Team Map" msgstr "Mappe d'équipes OAuth2 pour GitHub" -#: awx/sso/conf.py:675 +#: awx/sso/conf.py:709 msgid "GitHub Organization OAuth2 Callback URL" msgstr "URL de rappel OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:679 awx/sso/conf.py:691 awx/sso/conf.py:702 -#: awx/sso/conf.py:715 awx/sso/conf.py:726 awx/sso/conf.py:738 +#: awx/sso/conf.py:713 awx/sso/conf.py:725 awx/sso/conf.py:736 +#: awx/sso/conf.py:749 awx/sso/conf.py:760 awx/sso/conf.py:772 msgid "GitHub Organization OAuth2" msgstr "OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:689 +#: awx/sso/conf.py:723 msgid "GitHub Organization OAuth2 Key" msgstr "Clé OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:690 awx/sso/conf.py:768 +#: awx/sso/conf.py:724 awx/sso/conf.py:802 msgid "The OAuth2 key (Client ID) from your GitHub organization application." msgstr "Clé OAuth2 (ID client) de votre application d'organisation GitHub." -#: awx/sso/conf.py:700 +#: awx/sso/conf.py:734 msgid "GitHub Organization OAuth2 Secret" msgstr "Secret OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:701 awx/sso/conf.py:779 +#: awx/sso/conf.py:735 awx/sso/conf.py:813 msgid "" "The OAuth2 secret (Client Secret) from your GitHub organization application." msgstr "" "Secret OAuth2 (secret client) de votre application d'organisation GitHub." -#: awx/sso/conf.py:712 +#: awx/sso/conf.py:746 msgid "GitHub Organization Name" msgstr "Nom de l'organisation GitHub" -#: awx/sso/conf.py:713 +#: awx/sso/conf.py:747 msgid "" "The name of your GitHub organization, as used in your organization's URL: " "https://github.com//." @@ -4415,19 +5364,19 @@ msgstr "" "Nom de votre organisation GitHub, tel qu'utilisé dans l'URL de votre " "organisation : https://github.com//." -#: awx/sso/conf.py:724 +#: awx/sso/conf.py:758 msgid "GitHub Organization OAuth2 Organization Map" msgstr "Mappe d'organisations OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:736 +#: awx/sso/conf.py:770 msgid "GitHub Organization OAuth2 Team Map" msgstr "Mappe d'équipes OAuth2 pour les organisations GitHub" -#: awx/sso/conf.py:752 +#: awx/sso/conf.py:786 msgid "GitHub Team OAuth2 Callback URL" msgstr "URL de rappel OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:753 +#: awx/sso/conf.py:787 msgid "" "Create an organization-owned application at " "https://github.com/organizations//settings/applications and obtain " @@ -4439,24 +5388,24 @@ msgstr "" " une clé OAuth2 (ID client) et un secret (secret client). Entrez cette URL " "comme URL de rappel de votre application." -#: awx/sso/conf.py:757 awx/sso/conf.py:769 awx/sso/conf.py:780 -#: awx/sso/conf.py:793 awx/sso/conf.py:804 awx/sso/conf.py:816 +#: awx/sso/conf.py:791 awx/sso/conf.py:803 awx/sso/conf.py:814 +#: awx/sso/conf.py:827 awx/sso/conf.py:838 awx/sso/conf.py:850 msgid "GitHub Team OAuth2" msgstr "OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:767 +#: awx/sso/conf.py:801 msgid "GitHub Team OAuth2 Key" msgstr "Clé OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:778 +#: awx/sso/conf.py:812 msgid "GitHub Team OAuth2 Secret" msgstr "Secret OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:790 +#: awx/sso/conf.py:824 msgid "GitHub Team ID" msgstr "ID d'équipe GitHub" -#: awx/sso/conf.py:791 +#: awx/sso/conf.py:825 msgid "" "Find the numeric team ID using the Github API: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." @@ -4464,19 +5413,19 @@ msgstr "" "Recherchez votre ID d'équipe numérique à l'aide de l'API Github : http" "://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." -#: awx/sso/conf.py:802 +#: awx/sso/conf.py:836 msgid "GitHub Team OAuth2 Organization Map" msgstr "Mappe d'organisations OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:814 +#: awx/sso/conf.py:848 msgid "GitHub Team OAuth2 Team Map" msgstr "Mappe d'équipes OAuth2 pour les équipes GitHub" -#: awx/sso/conf.py:830 +#: awx/sso/conf.py:864 msgid "Azure AD OAuth2 Callback URL" msgstr "URL de rappel OAuth2 pour Azure AD" -#: awx/sso/conf.py:831 +#: awx/sso/conf.py:865 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4486,40 +5435,40 @@ msgstr "" "votre processus d'enregistrement. Voir la documentation Ansible Tower pour " "plus de détails." -#: awx/sso/conf.py:834 awx/sso/conf.py:846 awx/sso/conf.py:857 -#: awx/sso/conf.py:869 awx/sso/conf.py:881 +#: awx/sso/conf.py:868 awx/sso/conf.py:880 awx/sso/conf.py:891 +#: awx/sso/conf.py:903 awx/sso/conf.py:915 msgid "Azure AD OAuth2" msgstr "OAuth2 pour Azure AD" -#: awx/sso/conf.py:844 +#: awx/sso/conf.py:878 msgid "Azure AD OAuth2 Key" msgstr "Clé OAuth2 pour Azure AD" -#: awx/sso/conf.py:845 +#: awx/sso/conf.py:879 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "Clé OAuth2 (ID client) de votre application Azure AD." -#: awx/sso/conf.py:855 +#: awx/sso/conf.py:889 msgid "Azure AD OAuth2 Secret" msgstr "Secret OAuth2 pour Azure AD" -#: awx/sso/conf.py:856 +#: awx/sso/conf.py:890 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "Secret OAuth2 (secret client) de votre application Azure AD." -#: awx/sso/conf.py:867 +#: awx/sso/conf.py:901 msgid "Azure AD OAuth2 Organization Map" msgstr "Mappe d'organisations OAuth2 pour Azure AD" -#: awx/sso/conf.py:879 +#: awx/sso/conf.py:913 msgid "Azure AD OAuth2 Team Map" msgstr "Mappe d'équipes OAuth2 pour Azure AD" -#: awx/sso/conf.py:904 +#: awx/sso/conf.py:938 msgid "SAML Assertion Consumer Service (ACS) URL" msgstr "URL ACS (Assertion Consumer Service) SAML" -#: awx/sso/conf.py:905 +#: awx/sso/conf.py:939 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this ACS URL for your " @@ -4529,18 +5478,20 @@ msgstr "" "fournisseur d'identité (IdP) configuré. Entrez votre ID d'entité SP et cette" " URL ACS pour votre application." -#: awx/sso/conf.py:908 awx/sso/conf.py:922 awx/sso/conf.py:936 -#: awx/sso/conf.py:951 awx/sso/conf.py:965 awx/sso/conf.py:978 -#: awx/sso/conf.py:999 awx/sso/conf.py:1017 awx/sso/conf.py:1036 -#: awx/sso/conf.py:1070 awx/sso/conf.py:1083 awx/sso/models.py:16 +#: awx/sso/conf.py:942 awx/sso/conf.py:956 awx/sso/conf.py:970 +#: awx/sso/conf.py:985 awx/sso/conf.py:999 awx/sso/conf.py:1012 +#: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 +#: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 +#: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 +#: awx/sso/conf.py:1211 awx/sso/models.py:16 msgid "SAML" msgstr "SAML" -#: awx/sso/conf.py:919 +#: awx/sso/conf.py:953 msgid "SAML Service Provider Metadata URL" msgstr "URL de métadonnées du fournisseur de services SAML" -#: awx/sso/conf.py:920 +#: awx/sso/conf.py:954 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." @@ -4548,11 +5499,11 @@ msgstr "" "Si votre fournisseur d'identité (IdP) permet de télécharger un fichier de " "métadonnées XML, vous pouvez le faire à partir de cette URL." -#: awx/sso/conf.py:932 +#: awx/sso/conf.py:966 msgid "SAML Service Provider Entity ID" msgstr "ID d'entité du fournisseur de services SAML" -#: awx/sso/conf.py:933 +#: awx/sso/conf.py:967 msgid "" "The application-defined unique identifier used as the audience of the SAML " "service provider (SP) configuration. This is usually the URL for Tower." @@ -4561,11 +5512,11 @@ msgstr "" "configuration du fournisseur de services (SP) SAML. Il s'agit généralement " "de l'URL de Tower." -#: awx/sso/conf.py:948 +#: awx/sso/conf.py:982 msgid "SAML Service Provider Public Certificate" msgstr "Certificat public du fournisseur de services SAML" -#: awx/sso/conf.py:949 +#: awx/sso/conf.py:983 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " certificate content here." @@ -4573,11 +5524,11 @@ msgstr "" "Créez une paire de clés pour que Tower puisse être utilisé comme fournisseur" " de services (SP) et entrez le contenu du certificat ici." -#: awx/sso/conf.py:962 +#: awx/sso/conf.py:996 msgid "SAML Service Provider Private Key" msgstr "Clé privée du fournisseur de services SAML" -#: awx/sso/conf.py:963 +#: awx/sso/conf.py:997 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " private key content here." @@ -4585,11 +5536,11 @@ msgstr "" "Créez une paire de clés pour que Tower puisse être utilisé comme fournisseur" " de services (SP) et entrez le contenu de la clé privée ici." -#: awx/sso/conf.py:975 +#: awx/sso/conf.py:1009 msgid "SAML Service Provider Organization Info" msgstr "Infos organisationnelles du fournisseur de services SAML" -#: awx/sso/conf.py:976 +#: awx/sso/conf.py:1010 msgid "" "Provide the URL, display name, and the name of your app. Refer to the " "Ansible Tower documentation for example syntax." @@ -4597,11 +5548,11 @@ msgstr "" "Fournir cette URL, le nom d'affichage, le nom de votre app. Voir la " "documentation Ansible Tower pour obtenir des exemples de syntaxe." -#: awx/sso/conf.py:995 +#: awx/sso/conf.py:1029 msgid "SAML Service Provider Technical Contact" msgstr "Contact technique du fournisseur de services SAML" -#: awx/sso/conf.py:996 +#: awx/sso/conf.py:1030 msgid "" "Provide the name and email address of the technical contact for your service" " provider. Refer to the Ansible Tower documentation for example syntax." @@ -4610,11 +5561,11 @@ msgstr "" " de services. Voir la documentation Ansible Tower pour obtenir des exemples " "de syntaxe." -#: awx/sso/conf.py:1013 +#: awx/sso/conf.py:1047 msgid "SAML Service Provider Support Contact" msgstr "Contact support du fournisseur de services SAML" -#: awx/sso/conf.py:1014 +#: awx/sso/conf.py:1048 msgid "" "Provide the name and email address of the support contact for your service " "provider. Refer to the Ansible Tower documentation for example syntax." @@ -4623,11 +5574,11 @@ msgstr "" "de services. Voir la documentation Ansible Tower pour obtenir des exemples " "de syntaxe." -#: awx/sso/conf.py:1030 +#: awx/sso/conf.py:1064 msgid "SAML Enabled Identity Providers" msgstr "Fournisseurs d'identité compatibles SAML" -#: awx/sso/conf.py:1031 +#: awx/sso/conf.py:1065 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " @@ -4642,46 +5593,101 @@ msgstr "" "pour chaque IdP. Voir la documentation Ansible Tower pour obtenir des " "exemples de syntaxe." -#: awx/sso/conf.py:1068 +#: awx/sso/conf.py:1102 +msgid "SAML Security Config" +msgstr "Config de sécurité SAML" + +#: awx/sso/conf.py:1103 +msgid "" +"A dict of key value pairs that are passed to the underlying python-saml " +"security setting https://github.com/onelogin/python-saml#settings" +msgstr "" +"Un dictionnaire de paires de valeurs clés qui sont passées au paramètre de " +"sécurité saus-jacent python-saml https://github.com/onelogin/python-" +"saml#settings" + +#: awx/sso/conf.py:1135 +msgid "SAML Service Provider extra configuration data" +msgstr "" +"Données de configuration supplémentaires du fournisseur du service SAML" + +#: awx/sso/conf.py:1136 +msgid "" +"A dict of key value pairs to be passed to the underlying python-saml Service" +" Provider configuration setting." +msgstr "" +"Un dictionnaire de paires de valeurs clés qui sont passées au paramètre de " +"configuration sous-jacent du Fournisseur de service python-saml." + +#: awx/sso/conf.py:1149 +msgid "SAML IDP to extra_data attribute mapping" +msgstr "IDP SAM pour la mappage d'attributs extra_data" + +#: awx/sso/conf.py:1150 +msgid "" +"A list of tuples that maps IDP attributes to extra_attributes. Each " +"attribute will be a list of values, even if only 1 value." +msgstr "" +"Liste des tuples qui mappent les attributs IDP en extra_attributes. Chaque " +"attribut correspondra à une liste de valeurs, même si 1 seule valeur." + +#: awx/sso/conf.py:1167 msgid "SAML Organization Map" msgstr "Mappe d'organisations SAML" -#: awx/sso/conf.py:1081 +#: awx/sso/conf.py:1180 msgid "SAML Team Map" msgstr "Mappe d'équipes SAML" -#: awx/sso/fields.py:123 +#: awx/sso/conf.py:1193 +msgid "SAML Organization Attribute Mapping" +msgstr "Mappage d'attributs d'organisation SAML" + +#: awx/sso/conf.py:1194 +msgid "Used to translate user organization membership into Tower." +msgstr "" +"Utilisé pour traduire l'adhésion organisation de l'utilisateur dans Tower." + +#: awx/sso/conf.py:1209 +msgid "SAML Team Attribute Mapping" +msgstr "Mappage d'attributs d'équipe SAML" + +#: awx/sso/conf.py:1210 +msgid "Used to translate user team membership into Tower." +msgstr "Utilisé pour traduire l'adhésion équipe de l'utilisateur dans Tower." + +#: awx/sso/fields.py:183 #, python-brace-format msgid "Invalid connection option(s): {invalid_options}." msgstr "Option(s) de connexion non valide(s) : {invalid_options}." -#: awx/sso/fields.py:194 +#: awx/sso/fields.py:266 msgid "Base" msgstr "Base" -#: awx/sso/fields.py:195 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "Un niveau" -#: awx/sso/fields.py:196 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "Sous-arborescence" -#: awx/sso/fields.py:214 +#: awx/sso/fields.py:286 #, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "" "Une liste de trois éléments était attendue, mais {length} a été obtenu à la " "place." -#: awx/sso/fields.py:215 +#: awx/sso/fields.py:287 #, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" "Une instance de LDAPSearch était attendue, mais {input_type} a été obtenu à " "la place." -#: awx/sso/fields.py:251 +#: awx/sso/fields.py:323 #, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " @@ -4690,92 +5696,83 @@ msgstr "" "Une instance de LDAPSearch ou de LDAPSearchUnion était attendue, mais " "{input_type} a été obtenu à la place." -#: awx/sso/fields.py:289 +#: awx/sso/fields.py:361 #, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "Attribut(s) d'utilisateur non valide(s) : {invalid_attrs}." -#: awx/sso/fields.py:306 +#: awx/sso/fields.py:378 #, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" "Une instance de LDAPGroupType était attendue, mais {input_type} a été obtenu" " à la place." -#: awx/sso/fields.py:334 -#, python-brace-format -msgid "Invalid user flag: \"{invalid_flag}\"." -msgstr "Marqueur d'utilisateur non valide : \"{invalid_flag}\"." - -#: awx/sso/fields.py:350 awx/sso/fields.py:517 -#, python-brace-format -msgid "" -"Expected None, True, False, a string or list of strings but got {input_type}" -" instead." -msgstr "" -"Les valeurs None, True, False, une chaîne ou une liste de chaînes étaient " -"attendues, mais {input_type} a été obtenu à la place." - -#: awx/sso/fields.py:386 -#, python-brace-format -msgid "Missing key(s): {missing_keys}." -msgstr "Clé(s) manquante(s) : {missing_keys}." - -#: awx/sso/fields.py:387 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 #, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "Clé(s) non valide(s) : {invalid_keys}." -#: awx/sso/fields.py:436 awx/sso/fields.py:553 +#: awx/sso/fields.py:443 +#, python-brace-format +msgid "Invalid user flag: \"{invalid_flag}\"." +msgstr "Marqueur d'utilisateur non valide : \"{invalid_flag}\"." + +#: awx/sso/fields.py:464 +#, python-brace-format +msgid "Missing key(s): {missing_keys}." +msgstr "Clé(s) manquante(s) : {missing_keys}." + +#: awx/sso/fields.py:514 awx/sso/fields.py:631 #, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "Clé(s) non valide(s) pour la mappe d'organisations : {invalid_keys}." -#: awx/sso/fields.py:454 +#: awx/sso/fields.py:532 #, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "Clé obligatoire manquante pour la mappe d'équipes : {invalid_keys}." -#: awx/sso/fields.py:455 awx/sso/fields.py:572 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 #, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "Clé(s) non valide(s) pour la mappe d'équipes : {invalid_keys}." -#: awx/sso/fields.py:571 +#: awx/sso/fields.py:649 #, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "Clé obligatoire manquante pour la mappe d'équipes : {missing_keys}." -#: awx/sso/fields.py:589 +#: awx/sso/fields.py:667 #, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "" "Clé(s) obligatoire(s) manquante(s) pour l'enregistrement des infos organis. " ": {missing_keys}." -#: awx/sso/fields.py:602 +#: awx/sso/fields.py:680 #, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "" "Code(s) langage non valide(s) pour les infos organis. : " "{invalid_lang_codes}." -#: awx/sso/fields.py:621 +#: awx/sso/fields.py:699 #, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "Clé(s) obligatoire(s) manquante(s) pour le contact : {missing_keys}." -#: awx/sso/fields.py:633 +#: awx/sso/fields.py:711 #, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "Clé(s) obligatoire(s) manquante(s) pour l'IdP : {missing_keys}." -#: awx/sso/pipeline.py:24 +#: awx/sso/pipeline.py:31 #, python-brace-format msgid "An account cannot be found for {0}" msgstr "Impossible de trouver un compte pour {0}" -#: awx/sso/pipeline.py:30 +#: awx/sso/pipeline.py:37 msgid "Your account is inactive" msgstr "Votre compte est inactif" @@ -4804,70 +5801,48 @@ msgstr "Le secret TACACS+ ne permet pas l'utilisation de caractères non-ascii" msgid "AWX" msgstr "AWX" -#: awx/templates/rest_framework/api.html:39 +#: awx/templates/rest_framework/api.html:42 msgid "Ansible Tower API Guide" msgstr "Guide pour les API d'Ansible Tower" -#: awx/templates/rest_framework/api.html:40 +#: awx/templates/rest_framework/api.html:43 msgid "Back to Ansible Tower" msgstr "Retour à Ansible Tower" -#: awx/templates/rest_framework/api.html:41 +#: awx/templates/rest_framework/api.html:44 msgid "Resize" msgstr "Redimensionner" +#: awx/templates/rest_framework/base.html:37 +msgid "navbar" +msgstr "barnav" + +#: awx/templates/rest_framework/base.html:75 +msgid "content" +msgstr "contenu" + #: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 -#, python-format -msgid "Make a GET request on the %(name)s resource" -msgstr "Appliquez une requête GET sur la ressource %(name)s" +msgid "request form" +msgstr "formulaire de demande" -#: awx/templates/rest_framework/base.html:80 -msgid "Specify a format for the GET request" -msgstr "Spécifiez un format pour la requête GET" - -#: awx/templates/rest_framework/base.html:86 -#, python-format -msgid "" -"Make a GET request on the %(name)s resource with the format set to " -"`%(format)s`" -msgstr "" -"Appliquez une requête GET sur la ressource %(name)s avec un format défini " -"sur`%(format)s`" - -#: awx/templates/rest_framework/base.html:100 -#, python-format -msgid "Make an OPTIONS request on the %(name)s resource" -msgstr "Appliquez une requête OPTIONS sur la ressource %(name)s" - -#: awx/templates/rest_framework/base.html:106 -#, python-format -msgid "Make a DELETE request on the %(name)s resource" -msgstr "Appliquez une requête DELETE sur la ressource %(name)s" - -#: awx/templates/rest_framework/base.html:113 +#: awx/templates/rest_framework/base.html:134 msgid "Filters" msgstr "Filtres" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 -#, python-format -msgid "Make a POST request on the %(name)s resource" -msgstr "Appliquez une requête POST sur la ressource %(name)s" +#: awx/templates/rest_framework/base.html:139 +msgid "main content" +msgstr "informations principales" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 -#, python-format -msgid "Make a PUT request on the %(name)s resource" -msgstr "Appliquez une requête PUT sur la ressource %(name)s" +#: awx/templates/rest_framework/base.html:155 +msgid "request info" +msgstr "informations demandées" -#: awx/templates/rest_framework/base.html:233 -#, python-format -msgid "Make a PATCH request on the %(name)s resource" -msgstr "Appliquez une requête PATCH sur la ressource %(name)s" +#: awx/templates/rest_framework/base.html:159 +msgid "response info" +msgstr "Informations répondues" #: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:36 awx/ui/conf.py:51 -#: awx/ui/conf.py:63 +#: awx/ui/conf.py:63 awx/ui/conf.py:73 msgid "UI" msgstr "IU" @@ -4923,16 +5898,29 @@ msgstr "" "Les formats GIF, PNG et JPEG sont supportés." #: awx/ui/conf.py:60 -msgid "Max Job Events Retreived by UI" -msgstr "Max Événements de tâches par UI" +msgid "Max Job Events Retrieved by UI" +msgstr "Max Événements Job récupérés par l'IU" #: awx/ui/conf.py:61 msgid "" -"Maximum number of job events for the UI to retreive within a single request." +"Maximum number of job events for the UI to retrieve within a single request." msgstr "" -"Nombre maximum d'événements de tâches d'UI à extraire pour une requête " +"Nombre maximum d'événements liés à un Job que l'IU a extrait en une requête " "unique." +#: awx/ui/conf.py:70 +msgid "Enable Live Updates in the UI" +msgstr "Activer les mises à jour live dans l'IU" + +#: awx/ui/conf.py:71 +msgid "" +"If disabled, the page will not refresh when events are received. Reloading " +"the page will be required to get the latest details." +msgstr "" +"Si elle est désactivée, la page ne se rafraîchira pas lorsque des événements" +" sont reçus. Le rechargement de la page sera nécessaire pour obtenir les " +"derniers détails." + #: awx/ui/fields.py:29 msgid "" "Invalid format for custom logo. Must be a data URL with a base64-encoded " @@ -4944,98 +5932,3 @@ msgstr "" #: awx/ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "Données codées en base64 non valides dans l'URL de données" - -#: awx/ui/templates/ui/index.html:31 -msgid "" -"Your session will expire in 60 seconds, would you like to " -"continue?" -msgstr "" -"Votre session expirera dans 60 secondes, voulez-vous continuer ?" - -#: awx/ui/templates/ui/index.html:46 -msgid "CANCEL" -msgstr "ANNULER" - -#: awx/ui/templates/ui/index.html:98 -msgid "Set how many days of data should be retained." -msgstr "" -"Définissez le nombre de jours pendant lesquels les données doivent être " -"conservées." - -#: awx/ui/templates/ui/index.html:104 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Entrez un entier non négatif et inférieur à 9999." - -#: awx/ui/templates/ui/index.html:109 -msgid "" -"For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.\n" -"
\n" -"
CAUTION: Setting both numerical variables to \"0\" will delete all facts.\n" -"
\n" -"
" -msgstr "" -"Pour les faits collectés en amont de la période spécifiée, enregistrez un scan des faits (instantané) par fenêtre temporelle (fréquence). Par exemple, les faits antérieurs à 30 jours sont purgés, tandis qu'un scan de faits hebdomadaire est conservé.\n" -"
\n" -"
ATTENTION : le paramétrage des deux variables numériques sur \"0\" supprime l'ensemble des faits.\n" -"
\n" -"
" - -#: awx/ui/templates/ui/index.html:118 -msgid "Select a time period after which to remove old facts" -msgstr "" -"Sélectionnez un intervalle de temps après lequel les faits anciens pourront " -"être supprimés" - -#: awx/ui/templates/ui/index.html:132 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Entrez un entier non négatif et inférieur à " -"9999
." - -#: awx/ui/templates/ui/index.html:137 -msgid "Select a frequency for snapshot retention" -msgstr "Sélectionnez une fréquence pour la conservation des instantanés" - -#: awx/ui/templates/ui/index.html:151 -msgid "" -"Please enter an integer that is not" -" negative that is " -"lower than 9999." -msgstr "" -"Entrez un entier non " -"négatif et " -"inférieur à 9999." - -#: awx/ui/templates/ui/index.html:157 -msgid "working..." -msgstr "en cours..." diff --git a/awx/locale/ja/LC_MESSAGES/django.po b/awx/locale/ja/LC_MESSAGES/django.po index a86ce49b9f..cc271f4877 100644 --- a/awx/locale/ja/LC_MESSAGES/django.po +++ b/awx/locale/ja/LC_MESSAGES/django.po @@ -1,11 +1,13 @@ # asasaki , 2017. #zanata # mkim , 2017. #zanata +# myamamot , 2017. #zanata +# asasaki , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-30 20:23+0000\n" -"PO-Revision-Date: 2017-12-03 11:01+0000\n" +"POT-Creation-Date: 2018-06-14 18:30+0000\n" +"PO-Revision-Date: 2018-06-18 01:09+0000\n" "Last-Translator: asasaki \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -13,93 +15,110 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language: ja\n" "Plural-Forms: nplurals=1; plural=0\n" -"X-Generator: Zanata 4.3.2\n" +"X-Generator: Zanata 4.6.0\n" -#: awx/api/authentication.py:67 -msgid "Invalid token header. No credentials provided." -msgstr "無効なトークンヘッダーです。認証情報が提供されていません。" - -#: awx/api/authentication.py:70 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "無効なトークンヘッダーです。トークン文字列にはスペースを含めることができません。" - -#: awx/api/authentication.py:105 -msgid "User inactive or deleted" -msgstr "ユーザーが非アクティブか、または削除されています" - -#: awx/api/authentication.py:161 -msgid "Invalid task token" -msgstr "無効なタスクトークン" - -#: awx/api/conf.py:12 +#: awx/api/conf.py:15 msgid "Idle Time Force Log Out" -msgstr "アイドル時間、強制ログアウト" +msgstr "強制ログアウトまでのアイドル時間" -#: awx/api/conf.py:13 +#: awx/api/conf.py:16 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." msgstr "ユーザーが再ログインするまでに非アクティブな状態になる秒数です。" -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:85 -#: awx/sso/conf.py:96 awx/sso/conf.py:108 awx/sso/conf.py:123 +#: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 +#: awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/sso/conf.py:123 msgid "Authentication" msgstr "認証" -#: awx/api/conf.py:22 -msgid "Maximum number of simultaneous logins" -msgstr "同時ログインの最大数" +#: awx/api/conf.py:24 +msgid "Maximum number of simultaneous logged in sessions" +msgstr "同時ログインセッションの最大数" -#: awx/api/conf.py:23 +#: awx/api/conf.py:25 msgid "" -"Maximum number of simultaneous logins a user may have. To disable enter -1." -msgstr "ユーザーが実行できる同時ログインの最大数です。無効にするには -1 を入力します。" +"Maximum number of simultaneous logged in sessions a user may have. To " +"disable enter -1." +msgstr "ユーザーが実行できる同時ログインセッションの最大数です。無効にするには -1 を入力します。" -#: awx/api/conf.py:31 +#: awx/api/conf.py:32 msgid "Enable HTTP Basic Auth" msgstr "HTTP Basic 認証の有効化" -#: awx/api/conf.py:32 +#: awx/api/conf.py:33 msgid "Enable HTTP Basic Auth for the API Browser." msgstr "API ブラウザーの HTTP Basic 認証を有効にします。" -#: awx/api/filters.py:129 +#: awx/api/conf.py:42 +msgid "OAuth 2 Timeout Settings" +msgstr "OAuth 2 タイムアウト設定" + +#: awx/api/conf.py:43 +msgid "" +"Dictionary for customizing OAuth 2 timeouts, available items are " +"`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number " +"of seconds, and `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of " +"authorization grants in the number of seconds." +msgstr "" +"OAuth 2 " +"タイムアウトをカスタマイズするための辞書です。利用可能な項目は以下の通りです。`ACCESS_TOKEN_EXPIRE_SECONDS`: " +"アクセストークンの期間 (秒数)。 `AUTHORIZATION_CODE_EXPIRE_SECONDS`: 認証の付与期間 (秒数)。" + +#: awx/api/exceptions.py:20 +msgid "Resource is being used by running jobs." +msgstr "リソースが実行中のジョブで使用されています。" + +#: awx/api/fields.py:81 +#, python-brace-format +msgid "Invalid key names: {invalid_key_names}" +msgstr "無効なキー名: {invalid_key_names}" + +#: awx/api/fields.py:107 +msgid "Credential {} does not exist" +msgstr "認証情報 {} は存在しません" + +#: awx/api/filters.py:96 +msgid "No related model for field {}." +msgstr "フィールド {} の関連するモデルはありません。" + +#: awx/api/filters.py:113 msgid "Filtering on password fields is not allowed." msgstr "パスワードフィールドでのフィルターは許可されていません。" -#: awx/api/filters.py:141 awx/api/filters.py:143 +#: awx/api/filters.py:125 awx/api/filters.py:127 #, python-format msgid "Filtering on %s is not allowed." msgstr "%s でのフィルターは許可されていません。" -#: awx/api/filters.py:146 +#: awx/api/filters.py:130 msgid "Loops not allowed in filters, detected on field {}." msgstr "ループがフィルターで許可されていません。フィールド {} で検出されました。" -#: awx/api/filters.py:171 +#: awx/api/filters.py:159 +msgid "Query string field name not provided." +msgstr "クエリー文字列フィールド名は指定されていません。" + +#: awx/api/filters.py:186 #, python-brace-format msgid "Invalid {field_name} id: {field_id}" msgstr "無効な {field_name} ID: {field_id}" -#: awx/api/filters.py:302 +#: awx/api/filters.py:319 #, python-format msgid "cannot filter on kind %s" msgstr "%s の種類でフィルターできません。" -#: awx/api/filters.py:409 -#, python-format -msgid "cannot order by field %s" -msgstr "フィールド %s で順序付けできません" - -#: awx/api/generics.py:550 awx/api/generics.py:612 +#: awx/api/generics.py:620 awx/api/generics.py:682 msgid "\"id\" field must be an integer." msgstr "「id」フィールドは整数でなければなりません。" -#: awx/api/generics.py:609 +#: awx/api/generics.py:679 msgid "\"id\" is required to disassociate" msgstr "関連付けを解除するには 「id」が必要です" -#: awx/api/generics.py:660 +#: awx/api/generics.py:730 msgid "{} 'id' field is missing." msgstr "{} 「id」フィールドがありません。" @@ -139,11 +158,11 @@ msgstr "この {} の作成時のタイムスタンプ。" msgid "Timestamp when this {} was last modified." msgstr "この {} の最終変更時のタイムスタンプ。" -#: awx/api/parsers.py:64 +#: awx/api/parsers.py:33 msgid "JSON parse error - not a JSON object" msgstr "JSON パースエラー: JSON オブジェクトでありません" -#: awx/api/parsers.py:67 +#: awx/api/parsers.py:36 #, python-format msgid "" "JSON parse error - %s\n" @@ -152,402 +171,535 @@ msgstr "" "JSON パースエラー: %s\n" "考えられる原因: 末尾のコンマ。" -#: awx/api/serializers.py:268 +#: awx/api/serializers.py:153 +msgid "" +"The original object is already named {}, a copy from it cannot have the same" +" name." +msgstr "元のオブジェクトにはすでに {} という名前があり、このコピーに同じ名前を使用することはできません。" + +#: awx/api/serializers.py:295 msgid "Playbook Run" msgstr "Playbook 実行" -#: awx/api/serializers.py:269 +#: awx/api/serializers.py:296 msgid "Command" msgstr "コマンド" -#: awx/api/serializers.py:270 awx/main/models/unified_jobs.py:435 +#: awx/api/serializers.py:297 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "SCM 更新" -#: awx/api/serializers.py:271 +#: awx/api/serializers.py:298 msgid "Inventory Sync" msgstr "インベントリーの同期" -#: awx/api/serializers.py:272 +#: awx/api/serializers.py:299 msgid "Management Job" msgstr "管理ジョブ" -#: awx/api/serializers.py:273 +#: awx/api/serializers.py:300 msgid "Workflow Job" msgstr "ワークフロージョブ" -#: awx/api/serializers.py:274 +#: awx/api/serializers.py:301 msgid "Workflow Template" msgstr "ワークフローテンプレート" -#: awx/api/serializers.py:701 awx/api/serializers.py:759 awx/api/views.py:4365 -#, python-format -msgid "" -"Standard Output too large to display (%(text_size)d bytes), only download " -"supported for sizes over %(supported_size)d bytes" -msgstr "" -"標準出力が大きすぎて表示できません (%(text_size)d バイト)。サイズが %(supported_size)d " -"バイトを超える場合はダウンロードのみがサポートされます。" +#: awx/api/serializers.py:302 +msgid "Job Template" +msgstr "ジョブテンプレート" -#: awx/api/serializers.py:774 +#: awx/api/serializers.py:697 +msgid "" +"Indicates whether all of the events generated by this unified job have been " +"saved to the database." +msgstr "この統一されたジョブで生成されるイベントすべてがデータベースに保存されているかどうかを示します。" + +#: awx/api/serializers.py:854 msgid "Write-only field used to change the password." msgstr "パスワードを変更するために使用される書き込み専用フィールド。" -#: awx/api/serializers.py:776 +#: awx/api/serializers.py:856 msgid "Set if the account is managed by an external service" msgstr "アカウントが外部サービスで管理される場合に設定されます" -#: awx/api/serializers.py:800 +#: awx/api/serializers.py:880 msgid "Password required for new User." msgstr "新規ユーザーのパスワードを入力してください。" -#: awx/api/serializers.py:886 +#: awx/api/serializers.py:971 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "LDAP で管理されたユーザーの %s を変更できません。" -#: awx/api/serializers.py:1050 +#: awx/api/serializers.py:1057 +msgid "Must be a simple space-separated string with allowed scopes {}." +msgstr "許可されたスコープ {} のある単純なスペースで区切られた文字列でなければなりません。" + +#: awx/api/serializers.py:1151 +msgid "Authorization Grant Type" +msgstr "認証付与タイプ" + +#: awx/api/serializers.py:1153 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "クライアントシークレット" + +#: awx/api/serializers.py:1156 +msgid "Client Type" +msgstr "クライアントタイプ" + +#: awx/api/serializers.py:1159 +msgid "Redirect URIs" +msgstr "リダイレクト URI" + +#: awx/api/serializers.py:1162 +msgid "Skip Authorization" +msgstr "認証のスキップ" + +#: awx/api/serializers.py:1264 +msgid "This path is already being used by another manual project." +msgstr "このパスは別の手動プロジェクトですでに使用されています。" + +#: awx/api/serializers.py:1290 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "このフィールドは非推奨となり、今後のリリースで削除されます。" + +#: awx/api/serializers.py:1349 msgid "Organization is missing" msgstr "組織がありません" -#: awx/api/serializers.py:1054 +#: awx/api/serializers.py:1353 msgid "Update options must be set to false for manual projects." msgstr "手動プロジェクトについては更新オプションを false に設定する必要があります。" -#: awx/api/serializers.py:1060 +#: awx/api/serializers.py:1359 msgid "Array of playbooks available within this project." msgstr "このプロジェクト内で利用可能な一連の Playbook。" -#: awx/api/serializers.py:1079 +#: awx/api/serializers.py:1378 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." msgstr "このプロジェクト内で利用可能な一連のインベントリーファイルおよびディレクトリー (包括的な一覧ではありません)。" -#: awx/api/serializers.py:1201 +#: awx/api/serializers.py:1426 awx/api/serializers.py:3194 +msgid "A count of hosts uniquely assigned to each status." +msgstr "各ステータスに一意に割り当てられたホスト数です。" + +#: awx/api/serializers.py:1429 awx/api/serializers.py:3197 +msgid "A count of all plays and tasks for the job run." +msgstr "ジョブ実行用のすべてのプレイおよびタスクの数です。" + +#: awx/api/serializers.py:1554 msgid "Smart inventories must specify host_filter" msgstr "スマートインベントリーは host_filter を指定する必要があります" -#: awx/api/serializers.py:1303 +#: awx/api/serializers.py:1658 #, python-format msgid "Invalid port specification: %s" msgstr "無効なポート指定: %s" -#: awx/api/serializers.py:1314 +#: awx/api/serializers.py:1669 msgid "Cannot create Host for Smart Inventory" msgstr "スマートインベントリーのホストを作成できません" -#: awx/api/serializers.py:1336 awx/api/serializers.py:3321 -#: awx/api/serializers.py:3406 awx/main/validators.py:198 -msgid "Must be valid JSON or YAML." -msgstr "有効な JSON または YAML である必要があります。" - -#: awx/api/serializers.py:1432 +#: awx/api/serializers.py:1781 msgid "Invalid group name." msgstr "無効なグループ名。" -#: awx/api/serializers.py:1437 +#: awx/api/serializers.py:1786 msgid "Cannot create Group for Smart Inventory" msgstr "スマートインベントリーのグループを作成できません" -#: awx/api/serializers.py:1509 +#: awx/api/serializers.py:1861 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "スクリプトは hashbang シーケンスで開始する必要があります (例: .... #!/usr/bin/env python)" -#: awx/api/serializers.py:1555 +#: awx/api/serializers.py:1910 msgid "`{}` is a prohibited environment variable" msgstr "`{}` は禁止されている環境変数です" -#: awx/api/serializers.py:1566 +#: awx/api/serializers.py:1921 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "「source」が「custom」である場合、「source_script」を指定する必要があります。" -#: awx/api/serializers.py:1572 +#: awx/api/serializers.py:1927 msgid "Must provide an inventory." msgstr "インベントリーを指定する必要があります。" -#: awx/api/serializers.py:1576 +#: awx/api/serializers.py:1931 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "「source_script」はインベントリーと同じ組織に属しません。" -#: awx/api/serializers.py:1578 +#: awx/api/serializers.py:1933 msgid "'source_script' doesn't exist." msgstr "「source_script」は存在しません。" -#: awx/api/serializers.py:1602 +#: awx/api/serializers.py:1967 msgid "Automatic group relationship, will be removed in 3.3" msgstr "自動的なグループ関係は 3.3 で削除されます" -#: awx/api/serializers.py:1679 +#: awx/api/serializers.py:2053 msgid "Cannot use manual project for SCM-based inventory." msgstr "SCM ベースのインベントリーの手動プロジェクトを使用できません。" -#: awx/api/serializers.py:1685 +#: awx/api/serializers.py:2059 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." msgstr "手動のインベントリーソースは、グループが v1 API で作成される際に自動作成されます。" -#: awx/api/serializers.py:1690 +#: awx/api/serializers.py:2064 msgid "Setting not compatible with existing schedules." msgstr "設定は既存スケジュールとの互換性がありません。" -#: awx/api/serializers.py:1695 +#: awx/api/serializers.py:2069 msgid "Cannot create Inventory Source for Smart Inventory" msgstr "スマートインベントリーのインベントリーソースを作成できません" -#: awx/api/serializers.py:1709 +#: awx/api/serializers.py:2120 #, python-format msgid "Cannot set %s if not SCM type." msgstr "SCM タイプでない場合 %s を設定できません。" -#: awx/api/serializers.py:1950 +#: awx/api/serializers.py:2388 msgid "Modifications not allowed for managed credential types" msgstr "管理されている認証情報タイプで変更は許可されません" -#: awx/api/serializers.py:1955 +#: awx/api/serializers.py:2393 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "入力への変更は使用中の認証情報タイプで許可されません" -#: awx/api/serializers.py:1961 +#: awx/api/serializers.py:2399 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "「cloud」または「net」にする必要があります (%s ではない)" -#: awx/api/serializers.py:1967 +#: awx/api/serializers.py:2405 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "「ask_at_runtime」はカスタム認証情報ではサポートされません。" -#: awx/api/serializers.py:2140 +#: awx/api/serializers.py:2585 #, python-format msgid "\"%s\" is not a valid choice" msgstr "「%s」は有効な選択ではありません" -#: awx/api/serializers.py:2159 -#, python-format -msgid "'%s' is not a valid field for %s" -msgstr "「%s」は %s の有効なフィールドではありません" +#: awx/api/serializers.py:2604 +#, python-brace-format +msgid "'{field_name}' is not a valid field for {credential_type_name}" +msgstr "'{field_name}' は {credential_type_name} の有効なフィールドではありません" -#: awx/api/serializers.py:2180 +#: awx/api/serializers.py:2625 msgid "" "You cannot change the credential type of the credential, as it may break the" " functionality of the resources using it." msgstr "認証情報の認証情報タイプを変更することはできません。これにより、認証情報を使用するリソースの機能が中断する可能性があるためです。" -#: awx/api/serializers.py:2191 +#: awx/api/serializers.py:2637 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." msgstr "" "ユーザーを所有者ロールに追加するために使用される書き込み専用フィールドです。提供されている場合は、チームまたは組織のいずれも指定しないでください。作成時にのみ有効です。" -#: awx/api/serializers.py:2196 +#: awx/api/serializers.py:2642 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." msgstr "" "チームを所有者ロールに追加するために使用される書き込み専用フィールドです。提供されている場合は、ユーザーまたは組織のいずれも指定しないでください。作成時にのみ有効です。" -#: awx/api/serializers.py:2201 +#: awx/api/serializers.py:2647 msgid "" "Inherit permissions from organization roles. If provided on creation, do not" " give either user or team." msgstr "組織ロールからパーミッションを継承します。作成時に提供される場合は、ユーザーまたはチームのいずれも指定しないでください。" -#: awx/api/serializers.py:2217 +#: awx/api/serializers.py:2663 msgid "Missing 'user', 'team', or 'organization'." msgstr "「user」、「team」、または「organization」がありません。" -#: awx/api/serializers.py:2257 +#: awx/api/serializers.py:2703 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "認証情報の組織が設定され、一致している状態でチームに割り当てる必要があります。" -#: awx/api/serializers.py:2424 +#: awx/api/serializers.py:2904 msgid "You must provide a cloud credential." msgstr "クラウド認証情報を指定する必要があります。" -#: awx/api/serializers.py:2425 +#: awx/api/serializers.py:2905 msgid "You must provide a network credential." msgstr "ネットワーク認証情報を指定する必要があります。" -#: awx/api/serializers.py:2441 +#: awx/api/serializers.py:2906 awx/main/models/jobs.py:155 +msgid "You must provide an SSH credential." +msgstr "SSH 認証情報を指定する必要があります。" + +#: awx/api/serializers.py:2907 +msgid "You must provide a vault credential." +msgstr "Vault 認証情報を指定する必要があります。" + +#: awx/api/serializers.py:2926 msgid "This field is required." msgstr "このフィールドは必須です。" -#: awx/api/serializers.py:2443 awx/api/serializers.py:2445 +#: awx/api/serializers.py:2928 awx/api/serializers.py:2930 msgid "Playbook not found for project." msgstr "プロジェクトの Playbook が見つかりません。" -#: awx/api/serializers.py:2447 +#: awx/api/serializers.py:2932 msgid "Must select playbook for project." msgstr "プロジェクトの Playbook を選択してください。" -#: awx/api/serializers.py:2522 +#: awx/api/serializers.py:3013 +msgid "Cannot enable provisioning callback without an inventory set." +msgstr "インベントリー設定なしにプロビジョニングコールバックを有効にすることはできません。" + +#: awx/api/serializers.py:3016 msgid "Must either set a default value or ask to prompt on launch." msgstr "起動時にプロントを出すには、デフォルト値を設定するか、またはプロンプトを出すよう指定する必要があります。" -#: awx/api/serializers.py:2524 awx/main/models/jobs.py:326 +#: awx/api/serializers.py:3018 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "ジョブタイプ「run」および「check」によりプロジェクトが割り当てられている必要があります。" -#: awx/api/serializers.py:2611 +#: awx/api/serializers.py:3134 msgid "Invalid job template." msgstr "無効なジョブテンプレート。" -#: awx/api/serializers.py:2708 -msgid "Neither credential nor vault credential provided." -msgstr "認証情報および Vault 認証情報のいずれも指定されていません。" +#: awx/api/serializers.py:3249 +msgid "No change to job limit" +msgstr "ジョブ制限に変更はありません" -#: awx/api/serializers.py:2711 +#: awx/api/serializers.py:3250 +msgid "All failed and unreachable hosts" +msgstr "失敗している、到達できないすべてのホスト" + +#: awx/api/serializers.py:3265 +msgid "Missing passwords needed to start: {}" +msgstr "起動に必要なパスワードが見つかりません: {}" + +#: awx/api/serializers.py:3284 +msgid "Relaunch by host status not available until job finishes running." +msgstr "ホストのステータス別の再起動はジョブが実行を終了するまで利用できません。" + +#: awx/api/serializers.py:3298 msgid "Job Template Project is missing or undefined." msgstr "ジョブテンプレートプロジェクトが見つからないか、または定義されていません。" -#: awx/api/serializers.py:2713 +#: awx/api/serializers.py:3300 msgid "Job Template Inventory is missing or undefined." msgstr "ジョブテンプレートインベントリーが見つからないか、または定義されていません。" -#: awx/api/serializers.py:2782 awx/main/tasks.py:2186 +#: awx/api/serializers.py:3338 +msgid "" +"Unknown, job may have been ran before launch configurations were saved." +msgstr "不明です。ジョブは起動設定が保存される前に実行された可能性があります。" + +#: awx/api/serializers.py:3405 awx/main/tasks.py:2268 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} の使用はアドホックコマンドで禁止されています。" -#: awx/api/serializers.py:3008 -#, python-format -msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." -msgstr "%(job_type)s は有効なジョブタイプではありません。%(choices)s を選択できます。" +#: awx/api/serializers.py:3474 awx/api/views.py:4843 +#, python-brace-format +msgid "" +"Standard Output too large to display ({text_size} bytes), only download " +"supported for sizes over {supported_size} bytes." +msgstr "" +"標準出力が大きすぎて表示できません ({text_size} バイト)。サイズが {supported_size} " +"バイトを超える場合はダウンロードのみがサポートされます。" -#: awx/api/serializers.py:3013 -msgid "Workflow job template is missing during creation." -msgstr "ワークフロージョブテンプレートが作成時に見つかりません。" +#: awx/api/serializers.py:3671 +msgid "Provided variable {} has no database value to replace with." +msgstr "指定された変数 {} には置き換えるデータベースの値がありません。" -#: awx/api/serializers.py:3018 +#: awx/api/serializers.py:3747 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "ワークフロージョブテンプレート内に %s をネストできません" -#: awx/api/serializers.py:3291 -#, python-format -msgid "Job Template '%s' is missing or undefined." -msgstr "ジョブテンプレート「%s」が見つからない、または定義されていません。" +#: awx/api/serializers.py:3754 awx/api/views.py:783 +msgid "Related template is not configured to accept credentials on launch." +msgstr "関連するテンプレートは起動時に認証情報を受け入れるよう設定されていません。" -#: awx/api/serializers.py:3294 +#: awx/api/serializers.py:4211 msgid "The inventory associated with this Job Template is being deleted." msgstr "このジョブテンプレートに関連付けられているインベントリーが削除されています。" -#: awx/api/serializers.py:3335 awx/api/views.py:3023 -#, python-format -msgid "Cannot assign multiple %s credentials." -msgstr "複数の %s 認証情報を割り当てることができません。" +#: awx/api/serializers.py:4213 +msgid "The provided inventory is being deleted." +msgstr "指定されたインベントリーが削除されています。" -#: awx/api/serializers.py:3337 awx/api/views.py:3026 -msgid "Extra credentials must be network or cloud." -msgstr "追加の認証情報はネットワークまたはクラウドにする必要があります。" +#: awx/api/serializers.py:4221 +msgid "Cannot assign multiple {} credentials." +msgstr "複数の {} 認証情報を割り当てることができません。" -#: awx/api/serializers.py:3474 +#: awx/api/serializers.py:4234 +msgid "" +"Removing {} credential at launch time without replacement is not supported. " +"Provided list lacked credential(s): {}." +msgstr "置き換えなしで起動時に {} 認証情報を削除することはサポートされていません。指定された一覧には認証情報がありません: {}" + +#: awx/api/serializers.py:4360 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "通知設定の必須フィールドがありません: notification_type" -#: awx/api/serializers.py:3497 +#: awx/api/serializers.py:4383 msgid "No values specified for field '{}'" msgstr "フィールド '{}' に値が指定されていません" -#: awx/api/serializers.py:3502 +#: awx/api/serializers.py:4388 msgid "Missing required fields for Notification Configuration: {}." msgstr "通知設定の必須フィールドがありません: {}。" -#: awx/api/serializers.py:3505 +#: awx/api/serializers.py:4391 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "設定フィールド '{}' のタイプが正しくありません。{} が予期されました。" -#: awx/api/serializers.py:3558 -msgid "Inventory Source must be a cloud resource." -msgstr "インベントリーソースはクラウドリソースでなければなりません。" - -#: awx/api/serializers.py:3560 -msgid "Manual Project cannot have a schedule set." -msgstr "手動プロジェクトにはスケジュールを設定できません。" - -#: awx/api/serializers.py:3563 +#: awx/api/serializers.py:4453 msgid "" -"Inventory sources with `update_on_project_update` cannot be scheduled. " -"Schedule its source project `{}` instead." -msgstr "" -"「update_on_project_update」が設定されたインベントリーソースはスケジュールできません。代わりのそのソースプロジェクト「{}」 " -"をスケジュールします。" +"Valid DTSTART required in rrule. Value should start with: " +"DTSTART:YYYYMMDDTHHMMSSZ" +msgstr "有効な DTSTART が rrule で必要です。値は DTSTART:YYYYMMDDTHHMMSSZ で開始する必要があります。" -#: awx/api/serializers.py:3582 -msgid "Projects and inventory updates cannot accept extra variables." -msgstr "プロジェクトおよびインベントリーの更新は追加変数を受け入れません。" - -#: awx/api/serializers.py:3604 +#: awx/api/serializers.py:4455 msgid "" -"DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" -msgstr "DTSTART が rrule で必要です。値は、DSTART:YYYYMMDDTHHMMSSZ に一致する必要があります。" +"DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ." +msgstr "DTSTART をネイティブの日時にすることができません。;TZINFO= or YYYYMMDDTHHMMSSZZ を指定します。" -#: awx/api/serializers.py:3606 +#: awx/api/serializers.py:4457 msgid "Multiple DTSTART is not supported." msgstr "複数の DTSTART はサポートされません。" -#: awx/api/serializers.py:3608 -msgid "RRULE require in rrule." +#: awx/api/serializers.py:4459 +msgid "RRULE required in rrule." msgstr "RRULE が rrule で必要です。" -#: awx/api/serializers.py:3610 +#: awx/api/serializers.py:4461 msgid "Multiple RRULE is not supported." msgstr "複数の RRULE はサポートされません。" -#: awx/api/serializers.py:3612 +#: awx/api/serializers.py:4463 msgid "INTERVAL required in rrule." msgstr "INTERVAL が rrule で必要です。" -#: awx/api/serializers.py:3614 -msgid "TZID is not supported." -msgstr "TZID はサポートされません。" - -#: awx/api/serializers.py:3616 +#: awx/api/serializers.py:4465 msgid "SECONDLY is not supported." msgstr "SECONDLY はサポートされません。" -#: awx/api/serializers.py:3618 +#: awx/api/serializers.py:4467 msgid "Multiple BYMONTHDAYs not supported." msgstr "複数の BYMONTHDAY はサポートされません。" -#: awx/api/serializers.py:3620 +#: awx/api/serializers.py:4469 msgid "Multiple BYMONTHs not supported." msgstr "複数の BYMONTH はサポートされません。" -#: awx/api/serializers.py:3622 +#: awx/api/serializers.py:4471 msgid "BYDAY with numeric prefix not supported." msgstr "数字の接頭辞のある BYDAY はサポートされません。" -#: awx/api/serializers.py:3624 +#: awx/api/serializers.py:4473 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY はサポートされません。" -#: awx/api/serializers.py:3626 +#: awx/api/serializers.py:4475 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO はサポートされません。" -#: awx/api/serializers.py:3630 +#: awx/api/serializers.py:4477 +msgid "RRULE may not contain both COUNT and UNTIL" +msgstr "RRULE には COUNT と UNTIL の両方を含めることができません" + +#: awx/api/serializers.py:4481 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 はサポートされません。" -#: awx/api/serializers.py:3634 -msgid "rrule parsing failed validation." -msgstr "rrule の構文解析で検証に失敗しました。" +#: awx/api/serializers.py:4485 +msgid "rrule parsing failed validation: {}" +msgstr "rrule の構文解析で検証に失敗しました: {}" -#: awx/api/serializers.py:3760 +#: awx/api/serializers.py:4526 +msgid "Inventory Source must be a cloud resource." +msgstr "インベントリーソースはクラウドリソースでなければなりません。" + +#: awx/api/serializers.py:4528 +msgid "Manual Project cannot have a schedule set." +msgstr "手動プロジェクトにはスケジュールを設定できません。" + +#: awx/api/serializers.py:4541 +msgid "" +"Count of jobs in the running or waiting state that are targeted for this " +"instance" +msgstr "このインスタンスにターゲット設定されている実行中または待機状態のジョブの数" + +#: awx/api/serializers.py:4546 +msgid "Count of all jobs that target this instance" +msgstr "このインスタンスをターゲットに設定するすべてのジョブの数" + +#: awx/api/serializers.py:4579 +msgid "" +"Count of jobs in the running or waiting state that are targeted for this " +"instance group" +msgstr "このインスタンスグループにターゲット設定されている実行中または待機状態のジョブの数" + +#: awx/api/serializers.py:4584 +msgid "Count of all jobs that target this instance group" +msgstr "このインスタンスグループをターゲットに設定するすべてのジョブの数" + +#: awx/api/serializers.py:4592 +msgid "Policy Instance Percentage" +msgstr "ポリシーインスタンスのパーセンテージ" + +#: awx/api/serializers.py:4593 +msgid "" +"Minimum percentage of all instances that will be automatically assigned to " +"this group when new instances come online." +msgstr "新規インスタンスがオンライン状態になるとこのグループに自動的に割り当てられるすべてのインスタンスの最小パーセンテージです。" + +#: awx/api/serializers.py:4598 +msgid "Policy Instance Minimum" +msgstr "ポリシーインスタンスの最小値" + +#: awx/api/serializers.py:4599 +msgid "" +"Static minimum number of Instances that will be automatically assign to this" +" group when new instances come online." +msgstr "新規インスタンスがオンライン状態になるとこのグループに自動的に割り当てられるインスタンスの静的な最小数です。" + +#: awx/api/serializers.py:4604 +msgid "Policy Instance List" +msgstr "ポリシーインスタンスの一覧" + +#: awx/api/serializers.py:4605 +msgid "List of exact-match Instances that will be assigned to this group" +msgstr "このグループに割り当てられる完全一致のインスタンスの一覧" + +#: awx/api/serializers.py:4627 +msgid "Duplicate entry {}." +msgstr "重複するエントリー {}。" + +#: awx/api/serializers.py:4629 +msgid "{} is not a valid hostname of an existing instance." +msgstr "{} は既存インスタンスの有効なホスト名ではありません。" + +#: awx/api/serializers.py:4634 +msgid "tower instance group name may not be changed." +msgstr "tower インスタンスグループ名は変更できません。" + +#: awx/api/serializers.py:4704 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" msgstr "オブジェクトの作成、更新または削除時の新規値および変更された値の概要" -#: awx/api/serializers.py:3762 +#: awx/api/serializers.py:4706 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 " @@ -556,7 +708,7 @@ msgstr "" "作成、更新、および削除イベントの場合、これは影響を受けたオブジェクトタイプになります。関連付けおよび関連付け解除イベントの場合、これは object2 " "に関連付けられたか、またはその関連付けが解除されたオブジェクトタイプになります。" -#: awx/api/serializers.py:3765 +#: awx/api/serializers.py:4709 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated" @@ -565,253 +717,332 @@ msgstr "" "作成、更新、および削除イベントの場合は設定されません。関連付けおよび関連付け解除イベントの場合、これは object1 " "が関連付けられるオブジェクトタイプになります。" -#: awx/api/serializers.py:3768 +#: awx/api/serializers.py:4712 msgid "The action taken with respect to the given object(s)." msgstr "指定されたオブジェクトについて実行されたアクション。" -#: awx/api/serializers.py:3885 -msgid "Unable to login with provided credentials." -msgstr "提供される認証情報でログインできません。" - -#: awx/api/serializers.py:3887 -msgid "Must include \"username\" and \"password\"." -msgstr "「username」および「password」を含める必要があります。" - -#: awx/api/views.py:108 +#: awx/api/views.py:118 msgid "Your license does not allow use of the activity stream." msgstr "お使いのライセンスではアクティビティーストリームを使用できません。" -#: awx/api/views.py:118 +#: awx/api/views.py:128 msgid "Your license does not permit use of system tracking." msgstr "お使いのライセンスではシステムトラッキングを使用できません。" -#: awx/api/views.py:128 +#: awx/api/views.py:138 msgid "Your license does not allow use of workflows." msgstr "お使いのライセンスではワークフローを使用できません。" -#: awx/api/views.py:142 +#: awx/api/views.py:152 msgid "Cannot delete job resource when associated workflow job is running." msgstr "関連付けられたワークフロージョブが実行中の場合、ジョブリソースを削除できません。" -#: awx/api/views.py:146 +#: awx/api/views.py:157 msgid "Cannot delete running job resource." msgstr "実行中のジョブリソースを削除できません。" -#: awx/api/views.py:155 awx/templates/rest_framework/api.html:28 +#: awx/api/views.py:162 +msgid "Job has not finished processing events." +msgstr "ジョブはイベント処理を終了していません。" + +#: awx/api/views.py:221 +msgid "Related job {} is still processing events." +msgstr "関連するジョブ {} は依然としてイベントを処理しています。" + +#: awx/api/views.py:228 awx/templates/rest_framework/api.html:28 msgid "REST API" msgstr "REST API" -#: awx/api/views.py:164 awx/templates/rest_framework/api.html:4 +#: awx/api/views.py:238 awx/templates/rest_framework/api.html:4 msgid "AWX REST API" msgstr "AWX REST API" -#: awx/api/views.py:226 +#: awx/api/views.py:252 +msgid "API OAuth 2 Authorization Root" +msgstr "API OAuth 2 認証ルート" + +#: awx/api/views.py:317 msgid "Version 1" msgstr "バージョン 1" -#: awx/api/views.py:230 +#: awx/api/views.py:321 msgid "Version 2" msgstr "バージョン 2" -#: awx/api/views.py:241 +#: awx/api/views.py:330 msgid "Ping" msgstr "Ping" -#: awx/api/views.py:272 awx/conf/apps.py:12 +#: awx/api/views.py:361 awx/conf/apps.py:10 msgid "Configuration" msgstr "設定" -#: awx/api/views.py:325 +#: awx/api/views.py:418 msgid "Invalid license data" msgstr "無効なライセンスデータ" -#: awx/api/views.py:327 +#: awx/api/views.py:420 msgid "Missing 'eula_accepted' property" msgstr "'eula_accepted' プロパティーがありません" -#: awx/api/views.py:331 +#: awx/api/views.py:424 msgid "'eula_accepted' value is invalid" msgstr "'eula_accepted' 値は無効です。" -#: awx/api/views.py:334 +#: awx/api/views.py:427 msgid "'eula_accepted' must be True" msgstr "'eula_accepted' は True でなければなりません" -#: awx/api/views.py:341 +#: awx/api/views.py:434 msgid "Invalid JSON" msgstr "無効な JSON" -#: awx/api/views.py:349 +#: awx/api/views.py:442 msgid "Invalid License" msgstr "無効なライセンス" -#: awx/api/views.py:359 +#: awx/api/views.py:452 msgid "Invalid license" msgstr "無効なライセンス" -#: awx/api/views.py:367 +#: awx/api/views.py:460 #, python-format msgid "Failed to remove license (%s)" msgstr "ライセンスの削除に失敗しました (%s)" -#: awx/api/views.py:372 +#: awx/api/views.py:465 msgid "Dashboard" msgstr "ダッシュボード" -#: awx/api/views.py:471 +#: awx/api/views.py:564 msgid "Dashboard Jobs Graphs" msgstr "ダッシュボードのジョブグラフ" -#: awx/api/views.py:507 +#: awx/api/views.py:600 #, python-format msgid "Unknown period \"%s\"" msgstr "不明な期間 \"%s\"" -#: awx/api/views.py:521 +#: awx/api/views.py:614 msgid "Instances" msgstr "インスタンス" -#: awx/api/views.py:529 +#: awx/api/views.py:622 msgid "Instance Detail" msgstr "インスタンスの詳細" -#: awx/api/views.py:537 -msgid "Instance Running Jobs" -msgstr "ジョブを実行しているインスタンス" +#: awx/api/views.py:643 +msgid "Instance Jobs" +msgstr "インスタンスジョブ" -#: awx/api/views.py:552 +#: awx/api/views.py:657 msgid "Instance's Instance Groups" msgstr "インスタンスのインスタンスグループ" -#: awx/api/views.py:562 +#: awx/api/views.py:666 msgid "Instance Groups" msgstr "インスタンスグループ" -#: awx/api/views.py:570 +#: awx/api/views.py:674 msgid "Instance Group Detail" msgstr "インスタンスグループの詳細" -#: awx/api/views.py:578 +#: awx/api/views.py:682 +msgid "Isolated Groups can not be removed from the API" +msgstr "分離されたグループは API から削除できません" + +#: awx/api/views.py:684 +msgid "" +"Instance Groups acting as a controller for an Isolated Group can not be " +"removed from the API" +msgstr "分離されたグループのコントローラーとして動作するインスタンスグループは API から削除できません" + +#: awx/api/views.py:690 msgid "Instance Group Running Jobs" msgstr "ジョブを実行しているインスタンスグループ" -#: awx/api/views.py:588 +#: awx/api/views.py:699 msgid "Instance Group's Instances" msgstr "インスタンスグループのインスタンス" -#: awx/api/views.py:598 +#: awx/api/views.py:709 msgid "Schedules" msgstr "スケジュール" -#: awx/api/views.py:617 +#: awx/api/views.py:723 +msgid "Schedule Recurrence Rule Preview" +msgstr "繰り返しルールプレビューのスケジュール" + +#: awx/api/views.py:770 +msgid "Cannot assign credential when related template is null." +msgstr "関連するテンプレートが null の場合は認証情報を割り当てることができません。" + +#: awx/api/views.py:775 +msgid "Related template cannot accept {} on launch." +msgstr "関連するテンプレートは起動時に {} を受け入れません。" + +#: awx/api/views.py:777 +msgid "" +"Credential that requires user input on launch cannot be used in saved launch" +" configuration." +msgstr "起動時にユーザー入力を必要とする認証情報は保存された起動設定で使用できません。" + +#: awx/api/views.py:785 +#, python-brace-format +msgid "" +"This launch configuration already provides a {credential_type} credential." +msgstr "この起動設定は {credential_type} 認証情報をすでに指定しています。" + +#: awx/api/views.py:788 +#, python-brace-format +msgid "Related template already uses {credential_type} credential." +msgstr "関連するテンプレートは {credential_type} 認証情報をすでに使用しています。" + +#: awx/api/views.py:806 msgid "Schedule Jobs List" msgstr "スケジュールジョブの一覧" -#: awx/api/views.py:843 +#: awx/api/views.py:961 msgid "Your license only permits a single organization to exist." msgstr "お使いのライセンスでは、単一組織のみの存在が許可されます。" -#: awx/api/views.py:1079 awx/api/views.py:4666 +#: awx/api/views.py:1193 awx/api/views.py:5056 msgid "You cannot assign an Organization role as a child role for a Team." msgstr "組織ロールをチームの子ロールとして割り当てることができません。" -#: awx/api/views.py:1083 awx/api/views.py:4680 +#: awx/api/views.py:1197 awx/api/views.py:5070 msgid "You cannot grant system-level permissions to a team." msgstr "システムレベルのパーミッションをチームに付与できません。" -#: awx/api/views.py:1090 awx/api/views.py:4672 +#: awx/api/views.py:1204 awx/api/views.py:5062 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" msgstr "組織フィールドが設定されていないか、または別の組織に属する場合に認証情報のアクセス権をチームに付与できません" -#: awx/api/views.py:1180 -msgid "Cannot delete project." -msgstr "プロジェクトを削除できません。" - -#: awx/api/views.py:1215 +#: awx/api/views.py:1318 msgid "Project Schedules" msgstr "プロジェクトのスケジュール" -#: awx/api/views.py:1227 +#: awx/api/views.py:1329 msgid "Project SCM Inventory Sources" msgstr "プロジェクト SCM のインベントリーソース" -#: awx/api/views.py:1354 +#: awx/api/views.py:1430 +msgid "Project Update Events List" +msgstr "プロジェクト更新イベント一覧" + +#: awx/api/views.py:1444 +msgid "System Job Events List" +msgstr "システムジョブイベント一覧" + +#: awx/api/views.py:1458 +msgid "Inventory Update Events List" +msgstr "インベントリー更新イベント一覧" + +#: awx/api/views.py:1492 msgid "Project Update SCM Inventory Updates" msgstr "プロジェクト更新 SCM のインベントリー更新" -#: awx/api/views.py:1409 +#: awx/api/views.py:1551 msgid "Me" msgstr "自分" -#: awx/api/views.py:1453 awx/api/views.py:4623 -msgid "You may not perform any action with your own admin_role." -msgstr "独自の admin_role でアクションを実行することはできません。" +#: awx/api/views.py:1559 +msgid "OAuth 2 Applications" +msgstr "OAuth 2 アプリケーション" -#: awx/api/views.py:1459 awx/api/views.py:4627 -msgid "You may not change the membership of a users admin_role" -msgstr "ユーザーの admin_role のメンバーシップを変更することはできません" +#: awx/api/views.py:1568 +msgid "OAuth 2 Application Detail" +msgstr "OAuth 2 アプリケーションの詳細" -#: awx/api/views.py:1464 awx/api/views.py:4632 +#: awx/api/views.py:1577 +msgid "OAuth 2 Application Tokens" +msgstr "OAuth 2 アプリケーショントークン" + +#: awx/api/views.py:1599 +msgid "OAuth2 Tokens" +msgstr "OAuth2 トークン" + +#: awx/api/views.py:1608 +msgid "OAuth2 User Tokens" +msgstr "OAuth2 ユーザートークン" + +#: awx/api/views.py:1620 +msgid "OAuth2 User Authorized Access Tokens" +msgstr "OAuth2 ユーザー認可アクセストークン" + +#: awx/api/views.py:1635 +msgid "Organization OAuth2 Applications" +msgstr "組織 OAuth2 アプリケーション" + +#: awx/api/views.py:1647 +msgid "OAuth2 Personal Access Tokens" +msgstr "OAuth2 パーソナルアクセストークン" + +#: awx/api/views.py:1662 +msgid "OAuth Token Detail" +msgstr "OAuth トークンの詳細" + +#: awx/api/views.py:1722 awx/api/views.py:5023 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" msgstr "認証情報の組織に属さないユーザーに認証情報のアクセス権を付与することはできません" -#: awx/api/views.py:1468 awx/api/views.py:4636 +#: awx/api/views.py:1726 awx/api/views.py:5027 msgid "You cannot grant private credential access to another user" msgstr "非公開の認証情報のアクセス権を別のユーザーに付与することはできません" -#: awx/api/views.py:1566 +#: awx/api/views.py:1824 #, python-format msgid "Cannot change %s." msgstr "%s を変更できません。" -#: awx/api/views.py:1572 +#: awx/api/views.py:1830 msgid "Cannot delete user." msgstr "ユーザーを削除できません。" -#: awx/api/views.py:1601 +#: awx/api/views.py:1854 msgid "Deletion not allowed for managed credential types" msgstr "管理されている認証情報タイプで削除は許可されません" -#: awx/api/views.py:1603 +#: awx/api/views.py:1856 msgid "Credential types that are in use cannot be deleted" msgstr "使用中の認証情報タイプを削除できません" -#: awx/api/views.py:1781 +#: awx/api/views.py:2031 msgid "Cannot delete inventory script." msgstr "インベントリースクリプトを削除できません。" -#: awx/api/views.py:1866 +#: awx/api/views.py:2122 #, python-brace-format msgid "{0}" msgstr "{0}" -#: awx/api/views.py:2101 +#: awx/api/views.py:2353 msgid "Fact not found." msgstr "ファクトが見つかりませんでした。" -#: awx/api/views.py:2125 +#: awx/api/views.py:2375 msgid "SSLError while trying to connect to {}" msgstr "{} への接続試行中に SSL エラーが発生しました" -#: awx/api/views.py:2127 +#: awx/api/views.py:2377 msgid "Request to {} timed out." msgstr "{} の要求がタイムアウトになりました。" -#: awx/api/views.py:2129 -msgid "Unkown exception {} while trying to GET {}" +#: awx/api/views.py:2379 +msgid "Unknown exception {} while trying to GET {}" msgstr "GET {} の試行中に不明の例外 {} が発生しました" -#: awx/api/views.py:2132 +#: awx/api/views.py:2382 msgid "" "Unauthorized access. Please check your Insights Credential username and " "password." msgstr "不正アクセスです。Insights 認証情報のユーザー名およびパスワードを確認してください。" -#: awx/api/views.py:2134 +#: awx/api/views.py:2385 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" @@ -819,210 +1050,252 @@ msgstr "" "URL {} で Insights API からのレポートおよびメンテナンス計画を収集できませんでした。サーバーが {} " "ステータスコードおよびメッセージ {} を出して応答しました。" -#: awx/api/views.py:2140 +#: awx/api/views.py:2392 msgid "Expected JSON response from Insights but instead got {}" msgstr "Insights からの JSON 応答を予想していましたが、代わりに {} を取得しました。" -#: awx/api/views.py:2147 +#: awx/api/views.py:2399 msgid "This host is not recognized as an Insights host." msgstr "このホストは Insights ホストとして認識されていません。" -#: awx/api/views.py:2152 +#: awx/api/views.py:2404 msgid "The Insights Credential for \"{}\" was not found." msgstr "\"{}\" の Insights 認証情報が見つかりませんでした。" -#: awx/api/views.py:2221 +#: awx/api/views.py:2472 msgid "Cyclical Group association." msgstr "循環的なグループの関連付け" -#: awx/api/views.py:2499 +#: awx/api/views.py:2686 msgid "Inventory Source List" msgstr "インベントリーソース一覧" -#: awx/api/views.py:2512 +#: awx/api/views.py:2698 msgid "Inventory Sources Update" msgstr "インベントリーソースの更新" -#: awx/api/views.py:2542 +#: awx/api/views.py:2731 msgid "Could not start because `can_update` returned False" msgstr "`can_update` が False を返したので開始できませんでした" -#: awx/api/views.py:2550 +#: awx/api/views.py:2739 msgid "No inventory sources to update." msgstr "更新するインベントリーソースがありません。" -#: awx/api/views.py:2582 -msgid "Cannot delete inventory source." -msgstr "インベントリーソースを削除できません。" - -#: awx/api/views.py:2590 +#: awx/api/views.py:2768 msgid "Inventory Source Schedules" msgstr "インベントリーソースのスケジュール" -#: awx/api/views.py:2620 +#: awx/api/views.py:2796 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "ソースが {} のいずれかである場合、通知テンプレートのみを割り当てることができます。" #: awx/api/views.py:2851 +msgid "Vault credentials are not yet supported for inventory sources." +msgstr "Vault 認証情報はインベントリーソースではまだサポートされていません。" + +#: awx/api/views.py:2856 +msgid "Source already has cloud credential assigned." +msgstr "ソースにはクラウド認証情報がすでに割り当てられています。" + +#: awx/api/views.py:3016 +msgid "" +"'credentials' cannot be used in combination with 'credential', " +"'vault_credential', or 'extra_credentials'." +msgstr "" +"'credentials' は 'credential'、'vault_credential'、または 'extra_credentials' " +"との組み合わせで使用できません。" + +#: awx/api/views.py:3128 msgid "Job Template Schedules" msgstr "ジョブテンプレートスケジュール" -#: awx/api/views.py:2871 awx/api/views.py:2882 +#: awx/api/views.py:3146 awx/api/views.py:3157 msgid "Your license does not allow adding surveys." msgstr "お使いのライセンスでは Survey を追加できません。" -#: awx/api/views.py:2889 -msgid "'name' missing from survey spec." -msgstr "Survey の指定に「name」がありません。" +#: awx/api/views.py:3176 +msgid "Field '{}' is missing from survey spec." +msgstr "Survey の指定にフィールド '{}' がありません。" -#: awx/api/views.py:2891 -msgid "'description' missing from survey spec." -msgstr "Survey の指定に「description」がありません。" +#: awx/api/views.py:3178 +msgid "Expected {} for field '{}', received {} type." +msgstr "フィールド '{}' の予期される {}。{} タイプを受信しました。" -#: awx/api/views.py:2893 -msgid "'spec' missing from survey spec." -msgstr "Survey の指定に「spec」がありません。" - -#: awx/api/views.py:2895 -msgid "'spec' must be a list of items." -msgstr "「spec」は項目の一覧にする必要があります。" - -#: awx/api/views.py:2897 +#: awx/api/views.py:3182 msgid "'spec' doesn't contain any items." msgstr "「spec」には項目が含まれません。" -#: awx/api/views.py:2903 +#: awx/api/views.py:3191 #, python-format msgid "Survey question %s is not a json object." msgstr "Survey の質問 %s は json オブジェクトではありません。" -#: awx/api/views.py:2905 +#: awx/api/views.py:3193 #, python-format msgid "'type' missing from survey question %s." msgstr "Survey の質問 %s に「type」がありません。" -#: awx/api/views.py:2907 +#: awx/api/views.py:3195 #, python-format msgid "'question_name' missing from survey question %s." msgstr "Survey の質問 %s に「question_name」がありません。" -#: awx/api/views.py:2909 +#: awx/api/views.py:3197 #, python-format msgid "'variable' missing from survey question %s." msgstr "Survey の質問 %s に「variable」がありません。" -#: awx/api/views.py:2911 +#: awx/api/views.py:3199 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "Survey の質問%(survey)s で「variable」の「%(item)s」が重複しています。" -#: awx/api/views.py:2916 +#: awx/api/views.py:3204 #, python-format msgid "'required' missing from survey question %s." msgstr "Survey の質問 %s に「required」がありません。" -#: awx/api/views.py:2921 +#: awx/api/views.py:3209 #, python-brace-format msgid "" "Value {question_default} for '{variable_name}' expected to be a string." msgstr "'{variable_name}' の値 {question_default} は文字列であることが予想されます。" -#: awx/api/views.py:2928 +#: awx/api/views.py:3219 #, python-brace-format msgid "" -"$encrypted$ is reserved keyword for password questions and may not be used " -"as a default for '{variable_name}' in survey question {question_position}." +"$encrypted$ is a reserved keyword for password question defaults, survey " +"question {question_position} is type {question_type}." msgstr "" -"$encrypted$ はパスワードの質問の予約されたキーワードで、Survey の質問 {question_position} では " -"'{variable_name}' のデフォルトとして使用できません。" +"$encrypted$ はパスワードの質問のデフォルトの予約されたキーワードで、Survey の質問 {question_position} はタイプ " +"{question_type} です。" -#: awx/api/views.py:3049 +#: awx/api/views.py:3235 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword, may not be used for new default in " +"position {question_position}." +msgstr "$encrypted$ は予約されたキーワードで、位置 {question_position} の新規デフォルトに使用できません。" + +#: awx/api/views.py:3309 +#, python-brace-format +msgid "Cannot assign multiple {credential_type} credentials." +msgstr "複数の {credential_type} 認証情報を割り当てることができません。" + +#: awx/api/views.py:3327 +msgid "Extra credentials must be network or cloud." +msgstr "追加の認証情報はネットワークまたはクラウドにする必要があります。" + +#: awx/api/views.py:3349 msgid "Maximum number of labels for {} reached." msgstr "{} のラベルの最大数に達しました。" -#: awx/api/views.py:3170 +#: awx/api/views.py:3472 msgid "No matching host could be found!" msgstr "一致するホストが見つかりませんでした!" -#: awx/api/views.py:3173 +#: awx/api/views.py:3475 msgid "Multiple hosts matched the request!" msgstr "複数のホストが要求に一致しました!" -#: awx/api/views.py:3178 +#: awx/api/views.py:3480 msgid "Cannot start automatically, user input required!" msgstr "自動的に開始できません。ユーザー入力が必要です!" -#: awx/api/views.py:3185 +#: awx/api/views.py:3487 msgid "Host callback job already pending." msgstr "ホストのコールバックジョブがすでに保留中です。" -#: awx/api/views.py:3199 +#: awx/api/views.py:3502 awx/api/views.py:4284 msgid "Error starting job!" msgstr "ジョブの開始時にエラーが発生しました!" -#: awx/api/views.py:3306 +#: awx/api/views.py:3622 #, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr "{1} が関連付けられている場合に {0} を関連付けることはできません。" -#: awx/api/views.py:3331 +#: awx/api/views.py:3647 msgid "Multiple parent relationship not allowed." msgstr "複数の親関係は許可されません。" -#: awx/api/views.py:3336 +#: awx/api/views.py:3652 msgid "Cycle detected." msgstr "サイクルが検出されました。" -#: awx/api/views.py:3540 +#: awx/api/views.py:3850 msgid "Workflow Job Template Schedules" msgstr "ワークフロージョブテンプレートのスケジュール" -#: awx/api/views.py:3685 awx/api/views.py:4268 +#: awx/api/views.py:3986 awx/api/views.py:4690 msgid "Superuser privileges needed." msgstr "スーパーユーザー権限が必要です。" -#: awx/api/views.py:3717 +#: awx/api/views.py:4018 msgid "System Job Template Schedules" msgstr "システムジョブテンプレートのスケジュール" -#: awx/api/views.py:3780 +#: awx/api/views.py:4076 msgid "POST not allowed for Job launching in version 2 of the api" msgstr "POST は API のバージョン 2 でのジョブの起動では許可されません" -#: awx/api/views.py:3942 +#: awx/api/views.py:4100 awx/api/views.py:4106 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "PUT は API のバージョン 2 でのジョブ詳細では許可されません" + +#: awx/api/views.py:4267 +#, python-brace-format +msgid "Wait until job finishes before retrying on {status_value} hosts." +msgstr "ジョブの終了を待機してから {status_value} ホストで再試行します。" + +#: awx/api/views.py:4272 +#, python-brace-format +msgid "Cannot retry on {status_value} hosts, playbook stats not available." +msgstr "Playbook 統計を利用できないため、{status_value} ホストで再試行できません。" + +#: awx/api/views.py:4277 +#, python-brace-format +msgid "Cannot relaunch because previous job had 0 {status_value} hosts." +msgstr "直前のジョブにあるのが 0 {status_value} ホストであるため、再起動できません。" + +#: awx/api/views.py:4306 +msgid "Cannot create schedule because job requires credential passwords." +msgstr "ジョブには認証情報パスワードが必要なため、スケジュールを削除できません。" + +#: awx/api/views.py:4311 +msgid "Cannot create schedule because job was launched by legacy method." +msgstr "ジョブがレガシー方式で起動したため、スケジュールを作成できません。" + +#: awx/api/views.py:4313 +msgid "Cannot create schedule because a related resource is missing." +msgstr "関連するリソースがないため、スケジュールを作成できません。" + +#: awx/api/views.py:4368 msgid "Job Host Summaries List" msgstr "ジョブホスト概要一覧" -#: awx/api/views.py:3989 +#: awx/api/views.py:4417 msgid "Job Event Children List" msgstr "ジョブイベント子一覧" -#: awx/api/views.py:3998 +#: awx/api/views.py:4427 msgid "Job Event Hosts List" msgstr "ジョブイベントホスト一覧" -#: awx/api/views.py:4008 +#: awx/api/views.py:4436 msgid "Job Events List" msgstr "ジョブイベント一覧" -#: awx/api/views.py:4222 +#: awx/api/views.py:4647 msgid "Ad Hoc Command Events List" msgstr "アドホックコマンドイベント一覧" -#: awx/api/views.py:4437 -msgid "Error generating stdout download file: {}" -msgstr "stdout ダウンロードファイルの生成中にエラーが発生しました: {}" - -#: awx/api/views.py:4450 -#, python-format -msgid "Error generating stdout download file: %s" -msgstr "stdout ダウンロードファイルの生成中にエラーが発生しました: %s" - -#: awx/api/views.py:4495 +#: awx/api/views.py:4889 msgid "Delete not allowed while there are pending notifications" msgstr "保留中の通知がある場合に削除は許可されません" -#: awx/api/views.py:4502 +#: awx/api/views.py:4897 msgid "Notification Template Test" msgstr "通知テンプレートテスト" @@ -1174,19 +1447,32 @@ msgstr "設定例" msgid "Example setting which can be different for each user." msgstr "ユーザーごとに異なる設定例" -#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:56 +#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:55 msgid "User" msgstr "ユーザー" -#: awx/conf/fields.py:63 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 +#, python-brace-format +msgid "" +"Expected None, True, False, a string or list of strings but got {input_type}" +" instead." +msgstr "None、True、False、文字列または文字列の一覧が予期されましたが、{input_type} が取得されました。" + +#: awx/conf/fields.py:104 msgid "Enter a valid URL" msgstr "無効な URL の入力" -#: awx/conf/fields.py:95 +#: awx/conf/fields.py:136 #, python-brace-format msgid "\"{input}\" is not a valid string." msgstr "\"{input}\" は有効な文字列ではありません。" +#: awx/conf/fields.py:151 +#, python-brace-format +msgid "" +"Expected a list of tuples of max length 2 but got {input_type} instead." +msgstr "最大の長さが 2 のタプルの一覧が予想されましたが、代わりに {input_type} を取得しました。" + #: awx/conf/license.py:22 msgid "Your Tower license does not allow that." msgstr "お使いの Tower ライセンスではこれを許可しません。" @@ -1278,9 +1564,9 @@ msgstr "この値は設定ファイルに手動で設定されました。" #: awx/conf/tests/unit/test_settings.py:411 #: awx/conf/tests/unit/test_settings.py:430 #: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:51 -#: awx/main/conf.py:63 awx/main/conf.py:81 awx/main/conf.py:96 -#: awx/main/conf.py:121 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "システム" @@ -1292,103 +1578,108 @@ msgstr "システム" msgid "OtherSystem" msgstr "他のシステム" -#: awx/conf/views.py:48 +#: awx/conf/views.py:47 msgid "Setting Categories" msgstr "設定カテゴリー" -#: awx/conf/views.py:73 +#: awx/conf/views.py:71 msgid "Setting Detail" msgstr "設定の詳細" -#: awx/conf/views.py:168 +#: awx/conf/views.py:166 msgid "Logging Connectivity Test" msgstr "ロギング接続テスト" -#: awx/main/access.py:44 -msgid "Resource is being used by running jobs." -msgstr "リソースが実行中のジョブで使用されています。" +#: awx/main/access.py:57 +#, python-format +msgid "Required related field %s for permission check." +msgstr "パーミッションチェックに必要な関連フィールド %s です。" -#: awx/main/access.py:237 +#: awx/main/access.py:73 #, python-format msgid "Bad data found in related field %s." msgstr "関連フィールド %s に不正データが見つかりました。" -#: awx/main/access.py:281 +#: awx/main/access.py:293 msgid "License is missing." msgstr "ライセンスが見つかりません。" -#: awx/main/access.py:283 +#: awx/main/access.py:295 msgid "License has expired." msgstr "ライセンスの有効期限が切れました。" -#: awx/main/access.py:291 +#: awx/main/access.py:303 #, python-format msgid "License count of %s instances has been reached." msgstr "%s インスタンスのライセンス数に達しました。" -#: awx/main/access.py:293 +#: awx/main/access.py:305 #, python-format msgid "License count of %s instances has been exceeded." msgstr "%s インスタンスのライセンス数を超えました。" -#: awx/main/access.py:295 +#: awx/main/access.py:307 msgid "Host count exceeds available instances." msgstr "ホスト数が利用可能なインスタンスの上限を上回っています。" -#: awx/main/access.py:299 +#: awx/main/access.py:311 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "機能 %s はアクティブなライセンスで有効にされていません。" -#: awx/main/access.py:301 +#: awx/main/access.py:313 msgid "Features not found in active license." msgstr "各種機能はアクティブなライセンスにありません。" -#: awx/main/access.py:707 +#: awx/main/access.py:818 msgid "Unable to change inventory on a host." msgstr "ホストのインベントリーを変更できません。" -#: awx/main/access.py:724 awx/main/access.py:769 +#: awx/main/access.py:835 awx/main/access.py:880 msgid "Cannot associate two items from different inventories." msgstr "異なるインベントリーの 2 つの項目を関連付けることはできません。" -#: awx/main/access.py:757 +#: awx/main/access.py:868 msgid "Unable to change inventory on a group." msgstr "グループのインベントリーを変更できません。" -#: awx/main/access.py:1017 +#: awx/main/access.py:1129 msgid "Unable to change organization on a team." msgstr "チームの組織を変更できません。" -#: awx/main/access.py:1030 +#: awx/main/access.py:1146 msgid "The {} role cannot be assigned to a team" msgstr "{} ロールをチームに割り当てることができません" -#: awx/main/access.py:1032 +#: awx/main/access.py:1148 msgid "The admin_role for a User cannot be assigned to a team" msgstr "ユーザーの admin_role をチームに割り当てることができません" -#: awx/main/access.py:1479 +#: awx/main/access.py:1502 awx/main/access.py:1936 +msgid "Job was launched with prompts provided by another user." +msgstr "ジョブは別のユーザーによって提供されるプロンプトで起動されています。" + +#: awx/main/access.py:1522 msgid "Job has been orphaned from its job template." msgstr "ジョブはジョブテンプレートから孤立しています。" -#: awx/main/access.py:1481 -msgid "You do not have execute permission to related job template." -msgstr "関連するジョブテンプレートに対する実行パーミッションがありません。" +#: awx/main/access.py:1524 +msgid "Job was launched with unknown prompted fields." +msgstr "ジョブは不明のプロンプトが出されたフィールドで起動されています。" -#: awx/main/access.py:1484 +#: awx/main/access.py:1526 msgid "Job was launched with prompted fields." -msgstr "ジョブはプロンプトされたフィールドで起動されています。" +msgstr "ジョブはプロンプトが出されたフィールドで起動されています。" -#: awx/main/access.py:1486 +#: awx/main/access.py:1528 msgid " Organization level permissions required." msgstr "組織レベルのパーミッションが必要です。" -#: awx/main/access.py:1488 +#: awx/main/access.py:1530 msgid " You do not have permission to related resources." msgstr "関連リソースに対するパーミッションがありません。" -#: awx/main/access.py:1833 +#: awx/main/access.py:1950 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1422,112 +1713,145 @@ msgstr "組織管理者に表示されるすべてのユーザー" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." -msgstr "組織管理者が、それぞれの組織に関連付けられていないすべてのユーザーを閲覧できるかどうかを制御します。" +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." +msgstr "組織管理者が、それぞれの組織に関連付けられていないすべてのユーザーおよびチームを閲覧できるかどうかを制御します。" -#: awx/main/conf.py:49 +#: awx/main/conf.py:50 +msgid "Organization Admins Can Manage Users and Teams" +msgstr "組織管理者はユーザーおよびチームを管理できます" + +#: awx/main/conf.py:51 +msgid "" +"Controls whether any Organization Admin has the privileges to create and " +"manage users and teams. You may want to disable this ability if you are " +"using an LDAP or SAML integration." +msgstr "" +"組織管理者がユーザーおよびチームを作成し、管理する権限を持つようにするかどうかを制御します。LDAP または SAML " +"統合を使用している場合はこの機能を無効にする必要がある場合があります。" + +#: awx/main/conf.py:60 msgid "Enable Administrator Alerts" msgstr "管理者アラートの有効化" -#: awx/main/conf.py:50 +#: awx/main/conf.py:61 msgid "Email Admin users for system events that may require attention." msgstr "管理者ユーザーに対し、注意が必要になる可能性のあるシステムイベントについてのメールを送信します。" -#: awx/main/conf.py:60 +#: awx/main/conf.py:71 msgid "Base URL of the Tower host" msgstr "Tower ホストのベース URL" -#: awx/main/conf.py:61 +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to" " the Tower host." msgstr "この設定は、有効な URL を Tower ホストにレンダリングする通知などのサービスで使用されます。" -#: awx/main/conf.py:70 +#: awx/main/conf.py:81 msgid "Remote Host Headers" msgstr "リモートホストヘッダー" -#: awx/main/conf.py:71 +#: awx/main/conf.py:82 msgid "" -"HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy.\n" -"\n" -"Note: The headers will be searched in order and the first found remote host name or IP will be used.\n" -"\n" -"In the below example 8.8.8.7 would be the chosen IP address.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"HTTP headers and meta keys to search to determine remote host name or IP. " +"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " +"behind a reverse proxy. See the \"Proxy Support\" section of the " +"Adminstrator guide for more details." msgstr "" -"リモートホスト名または IP を判別するために検索する HTTP ヘッダーおよびメタキーです。リバースプロキシーの後ろの場合は、\"HTTP_X_FORWARDED_FOR\" のように項目をこの一覧に追加します。\n" -"\n" -"注: ヘッダーが順番に検索され、最初に検出されるリモートホスト名または IP が使用されます。\n" -"\n" -"以下の例では、8.8.8.7 が選択された IP アドレスになります。\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"リモートホスト名または IP を判別するために検索する HTTP " +"ヘッダーおよびメタキーです。リバースプロキシーの後ろの場合は、\"HTTP_X_FORWARDED_FOR\" " +"のように項目をこの一覧に追加します。詳細は、Administrator Guide の「Proxy Support」セクションを参照してください。" -#: awx/main/conf.py:88 +#: awx/main/conf.py:94 msgid "Proxy IP Whitelist" msgstr "プロキシー IP ホワイトリスト" -#: awx/main/conf.py:89 +#: awx/main/conf.py:95 msgid "" -"If Tower is behind a reverse proxy/load balancer, use this setting to whitelist the proxy IP addresses from which Tower should trust custom REMOTE_HOST_HEADERS header values\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"If this setting is an empty list (the default), the headers specified by REMOTE_HOST_HEADERS will be trusted unconditionally')" +"If Tower is behind a reverse proxy/load balancer, use this setting to " +"whitelist the proxy IP addresses from which Tower should trust custom " +"REMOTE_HOST_HEADERS header values. If this setting is an empty list (the " +"default), the headers specified by REMOTE_HOST_HEADERS will be trusted " +"unconditionally')" msgstr "" -"Tower がリバースプロキシー/ロードバランサーの背後にある場合、この設定を使用し、Tower がカスタム REMOTE_HOST_HEADERS ヘッダーの値\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR'、''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101'] を信用するのに使用するプロキシー IP アドレスをホワイトリスト化します。\n" -"この設定が空のリスト (デフォルト) の場合、REMOTE_HOST_HEADERS で指定されるヘッダーは条件なしに信用されます')" +"Tower がリバースプロキシー/ロードバランサーの背後にある場合、この設定を使用し、Tower がカスタム REMOTE_HOST_HEADERS " +"ヘッダーの値を信頼するのに使用するプロキシー IP アドレスをホワイトリスト化します。この設定が空のリスト (デフォルト) " +"の場合、REMOTE_HOST_HEADERS で指定されるヘッダーは条件なしに信頼されます')" -#: awx/main/conf.py:117 +#: awx/main/conf.py:121 msgid "License" msgstr "ライセンス" -#: awx/main/conf.py:118 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use " "/api/v1/config/ to update or change the license." msgstr "" "ライセンスによって、有効にされる特長および機能が制御されます。ライセンスを更新または変更するには、/api/v1/config/ を使用します。" -#: awx/main/conf.py:128 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "アドホックジョブで許可される Ansible モジュール" -#: awx/main/conf.py:129 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "アドホックジョブで使用できるモジュール一覧。" -#: awx/main/conf.py:130 awx/main/conf.py:140 awx/main/conf.py:151 -#: awx/main/conf.py:161 awx/main/conf.py:171 awx/main/conf.py:181 -#: awx/main/conf.py:192 awx/main/conf.py:204 awx/main/conf.py:216 -#: awx/main/conf.py:229 awx/main/conf.py:241 awx/main/conf.py:251 -#: awx/main/conf.py:262 awx/main/conf.py:272 awx/main/conf.py:282 -#: awx/main/conf.py:292 awx/main/conf.py:304 awx/main/conf.py:316 -#: awx/main/conf.py:328 awx/main/conf.py:341 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "ジョブ" -#: awx/main/conf.py:138 +#: awx/main/conf.py:143 +msgid "Always" +msgstr "常時" + +#: awx/main/conf.py:144 +msgid "Never" +msgstr "なし" + +#: awx/main/conf.py:145 +msgid "Only On Job Template Definitions" +msgstr "ジョブテンプレートの定義のみ" + +#: awx/main/conf.py:148 +msgid "When can extra variables contain Jinja templates?" +msgstr "いつ追加変数に Jinja テンプレートが含まれるか?" + +#: awx/main/conf.py:150 +msgid "" +"Ansible allows variable substitution via the Jinja2 templating language for " +"--extra-vars. This poses a potential security risk where Tower users with " +"the ability to specify extra vars at job launch time can use Jinja2 " +"templates to run arbitrary Python. It is recommended that this value be set" +" to \"template\" or \"never\"." +msgstr "" +"Ansible は Jinja2 テンプレート言語を使用した --extra-vars " +"の変数置き換えを許可します。これにより、ジョブの起動時に追加変数を指定できる Tower ユーザーが Jinja2 テンプレートを使用して任意の " +"Python " +"を実行できるようになるためセキュリティー上のリスクが生じます。この値は「template」または「never」に設定することをお勧めします。" + +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "ジョブの分離の有効化" -#: awx/main/conf.py:139 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." msgstr "機密情報の公開を防ぐためにシステムの保護された部分から Ansible ジョブを分離します。" -#: awx/main/conf.py:147 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "ジョブの実行パス" -#: awx/main/conf.py:148 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " @@ -1536,40 +1860,40 @@ msgstr "" "Tower がジョブの実行および分離用に新規の一時ディレクトリーを作成するディレクトリーです " "(認証情報ファイルおよびカスタムインベントリースクリプトなど)。" -#: awx/main/conf.py:159 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" -msgstr "分離されたジョブの非表示にするパス" +msgstr "分離されたジョブには公開しないパス" -#: awx/main/conf.py:160 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." -msgstr "分離されたプロセスから非表示にする追加パスです。1 行に 1 つのパスを入力します。" +msgstr "分離されたプロセスには公開しないその他のパスです。1 行に 1 つのパスを入力します。" -#: awx/main/conf.py:169 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" -msgstr "分離されたジョブの公開するパス" +msgstr "分離されたジョブに公開するパス" -#: awx/main/conf.py:170 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." msgstr "分離されたジョブに公開するために非表示にされるパスのホワイトリストです。1 行に 1 つのパスを入力します。" -#: awx/main/conf.py:179 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "分離されたステータスチェックの間隔" -#: awx/main/conf.py:180 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." msgstr "分離されたインスタンスで実行されるジョブに対するステータスチェック間にスリープ状態になる期間の秒数。" -#: awx/main/conf.py:189 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "分離された起動のタイムアウト" -#: awx/main/conf.py:190 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " @@ -1578,11 +1902,11 @@ msgstr "" "分離されたインスタンスでジョブを起動する際のタイムアウト (秒数)。これにはソースコントロールファイル (Playbook) " "を分離されたインスタンスにコピーするために必要な時間が含まれます。" -#: awx/main/conf.py:201 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "分離された接続のタイムアウト" -#: awx/main/conf.py:202 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " @@ -1591,11 +1915,11 @@ msgstr "" "分離されたインスタンスと通信する際に使用される Ansible SSH 接続のタイムアウト (秒数) " "です。値は予想されるネットワークの待ち時間よりも大幅に大きな値になるはずです。" -#: awx/main/conf.py:212 +#: awx/main/conf.py:237 msgid "Generate RSA keys for isolated instances" msgstr "分離されたインスタンスの RSA 鍵の生成" -#: awx/main/conf.py:213 +#: awx/main/conf.py:238 msgid "" "If set, a random RSA key will be generated and distributed to isolated " "instances. To disable this behavior and manage authentication for isolated " @@ -1604,39 +1928,39 @@ msgstr "" "設定されている場合、RSA 鍵が生成され、分離されたインスタンスに配布されます。この動作を無効にし、Tower " "の外部にある分離されたインスタンスの認証を管理するには、この設定を無効にします。" -#: awx/main/conf.py:227 awx/main/conf.py:228 +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "分離されたインスタンスへの SSH トラフィック用の RSA 秘密鍵" -#: awx/main/conf.py:239 awx/main/conf.py:240 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "分離されたインスタンスへの SSH トラフィック用の RSA 公開鍵" -#: awx/main/conf.py:249 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "追加の環境変数" -#: awx/main/conf.py:250 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." msgstr "Playbook 実行、インベントリー更新、プロジェクト更新および通知の送信に設定される追加の環境変数。" -#: awx/main/conf.py:260 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" msgstr "標準出力の最大表示サイズ" -#: awx/main/conf.py:261 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." msgstr "出力のダウンロードを要求する前に表示される標準出力の最大サイズ (バイト単位)。" -#: awx/main/conf.py:270 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "ジョブイベントの標準出力の最大表示サイズ" -#: awx/main/conf.py:271 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." @@ -1644,31 +1968,31 @@ msgstr "" "単一ジョブまたはアドホックコマンドイベントについて表示される標準出力の最大サイズ (バイト単位)。`stdout` は切り捨てが実行されると `…` " "で終了します。" -#: awx/main/conf.py:280 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" msgstr "スケジュール済みジョブの最大数" -#: awx/main/conf.py:281 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." msgstr "スケジュールからの起動時に実行を待機している同じジョブテンプレートの最大数です (これ以上作成されることはありません)。" -#: awx/main/conf.py:290 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "Ansible コールバックプラグイン" -#: awx/main/conf.py:291 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." msgstr "ジョブの実行時に使用される追加のコールバックプラグインを検索する際のパスの一覧です。1 行に 1 つのパスを入力します。" -#: awx/main/conf.py:301 +#: awx/main/conf.py:327 msgid "Default Job Timeout" msgstr "デフォルトのジョブタイムアウト" -#: awx/main/conf.py:302 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " @@ -1677,11 +2001,11 @@ msgstr "" "ジョブの実行可能な最大時間 (秒数) です。値 0 " "が使用されている場合はタイムアウトを設定できないことを示します。個別のジョブテンプレートに設定されるタイムアウトはこれを上書きします。" -#: awx/main/conf.py:313 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "デフォルトのインベントリー更新タイムアウト" -#: awx/main/conf.py:314 +#: awx/main/conf.py:340 msgid "" "Maximum time in seconds to allow inventory updates to run. Use value of 0 to" " indicate that no timeout should be imposed. A timeout set on an individual " @@ -1690,11 +2014,11 @@ msgstr "" "インベントリー更新の実行可能な最大時間 (秒数)。値 0 " "が設定されている場合はタイムアウトを設定できないことを示します。個別のインベントリーソースに設定されるタイムアウトはこれを上書きします。" -#: awx/main/conf.py:325 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" msgstr "デフォルトのプロジェクト更新タイムアウト" -#: awx/main/conf.py:326 +#: awx/main/conf.py:352 msgid "" "Maximum time in seconds to allow project updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " @@ -1703,76 +2027,78 @@ msgstr "" "プロジェクト更新の実行可能な最大時間 (秒数)。値 0 " "が設定されている場合はタイムアウトを設定できないことを示します。個別のプロジェクトに設定されるタイムアウトはこれを上書きします。" -#: awx/main/conf.py:337 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "ホストあたりの Ansible ファクトキャッシュのタイムアウト" -#: awx/main/conf.py:338 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be" " accessible by a playbook. Note, this does not influence the deletion of " -"ansible_facts from the database." +"ansible_facts from the database. Use a value of 0 to indicate that no " +"timeout should be imposed." msgstr "" "保存される Ansible ファクトが最後に変更されてから有効とみなされる最大時間 (秒数) です。有効な新規のファクトのみが Playbook " -"でアクセスできます。ansible_facts のデータベースからの削除はこれによる影響を受けません。" +"でアクセスできます。ansible_facts のデータベースからの削除はこれによる影響を受けません。タイムアウトが設定されないことを示すには 0 " +"の値を使用します。" -#: awx/main/conf.py:350 +#: awx/main/conf.py:377 msgid "Logging Aggregator" msgstr "ログアグリゲーター" -#: awx/main/conf.py:351 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." msgstr "外部ログの送信先のホスト名/IP" -#: awx/main/conf.py:352 awx/main/conf.py:363 awx/main/conf.py:375 -#: awx/main/conf.py:385 awx/main/conf.py:397 awx/main/conf.py:412 -#: awx/main/conf.py:424 awx/main/conf.py:433 awx/main/conf.py:443 -#: awx/main/conf.py:453 awx/main/conf.py:464 awx/main/conf.py:476 -#: awx/main/conf.py:489 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:480 awx/main/conf.py:491 awx/main/conf.py:503 +#: awx/main/conf.py:516 msgid "Logging" msgstr "ロギング" -#: awx/main/conf.py:360 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "ログアグリゲーターポート" -#: awx/main/conf.py:361 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." msgstr "ログの送信先のログアグリゲーターのポート (必要な場合。ログアグリゲーターでは指定されません)。" -#: awx/main/conf.py:373 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" msgstr "ログアグリゲーターのタイプ" -#: awx/main/conf.py:374 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." msgstr "選択されたログアグリゲーターのメッセージのフォーマット。" -#: awx/main/conf.py:383 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "ログアグリゲーターのユーザー名" -#: awx/main/conf.py:384 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "外部ログアグリゲーターのユーザー名 (必要な場合)。" -#: awx/main/conf.py:395 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "ログアグリゲーターのパスワード/トークン" -#: awx/main/conf.py:396 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "外部ログアグリゲーターのパスワードまたは認証トークン (必要な場合)。" -#: awx/main/conf.py:405 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "ログアグリゲーターフォームにデータを送信するロガー" -#: awx/main/conf.py:406 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include any or all of: \n" "awx - service logs\n" @@ -1786,59 +2112,59 @@ msgstr "" "job_events - Ansible ジョブイベントからのコールバックデータ\n" "system_tracking - スキャンジョブから生成されるファクト" -#: awx/main/conf.py:419 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" msgstr "ログシステムによるファクトの個別トラッキング" -#: awx/main/conf.py:420 +#: awx/main/conf.py:447 msgid "" -"If set, system tracking facts will be sent for each package, service, " -"orother item found in a scan, allowing for greater search query granularity." -" If unset, facts will be sent as a single dictionary, allowing for greater " +"If set, system tracking facts will be sent for each package, service, or " +"other item found in a scan, allowing for greater search query granularity. " +"If unset, facts will be sent as a single dictionary, allowing for greater " "efficiency in fact processing." msgstr "" -"設定されている場合、スキャンで見つかる各パッケージ、サービスその他の項目についてのサービスシステムトラッキングのファクトが送信され、検索クエリーの詳細度が上がります。設定されていない場合、ファクトは単一辞書として送信され、ファクトの処理の効率が上がります。" +"設定されている場合、スキャンで見つかる各パッケージ、サービスその他の項目についてのシステムトラッキングのファクトが送信され、検索クエリーの詳細度が上がります。設定されていない場合、ファクトは単一辞書として送信され、ファクトの処理の効率が上がります。" -#: awx/main/conf.py:431 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "外部ログの有効化" -#: awx/main/conf.py:432 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "外部ログアグリゲーターへのログ送信の有効化" -#: awx/main/conf.py:441 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "クラスター全体での Tower 固有識別子。" -#: awx/main/conf.py:442 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." msgstr "Tower インスタンスを一意に識別するのに役立ちます。" -#: awx/main/conf.py:451 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "ログアグリゲーターのプロトコル" -#: awx/main/conf.py:452 +#: awx/main/conf.py:479 msgid "Protocol used to communicate with log aggregator." msgstr "ログアグリゲーターとの通信に使用されるプロトコルです。" -#: awx/main/conf.py:460 +#: awx/main/conf.py:487 msgid "TCP Connection Timeout" msgstr "TCP 接続のタイムアウト" -#: awx/main/conf.py:461 +#: awx/main/conf.py:488 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." msgstr "" "外部ログアグリゲーターへの TCP 接続がタイムアウトする秒数です。HTTPS および TCP ログアグリゲータープロトコルに適用されます。" -#: awx/main/conf.py:471 +#: awx/main/conf.py:498 msgid "Enable/disable HTTPS certificate verification" msgstr "HTTPS 証明書の検証を有効化/無効化" -#: awx/main/conf.py:472 +#: awx/main/conf.py:499 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -1849,11 +2175,11 @@ msgstr "" "が「https」の場合の証明書の検証の有効化/無効化を制御するフラグです。有効にされている場合、Tower " "のログハンドラーは接続を確立する前に外部ログアグリゲーターによって送信される証明書を検証します。" -#: awx/main/conf.py:484 +#: awx/main/conf.py:511 msgid "Logging Aggregator Level Threshold" msgstr "ログアグリゲーターレベルのしきい値" -#: awx/main/conf.py:485 +#: awx/main/conf.py:512 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -1864,103 +2190,157 @@ msgstr "" "になります。しきい値より重大度の低いメッセージはログハンドラーによって無視されます (カテゴリー awx.anlytics " "の下にあるメッセージはこの設定を無視します)。" -#: awx/main/conf.py:508 awx/sso/conf.py:1105 +#: awx/main/conf.py:535 awx/sso/conf.py:1262 msgid "\n" msgstr "\n" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Sudo" msgstr "Sudo" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Su" msgstr "Su" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pbrun" msgstr "Pbrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pfexec" msgstr "Pfexec" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "DZDO" msgstr "DZDO" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Pmrun" msgstr "Pmrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Runas" msgstr "Runas" -#: awx/main/fields.py:57 -#, python-format -msgid "'%s' is not one of ['%s']" -msgstr "'%s' は ['%s'] のいずれでもありません。" +#: awx/main/constants.py:19 +msgid "Enable" +msgstr "有効化" -#: awx/main/fields.py:533 +#: awx/main/constants.py:19 +msgid "Doas" +msgstr "Doas" + +#: awx/main/constants.py:21 +msgid "None" +msgstr "なし" + +#: awx/main/fields.py:62 +#, python-brace-format +msgid "'{value}' is not one of ['{allowed_values}']" +msgstr "'{value}' は ['{allowed_values}'] のいずれでもありません" + +#: awx/main/fields.py:421 +#, python-brace-format +msgid "{type} provided in relative path {path}, expected {expected_type}" +msgstr "相対パス {path} で指定される {type} です。{expected_type} が予想されます。" + +#: awx/main/fields.py:426 +#, python-brace-format +msgid "{type} provided, expected {expected_type}" +msgstr "{type} が指定されます。{expected_type} が予想されます。" + +#: awx/main/fields.py:431 +#, python-brace-format +msgid "Schema validation error in relative path {path} ({error})" +msgstr "相対パス {path} でスキーマの検証エラーが生じました ({error})" + +#: awx/main/fields.py:552 +msgid "secret values must be of type string, not {}" +msgstr "シークレットの値は文字列型にする必要があります ({}ではない)" + +#: awx/main/fields.py:587 #, python-format msgid "cannot be set unless \"%s\" is set" msgstr "\"%s\" が設定されていない場合は設定できません" -#: awx/main/fields.py:549 +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "%s に必須です" -#: awx/main/fields.py:573 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "SSH キーが暗号化されている場合に設定する必要があります。" -#: awx/main/fields.py:579 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "SSH キーが暗号化されていない場合は設定できません。" -#: awx/main/fields.py:637 +#: awx/main/fields.py:691 msgid "'dependencies' is not supported for custom credentials." msgstr "「dependencies (依存関係)」はカスタム認証情報の場合にはサポートされません。" -#: awx/main/fields.py:651 +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "「tower」は予約されたフィールド名です" -#: awx/main/fields.py:658 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "フィールド ID は固有でなければなりません (%s)" -#: awx/main/fields.py:671 -#, python-format -msgid "%s not allowed for %s type (%s)" -msgstr "%s は %s タイプに許可されません (%s)" +#: awx/main/fields.py:725 +msgid "become_method is a reserved type name" +msgstr "become_method は予約された型名です。" -#: awx/main/fields.py:755 -#, python-format -msgid "%s uses an undefined field (%s)" -msgstr "%s は未定義のフィールドを使用します (%s)" +#: awx/main/fields.py:736 +#, python-brace-format +msgid "{sub_key} not allowed for {element_type} type ({element_id})" +msgstr "{sub_key} は {element_type} 型の ({element_id}) には許可されません" -#: awx/main/middleware.py:157 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "`tower.filename`を参照するために名前が設定されていないファイルインジェクターを定義する必要があります。" + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "予約された `tower` 名前空間コンテナーを直接参照することができません。" + +#: awx/main/fields.py:827 +msgid "Must use multi-file syntax when injecting multiple files" +msgstr "複数ファイルの挿入時に複数ファイル構文を使用する必要があります。" + +#: awx/main/fields.py:844 +#, python-brace-format +msgid "{sub_key} uses an undefined field ({error_msg})" +msgstr "{sub_key} は未定義フィールド ({error_msg}) を使用します。" + +#: awx/main/fields.py:851 +#, python-brace-format +msgid "" +"Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" +msgstr "{type} 内で {sub_key} のテンプレートのレンダリング中に構文エラーが発生しました({error_msg}) " + +#: awx/main/middleware.py:146 msgid "Formats of all available named urls" msgstr "利用可能なすべての名前付き url の形式" -#: awx/main/middleware.py:158 +#: awx/main/middleware.py:147 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." msgstr "名前付き URL を持つ利用可能なすべての標準形式を表示するキーと値のペアの読み取り専用リストです。" -#: awx/main/middleware.py:160 awx/main/middleware.py:170 +#: awx/main/middleware.py:149 awx/main/middleware.py:159 msgid "Named URL" msgstr "名前付き URL" -#: awx/main/middleware.py:167 +#: awx/main/middleware.py:156 msgid "List of all named url graph nodes." msgstr "すべての名前付き URL グラフノードの一覧です。" -#: awx/main/middleware.py:168 +#: awx/main/middleware.py:157 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use" " this list to programmatically generate named URLs for resources" @@ -1968,31 +2348,35 @@ msgstr "" "名前付き URL グラフトポロジーを公開するキーと値のペアの読み取り専用一覧です。この一覧を使用してリソースの名前付き URL " "をプログラムで生成します。" -#: awx/main/migrations/_reencrypt.py:25 awx/main/models/notifications.py:33 +#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:35 msgid "Email" msgstr "メール" -#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:34 +#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:36 msgid "Slack" msgstr "Slack" -#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:35 +#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:37 msgid "Twilio" msgstr "Twilio" -#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:36 +#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:38 msgid "Pagerduty" msgstr "Pagerduty" -#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:37 +#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:39 msgid "HipChat" msgstr "HipChat" -#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:38 +#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:41 +msgid "Mattermost" +msgstr "Mattermost" + +#: awx/main/migrations/_reencrypt.py:32 awx/main/models/notifications.py:40 msgid "Webhook" msgstr "Webhook" -#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:39 +#: awx/main/migrations/_reencrypt.py:33 awx/main/models/notifications.py:43 msgid "IRC" msgstr "IRC" @@ -2016,204 +2400,181 @@ msgstr "エンティティーの別のエンティティーへの関連付け" msgid "Entity was Disassociated with another Entity" msgstr "エンティティーの別のエンティティーとの関連付けの解除" -#: awx/main/models/ad_hoc_commands.py:100 +#: awx/main/models/ad_hoc_commands.py:95 msgid "No valid inventory." msgstr "有効なインベントリーはありません。" -#: awx/main/models/ad_hoc_commands.py:107 +#: awx/main/models/ad_hoc_commands.py:102 msgid "You must provide a machine / SSH credential." msgstr "マシン/SSH 認証情報を入力してください。" -#: awx/main/models/ad_hoc_commands.py:118 -#: awx/main/models/ad_hoc_commands.py:126 +#: awx/main/models/ad_hoc_commands.py:113 +#: awx/main/models/ad_hoc_commands.py:121 msgid "Invalid type for ad hoc command" msgstr "アドホックコマンドの無効なタイプ" -#: awx/main/models/ad_hoc_commands.py:121 +#: awx/main/models/ad_hoc_commands.py:116 msgid "Unsupported module for ad hoc commands." msgstr "アドホックコマンドのサポートされていないモジュール。" -#: awx/main/models/ad_hoc_commands.py:129 +#: awx/main/models/ad_hoc_commands.py:124 #, python-format msgid "No argument passed to %s module." msgstr "%s モジュールに渡される引数はありません。" -#: awx/main/models/ad_hoc_commands.py:245 awx/main/models/jobs.py:911 -msgid "Host Failed" -msgstr "ホストの失敗" - -#: awx/main/models/ad_hoc_commands.py:246 awx/main/models/jobs.py:912 -msgid "Host OK" -msgstr "ホスト OK" - -#: awx/main/models/ad_hoc_commands.py:247 awx/main/models/jobs.py:915 -msgid "Host Unreachable" -msgstr "ホストに到達できません" - -#: awx/main/models/ad_hoc_commands.py:252 awx/main/models/jobs.py:914 -msgid "Host Skipped" -msgstr "ホストがスキップされました" - -#: awx/main/models/ad_hoc_commands.py:262 awx/main/models/jobs.py:942 -msgid "Debug" -msgstr "デバッグ" - -#: awx/main/models/ad_hoc_commands.py:263 awx/main/models/jobs.py:943 -msgid "Verbose" -msgstr "詳細" - -#: awx/main/models/ad_hoc_commands.py:264 awx/main/models/jobs.py:944 -msgid "Deprecated" -msgstr "非推奨" - -#: awx/main/models/ad_hoc_commands.py:265 awx/main/models/jobs.py:945 -msgid "Warning" -msgstr "警告" - -#: awx/main/models/ad_hoc_commands.py:266 awx/main/models/jobs.py:946 -msgid "System Warning" -msgstr "システム警告" - -#: awx/main/models/ad_hoc_commands.py:267 awx/main/models/jobs.py:947 -#: awx/main/models/unified_jobs.py:64 -msgid "Error" -msgstr "エラー" - -#: awx/main/models/base.py:40 awx/main/models/base.py:46 -#: awx/main/models/base.py:51 +#: awx/main/models/base.py:33 awx/main/models/base.py:39 +#: awx/main/models/base.py:44 awx/main/models/base.py:49 msgid "Run" msgstr "実行" -#: awx/main/models/base.py:41 awx/main/models/base.py:47 -#: awx/main/models/base.py:52 +#: awx/main/models/base.py:34 awx/main/models/base.py:40 +#: awx/main/models/base.py:45 awx/main/models/base.py:50 msgid "Check" msgstr "チェック" -#: awx/main/models/base.py:42 +#: awx/main/models/base.py:35 msgid "Scan" msgstr "スキャン" -#: awx/main/models/credential.py:86 +#: awx/main/models/credential/__init__.py:110 msgid "Host" msgstr "ホスト" -#: awx/main/models/credential.py:87 +#: awx/main/models/credential/__init__.py:111 msgid "The hostname or IP address to use." msgstr "使用するホスト名または IP アドレス。" -#: awx/main/models/credential.py:93 +#: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "ユーザー名" -#: awx/main/models/credential.py:94 +#: awx/main/models/credential/__init__.py:118 msgid "Username for this credential." msgstr "この認証情報のユーザー名。" -#: awx/main/models/credential.py:100 +#: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "パスワード" -#: awx/main/models/credential.py:101 +#: awx/main/models/credential/__init__.py:125 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." msgstr "この認証情報のパスワード (またはマシンの認証情報を求めるプロンプトを出すには 「ASK」)。" -#: awx/main/models/credential.py:108 +#: awx/main/models/credential/__init__.py:132 msgid "Security Token" msgstr "セキュリティートークン" -#: awx/main/models/credential.py:109 +#: awx/main/models/credential/__init__.py:133 msgid "Security Token for this credential" msgstr "この認証情報のセキュリティートークン" -#: awx/main/models/credential.py:115 +#: awx/main/models/credential/__init__.py:139 msgid "Project" msgstr "プロジェクト" -#: awx/main/models/credential.py:116 +#: awx/main/models/credential/__init__.py:140 msgid "The identifier for the project." msgstr "プロジェクトの識別子。" -#: awx/main/models/credential.py:122 +#: awx/main/models/credential/__init__.py:146 msgid "Domain" msgstr "ドメイン" -#: awx/main/models/credential.py:123 +#: awx/main/models/credential/__init__.py:147 msgid "The identifier for the domain." msgstr "ドメインの識別子。" -#: awx/main/models/credential.py:128 +#: awx/main/models/credential/__init__.py:152 msgid "SSH private key" msgstr "SSH 秘密鍵" -#: awx/main/models/credential.py:129 +#: awx/main/models/credential/__init__.py:153 msgid "RSA or DSA private key to be used instead of password." msgstr "パスワードの代わりに使用される RSA または DSA 秘密鍵。" -#: awx/main/models/credential.py:135 +#: awx/main/models/credential/__init__.py:159 msgid "SSH key unlock" msgstr "SSH キーのロック解除" -#: awx/main/models/credential.py:136 +#: awx/main/models/credential/__init__.py:160 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." msgstr "" "暗号化されている場合は SSH 秘密鍵のロックを解除するためのパスフレーズ (またはマシンの認証情報を求めるプロンプトを出すには「ASK」)。" -#: awx/main/models/credential.py:143 -msgid "None" -msgstr "なし" - -#: awx/main/models/credential.py:144 +#: awx/main/models/credential/__init__.py:168 msgid "Privilege escalation method." msgstr "権限昇格メソッド。" -#: awx/main/models/credential.py:150 +#: awx/main/models/credential/__init__.py:174 msgid "Privilege escalation username." msgstr "権限昇格ユーザー名。" -#: awx/main/models/credential.py:156 +#: awx/main/models/credential/__init__.py:180 msgid "Password for privilege escalation method." msgstr "権限昇格メソッドのパスワード。" -#: awx/main/models/credential.py:162 +#: awx/main/models/credential/__init__.py:186 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "Vault パスワード (またはユーザーにプロンプトを出すには「ASK」)。" -#: awx/main/models/credential.py:166 +#: awx/main/models/credential/__init__.py:190 msgid "Whether to use the authorize mechanism." msgstr "認証メカニズムを使用するかどうか。" -#: awx/main/models/credential.py:172 +#: awx/main/models/credential/__init__.py:196 msgid "Password used by the authorize mechanism." msgstr "認証メカニズムで使用されるパスワード。" -#: awx/main/models/credential.py:178 +#: awx/main/models/credential/__init__.py:202 msgid "Client Id or Application Id for the credential" msgstr "認証情報のクライアント ID またはアプリケーション ID" -#: awx/main/models/credential.py:184 +#: awx/main/models/credential/__init__.py:208 msgid "Secret Token for this credential" msgstr "この認証情報のシークレットトークン" -#: awx/main/models/credential.py:190 +#: awx/main/models/credential/__init__.py:214 msgid "Subscription identifier for this credential" msgstr "この認証情報のサブスクリプション識別子" -#: awx/main/models/credential.py:196 +#: awx/main/models/credential/__init__.py:220 msgid "Tenant identifier for this credential" msgstr "この認証情報のテナント識別子" -#: awx/main/models/credential.py:220 +#: awx/main/models/credential/__init__.py:244 msgid "" "Specify the type of credential you want to create. Refer to the Ansible " "Tower documentation for details on each type." msgstr "" "作成する必要のある証明書のタイプを指定します。それぞれのタイプの詳細については、Ansible Tower ドキュメントを参照してください。" -#: awx/main/models/credential.py:234 awx/main/models/credential.py:420 +#: awx/main/models/credential/__init__.py:258 +#: awx/main/models/credential/__init__.py:476 msgid "" "Enter inputs using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2222,31 +2583,36 @@ msgstr "" "JSON または YAML 構文のいずれかを使用して入力を行います。ラジオボタンを使用してこれらの間で切り替えを行います。構文のサンプルについては " "Ansible Tower ドキュメントを参照してください。" -#: awx/main/models/credential.py:401 +#: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "マシン" -#: awx/main/models/credential.py:402 +#: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "Vault" -#: awx/main/models/credential.py:403 +#: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "ネットワーク" -#: awx/main/models/credential.py:404 +#: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "ソースコントロール" -#: awx/main/models/credential.py:405 +#: awx/main/models/credential/__init__.py:461 msgid "Cloud" msgstr "クラウド" -#: awx/main/models/credential.py:406 +#: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "Insights" -#: awx/main/models/credential.py:427 +#: awx/main/models/credential/__init__.py:483 msgid "" "Enter injectors using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2256,11 +2622,397 @@ msgstr "" "構文のいずれかを使用してインジェクターを入力します。ラジオボタンを使用してこれらの間で切り替えを行います。構文のサンプルについては Ansible " "Tower ドキュメントを参照してください。" -#: awx/main/models/credential.py:478 +#: awx/main/models/credential/__init__.py:534 #, python-format msgid "adding %s credential type" msgstr "%s 認証情報タイプの追加" +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "SSH 秘密鍵" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "秘密鍵のパスフレーズ" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "権限昇格方法" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying" +" the --become-method Ansible parameter." +msgstr "「become」操作の方式を指定します。これは --become-method Ansible パラメーターを指定することに相当します。" + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "権限昇格ユーザー名" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "権限昇格パスワード" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "SCM 秘密鍵" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "Vault パスワード" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "Vault ID" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the " +"--vault-id Ansible parameter for providing multiple Vault passwords. Note: " +"this feature only works in Ansible 2.4+." +msgstr "" +"(オプションの) Vault ID を指定します。これは、複数の Vault パスワードを指定するために --vault-id Ansible " +"パラメーターを指定することに相当します。注: この機能は Ansible 2.4+ でのみ機能します。" + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "認証" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "認証パスワード" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "Amazon Web Services" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "アクセスキー" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "シークレットキー" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "STS トークン" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"Management (IAM) users." +msgstr "" +"セキュリティートークンサービス (STS) は、AWS Identity and Access Management (IAM) " +"ユーザーの一時的な、権限の制限された認証情報を要求できる web サービスです。" + +#: awx/main/models/credential/__init__.py:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "OpenStack" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "パスワード (API キー)" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "ホスト (認証 URL)" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, " +"https://openstack.business.com/v2.0/" +msgstr "認証に使用するホスト。例: https://openstack.business.com/v2.0/" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "プロジェクト (テナント名)" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "ドメイン名" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" +"OpenStack ドメインは管理上の境界を定義します。これは Keystone v3 認証 URL にのみ必要です。共通するシナリオについては " +"Ansible Tower ドキュメントを参照してください。" + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "VMware vCenter" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "vCenter ホスト" + +#: awx/main/models/credential/__init__.py:926 +msgid "" +"Enter the hostname or IP address that corresponds to your VMware vCenter." +msgstr "VMware vCenter に対応するホスト名または IP アドレスを入力します。" + +#: awx/main/models/credential/__init__.py:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "Red Hat Satellite 6" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "Satellite 6 URL" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"example, https://satellite.example.org" +msgstr "" +"Red Hat Satellite 6 Server に対応する URL を入力します (例: " +"https://satellite.example.org)。" + +#: awx/main/models/credential/__init__.py:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "Red Hat CloudForms" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "CloudForms URL" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"instance. For example, https://cloudforms.example.org" +msgstr "" +"CloudForms インスタンスに対応する仮想マシンの URL を入力します (例: https://cloudforms.example.org)。" + +#: awx/main/models/credential/__init__.py:1004 +#: awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "Google Compute Engine" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "サービスアカウントのメールアドレス" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "Google Compute Engine サービスアカウントに割り当てられたメールアドレス。" + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" +"プロジェクト ID は GCE によって割り当てられる識別情報です。これは 3 語か、または 2 語とそれに続く 3 " +"桁の数字のいずれかで構成されます。例: project-id-000、another-project-id" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "RSA 秘密鍵" + +#: awx/main/models/credential/__init__.py:1028 +msgid "" +"Paste the contents of the PEM file associated with the service account " +"email." +msgstr "サービスアカウントメールに関連付けられた PEM ファイルの内容を貼り付けます。" + +#: awx/main/models/credential/__init__.py:1040 +#: awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "Microsoft Azure Resource Manager" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "サブスクリプション ID" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "サブスクリプション ID は、ユーザー名にマップされる Azure コンストラクトです。" + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "クライアント ID" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "テナント ID" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "Azure クラウド環境" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "Azure GovCloud または Azure スタック使用時の環境変数 AZURE_CLOUD_ENVIRONMENT。" + +#: awx/main/models/credential/__init__.py:1115 +#: awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "Red Hat Virtualization" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "認証に使用するホスト。" + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "CA ファイル" + +#: awx/main/models/credential/__init__.py:1136 +msgid "Absolute file path to the CA file to use (optional)" +msgstr "使用する CA ファイルへの絶対ファイルパス (オプション)" + +#: awx/main/models/credential/__init__.py:1167 +#: awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "Ansible Tower" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "Ansible Tower ホスト名" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "認証で使用する Ansible Tower ベース URL。" + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "SSL の検証" + +#: awx/main/models/events.py:89 awx/main/models/events.py:608 +msgid "Host Failed" +msgstr "ホストの失敗" + +#: awx/main/models/events.py:90 awx/main/models/events.py:609 +msgid "Host OK" +msgstr "ホスト OK" + +#: awx/main/models/events.py:91 +msgid "Host Failure" +msgstr "ホストの失敗" + +#: awx/main/models/events.py:92 awx/main/models/events.py:615 +msgid "Host Skipped" +msgstr "ホストがスキップされました" + +#: awx/main/models/events.py:93 awx/main/models/events.py:610 +msgid "Host Unreachable" +msgstr "ホストに到達できません" + +#: awx/main/models/events.py:94 awx/main/models/events.py:108 +msgid "No Hosts Remaining" +msgstr "残りのホストがありません" + +#: awx/main/models/events.py:95 +msgid "Host Polling" +msgstr "ホストのポーリング" + +#: awx/main/models/events.py:96 +msgid "Host Async OK" +msgstr "ホストの非同期 OK" + +#: awx/main/models/events.py:97 +msgid "Host Async Failure" +msgstr "ホストの非同期失敗" + +#: awx/main/models/events.py:98 +msgid "Item OK" +msgstr "項目 OK" + +#: awx/main/models/events.py:99 +msgid "Item Failed" +msgstr "項目の失敗" + +#: awx/main/models/events.py:100 +msgid "Item Skipped" +msgstr "項目のスキップ" + +#: awx/main/models/events.py:101 +msgid "Host Retry" +msgstr "ホストの再試行" + +#: awx/main/models/events.py:103 +msgid "File Difference" +msgstr "ファイルの相違点" + +#: awx/main/models/events.py:104 +msgid "Playbook Started" +msgstr "Playbook の開始" + +#: awx/main/models/events.py:105 +msgid "Running Handlers" +msgstr "実行中のハンドラー" + +#: awx/main/models/events.py:106 +msgid "Including File" +msgstr "組み込みファイル" + +#: awx/main/models/events.py:107 +msgid "No Hosts Matched" +msgstr "一致するホストがありません" + +#: awx/main/models/events.py:109 +msgid "Task Started" +msgstr "タスクの開始" + +#: awx/main/models/events.py:111 +msgid "Variables Prompted" +msgstr "変数のプロモート" + +#: awx/main/models/events.py:112 +msgid "Gathering Facts" +msgstr "ファクトの収集" + +#: awx/main/models/events.py:113 +msgid "internal: on Import for Host" +msgstr "内部: ホストのインポート時" + +#: awx/main/models/events.py:114 +msgid "internal: on Not Import for Host" +msgstr "内部: ホストの非インポート時" + +#: awx/main/models/events.py:115 +msgid "Play Started" +msgstr "プレイの開始" + +#: awx/main/models/events.py:116 +msgid "Playbook Complete" +msgstr "Playbook の完了" + +#: awx/main/models/events.py:120 awx/main/models/events.py:625 +msgid "Debug" +msgstr "デバッグ" + +#: awx/main/models/events.py:121 awx/main/models/events.py:626 +msgid "Verbose" +msgstr "詳細" + +#: awx/main/models/events.py:122 awx/main/models/events.py:627 +msgid "Deprecated" +msgstr "非推奨" + +#: awx/main/models/events.py:123 awx/main/models/events.py:628 +msgid "Warning" +msgstr "警告" + +#: awx/main/models/events.py:124 awx/main/models/events.py:629 +msgid "System Warning" +msgstr "システム警告" + +#: awx/main/models/events.py:125 awx/main/models/events.py:630 +#: awx/main/models/unified_jobs.py:67 +msgid "Error" +msgstr "エラー" + #: awx/main/models/fact.py:25 msgid "Host for the facts that the fact scan captured." msgstr "ファクトスキャンがキャプチャーしたファクトのホスト。" @@ -2275,348 +3027,335 @@ msgid "" "host." msgstr "単一ホストのタイムスタンプでキャプチャーされるモジュールファクトの任意の JSON 構造。" -#: awx/main/models/ha.py:78 +#: awx/main/models/ha.py:153 msgid "Instances that are members of this InstanceGroup" msgstr "このインスタンスグループのメンバーであるインスタンス" -#: awx/main/models/ha.py:83 +#: awx/main/models/ha.py:158 msgid "Instance Group to remotely control this group." msgstr "このグループをリモートで制御するためのインスタンスグループ" -#: awx/main/models/inventory.py:52 +#: awx/main/models/ha.py:165 +msgid "Percentage of Instances to automatically assign to this group" +msgstr "このグループに自動的に割り当てるインスタンスのパーセンテージ" + +#: awx/main/models/ha.py:169 +msgid "" +"Static minimum number of Instances to automatically assign to this group" +msgstr "このグループに自動的に割り当てるインスタンスの静的な最小数。" + +#: awx/main/models/ha.py:174 +msgid "" +"List of exact-match Instances that will always be automatically assigned to " +"this group" +msgstr "このグループに常に自動的に割り当てられる完全一致のインスタンスの一覧" + +#: awx/main/models/inventory.py:61 msgid "Hosts have a direct link to this inventory." msgstr "ホストにはこのインベントリーへの直接のリンクがあります。" -#: awx/main/models/inventory.py:53 +#: awx/main/models/inventory.py:62 msgid "Hosts for inventory generated using the host_filter property." msgstr "host_filter プロパティーを使用して生成されたインベントリーのホスト。" -#: awx/main/models/inventory.py:58 +#: awx/main/models/inventory.py:67 msgid "inventories" msgstr "インベントリー" -#: awx/main/models/inventory.py:65 +#: awx/main/models/inventory.py:74 msgid "Organization containing this inventory." msgstr "このインベントリーを含む組織。" -#: awx/main/models/inventory.py:72 +#: awx/main/models/inventory.py:81 msgid "Inventory variables in JSON or YAML format." msgstr "JSON または YAML 形式のインベントリー変数。" -#: awx/main/models/inventory.py:77 +#: awx/main/models/inventory.py:86 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "このインベントリーのホストが失敗したかどうかを示すフラグ。" -#: awx/main/models/inventory.py:82 +#: awx/main/models/inventory.py:91 msgid "Total number of hosts in this inventory." msgstr "このインべントリー内のホストの合計数。" -#: awx/main/models/inventory.py:87 +#: awx/main/models/inventory.py:96 msgid "Number of hosts in this inventory with active failures." msgstr "アクティブなエラーのあるこのインベントリー内のホストの数。" -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:101 msgid "Total number of groups in this inventory." msgstr "このインべントリー内のグループの合計数。" -#: awx/main/models/inventory.py:97 +#: awx/main/models/inventory.py:106 msgid "Number of groups in this inventory with active failures." msgstr "アクティブなエラーのあるこのインベントリー内のグループの数。" -#: awx/main/models/inventory.py:102 +#: awx/main/models/inventory.py:111 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "このインベントリーに外部のインベントリーソースがあるかどうかを示すフラグ。" -#: awx/main/models/inventory.py:107 +#: awx/main/models/inventory.py:116 msgid "" "Total number of external inventory sources configured within this inventory." msgstr "このインベントリー内で設定される外部インベントリーソースの合計数。" -#: awx/main/models/inventory.py:112 +#: awx/main/models/inventory.py:121 msgid "Number of external inventory sources in this inventory with failures." msgstr "エラーのあるこのインベントリー内の外部インベントリーソースの数。" -#: awx/main/models/inventory.py:119 +#: awx/main/models/inventory.py:128 msgid "Kind of inventory being represented." msgstr "表示されているインベントリーの種類。" -#: awx/main/models/inventory.py:125 +#: awx/main/models/inventory.py:134 msgid "Filter that will be applied to the hosts of this inventory." msgstr "このインべントリーのホストに適用されるフィルター。" -#: awx/main/models/inventory.py:152 +#: awx/main/models/inventory.py:161 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:161 +#: awx/main/models/inventory.py:170 msgid "Flag indicating the inventory is being deleted." msgstr "このインベントリーが削除されていることを示すフラグ。" -#: awx/main/models/inventory.py:374 +#: awx/main/models/inventory.py:459 msgid "Assignment not allowed for Smart Inventory" msgstr "割り当てはスマートインベントリーでは許可されません" -#: awx/main/models/inventory.py:376 awx/main/models/projects.py:148 +#: awx/main/models/inventory.py:461 awx/main/models/projects.py:159 msgid "Credential kind must be 'insights'." msgstr "認証情報の種類は「insights」である必要があります。" -#: awx/main/models/inventory.py:443 +#: awx/main/models/inventory.py:546 msgid "Is this host online and available for running jobs?" msgstr "このホストはオンラインで、ジョブを実行するために利用できますか?" -#: awx/main/models/inventory.py:449 +#: awx/main/models/inventory.py:552 msgid "" "The value used by the remote inventory source to uniquely identify the host" msgstr "ホストを一意に識別するためにリモートインベントリーソースで使用される値" -#: awx/main/models/inventory.py:454 +#: awx/main/models/inventory.py:557 msgid "Host variables in JSON or YAML format." msgstr "JSON または YAML 形式のホスト変数。" -#: awx/main/models/inventory.py:476 +#: awx/main/models/inventory.py:579 msgid "Flag indicating whether the last job failed for this host." msgstr "このホストの最後のジョブが失敗したかどうかを示すフラグ。" -#: awx/main/models/inventory.py:481 +#: awx/main/models/inventory.py:584 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." msgstr "このホストが外部インベントリーソースから作成/更新されたかどうかを示すフラグ。" -#: awx/main/models/inventory.py:487 +#: awx/main/models/inventory.py:590 msgid "Inventory source(s) that created or modified this host." msgstr "このホストを作成または変更したインベントリーソース。" -#: awx/main/models/inventory.py:492 +#: awx/main/models/inventory.py:595 msgid "Arbitrary JSON structure of most recent ansible_facts, per-host." msgstr "ホスト別の最新 ansible_facts の任意の JSON 構造。" -#: awx/main/models/inventory.py:498 +#: awx/main/models/inventory.py:601 msgid "The date and time ansible_facts was last modified." msgstr "ansible_facts の最終変更日時。" -#: awx/main/models/inventory.py:505 +#: awx/main/models/inventory.py:608 msgid "Red Hat Insights host unique identifier." msgstr "Red Hat Insights ホスト固有 ID。" -#: awx/main/models/inventory.py:633 +#: awx/main/models/inventory.py:743 msgid "Group variables in JSON or YAML format." msgstr "JSON または YAML 形式のグループ変数。" -#: awx/main/models/inventory.py:639 +#: awx/main/models/inventory.py:749 msgid "Hosts associated directly with this group." msgstr "このグループに直接関連付けられたホスト。" -#: awx/main/models/inventory.py:644 +#: awx/main/models/inventory.py:754 msgid "Total number of hosts directly or indirectly in this group." msgstr "このグループに直接的または間接的に属するホストの合計数。" -#: awx/main/models/inventory.py:649 +#: awx/main/models/inventory.py:759 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "このグループにアクティブなエラーのあるホストがあるかどうかを示すフラグ。" -#: awx/main/models/inventory.py:654 +#: awx/main/models/inventory.py:764 msgid "Number of hosts in this group with active failures." msgstr "アクティブなエラーのあるこのグループ内のホストの数。" -#: awx/main/models/inventory.py:659 +#: awx/main/models/inventory.py:769 msgid "Total number of child groups contained within this group." msgstr "このグループに含まれる子グループの合計数。" -#: awx/main/models/inventory.py:664 +#: awx/main/models/inventory.py:774 msgid "Number of child groups within this group that have active failures." msgstr "アクティブなエラーのあるこのグループ内の子グループの数。" -#: awx/main/models/inventory.py:669 +#: awx/main/models/inventory.py:779 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." msgstr "このグループが外部インベントリーソースから作成/更新されたかどうかを示すフラグ。" -#: awx/main/models/inventory.py:675 +#: awx/main/models/inventory.py:785 msgid "Inventory source(s) that created or modified this group." msgstr "このグループを作成または変更したインベントリーソース。" -#: awx/main/models/inventory.py:865 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:428 +#: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "手動" -#: awx/main/models/inventory.py:866 +#: awx/main/models/inventory.py:982 msgid "File, Directory or Script" msgstr "ファイル、ディレクトリーまたはスクリプト" -#: awx/main/models/inventory.py:867 +#: awx/main/models/inventory.py:983 msgid "Sourced from a Project" msgstr "ソース: プロジェクト" -#: awx/main/models/inventory.py:868 +#: awx/main/models/inventory.py:984 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:869 -msgid "Google Compute Engine" -msgstr "Google Compute Engine" - -#: awx/main/models/inventory.py:870 -msgid "Microsoft Azure Resource Manager" -msgstr "Microsoft Azure Resource Manager" - -#: awx/main/models/inventory.py:871 -msgid "VMware vCenter" -msgstr "VMware vCenter" - -#: awx/main/models/inventory.py:872 -msgid "Red Hat Satellite 6" -msgstr "Red Hat Satellite 6" - -#: awx/main/models/inventory.py:873 -msgid "Red Hat CloudForms" -msgstr "Red Hat CloudForms" - -#: awx/main/models/inventory.py:874 -msgid "OpenStack" -msgstr "OpenStack" - -#: awx/main/models/inventory.py:875 -msgid "oVirt4" -msgstr "oVirt4" - -#: awx/main/models/inventory.py:876 -msgid "Ansible Tower" -msgstr "Ansible Tower" - -#: awx/main/models/inventory.py:877 +#: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "カスタムスクリプト" -#: awx/main/models/inventory.py:994 +#: awx/main/models/inventory.py:1110 msgid "Inventory source variables in YAML or JSON format." msgstr "YAML または JSON 形式のインベントリーソース変数。" -#: awx/main/models/inventory.py:1013 +#: awx/main/models/inventory.py:1121 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:1019 +#: awx/main/models/inventory.py:1127 msgid "Limit groups automatically created from inventory source (EC2 only)." msgstr "インベントリーソースから自動的に作成されるグループを制限します (EC2 のみ)。" -#: awx/main/models/inventory.py:1023 +#: awx/main/models/inventory.py:1131 msgid "Overwrite local groups and hosts from remote inventory source." msgstr "リモートインベントリーソースからのローカルグループおよびホストを上書きします。" -#: awx/main/models/inventory.py:1027 +#: awx/main/models/inventory.py:1135 msgid "Overwrite local variables from remote inventory source." msgstr "リモートインベントリーソースからのローカル変数を上書きします。" -#: awx/main/models/inventory.py:1032 awx/main/models/jobs.py:160 -#: awx/main/models/projects.py:117 +#: awx/main/models/inventory.py:1140 awx/main/models/jobs.py:140 +#: awx/main/models/projects.py:128 msgid "The amount of time (in seconds) to run before the task is canceled." msgstr "タスクが取り消される前の実行時間 (秒数)。" -#: awx/main/models/inventory.py:1065 +#: awx/main/models/inventory.py:1173 msgid "Image ID" msgstr "イメージ ID" -#: awx/main/models/inventory.py:1066 +#: awx/main/models/inventory.py:1174 msgid "Availability Zone" msgstr "アベイラビリティーゾーン" -#: awx/main/models/inventory.py:1067 +#: awx/main/models/inventory.py:1175 msgid "Account" msgstr "アカウント" -#: awx/main/models/inventory.py:1068 +#: awx/main/models/inventory.py:1176 msgid "Instance ID" msgstr "インスタンス ID" -#: awx/main/models/inventory.py:1069 +#: awx/main/models/inventory.py:1177 msgid "Instance State" msgstr "インスタンスの状態" -#: awx/main/models/inventory.py:1070 +#: awx/main/models/inventory.py:1178 +msgid "Platform" +msgstr "プラットフォーム" + +#: awx/main/models/inventory.py:1179 msgid "Instance Type" msgstr "インスタンスタイプ" -#: awx/main/models/inventory.py:1071 +#: awx/main/models/inventory.py:1180 msgid "Key Name" msgstr "キー名" -#: awx/main/models/inventory.py:1072 +#: awx/main/models/inventory.py:1181 msgid "Region" msgstr "リージョン" -#: awx/main/models/inventory.py:1073 +#: awx/main/models/inventory.py:1182 msgid "Security Group" msgstr "セキュリティーグループ" -#: awx/main/models/inventory.py:1074 +#: awx/main/models/inventory.py:1183 msgid "Tags" msgstr "タグ" -#: awx/main/models/inventory.py:1075 +#: awx/main/models/inventory.py:1184 msgid "Tag None" msgstr "タグ None" -#: awx/main/models/inventory.py:1076 +#: awx/main/models/inventory.py:1185 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1145 +#: awx/main/models/inventory.py:1253 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " "matching cloud service." msgstr "クラウドベースのインベントリーソース (%s など) には一致するクラウドサービスの認証情報が必要です。" -#: awx/main/models/inventory.py:1152 +#: awx/main/models/inventory.py:1259 msgid "Credential is required for a cloud source." msgstr "認証情報がクラウドソースに必要です。" -#: awx/main/models/inventory.py:1155 +#: awx/main/models/inventory.py:1262 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." msgstr "タイプがマシン、ソースコントロール、Insights および Vault の認証情報はカスタムインベントリーソースには許可されません。" -#: awx/main/models/inventory.py:1179 +#: awx/main/models/inventory.py:1314 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "無効な %(source)s リージョン: %(region)s" -#: awx/main/models/inventory.py:1203 +#: awx/main/models/inventory.py:1338 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "無効なフィルター式: %(filter)s" -#: awx/main/models/inventory.py:1224 +#: awx/main/models/inventory.py:1359 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "無効なグループ (選択による): %(choice)s" -#: awx/main/models/inventory.py:1259 +#: awx/main/models/inventory.py:1394 msgid "Project containing inventory file used as source." msgstr "ソースとして使用されるインベントリーファイルが含まれるプロジェクト。" -#: awx/main/models/inventory.py:1407 +#: awx/main/models/inventory.py:1555 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." msgstr "クラウド同期用にこの項目を設定できません。すでに %s によって管理されています。" -#: awx/main/models/inventory.py:1417 +#: awx/main/models/inventory.py:1565 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." msgstr "複数の SCM ベースのインベントリーソースについて、インベントリー別のプロジェクト更新時の更新は許可されません。" -#: awx/main/models/inventory.py:1424 +#: awx/main/models/inventory.py:1572 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " @@ -2625,24 +3364,25 @@ msgstr "" "プロジェクト更新時の更新に設定している場合、SCM " "ベースのインベントリーソースを更新できません。その代わりに起動時に更新するように対応するソースプロジェクトを設定します。" -#: awx/main/models/inventory.py:1430 -msgid "SCM type sources must set `overwrite_vars` to `true`." -msgstr "SCM タイプソースは「overwrite_vars」を「true」に設定する必要があります。" +#: awx/main/models/inventory.py:1579 +msgid "" +"SCM type sources must set `overwrite_vars` to `true` until Ansible 2.5." +msgstr "SCM タイプソースは「overwrite_vars」を「true」に設定する必要があります (Ansible 2.5 まで)。" -#: awx/main/models/inventory.py:1435 +#: awx/main/models/inventory.py:1584 msgid "Cannot set source_path if not SCM type." msgstr "SCM タイプでない場合 source_path を設定できません。" -#: awx/main/models/inventory.py:1460 +#: awx/main/models/inventory.py:1615 msgid "" "Inventory files from this Project Update were used for the inventory update." msgstr "このプロジェクト更新のインベントリーファイルがインベントリー更新に使用されました。" -#: awx/main/models/inventory.py:1573 +#: awx/main/models/inventory.py:1725 msgid "Inventory script contents" msgstr "インベントリースクリプトの内容" -#: awx/main/models/inventory.py:1578 +#: awx/main/models/inventory.py:1730 msgid "Organization owning this inventory script" msgstr "このインベントリースクリプトを所有する組織" @@ -2652,7 +3392,7 @@ msgid "" "shown in the standard output" msgstr "有効にされている場合、ホストのテンプレート化されたファイルに追加されるテキストの変更が標準出力に表示されます。" -#: awx/main/models/jobs.py:164 +#: awx/main/models/jobs.py:145 msgid "" "If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts" " at the end of a playbook run to the database and caching facts for use by " @@ -2661,282 +3401,280 @@ msgstr "" "有効にされている場合、 Tower は Ansible ファクトキャッシュプラグインとして機能します。データベースに対する Playbook " "実行の終了時にファクトが保持され、 Ansible で使用できるようにキャッシュされます。" -#: awx/main/models/jobs.py:173 -msgid "You must provide an SSH credential." -msgstr "SSH 認証情報を指定する必要があります。" - -#: awx/main/models/jobs.py:181 +#: awx/main/models/jobs.py:163 msgid "You must provide a Vault credential." msgstr "Vault 認証情報を指定する必要があります。" -#: awx/main/models/jobs.py:317 +#: awx/main/models/jobs.py:308 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "ジョブテンプレートは「inventory」を指定するか、このプロンプトを許可する必要があります。" -#: awx/main/models/jobs.py:321 -msgid "Job Template must provide 'credential' or allow prompting for it." -msgstr "ジョブテンプレートは「credential」を指定するか、このプロンプトを許可する必要があります。" +#: awx/main/models/jobs.py:403 +msgid "Field is not configured to prompt on launch." +msgstr "フィールドは起動時にプロンプトを出すよう設定されていません。" -#: awx/main/models/jobs.py:427 -msgid "Cannot override job_type to or from a scan job." -msgstr "スキャンジョブから/への job_type を上書きを実行できません。" +#: awx/main/models/jobs.py:409 +msgid "Saved launch configurations cannot provide passwords needed to start." +msgstr "保存された起動設定は、開始に必要なパスワードを提供しません。" -#: awx/main/models/jobs.py:493 awx/main/models/projects.py:263 +#: awx/main/models/jobs.py:417 +msgid "Job Template {} is missing or undefined." +msgstr "ジョブテンプレート {} が見つからないか、または定義されていません。" + +#: awx/main/models/jobs.py:498 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "SCM リビジョン" -#: awx/main/models/jobs.py:494 +#: awx/main/models/jobs.py:499 msgid "The SCM Revision from the Project used for this job, if available" msgstr "このジョブに使用されるプロジェクトからの SCM リビジョン (ある場合)" -#: awx/main/models/jobs.py:502 +#: awx/main/models/jobs.py:507 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" msgstr "SCM 更新タスクは、Playbook がジョブの実行で利用可能であったことを確認するために使用されます" -#: awx/main/models/jobs.py:809 +#: awx/main/models/jobs.py:634 +#, python-brace-format +msgid "{status_value} is not a valid status option." +msgstr "{status_value} は有効なステータスオプションではありません。" + +#: awx/main/models/jobs.py:999 msgid "job host summaries" msgstr "ジョブホストの概要" -#: awx/main/models/jobs.py:913 -msgid "Host Failure" -msgstr "ホストの失敗" - -#: awx/main/models/jobs.py:916 awx/main/models/jobs.py:930 -msgid "No Hosts Remaining" -msgstr "残りのホストがありません" - -#: awx/main/models/jobs.py:917 -msgid "Host Polling" -msgstr "ホストのポーリング" - -#: awx/main/models/jobs.py:918 -msgid "Host Async OK" -msgstr "ホストの非同期 OK" - -#: awx/main/models/jobs.py:919 -msgid "Host Async Failure" -msgstr "ホストの非同期失敗" - -#: awx/main/models/jobs.py:920 -msgid "Item OK" -msgstr "項目 OK" - -#: awx/main/models/jobs.py:921 -msgid "Item Failed" -msgstr "項目の失敗" - -#: awx/main/models/jobs.py:922 -msgid "Item Skipped" -msgstr "項目のスキップ" - -#: awx/main/models/jobs.py:923 -msgid "Host Retry" -msgstr "ホストの再試行" - -#: awx/main/models/jobs.py:925 -msgid "File Difference" -msgstr "ファイルの相違点" - -#: awx/main/models/jobs.py:926 -msgid "Playbook Started" -msgstr "Playbook の開始" - -#: awx/main/models/jobs.py:927 -msgid "Running Handlers" -msgstr "実行中のハンドラー" - -#: awx/main/models/jobs.py:928 -msgid "Including File" -msgstr "組み込みファイル" - -#: awx/main/models/jobs.py:929 -msgid "No Hosts Matched" -msgstr "一致するホストがありません" - -#: awx/main/models/jobs.py:931 -msgid "Task Started" -msgstr "タスクの開始" - -#: awx/main/models/jobs.py:933 -msgid "Variables Prompted" -msgstr "変数のプロモート" - -#: awx/main/models/jobs.py:934 -msgid "Gathering Facts" -msgstr "ファクトの収集" - -#: awx/main/models/jobs.py:935 -msgid "internal: on Import for Host" -msgstr "内部: ホストのインポート時" - -#: awx/main/models/jobs.py:936 -msgid "internal: on Not Import for Host" -msgstr "内部: ホストの非インポート時" - -#: awx/main/models/jobs.py:937 -msgid "Play Started" -msgstr "プレイの開始" - -#: awx/main/models/jobs.py:938 -msgid "Playbook Complete" -msgstr "Playbook の完了" - -#: awx/main/models/jobs.py:1351 +#: awx/main/models/jobs.py:1070 msgid "Remove jobs older than a certain number of days" msgstr "特定の日数より前のジョブを削除" -#: awx/main/models/jobs.py:1352 +#: awx/main/models/jobs.py:1071 msgid "Remove activity stream entries older than a certain number of days" msgstr "特定の日数より前のアクティビティーストリームのエントリーを削除" -#: awx/main/models/jobs.py:1353 +#: awx/main/models/jobs.py:1072 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "システムトラッキングデータの詳細度の削除/削減" +#: awx/main/models/jobs.py:1142 +#, python-brace-format +msgid "Variables {list_of_keys} are not allowed for system jobs." +msgstr "変数 {list_of_keys} はシステムジョブに許可されていません。" + +#: awx/main/models/jobs.py:1157 +msgid "days must be a positive integer." +msgstr "日数は正の整数である必要があります。" + #: awx/main/models/label.py:29 msgid "Organization this label belongs to." msgstr "このラベルが属する組織。" -#: awx/main/models/notifications.py:138 awx/main/models/unified_jobs.py:59 +#: awx/main/models/mixins.py:309 +#, python-brace-format +msgid "" +"Variables {list_of_keys} are not allowed on launch. Check the Prompt on " +"Launch setting on the Job Template to include Extra Variables." +msgstr "" +"変数 {list_of_keys} は起動時に許可されていません。ジョブテンプレートで起動時のプロンプト設定を確認し、追加変数を組み込みます。" + +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "使用するカスタム Python virtualenv を含むローカルの絶対ファイルパス" + +#: awx/main/models/mixins.py:447 +msgid "{} is not a valid virtualenv in {}" +msgstr "{}は{}の有効な virtualenv ではありません" + +#: awx/main/models/notifications.py:42 +msgid "Rocket.Chat" +msgstr "Rocket.Chat" + +#: awx/main/models/notifications.py:142 awx/main/models/unified_jobs.py:62 msgid "Pending" msgstr "保留中" -#: awx/main/models/notifications.py:139 awx/main/models/unified_jobs.py:62 +#: awx/main/models/notifications.py:143 awx/main/models/unified_jobs.py:65 msgid "Successful" msgstr "成功" -#: awx/main/models/notifications.py:140 awx/main/models/unified_jobs.py:63 +#: awx/main/models/notifications.py:144 awx/main/models/unified_jobs.py:66 msgid "Failed" msgstr "失敗" -#: awx/main/models/organization.py:132 -msgid "Token not invalidated" -msgstr "トークンが無効にされませんでした" +#: awx/main/models/notifications.py:218 +msgid "status_str must be either succeeded or failed" +msgstr "status_str は成功または失敗のいずれかである必要があります" -#: awx/main/models/organization.py:133 -msgid "Token is expired" -msgstr "トークンは期限切れです" +#: awx/main/models/oauth.py:27 +msgid "application" +msgstr "アプリケーション" -#: awx/main/models/organization.py:134 +#: awx/main/models/oauth.py:32 +msgid "Confidential" +msgstr "機密" + +#: awx/main/models/oauth.py:33 +msgid "Public" +msgstr "公開" + +#: awx/main/models/oauth.py:41 +msgid "Authorization code" +msgstr "認証コード" + +#: awx/main/models/oauth.py:42 +msgid "Implicit" +msgstr "暗黙的" + +#: awx/main/models/oauth.py:43 +msgid "Resource owner password-based" +msgstr "リソース所有者のパスワードベース" + +#: awx/main/models/oauth.py:44 +msgid "Client credentials" +msgstr "クライアント認証情報" + +#: awx/main/models/oauth.py:59 +msgid "Organization containing this application." +msgstr "このアプリケーションを含む組織。" + +#: awx/main/models/oauth.py:68 msgid "" -"The maximum number of allowed sessions for this user has been exceeded." -msgstr "このユーザーに許可される最大セッション数を超えました。" +"Used for more stringent verification of access to an application when " +"creating a token." +msgstr "トークン作成時のアプリケーションへのアクセスのより厳しい検証に使用されます。" -#: awx/main/models/organization.py:137 -msgid "Invalid token" -msgstr "無効なトークン" +#: awx/main/models/oauth.py:73 +msgid "" +"Set to Public or Confidential depending on how secure the client device is." +msgstr "クライアントデバイスのセキュリティーレベルに応じて「公開」または「機密」に設定します。" -#: awx/main/models/organization.py:155 -msgid "Reason the auth token was invalidated." -msgstr "認証トークンが無効にされた理由。" +#: awx/main/models/oauth.py:77 +msgid "" +"Set True to skip authorization step for completely trusted applications." +msgstr "完全に信頼されたアプリケーションの認証手順をスキップするには「True」を設定します。" -#: awx/main/models/organization.py:194 -msgid "Invalid reason specified" -msgstr "無効な理由が特定されました" +#: awx/main/models/oauth.py:82 +msgid "" +"The Grant type the user must use for acquire tokens for this application." +msgstr "ユーザーがこのアプリケーションのトークンを取得するために使用する必要のある付与タイプです。" -#: awx/main/models/projects.py:43 +#: awx/main/models/oauth.py:90 +msgid "access token" +msgstr "アクセストークン" + +#: awx/main/models/oauth.py:98 +msgid "The user representing the token owner" +msgstr "トークンの所有者を表すユーザー" + +#: awx/main/models/oauth.py:113 +msgid "" +"Allowed scopes, further restricts user's permissions. Must be a simple " +"space-separated string with allowed scopes ['read', 'write']." +msgstr "" +"許可されたスコープで、ユーザーのパーミッションをさらに制限します。許可されたスコープ ['read', 'write'] " +"のある単純なスペースで区切られた文字列でなければなりません。" + +#: awx/main/models/projects.py:54 msgid "Git" msgstr "Git" -#: awx/main/models/projects.py:44 +#: awx/main/models/projects.py:55 msgid "Mercurial" msgstr "Mercurial" -#: awx/main/models/projects.py:45 +#: awx/main/models/projects.py:56 msgid "Subversion" msgstr "Subversion" -#: awx/main/models/projects.py:46 +#: awx/main/models/projects.py:57 msgid "Red Hat Insights" msgstr "Red Hat Insights" -#: awx/main/models/projects.py:72 +#: awx/main/models/projects.py:83 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." msgstr "このプロジェクトの Playbook および関連するファイルを含むローカルパス (PROJECTS_ROOT との相対)。" -#: awx/main/models/projects.py:81 +#: awx/main/models/projects.py:92 msgid "SCM Type" msgstr "SCM タイプ" -#: awx/main/models/projects.py:82 +#: awx/main/models/projects.py:93 msgid "Specifies the source control system used to store the project." msgstr "プロジェクトを保存するために使用されるソースコントロールシステムを指定します。" -#: awx/main/models/projects.py:88 +#: awx/main/models/projects.py:99 msgid "SCM URL" msgstr "SCM URL" -#: awx/main/models/projects.py:89 +#: awx/main/models/projects.py:100 msgid "The location where the project is stored." msgstr "プロジェクトが保存される場所。" -#: awx/main/models/projects.py:95 +#: awx/main/models/projects.py:106 msgid "SCM Branch" msgstr "SCM ブランチ" -#: awx/main/models/projects.py:96 +#: awx/main/models/projects.py:107 msgid "Specific branch, tag or commit to checkout." msgstr "チェックアウトする特定のブランチ、タグまたはコミット。" -#: awx/main/models/projects.py:100 +#: awx/main/models/projects.py:111 msgid "Discard any local changes before syncing the project." msgstr "ローカル変更を破棄してからプロジェクトを同期します。" -#: awx/main/models/projects.py:104 +#: awx/main/models/projects.py:115 msgid "Delete the project before syncing." msgstr "プロジェクトを削除してから同期します。" -#: awx/main/models/projects.py:133 +#: awx/main/models/projects.py:144 msgid "Invalid SCM URL." msgstr "無効な SCM URL。" -#: awx/main/models/projects.py:136 +#: awx/main/models/projects.py:147 msgid "SCM URL is required." msgstr "SCM URL が必要です。" -#: awx/main/models/projects.py:144 +#: awx/main/models/projects.py:155 msgid "Insights Credential is required for an Insights Project." msgstr "Insights 認証情報が Insights プロジェクトに必要です。" -#: awx/main/models/projects.py:150 +#: awx/main/models/projects.py:161 msgid "Credential kind must be 'scm'." msgstr "認証情報の種類は 'scm' にする必要があります。" -#: awx/main/models/projects.py:167 +#: awx/main/models/projects.py:178 msgid "Invalid credential." msgstr "無効な認証情報。" -#: awx/main/models/projects.py:249 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "プロジェクトを使用するジョブの起動時にプロジェクトを更新します。" -#: awx/main/models/projects.py:254 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." msgstr "新規プロジェクトの更新がジョブの依存関係として起動される最終プロジェクト更新後の秒数。" -#: awx/main/models/projects.py:264 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "プロジェクト更新で取得される最新リビジョン" -#: awx/main/models/projects.py:271 +#: awx/main/models/projects.py:285 msgid "Playbook Files" msgstr "Playbook ファイル" -#: awx/main/models/projects.py:272 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "プロジェクトにある Playbook の一覧" -#: awx/main/models/projects.py:279 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "インベントリーファイル" -#: awx/main/models/projects.py:280 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "プロジェクト内の Ansible インベントリーの可能性のあるコンテンツの一覧" @@ -2958,202 +3696,270 @@ msgid "Admin" msgstr "管理者" #: awx/main/models/rbac.py:40 +msgid "Project Admin" +msgstr "プロジェクト管理者" + +#: awx/main/models/rbac.py:41 +msgid "Inventory Admin" +msgstr "インベントリー管理者" + +#: awx/main/models/rbac.py:42 +msgid "Credential Admin" +msgstr "認証情報管理者" + +#: awx/main/models/rbac.py:43 +msgid "Workflow Admin" +msgstr "ワークフロー管理者" + +#: awx/main/models/rbac.py:44 +msgid "Notification Admin" +msgstr "通知管理者" + +#: awx/main/models/rbac.py:45 msgid "Auditor" msgstr "監査者" -#: awx/main/models/rbac.py:41 +#: awx/main/models/rbac.py:46 msgid "Execute" msgstr "実行" -#: awx/main/models/rbac.py:42 +#: awx/main/models/rbac.py:47 msgid "Member" msgstr "メンバー" -#: awx/main/models/rbac.py:43 +#: awx/main/models/rbac.py:48 msgid "Read" msgstr "読み込み" -#: awx/main/models/rbac.py:44 +#: awx/main/models/rbac.py:49 msgid "Update" msgstr "更新" -#: awx/main/models/rbac.py:45 +#: awx/main/models/rbac.py:50 msgid "Use" msgstr "使用" -#: awx/main/models/rbac.py:49 +#: awx/main/models/rbac.py:54 msgid "Can manage all aspects of the system" msgstr "システムのすべての側面を管理可能" -#: awx/main/models/rbac.py:50 +#: awx/main/models/rbac.py:55 msgid "Can view all settings on the system" msgstr "システムのすべての設定を表示可能" -#: awx/main/models/rbac.py:51 +#: awx/main/models/rbac.py:56 msgid "May run ad hoc commands on an inventory" msgstr "インベントリーでアドホックコマンドを実行可能" -#: awx/main/models/rbac.py:52 +#: awx/main/models/rbac.py:57 #, python-format msgid "Can manage all aspects of the %s" msgstr "%s のすべての側面を管理可能" -#: awx/main/models/rbac.py:53 +#: awx/main/models/rbac.py:58 +#, python-format +msgid "Can manage all projects of the %s" +msgstr "%s のすべてのプロジェクトを管理可能" + +#: awx/main/models/rbac.py:59 +#, python-format +msgid "Can manage all inventories of the %s" +msgstr "%s のすべてのインベントリーを管理可能" + +#: awx/main/models/rbac.py:60 +#, python-format +msgid "Can manage all credentials of the %s" +msgstr "%s のすべての認証情報を管理可能" + +#: awx/main/models/rbac.py:61 +#, python-format +msgid "Can manage all workflows of the %s" +msgstr "%s のすべてのワークフローを管理可能" + +#: awx/main/models/rbac.py:62 +#, python-format +msgid "Can manage all notifications of the %s" +msgstr "%s のすべての通知を管理可能" + +#: awx/main/models/rbac.py:63 #, python-format msgid "Can view all settings for the %s" msgstr "%s のすべての設定を表示可能" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:65 +msgid "May run any executable resources in the organization" +msgstr "組織で実行可能なリソースを実行できます" + +#: awx/main/models/rbac.py:66 #, python-format msgid "May run the %s" msgstr "%s を実行可能" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:68 #, python-format msgid "User is a member of the %s" msgstr "ユーザーは %s のメンバーです" -#: awx/main/models/rbac.py:56 +#: awx/main/models/rbac.py:69 #, python-format msgid "May view settings for the %s" msgstr "%s の設定を表示可能" -#: awx/main/models/rbac.py:57 +#: awx/main/models/rbac.py:70 msgid "" "May update project or inventory or group using the configured source update " "system" msgstr "設定済みのソース更新システムを使用してプロジェクト、インベントリーまたはグループを更新可能" -#: awx/main/models/rbac.py:58 +#: awx/main/models/rbac.py:71 #, python-format msgid "Can use the %s in a job template" msgstr "ジョブテンプレートで %s を使用可能" -#: awx/main/models/rbac.py:122 +#: awx/main/models/rbac.py:135 msgid "roles" msgstr "ロール" -#: awx/main/models/rbac.py:434 +#: awx/main/models/rbac.py:441 msgid "role_ancestors" msgstr "role_ancestors" -#: awx/main/models/schedules.py:71 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "このスケジュールの処理を有効にします。" -#: awx/main/models/schedules.py:77 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." msgstr "スケジュールの最初のオカレンスはこの時間またはこの時間の後に生じます。" -#: awx/main/models/schedules.py:83 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." msgstr "スケジュールの最後のオカレンスはこの時間の前に生じます。その後スケジュールが期限切れになります。" -#: awx/main/models/schedules.py:87 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "スケジュールの iCal 繰り返しルールを表す値。" -#: awx/main/models/schedules.py:93 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." msgstr "スケジュールされたアクションが次に実行される時間。" -#: awx/main/models/schedules.py:109 -msgid "Expected JSON" -msgstr "予想される JSON" - -#: awx/main/models/schedules.py:121 -msgid "days must be a positive integer." -msgstr "日数は正の整数である必要があります。" - -#: awx/main/models/unified_jobs.py:58 +#: awx/main/models/unified_jobs.py:61 msgid "New" msgstr "新規" -#: awx/main/models/unified_jobs.py:60 +#: awx/main/models/unified_jobs.py:63 msgid "Waiting" msgstr "待機中" -#: awx/main/models/unified_jobs.py:61 +#: awx/main/models/unified_jobs.py:64 msgid "Running" msgstr "実行中" -#: awx/main/models/unified_jobs.py:65 +#: awx/main/models/unified_jobs.py:68 msgid "Canceled" msgstr "取り消し" -#: awx/main/models/unified_jobs.py:69 +#: awx/main/models/unified_jobs.py:72 msgid "Never Updated" msgstr "未更新" -#: awx/main/models/unified_jobs.py:73 awx/ui/templates/ui/index.html:67 -#: awx/ui/templates/ui/index.html.py:86 +#: awx/main/models/unified_jobs.py:76 msgid "OK" msgstr "OK" -#: awx/main/models/unified_jobs.py:74 +#: awx/main/models/unified_jobs.py:77 msgid "Missing" msgstr "不明" -#: awx/main/models/unified_jobs.py:78 +#: awx/main/models/unified_jobs.py:81 msgid "No External Source" msgstr "外部ソースがありません" -#: awx/main/models/unified_jobs.py:85 +#: awx/main/models/unified_jobs.py:88 msgid "Updating" msgstr "更新中" -#: awx/main/models/unified_jobs.py:429 +#: awx/main/models/unified_jobs.py:427 +msgid "Field is not allowed on launch." +msgstr "フィールドは起動時に許可されません。" + +#: awx/main/models/unified_jobs.py:455 +#, python-brace-format +msgid "" +"Variables {list_of_keys} provided, but this template cannot accept " +"variables." +msgstr "変数 {list_of_keys} が指定されますが、このテンプレートは変数を受け入れません。" + +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "再起動" -#: awx/main/models/unified_jobs.py:430 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "コールバック" -#: awx/main/models/unified_jobs.py:431 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "スケジュール済み" -#: awx/main/models/unified_jobs.py:432 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "依存関係" -#: awx/main/models/unified_jobs.py:433 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "ワークフロー" -#: awx/main/models/unified_jobs.py:434 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "同期" -#: awx/main/models/unified_jobs.py:481 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "ジョブが実行されるノード。" -#: awx/main/models/unified_jobs.py:507 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "分離された実行環境を管理したインスタンス。" + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." msgstr "ジョブが開始のために待機した日時。" -#: awx/main/models/unified_jobs.py:513 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." msgstr "ジョブが実行を完了した日時。" -#: awx/main/models/unified_jobs.py:519 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." msgstr "ジョブ実行の経過時間 (秒単位)" -#: awx/main/models/unified_jobs.py:541 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and" " capture stdout" msgstr "stdout の実行およびキャプチャーを実行できない場合のジョブの状態を示すための状態フィールド" -#: awx/main/models/unified_jobs.py:580 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "ジョブが実行された Rampart/インスタンスグループ" +#: awx/main/models/workflow.py:203 +#, python-brace-format +msgid "" +"Bad launch configuration starting template {template_pk} as part of workflow {workflow_pk}. Errors:\n" +"{error_text}" +msgstr "" +"{workflow_pk} の一部としてテンプレート {template_pk} を起動する起動設定が正しくありません。エラー:\n" +"{error_text}" + +#: awx/main/models/workflow.py:388 +msgid "Field is not allowed for use in workflows." +msgstr "フィールドはワークフローでの使用に許可されません。" + #: awx/main/notifications/base.py:17 #: awx/main/notifications/email_backend.py:28 msgid "" @@ -3163,11 +3969,11 @@ msgstr "" "{} #{} のステータスは {} です。詳細を {} で確認してください。\n" "\n" -#: awx/main/notifications/hipchat_backend.py:47 +#: awx/main/notifications/hipchat_backend.py:48 msgid "Error sending messages: {}" msgstr "メッセージの送信時のエラー: {}" -#: awx/main/notifications/hipchat_backend.py:49 +#: awx/main/notifications/hipchat_backend.py:50 msgid "Error sending message to hipchat: {}" msgstr "メッセージの hipchat への送信時のエラー: {}" @@ -3175,16 +3981,27 @@ msgstr "メッセージの hipchat への送信時のエラー: {}" msgid "Exception connecting to irc server: {}" msgstr "irc サーバーへの接続時の例外: {}" +#: awx/main/notifications/mattermost_backend.py:48 +#: awx/main/notifications/mattermost_backend.py:50 +msgid "Error sending notification mattermost: {}" +msgstr "通知 mattermost の送信時のエラー: {}" + #: awx/main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" msgstr "PagerDuty への接続時の例外: {}" #: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 +#: awx/main/notifications/slack_backend.py:82 +#: awx/main/notifications/slack_backend.py:99 #: awx/main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" msgstr "メッセージの送信時の例外: {}" +#: awx/main/notifications/rocketchat_backend.py:46 +#: awx/main/notifications/rocketchat_backend.py:49 +msgid "Error sending notification rocket.chat: {}" +msgstr "通知 rocket.chat 送信時のエラー: {}" + #: awx/main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" msgstr "Twilio への接続時の例外: {}" @@ -3194,141 +4011,157 @@ msgstr "Twilio への接続時の例外: {}" msgid "Error sending notification webhook: {}" msgstr "通知 webhook の送信時のエラー: {}" -#: awx/main/scheduler/task_manager.py:197 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" msgstr "ワークフローから起動されるジョブは、正常な状態にないか、または手動の認証が必要であるために開始できませんでした" -#: awx/main/scheduler/task_manager.py:201 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" msgstr "ワークフローから起動されるジョブは、プロジェクトまたはインベントリーなどの関連するリソースがないために開始できませんでした" -#: awx/main/tasks.py:184 +#: awx/main/signals.py:616 +msgid "limit_reached" +msgstr "limit_reached" + +#: awx/main/tasks.py:282 msgid "Ansible Tower host usage over 90%" msgstr "Ansible Tower ホストの使用率が 90% を超えました" -#: awx/main/tasks.py:189 +#: awx/main/tasks.py:287 msgid "Ansible Tower license will expire soon" msgstr "Ansible Tower ライセンスがまもなく期限切れになります" -#: awx/main/tasks.py:318 -msgid "status_str must be either succeeded or failed" -msgstr "status_str は成功または失敗のいずれかである必要があります" +#: awx/main/tasks.py:1335 +msgid "Job could not start because it does not have a valid inventory." +msgstr "ジョブは有効なインベントリーがないために開始できませんでした。" -#: awx/main/tasks.py:1549 -msgid "Dependent inventory update {} was canceled." -msgstr "依存するインベントリーの更新 {} が取り消されました。" - -#: awx/main/utils/common.py:89 +#: awx/main/utils/common.py:97 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "\"%s\" をブール値に変換できません" -#: awx/main/utils/common.py:235 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "サポートされない SCM タイプ \"%s\"" -#: awx/main/utils/common.py:242 awx/main/utils/common.py:254 -#: awx/main/utils/common.py:273 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" msgstr "無効な %s URL" -#: awx/main/utils/common.py:244 awx/main/utils/common.py:283 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "サポートされていない %s URL" -#: awx/main/utils/common.py:285 +#: awx/main/utils/common.py:304 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "ファイル:// URL のサポートされていないホスト \"%s\" " -#: awx/main/utils/common.py:287 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "%s URL にはホストが必要です" -#: awx/main/utils/common.py:305 +#: awx/main/utils/common.py:324 #, python-format msgid "Username must be \"git\" for SSH access to %s." msgstr "%s への SSH アクセスではユーザー名を \"git\" にする必要があります。" -#: awx/main/utils/common.py:311 +#: awx/main/utils/common.py:330 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "%s への SSH アクセスではユーザー名を \"hg\" にする必要があります。" -#: awx/main/validators.py:60 +#: awx/main/utils/common.py:611 +#, python-brace-format +msgid "Input type `{data_type}` is not a dictionary" +msgstr "入力タイプ `{data_type}` は辞書ではありません" + +#: awx/main/utils/common.py:644 +#, python-brace-format +msgid "Variables not compatible with JSON standard (error: {json_error})" +msgstr "変数には JSON 標準との互換性がありません (エラー: {json_error})" + +#: awx/main/utils/common.py:650 +#, python-brace-format +msgid "" +"Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." +msgstr "JSON (エラー: {json_error}) または YAML (エラー: {yaml_error}) として解析できません。" + +#: awx/main/validators.py:67 #, python-format msgid "Invalid certificate or key: %s..." msgstr "無効な証明書またはキー: %s..." -#: awx/main/validators.py:74 +#: awx/main/validators.py:83 #, python-format msgid "Invalid private key: unsupported type \"%s\"" msgstr "無効な秘密鍵: サポートされていないタイプ \"%s\"" -#: awx/main/validators.py:78 +#: awx/main/validators.py:87 #, python-format msgid "Unsupported PEM object type: \"%s\"" msgstr "サポートされていない PEM オブジェクトタイプ: \"%s\"" -#: awx/main/validators.py:103 +#: awx/main/validators.py:112 msgid "Invalid base64-encoded data" msgstr "無効な base64 エンコードされたデータ" -#: awx/main/validators.py:122 +#: awx/main/validators.py:131 msgid "Exactly one private key is required." msgstr "秘密鍵が 1 つのみ必要です。" -#: awx/main/validators.py:124 +#: awx/main/validators.py:133 msgid "At least one private key is required." msgstr "1 つ以上の秘密鍵が必要です。" -#: awx/main/validators.py:126 +#: awx/main/validators.py:135 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d " "provided." msgstr "%(min_keys)d 以上の秘密鍵が必要です。提供数: %(key_count)d のみ。" -#: awx/main/validators.py:129 +#: awx/main/validators.py:138 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." msgstr "秘密鍵が 1 つのみ許可されます。提供数: %(key_count)d" -#: awx/main/validators.py:131 +#: awx/main/validators.py:140 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." msgstr "%(max_keys)d を超える秘密鍵は許可されません。提供数: %(key_count)d " -#: awx/main/validators.py:136 +#: awx/main/validators.py:145 msgid "Exactly one certificate is required." msgstr "証明書が 1 つのみ必要です。" -#: awx/main/validators.py:138 +#: awx/main/validators.py:147 msgid "At least one certificate is required." msgstr "1 つ以上の証明書が必要です。" -#: awx/main/validators.py:140 +#: awx/main/validators.py:149 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " "provided." msgstr "%(min_certs)d 以上の証明書が必要です。提供数: %(cert_count)d のみ。" -#: awx/main/validators.py:143 +#: awx/main/validators.py:152 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." msgstr "証明書が 1 つのみ許可されます。提供数: %(cert_count)d" -#: awx/main/validators.py:145 +#: awx/main/validators.py:154 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d " @@ -3371,287 +4204,287 @@ msgstr "サーバーエラー" msgid "A server error has occurred." msgstr "サーバーエラーが発生しました。" -#: awx/settings/defaults.py:665 +#: awx/settings/defaults.py:721 msgid "US East (Northern Virginia)" msgstr "米国東部 (バージニア北部)" -#: awx/settings/defaults.py:666 +#: awx/settings/defaults.py:722 msgid "US East (Ohio)" msgstr "米国東部 (オハイオ)" -#: awx/settings/defaults.py:667 +#: awx/settings/defaults.py:723 msgid "US West (Oregon)" msgstr "米国西部 (オレゴン)" -#: awx/settings/defaults.py:668 +#: awx/settings/defaults.py:724 msgid "US West (Northern California)" msgstr "米国西部 (北カリフォルニア)" -#: awx/settings/defaults.py:669 +#: awx/settings/defaults.py:725 msgid "Canada (Central)" msgstr "カナダ (中部)" -#: awx/settings/defaults.py:670 +#: awx/settings/defaults.py:726 msgid "EU (Frankfurt)" msgstr "EU (フランクフルト)" -#: awx/settings/defaults.py:671 +#: awx/settings/defaults.py:727 msgid "EU (Ireland)" msgstr "EU (アイルランド)" -#: awx/settings/defaults.py:672 +#: awx/settings/defaults.py:728 msgid "EU (London)" msgstr "EU (ロンドン)" -#: awx/settings/defaults.py:673 +#: awx/settings/defaults.py:729 msgid "Asia Pacific (Singapore)" msgstr "アジア太平洋 (シンガポール)" -#: awx/settings/defaults.py:674 +#: awx/settings/defaults.py:730 msgid "Asia Pacific (Sydney)" msgstr "アジア太平洋 (シドニー)" -#: awx/settings/defaults.py:675 +#: awx/settings/defaults.py:731 msgid "Asia Pacific (Tokyo)" msgstr "アジア太平洋 (東京)" -#: awx/settings/defaults.py:676 +#: awx/settings/defaults.py:732 msgid "Asia Pacific (Seoul)" msgstr "アジア太平洋 (ソウル)" -#: awx/settings/defaults.py:677 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Mumbai)" msgstr "アジア太平洋 (ムンバイ)" -#: awx/settings/defaults.py:678 +#: awx/settings/defaults.py:734 msgid "South America (Sao Paulo)" msgstr "南アメリカ (サンパウロ)" -#: awx/settings/defaults.py:679 +#: awx/settings/defaults.py:735 msgid "US West (GovCloud)" msgstr "米国西部 (GovCloud)" -#: awx/settings/defaults.py:680 +#: awx/settings/defaults.py:736 msgid "China (Beijing)" msgstr "中国 (北京)" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:785 msgid "US East 1 (B)" msgstr "米国東部 1 (B)" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:786 msgid "US East 1 (C)" msgstr "米国東部 1 (C)" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:787 msgid "US East 1 (D)" msgstr "米国東部 1 (D)" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:788 msgid "US East 4 (A)" msgstr "米国東部 4 (A)" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:789 msgid "US East 4 (B)" msgstr "米国東部 4 (B)" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:790 msgid "US East 4 (C)" msgstr "米国東部 4 (C)" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:791 msgid "US Central (A)" msgstr "米国中部 (A)" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:792 msgid "US Central (B)" msgstr "米国中部 (B)" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:793 msgid "US Central (C)" msgstr "米国中部 (C)" -#: awx/settings/defaults.py:738 +#: awx/settings/defaults.py:794 msgid "US Central (F)" msgstr "米国中部 (F)" -#: awx/settings/defaults.py:739 +#: awx/settings/defaults.py:795 msgid "US West (A)" msgstr "米国西部 (A)" -#: awx/settings/defaults.py:740 +#: awx/settings/defaults.py:796 msgid "US West (B)" msgstr "米国西部 (B)" -#: awx/settings/defaults.py:741 +#: awx/settings/defaults.py:797 msgid "US West (C)" msgstr "米国西部 (C)" -#: awx/settings/defaults.py:742 +#: awx/settings/defaults.py:798 msgid "Europe West 1 (B)" msgstr "欧州西部 1 (B)" -#: awx/settings/defaults.py:743 +#: awx/settings/defaults.py:799 msgid "Europe West 1 (C)" msgstr "欧州西部 1 (C)" -#: awx/settings/defaults.py:744 +#: awx/settings/defaults.py:800 msgid "Europe West 1 (D)" msgstr "欧州西部 1 (D)" -#: awx/settings/defaults.py:745 +#: awx/settings/defaults.py:801 msgid "Europe West 2 (A)" msgstr "欧州西部 2 (A)" -#: awx/settings/defaults.py:746 +#: awx/settings/defaults.py:802 msgid "Europe West 2 (B)" msgstr "欧州西部 2 (B)" -#: awx/settings/defaults.py:747 +#: awx/settings/defaults.py:803 msgid "Europe West 2 (C)" msgstr "欧州西部 2 (C)" -#: awx/settings/defaults.py:748 +#: awx/settings/defaults.py:804 msgid "Asia East (A)" msgstr "アジア東部 (A)" -#: awx/settings/defaults.py:749 +#: awx/settings/defaults.py:805 msgid "Asia East (B)" msgstr "アジア東部 (B)" -#: awx/settings/defaults.py:750 +#: awx/settings/defaults.py:806 msgid "Asia East (C)" msgstr "アジア東部 (C)" -#: awx/settings/defaults.py:751 +#: awx/settings/defaults.py:807 msgid "Asia Southeast (A)" msgstr "アジア南東部 (A)" -#: awx/settings/defaults.py:752 +#: awx/settings/defaults.py:808 msgid "Asia Southeast (B)" msgstr "アジア南東部 (B)" -#: awx/settings/defaults.py:753 +#: awx/settings/defaults.py:809 msgid "Asia Northeast (A)" msgstr "アジア北東部 (A)" -#: awx/settings/defaults.py:754 +#: awx/settings/defaults.py:810 msgid "Asia Northeast (B)" msgstr "アジア北東部 (B)" -#: awx/settings/defaults.py:755 +#: awx/settings/defaults.py:811 msgid "Asia Northeast (C)" msgstr "アジア北東部 (C)" -#: awx/settings/defaults.py:756 +#: awx/settings/defaults.py:812 msgid "Australia Southeast (A)" msgstr "オーストラリア南東部 (A)" -#: awx/settings/defaults.py:757 +#: awx/settings/defaults.py:813 msgid "Australia Southeast (B)" msgstr "オーストラリア南東部 (B)" -#: awx/settings/defaults.py:758 +#: awx/settings/defaults.py:814 msgid "Australia Southeast (C)" msgstr "オーストラリア南東部 (C)" -#: awx/settings/defaults.py:780 +#: awx/settings/defaults.py:836 msgid "US East" msgstr "米国東部" -#: awx/settings/defaults.py:781 +#: awx/settings/defaults.py:837 msgid "US East 2" msgstr "米国東部 2" -#: awx/settings/defaults.py:782 +#: awx/settings/defaults.py:838 msgid "US Central" msgstr "米国中部" -#: awx/settings/defaults.py:783 +#: awx/settings/defaults.py:839 msgid "US North Central" msgstr "米国中北部" -#: awx/settings/defaults.py:784 +#: awx/settings/defaults.py:840 msgid "US South Central" msgstr "米国中南部" -#: awx/settings/defaults.py:785 +#: awx/settings/defaults.py:841 msgid "US West Central" msgstr "米国中西部" -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:842 msgid "US West" msgstr "米国西部" -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:843 msgid "US West 2" msgstr "米国西部 2" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:844 msgid "Canada East" msgstr "カナダ東部" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:845 msgid "Canada Central" msgstr "カナダ中部" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:846 msgid "Brazil South" msgstr "ブラジル南部" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:847 msgid "Europe North" msgstr "欧州北部" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:848 msgid "Europe West" msgstr "欧州西部" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:849 msgid "UK West" msgstr "英国西部" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:850 msgid "UK South" msgstr "英国南部" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:851 msgid "Asia East" msgstr "アジア東部" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:852 msgid "Asia Southeast" msgstr "アジア南東部" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:853 msgid "Australia East" msgstr "オーストラリア東部" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:854 msgid "Australia Southeast" msgstr "オーストラリア南東部 " -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:855 msgid "India West" msgstr "インド西部" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:856 msgid "India South" msgstr "インド南部" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:857 msgid "Japan East" msgstr "日本東部" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:858 msgid "Japan West" msgstr "日本西部" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:859 msgid "Korea Central" msgstr "韓国中部" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:860 msgid "Korea South" msgstr "韓国南部" @@ -3664,7 +4497,7 @@ msgid "" "Mapping to organization admins/users from social auth accounts. This setting\n" "controls which users are placed into which Tower organizations based on their\n" "username and email address. Configuration details are available in the Ansible\n" -"Tower documentation.'" +"Tower documentation." msgstr "" "ソーシャル認証アカウントから組織管理者/ユーザーへのマッピングです。この設定は、ユーザー名およびメールアドレスに基づいてどのユーザーをどの Tower " "組織に配置するかを管理します。設定の詳細については、Ansible Tower ドキュメントを参照してください。" @@ -3707,11 +4540,11 @@ msgstr "" "空リスト " "`[]`に設定される場合、この設定により新規ユーザーアカウントは作成できなくなります。ソーシャル認証を使ってログインしたことのあるユーザーまたは一致するメールアドレスのユーザーアカウントを持つユーザーのみがログインできます。" -#: awx/sso/conf.py:137 +#: awx/sso/conf.py:141 msgid "LDAP Server URI" msgstr "LDAP サーバー URI" -#: awx/sso/conf.py:138 +#: awx/sso/conf.py:142 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be" @@ -3722,19 +4555,20 @@ msgstr "" " (SSL) などの LDAP サーバーに接続する URI です。複数の LDAP サーバーをスペースまたはカンマで区切って指定できます。LDAP " "認証は、このパラメーターが空の場合は無効になります。" -#: awx/sso/conf.py:142 awx/sso/conf.py:158 awx/sso/conf.py:170 -#: awx/sso/conf.py:182 awx/sso/conf.py:198 awx/sso/conf.py:218 -#: awx/sso/conf.py:240 awx/sso/conf.py:255 awx/sso/conf.py:273 -#: awx/sso/conf.py:290 awx/sso/conf.py:307 awx/sso/conf.py:323 -#: awx/sso/conf.py:337 awx/sso/conf.py:354 awx/sso/conf.py:380 +#: awx/sso/conf.py:146 awx/sso/conf.py:162 awx/sso/conf.py:174 +#: awx/sso/conf.py:186 awx/sso/conf.py:202 awx/sso/conf.py:222 +#: awx/sso/conf.py:244 awx/sso/conf.py:259 awx/sso/conf.py:277 +#: awx/sso/conf.py:294 awx/sso/conf.py:306 awx/sso/conf.py:332 +#: awx/sso/conf.py:348 awx/sso/conf.py:362 awx/sso/conf.py:380 +#: awx/sso/conf.py:406 msgid "LDAP" msgstr "LDAP" -#: awx/sso/conf.py:154 +#: awx/sso/conf.py:158 msgid "LDAP Bind DN" msgstr "LDAP バインド DN" -#: awx/sso/conf.py:155 +#: awx/sso/conf.py:159 msgid "" "DN (Distinguished Name) of user to bind for all search queries. This is the " "system user account we will use to login to query LDAP for other user " @@ -3743,27 +4577,27 @@ msgstr "" "すべての検索クエリーについてバインドするユーザーの DN (識別名) です。これは、他のユーザー情報についての LDAP " "クエリー実行時のログインに使用するシステムユーザーアカウントです。構文のサンプルについては Ansible Tower ドキュメントを参照してください。" -#: awx/sso/conf.py:168 +#: awx/sso/conf.py:172 msgid "LDAP Bind Password" msgstr "LDAP バインドパスワード" -#: awx/sso/conf.py:169 +#: awx/sso/conf.py:173 msgid "Password used to bind LDAP user account." msgstr "LDAP ユーザーアカウントをバインドするために使用されるパスワード。" -#: awx/sso/conf.py:180 +#: awx/sso/conf.py:184 msgid "LDAP Start TLS" msgstr "LDAP Start TLS" -#: awx/sso/conf.py:181 +#: awx/sso/conf.py:185 msgid "Whether to enable TLS when the LDAP connection is not using SSL." msgstr "LDAP 接続が SSL を使用していない場合に TLS を有効にするかどうか。" -#: awx/sso/conf.py:191 +#: awx/sso/conf.py:195 msgid "LDAP Connection Options" msgstr "LDAP 接続オプション" -#: awx/sso/conf.py:192 +#: awx/sso/conf.py:196 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -3776,11 +4610,11 @@ msgstr "" "\"OPT_REFERRALS\")。可能なオプションおよび設定できる値については、https://www.python-" "ldap.org/doc/html/ldap.html#options を参照してください。" -#: awx/sso/conf.py:211 +#: awx/sso/conf.py:215 msgid "LDAP User Search" msgstr "LDAP ユーザー検索" -#: awx/sso/conf.py:212 +#: awx/sso/conf.py:216 msgid "" "LDAP search query to find users. Any user that matches the given pattern " "will be able to login to Tower. The user should also be mapped into a Tower" @@ -3793,11 +4627,11 @@ msgstr "" "設定で定義)。複数の検索クエリーをサポートする必要がある場合、\"LDAPUnion\" を使用できます。詳細は、Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:234 +#: awx/sso/conf.py:238 msgid "LDAP User DN Template" msgstr "LDAP ユーザー DN テンプレート" -#: awx/sso/conf.py:235 +#: awx/sso/conf.py:239 msgid "" "Alternative to user search, if user DNs are all of the same format. This " "approach is more efficient for user lookups than searching if it is usable " @@ -3808,26 +4642,26 @@ msgstr "" "の形式がすべて同じである場合のユーザー検索の代替法になります。この方法は、組織の環境で使用可能であるかどうかを検索する場合よりも効率的なユーザー検索方法になります。この設定に値がある場合、それが" " AUTH_LDAP_USER_SEARCH の代わりに使用されます。" -#: awx/sso/conf.py:250 +#: awx/sso/conf.py:254 msgid "LDAP User Attribute Map" msgstr "LDAP ユーザー属性マップ" -#: awx/sso/conf.py:251 +#: awx/sso/conf.py:255 msgid "" "Mapping of LDAP user schema to Tower API user attributes. The default " "setting is valid for ActiveDirectory but users with other LDAP " "configurations may need to change the values. Refer to the Ansible Tower " -"documentation for additonal details." +"documentation for additional details." msgstr "" "LDAP ユーザースキーマの Tower API ユーザー属性へのマッピングです 。デフォルト設定は ActiveDirectory で有効ですが、他の" " LDAP 設定を持つユーザーは値を変更する必要が生じる場合があります。追加の詳細情報については、Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:269 +#: awx/sso/conf.py:273 msgid "LDAP Group Search" msgstr "LDAP グループ検索" -#: awx/sso/conf.py:270 +#: awx/sso/conf.py:274 msgid "" "Users are mapped to organizations based on their membership in LDAP groups. " "This setting defines the LDAP search query to find groups. Unlike the user " @@ -3836,24 +4670,32 @@ msgstr "" "ユーザーは LDAP グループのメンバーシップに基づいて組織にマップされます。この設定は、グループを検索できるように LDAP " "検索クエリーを定義します。ユーザー検索とは異なり、グループ検索は LDAPSearchUnion をサポートしません。" -#: awx/sso/conf.py:286 +#: awx/sso/conf.py:290 msgid "LDAP Group Type" msgstr "LDAP グループタイプ" -#: awx/sso/conf.py:287 +#: awx/sso/conf.py:291 msgid "" "The group type may need to be changed based on the type of the LDAP server." -" Values are listed at: http://pythonhosted.org/django-auth-ldap/groups.html" -"#types-of-groups" +" Values are listed at: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" msgstr "" -"グループタイプは LDAP サーバーのタイプに基づいて変更する必要がある場合があります。値は以下に記載されています: " -"http://pythonhosted.org/django-auth-ldap/groups.html#types-of-groups" +"グループタイプは LDAP サーバーのタイプに基づいて変更する必要がある場合があります。値は以下に記載されています: https://django-" +"auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups" -#: awx/sso/conf.py:302 +#: awx/sso/conf.py:304 +msgid "LDAP Group Type Parameters" +msgstr "LDAP グループタイプパラメーター" + +#: awx/sso/conf.py:305 +msgid "Key value parameters to send the chosen group type init method." +msgstr "選択されたグループタイプの init メソッドを送信するためのキー値パラメーター。" + +#: awx/sso/conf.py:327 msgid "LDAP Require Group" msgstr "LDAP 要求グループ" -#: awx/sso/conf.py:303 +#: awx/sso/conf.py:328 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " @@ -3863,11 +4705,11 @@ msgstr "" "経由でログインするにはユーザーはこのグループのメンバーである必要があります。設定されていない場合は、ユーザー検索に一致する LDAP " "のすべてのユーザーが Tower 経由でログインできます。1つの要求グループのみがサポートされます。" -#: awx/sso/conf.py:319 +#: awx/sso/conf.py:344 msgid "LDAP Deny Group" msgstr "LDAP 拒否グループ" -#: awx/sso/conf.py:320 +#: awx/sso/conf.py:345 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." @@ -3875,11 +4717,11 @@ msgstr "" "グループ DN がログインで拒否されます。指定されている場合、ユーザーはこのグループのメンバーの場合にログインできません。1 " "つの拒否グループのみがサポートされます。" -#: awx/sso/conf.py:333 +#: awx/sso/conf.py:358 msgid "LDAP User Flags By Group" msgstr "LDAP ユーザーフラグ (グループ別)" -#: awx/sso/conf.py:334 +#: awx/sso/conf.py:359 msgid "" "Retrieve users from a given group. At this time, superuser and system " "auditors are the only groups supported. Refer to the Ansible Tower " @@ -3888,11 +4730,11 @@ msgstr "" "指定されたグループからユーザーを検索します。この場合、サポートされるグループは、スーパーユーザーおよびシステム監査者のみになります。詳細は、Ansible" " Tower ドキュメントを参照してください。" -#: awx/sso/conf.py:349 +#: awx/sso/conf.py:375 msgid "LDAP Organization Map" msgstr "LDAP 組織マップ" -#: awx/sso/conf.py:350 +#: awx/sso/conf.py:376 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " "which users are placed into which Tower organizations relative to their LDAP" @@ -3902,11 +4744,11 @@ msgstr "" "組織管理者/ユーザーと LDAP グループ間のマッピングです。この設定は、LDAP グループのメンバーシップに基づいてどのユーザーをどの Tower " "組織に配置するかを管理します。設定の詳細については、Ansible Tower ドキュメントを参照してください。" -#: awx/sso/conf.py:377 +#: awx/sso/conf.py:403 msgid "LDAP Team Map" msgstr "LDAP チームマップ" -#: awx/sso/conf.py:378 +#: awx/sso/conf.py:404 msgid "" "Mapping between team members (users) and LDAP groups. Configuration details " "are available in the Ansible Tower documentation." @@ -3914,87 +4756,87 @@ msgstr "" "チームメンバー (ユーザー) と LDAP グループ間のマッピングです。設定の詳細については、Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:406 +#: awx/sso/conf.py:440 msgid "RADIUS Server" msgstr "RADIUS サーバー" -#: awx/sso/conf.py:407 +#: awx/sso/conf.py:441 msgid "" "Hostname/IP of RADIUS server. RADIUS authentication is disabled if this " "setting is empty." msgstr "RADIUS サーバーのホスト名/IP です。この設定が空の場合は RADIUS 認証は無効にされます。" -#: awx/sso/conf.py:409 awx/sso/conf.py:423 awx/sso/conf.py:435 +#: awx/sso/conf.py:443 awx/sso/conf.py:457 awx/sso/conf.py:469 #: awx/sso/models.py:14 msgid "RADIUS" msgstr "RADIUS" -#: awx/sso/conf.py:421 +#: awx/sso/conf.py:455 msgid "RADIUS Port" msgstr "RADIUS ポート" -#: awx/sso/conf.py:422 +#: awx/sso/conf.py:456 msgid "Port of RADIUS server." msgstr "RADIUS サーバーのポート。" -#: awx/sso/conf.py:433 +#: awx/sso/conf.py:467 msgid "RADIUS Secret" msgstr "RADIUS シークレット" -#: awx/sso/conf.py:434 +#: awx/sso/conf.py:468 msgid "Shared secret for authenticating to RADIUS server." msgstr "RADIUS サーバーに対して認証するための共有シークレット。" -#: awx/sso/conf.py:450 +#: awx/sso/conf.py:484 msgid "TACACS+ Server" msgstr "TACACS+ サーバー" -#: awx/sso/conf.py:451 +#: awx/sso/conf.py:485 msgid "Hostname of TACACS+ server." msgstr "TACACS+ サーバーのホスト名。" -#: awx/sso/conf.py:452 awx/sso/conf.py:465 awx/sso/conf.py:478 -#: awx/sso/conf.py:491 awx/sso/conf.py:503 awx/sso/models.py:15 +#: awx/sso/conf.py:486 awx/sso/conf.py:499 awx/sso/conf.py:512 +#: awx/sso/conf.py:525 awx/sso/conf.py:537 awx/sso/models.py:15 msgid "TACACS+" msgstr "TACACS+" -#: awx/sso/conf.py:463 +#: awx/sso/conf.py:497 msgid "TACACS+ Port" msgstr "TACACS+ ポート" -#: awx/sso/conf.py:464 +#: awx/sso/conf.py:498 msgid "Port number of TACACS+ server." msgstr "TACACS+ サーバーのポート番号。" -#: awx/sso/conf.py:476 +#: awx/sso/conf.py:510 msgid "TACACS+ Secret" msgstr "TACACS+ シークレット" -#: awx/sso/conf.py:477 +#: awx/sso/conf.py:511 msgid "Shared secret for authenticating to TACACS+ server." msgstr "TACACS+ サーバーに対して認証するための共有シークレット。" -#: awx/sso/conf.py:489 +#: awx/sso/conf.py:523 msgid "TACACS+ Auth Session Timeout" msgstr "TACACS+ 認証セッションタイムアウト" -#: awx/sso/conf.py:490 +#: awx/sso/conf.py:524 msgid "TACACS+ session timeout value in seconds, 0 disables timeout." msgstr "TACACS+ セッションのタイムアウト値 (秒数) です。0 はタイムアウトを無効にします。" -#: awx/sso/conf.py:501 +#: awx/sso/conf.py:535 msgid "TACACS+ Authentication Protocol" msgstr "TACACS+ 認証プロトコル" -#: awx/sso/conf.py:502 +#: awx/sso/conf.py:536 msgid "Choose the authentication protocol used by TACACS+ client." msgstr "TACACS+ クライアントによって使用される認証プロトコルを選択します。" -#: awx/sso/conf.py:517 +#: awx/sso/conf.py:551 msgid "Google OAuth2 Callback URL" msgstr "Google OAuth2 コールバック URL" -#: awx/sso/conf.py:518 awx/sso/conf.py:611 awx/sso/conf.py:676 +#: awx/sso/conf.py:552 awx/sso/conf.py:645 awx/sso/conf.py:710 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4003,43 +4845,43 @@ msgstr "" "登録プロセスの一環として、この URL をアプリケーションのコールバック URL として指定します。詳細については、Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:521 awx/sso/conf.py:533 awx/sso/conf.py:545 -#: awx/sso/conf.py:558 awx/sso/conf.py:572 awx/sso/conf.py:584 -#: awx/sso/conf.py:596 +#: awx/sso/conf.py:555 awx/sso/conf.py:567 awx/sso/conf.py:579 +#: awx/sso/conf.py:592 awx/sso/conf.py:606 awx/sso/conf.py:618 +#: awx/sso/conf.py:630 msgid "Google OAuth2" msgstr "Google OAuth2" -#: awx/sso/conf.py:531 +#: awx/sso/conf.py:565 msgid "Google OAuth2 Key" msgstr "Google OAuth2 キー" -#: awx/sso/conf.py:532 +#: awx/sso/conf.py:566 msgid "The OAuth2 key from your web application." msgstr "web アプリケーションの OAuth2 キー " -#: awx/sso/conf.py:543 +#: awx/sso/conf.py:577 msgid "Google OAuth2 Secret" msgstr "Google OAuth2 シークレット" -#: awx/sso/conf.py:544 +#: awx/sso/conf.py:578 msgid "The OAuth2 secret from your web application." msgstr "web アプリケーションの OAuth2 シークレット" -#: awx/sso/conf.py:555 +#: awx/sso/conf.py:589 msgid "Google OAuth2 Whitelisted Domains" msgstr "Google OAuth2 ホワイトリストドメイン" -#: awx/sso/conf.py:556 +#: awx/sso/conf.py:590 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." msgstr "この設定を更新し、Google OAuth2 を使用してログインできるドメインを制限します。" -#: awx/sso/conf.py:567 +#: awx/sso/conf.py:601 msgid "Google OAuth2 Extra Arguments" msgstr "Google OAuth2 追加引数" -#: awx/sso/conf.py:568 +#: awx/sso/conf.py:602 msgid "" "Extra arguments for Google OAuth2 login. You can restrict it to only allow a" " single domain to authenticate, even if the user is logged in with multple " @@ -4049,97 +4891,97 @@ msgstr "" "アカウントでログインしている場合でも、単一ドメインの認証のみを許可するように制限できます。詳細については、Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:582 +#: awx/sso/conf.py:616 msgid "Google OAuth2 Organization Map" msgstr "Google OAuth2 組織マップ" -#: awx/sso/conf.py:594 +#: awx/sso/conf.py:628 msgid "Google OAuth2 Team Map" msgstr "Google OAuth2 チームマップ" -#: awx/sso/conf.py:610 +#: awx/sso/conf.py:644 msgid "GitHub OAuth2 Callback URL" msgstr "GitHub OAuth2 コールバック URL" -#: awx/sso/conf.py:614 awx/sso/conf.py:626 awx/sso/conf.py:637 -#: awx/sso/conf.py:649 awx/sso/conf.py:661 +#: awx/sso/conf.py:648 awx/sso/conf.py:660 awx/sso/conf.py:671 +#: awx/sso/conf.py:683 awx/sso/conf.py:695 msgid "GitHub OAuth2" msgstr "GitHub OAuth2" -#: awx/sso/conf.py:624 +#: awx/sso/conf.py:658 msgid "GitHub OAuth2 Key" msgstr "GitHub OAuth2 キー" -#: awx/sso/conf.py:625 +#: awx/sso/conf.py:659 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "GitHub 開発者アプリケーションからの OAuth2 キー (クライアント ID)。" -#: awx/sso/conf.py:635 +#: awx/sso/conf.py:669 msgid "GitHub OAuth2 Secret" msgstr "GitHub OAuth2 シークレット" -#: awx/sso/conf.py:636 +#: awx/sso/conf.py:670 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "GitHub 開発者アプリケーションからの OAuth2 シークレット (クライアントシークレット)。" -#: awx/sso/conf.py:647 +#: awx/sso/conf.py:681 msgid "GitHub OAuth2 Organization Map" msgstr "GitHub OAuth2 組織マップ" -#: awx/sso/conf.py:659 +#: awx/sso/conf.py:693 msgid "GitHub OAuth2 Team Map" msgstr "GitHub OAuth2 チームマップ" -#: awx/sso/conf.py:675 +#: awx/sso/conf.py:709 msgid "GitHub Organization OAuth2 Callback URL" msgstr "GitHub 組織 OAuth2 コールバック URL" -#: awx/sso/conf.py:679 awx/sso/conf.py:691 awx/sso/conf.py:702 -#: awx/sso/conf.py:715 awx/sso/conf.py:726 awx/sso/conf.py:738 +#: awx/sso/conf.py:713 awx/sso/conf.py:725 awx/sso/conf.py:736 +#: awx/sso/conf.py:749 awx/sso/conf.py:760 awx/sso/conf.py:772 msgid "GitHub Organization OAuth2" msgstr "GitHub 組織 OAuth2" -#: awx/sso/conf.py:689 +#: awx/sso/conf.py:723 msgid "GitHub Organization OAuth2 Key" msgstr "GitHub 組織 OAuth2 キー" -#: awx/sso/conf.py:690 awx/sso/conf.py:768 +#: awx/sso/conf.py:724 awx/sso/conf.py:802 msgid "The OAuth2 key (Client ID) from your GitHub organization application." msgstr "GitHub 組織アプリケーションからの OAuth2 キー (クライアント ID)。" -#: awx/sso/conf.py:700 +#: awx/sso/conf.py:734 msgid "GitHub Organization OAuth2 Secret" msgstr "GitHub 組織 OAuth2 シークレット" -#: awx/sso/conf.py:701 awx/sso/conf.py:779 +#: awx/sso/conf.py:735 awx/sso/conf.py:813 msgid "" "The OAuth2 secret (Client Secret) from your GitHub organization application." msgstr "GitHub 組織アプリケーションからの OAuth2 シークレット (クライアントシークレット)。" -#: awx/sso/conf.py:712 +#: awx/sso/conf.py:746 msgid "GitHub Organization Name" msgstr "GitHub 組織名" -#: awx/sso/conf.py:713 +#: awx/sso/conf.py:747 msgid "" "The name of your GitHub organization, as used in your organization's URL: " "https://github.com//." msgstr "GitHub 組織の名前で、組織の URL (https://github.com//) で使用されます。" -#: awx/sso/conf.py:724 +#: awx/sso/conf.py:758 msgid "GitHub Organization OAuth2 Organization Map" msgstr "GitHub 組織 OAuth2 組織マップ" -#: awx/sso/conf.py:736 +#: awx/sso/conf.py:770 msgid "GitHub Organization OAuth2 Team Map" msgstr "GitHub 組織 OAuth2 チームマップ" -#: awx/sso/conf.py:752 +#: awx/sso/conf.py:786 msgid "GitHub Team OAuth2 Callback URL" msgstr "GitHub チーム OAuth2 コールバック URL" -#: awx/sso/conf.py:753 +#: awx/sso/conf.py:787 msgid "" "Create an organization-owned application at " "https://github.com/organizations//settings/applications and obtain " @@ -4151,24 +4993,24 @@ msgstr "" " キー (クライアント ID) およびシークレット (クライアントシークレット) を取得します。この URL をアプリケーションのコールバック URL " "として指定します。" -#: awx/sso/conf.py:757 awx/sso/conf.py:769 awx/sso/conf.py:780 -#: awx/sso/conf.py:793 awx/sso/conf.py:804 awx/sso/conf.py:816 +#: awx/sso/conf.py:791 awx/sso/conf.py:803 awx/sso/conf.py:814 +#: awx/sso/conf.py:827 awx/sso/conf.py:838 awx/sso/conf.py:850 msgid "GitHub Team OAuth2" msgstr "GitHub チーム OAuth2" -#: awx/sso/conf.py:767 +#: awx/sso/conf.py:801 msgid "GitHub Team OAuth2 Key" msgstr "GitHub チーム OAuth2 キー" -#: awx/sso/conf.py:778 +#: awx/sso/conf.py:812 msgid "GitHub Team OAuth2 Secret" msgstr "GitHub チーム OAuth2 シークレット" -#: awx/sso/conf.py:790 +#: awx/sso/conf.py:824 msgid "GitHub Team ID" msgstr "GitHub チーム ID" -#: awx/sso/conf.py:791 +#: awx/sso/conf.py:825 msgid "" "Find the numeric team ID using the Github API: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." @@ -4176,19 +5018,19 @@ msgstr "" "Github API を使用して数値のチーム ID を検索します: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/" -#: awx/sso/conf.py:802 +#: awx/sso/conf.py:836 msgid "GitHub Team OAuth2 Organization Map" msgstr "GitHub チーム OAuth2 組織マップ" -#: awx/sso/conf.py:814 +#: awx/sso/conf.py:848 msgid "GitHub Team OAuth2 Team Map" msgstr "GitHub チーム OAuth2 チームマップ" -#: awx/sso/conf.py:830 +#: awx/sso/conf.py:864 msgid "Azure AD OAuth2 Callback URL" msgstr "Azure AD OAuth2 コールバック URL" -#: awx/sso/conf.py:831 +#: awx/sso/conf.py:865 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4197,40 +5039,40 @@ msgstr "" "登録プロセスの一環として、この URL をアプリケーションのコールバック URL として指定します。詳細については、Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:834 awx/sso/conf.py:846 awx/sso/conf.py:857 -#: awx/sso/conf.py:869 awx/sso/conf.py:881 +#: awx/sso/conf.py:868 awx/sso/conf.py:880 awx/sso/conf.py:891 +#: awx/sso/conf.py:903 awx/sso/conf.py:915 msgid "Azure AD OAuth2" msgstr "Azure AD OAuth2" -#: awx/sso/conf.py:844 +#: awx/sso/conf.py:878 msgid "Azure AD OAuth2 Key" msgstr "Azure AD OAuth2 キー" -#: awx/sso/conf.py:845 +#: awx/sso/conf.py:879 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "Azure AD アプリケーションからの OAuth2 キー (クライアント ID)。" -#: awx/sso/conf.py:855 +#: awx/sso/conf.py:889 msgid "Azure AD OAuth2 Secret" msgstr "Azure AD OAuth2 シークレット" -#: awx/sso/conf.py:856 +#: awx/sso/conf.py:890 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "Azure AD アプリケーションからの OAuth2 シークレット (クライアントシークレット)。" -#: awx/sso/conf.py:867 +#: awx/sso/conf.py:901 msgid "Azure AD OAuth2 Organization Map" msgstr "Azure AD OAuth2 組織マップ" -#: awx/sso/conf.py:879 +#: awx/sso/conf.py:913 msgid "Azure AD OAuth2 Team Map" msgstr "Azure AD OAuth2 チームマップ" -#: awx/sso/conf.py:904 +#: awx/sso/conf.py:938 msgid "SAML Assertion Consumer Service (ACS) URL" msgstr "SAML アサーションコンシューマー サービス (ACS) URL" -#: awx/sso/conf.py:905 +#: awx/sso/conf.py:939 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this ACS URL for your " @@ -4239,29 +5081,31 @@ msgstr "" "設定済みの各アイデンティティープロバイダー (IdP) で Tower をサービスプロバイダー (SP) として登録します。SP エンティティー ID " "およびアプリケーションのこの ACS URL を指定します。" -#: awx/sso/conf.py:908 awx/sso/conf.py:922 awx/sso/conf.py:936 -#: awx/sso/conf.py:951 awx/sso/conf.py:965 awx/sso/conf.py:978 -#: awx/sso/conf.py:999 awx/sso/conf.py:1017 awx/sso/conf.py:1036 -#: awx/sso/conf.py:1070 awx/sso/conf.py:1083 awx/sso/models.py:16 +#: awx/sso/conf.py:942 awx/sso/conf.py:956 awx/sso/conf.py:970 +#: awx/sso/conf.py:985 awx/sso/conf.py:999 awx/sso/conf.py:1012 +#: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 +#: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 +#: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 +#: awx/sso/conf.py:1211 awx/sso/models.py:16 msgid "SAML" msgstr "SAML" -#: awx/sso/conf.py:919 +#: awx/sso/conf.py:953 msgid "SAML Service Provider Metadata URL" msgstr "SAML サービスプロバイダーメタデータ URL" -#: awx/sso/conf.py:920 +#: awx/sso/conf.py:954 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." msgstr "" "アイデンティティープロバイダー (IdP) が XML メタデータファイルのアップロードを許可する場合、この URL からダウンロードできます。" -#: awx/sso/conf.py:932 +#: awx/sso/conf.py:966 msgid "SAML Service Provider Entity ID" msgstr "SAML サービスプロバイダーエンティティー ID" -#: awx/sso/conf.py:933 +#: awx/sso/conf.py:967 msgid "" "The application-defined unique identifier used as the audience of the SAML " "service provider (SP) configuration. This is usually the URL for Tower." @@ -4269,42 +5113,42 @@ msgstr "" "SAML サービスプロバイダー (SP) 設定の対象として使用されるアプリケーションで定義される固有識別子です。通常これは Tower の URL " "になります。" -#: awx/sso/conf.py:948 +#: awx/sso/conf.py:982 msgid "SAML Service Provider Public Certificate" msgstr "SAML サービスプロバイダーの公開証明書" -#: awx/sso/conf.py:949 +#: awx/sso/conf.py:983 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " certificate content here." msgstr "サービスプロバイダー (SP) として使用するための Tower のキーペアを作成し、ここに証明書の内容を組み込みます。" -#: awx/sso/conf.py:962 +#: awx/sso/conf.py:996 msgid "SAML Service Provider Private Key" msgstr "SAML サービスプロバイダーの秘密鍵|" -#: awx/sso/conf.py:963 +#: awx/sso/conf.py:997 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " private key content here." msgstr "サービスプロバイダー (SP) として使用するための Tower のキーペアを作成し、ここに秘密鍵の内容を組み込みます。" -#: awx/sso/conf.py:975 +#: awx/sso/conf.py:1009 msgid "SAML Service Provider Organization Info" msgstr "SAML サービスプロバイダーの組織情報" -#: awx/sso/conf.py:976 +#: awx/sso/conf.py:1010 msgid "" "Provide the URL, display name, and the name of your app. Refer to the " "Ansible Tower documentation for example syntax." msgstr "" "アプリケーションの URL、表示名、および名前を指定します。構文のサンプルについては Ansible Tower ドキュメントを参照してください。" -#: awx/sso/conf.py:995 +#: awx/sso/conf.py:1029 msgid "SAML Service Provider Technical Contact" msgstr "SAML サービスプロバイダーテクニカルサポートの問い合わせ先" -#: awx/sso/conf.py:996 +#: awx/sso/conf.py:1030 msgid "" "Provide the name and email address of the technical contact for your service" " provider. Refer to the Ansible Tower documentation for example syntax." @@ -4312,11 +5156,11 @@ msgstr "" "サービスプロバイダーのテクニカルサポート担当の名前およびメールアドレスを指定します。構文のサンプルについては Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:1013 +#: awx/sso/conf.py:1047 msgid "SAML Service Provider Support Contact" msgstr "SAML サービスプロバイダーサポートの問い合わせ先" -#: awx/sso/conf.py:1014 +#: awx/sso/conf.py:1048 msgid "" "Provide the name and email address of the support contact for your service " "provider. Refer to the Ansible Tower documentation for example syntax." @@ -4324,11 +5168,11 @@ msgstr "" "サービスプロバイダーのサポート担当の名前およびメールアドレスを指定します。構文のサンプルについては Ansible Tower " "ドキュメントを参照してください。" -#: awx/sso/conf.py:1030 +#: awx/sso/conf.py:1064 msgid "SAML Enabled Identity Providers" msgstr "SAML で有効にされたアイデンティティープロバイダー" -#: awx/sso/conf.py:1031 +#: awx/sso/conf.py:1065 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " @@ -4340,42 +5184,90 @@ msgstr "" " IdP がサポートされます。一部の IdP はデフォルト OID とは異なる属性名を使用してユーザーデータを提供することがあります。それぞれの IdP" " について属性名が上書きされる可能性があります。追加の詳細および構文については、Ansible ドキュメントを参照してください。" -#: awx/sso/conf.py:1068 +#: awx/sso/conf.py:1102 +msgid "SAML Security Config" +msgstr "SAML セキュリティー設定" + +#: awx/sso/conf.py:1103 +msgid "" +"A dict of key value pairs that are passed to the underlying python-saml " +"security setting https://github.com/onelogin/python-saml#settings" +msgstr "" +"基礎となる python-saml セキュリティー設定に渡されるキー値ペアの辞書: https://github.com/onelogin" +"/python-saml#settings" + +#: awx/sso/conf.py:1135 +msgid "SAML Service Provider extra configuration data" +msgstr "SAML サービスプロバイダーの追加設定データ" + +#: awx/sso/conf.py:1136 +msgid "" +"A dict of key value pairs to be passed to the underlying python-saml Service" +" Provider configuration setting." +msgstr "基礎となる python-saml サービスプロバイダー設定に渡されるキー値ペアの辞書。" + +#: awx/sso/conf.py:1149 +msgid "SAML IDP to extra_data attribute mapping" +msgstr "SAML IDP の extra_data 属性へのマッピング" + +#: awx/sso/conf.py:1150 +msgid "" +"A list of tuples that maps IDP attributes to extra_attributes. Each " +"attribute will be a list of values, even if only 1 value." +msgstr "IDP 属性を extra_attributes にマップするタプルの一覧です。各属性は 1 つの値のみの場合も値の一覧となります。" + +#: awx/sso/conf.py:1167 msgid "SAML Organization Map" msgstr "SAML 組織マップ" -#: awx/sso/conf.py:1081 +#: awx/sso/conf.py:1180 msgid "SAML Team Map" msgstr "SAML チームマップ" -#: awx/sso/fields.py:123 +#: awx/sso/conf.py:1193 +msgid "SAML Organization Attribute Mapping" +msgstr "SAML 組織属性マッピング" + +#: awx/sso/conf.py:1194 +msgid "Used to translate user organization membership into Tower." +msgstr "ユーザー組織メンバーシップを Tower に変換するために使用されます。" + +#: awx/sso/conf.py:1209 +msgid "SAML Team Attribute Mapping" +msgstr "SAML チーム属性マッピング" + +#: awx/sso/conf.py:1210 +msgid "Used to translate user team membership into Tower." +msgstr "ユーザーチームメンバーシップを Tower に変換するために使用されます。" + +#: awx/sso/fields.py:183 #, python-brace-format msgid "Invalid connection option(s): {invalid_options}." msgstr "無効な接続オプション: {invalid_options}" -#: awx/sso/fields.py:194 +#: awx/sso/fields.py:266 msgid "Base" msgstr "ベース" -#: awx/sso/fields.py:195 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "1 レベル" -#: awx/sso/fields.py:196 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "サブツリー" -#: awx/sso/fields.py:214 +#: awx/sso/fields.py:286 #, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "3 つの項目の一覧が予期されましが、{length} が取得されました。" -#: awx/sso/fields.py:215 +#: awx/sso/fields.py:287 #, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "LDAPSearch のインスタンスが予期されましたが、{input_type} が取得されました。" -#: awx/sso/fields.py:251 +#: awx/sso/fields.py:323 #, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " @@ -4383,84 +5275,77 @@ msgid "" msgstr "" "LDAPSearch または LDAPSearchUnion のインスタンスが予期されましたが、{input_type} が取得されました。" -#: awx/sso/fields.py:289 +#: awx/sso/fields.py:361 #, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "無効なユーザー属性: {invalid_attrs}" -#: awx/sso/fields.py:306 +#: awx/sso/fields.py:378 #, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "LDAPGroupType のインスタンスが予期されましたが、{input_type} が取得されました。" -#: awx/sso/fields.py:334 -#, python-brace-format -msgid "Invalid user flag: \"{invalid_flag}\"." -msgstr "無効なユーザーフラグ: \"{invalid_flag}\"" - -#: awx/sso/fields.py:350 awx/sso/fields.py:517 -#, python-brace-format -msgid "" -"Expected None, True, False, a string or list of strings but got {input_type}" -" instead." -msgstr "None、True、False、文字列または文字列の一覧が予期されましたが、{input_type} が取得されました。" - -#: awx/sso/fields.py:386 -#, python-brace-format -msgid "Missing key(s): {missing_keys}." -msgstr "キーがありません: {missing_keys}" - -#: awx/sso/fields.py:387 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 #, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "無効なキー: {invalid_keys}" -#: awx/sso/fields.py:436 awx/sso/fields.py:553 +#: awx/sso/fields.py:443 +#, python-brace-format +msgid "Invalid user flag: \"{invalid_flag}\"." +msgstr "無効なユーザーフラグ: \"{invalid_flag}\"" + +#: awx/sso/fields.py:464 +#, python-brace-format +msgid "Missing key(s): {missing_keys}." +msgstr "キーがありません: {missing_keys}" + +#: awx/sso/fields.py:514 awx/sso/fields.py:631 #, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "組織マップの無効なキー: {invalid_keys}" -#: awx/sso/fields.py:454 +#: awx/sso/fields.py:532 #, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "チームマップの必要なキーがありません: {invalid_keys}" -#: awx/sso/fields.py:455 awx/sso/fields.py:572 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 #, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "チームマップの無効なキー: {invalid_keys}" -#: awx/sso/fields.py:571 +#: awx/sso/fields.py:649 #, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "チームマップで必要なキーがありません: {missing_keys}" -#: awx/sso/fields.py:589 +#: awx/sso/fields.py:667 #, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "組織情報レコードで必要なキーがありません: {missing_keys}" -#: awx/sso/fields.py:602 +#: awx/sso/fields.py:680 #, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "組織情報の無効な言語コード: {invalid_lang_codes}" -#: awx/sso/fields.py:621 +#: awx/sso/fields.py:699 #, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "問い合わせ先の必要なキーがありません: {missing_keys}" -#: awx/sso/fields.py:633 +#: awx/sso/fields.py:711 #, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "IdP で必要なキーがありません: {missing_keys}" -#: awx/sso/pipeline.py:24 +#: awx/sso/pipeline.py:31 #, python-brace-format msgid "An account cannot be found for {0}" msgstr "{0} のアカウントが見つかりません" -#: awx/sso/pipeline.py:30 +#: awx/sso/pipeline.py:37 msgid "Your account is inactive" msgstr "アカウントが非アクティブです" @@ -4487,68 +5372,48 @@ msgstr "TACACS+ シークレットは ASCII 以外の文字を許可しません msgid "AWX" msgstr "AWX" -#: awx/templates/rest_framework/api.html:39 +#: awx/templates/rest_framework/api.html:42 msgid "Ansible Tower API Guide" msgstr "Ansible Tower API ガイド" -#: awx/templates/rest_framework/api.html:40 +#: awx/templates/rest_framework/api.html:43 msgid "Back to Ansible Tower" msgstr "Ansible Tower に戻る" -#: awx/templates/rest_framework/api.html:41 +#: awx/templates/rest_framework/api.html:44 msgid "Resize" msgstr "サイズの変更" +#: awx/templates/rest_framework/base.html:37 +msgid "navbar" +msgstr "ナビゲーションバー" + +#: awx/templates/rest_framework/base.html:75 +msgid "content" +msgstr "コンテンツ" + #: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 -#, python-format -msgid "Make a GET request on the %(name)s resource" -msgstr "%(name)s リソースでの GET 要求" +msgid "request form" +msgstr "要求フォーム" -#: awx/templates/rest_framework/base.html:80 -msgid "Specify a format for the GET request" -msgstr "GET 要求の形式を指定" - -#: awx/templates/rest_framework/base.html:86 -#, python-format -msgid "" -"Make a GET request on the %(name)s resource with the format set to " -"`%(format)s`" -msgstr "形式が `%(format)s` に設定された状態での %(name)s リソースでの GET 要求" - -#: awx/templates/rest_framework/base.html:100 -#, python-format -msgid "Make an OPTIONS request on the %(name)s resource" -msgstr "%(name)s リソースでの OPTIONS 要求" - -#: awx/templates/rest_framework/base.html:106 -#, python-format -msgid "Make a DELETE request on the %(name)s resource" -msgstr "%(name)s リソースでの DELETE 要求" - -#: awx/templates/rest_framework/base.html:113 +#: awx/templates/rest_framework/base.html:134 msgid "Filters" msgstr "フィルター" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 -#, python-format -msgid "Make a POST request on the %(name)s resource" -msgstr "%(name)s リソースでの POST 要求" +#: awx/templates/rest_framework/base.html:139 +msgid "main content" +msgstr "主な内容" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 -#, python-format -msgid "Make a PUT request on the %(name)s resource" -msgstr "%(name)s リソースでの PUT 要求" +#: awx/templates/rest_framework/base.html:155 +msgid "request info" +msgstr "要求情報" -#: awx/templates/rest_framework/base.html:233 -#, python-format -msgid "Make a PATCH request on the %(name)s resource" -msgstr "%(name)s リソースでの PATCH 要求" +#: awx/templates/rest_framework/base.html:159 +msgid "response info" +msgstr "応答情報" #: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:36 awx/ui/conf.py:51 -#: awx/ui/conf.py:63 +#: awx/ui/conf.py:63 awx/ui/conf.py:73 msgid "UI" msgstr "UI" @@ -4600,14 +5465,24 @@ msgstr "" "および JPEG 形式がサポートされます。" #: awx/ui/conf.py:60 -msgid "Max Job Events Retreived by UI" +msgid "Max Job Events Retrieved by UI" msgstr "UI で検索される最大ジョブイベント" #: awx/ui/conf.py:61 msgid "" -"Maximum number of job events for the UI to retreive within a single request." +"Maximum number of job events for the UI to retrieve within a single request." msgstr "単一要求内で検索される UI についての最大ジョブイベント数。" +#: awx/ui/conf.py:70 +msgid "Enable Live Updates in the UI" +msgstr "UI でのライブ更新の有効化" + +#: awx/ui/conf.py:71 +msgid "" +"If disabled, the page will not refresh when events are received. Reloading " +"the page will be required to get the latest details." +msgstr "無効にされている場合、ページはイベントの受信時に更新されません。最新情報の詳細を取得するには、ページをリロードする必要があります。" + #: awx/ui/fields.py:29 msgid "" "Invalid format for custom logo. Must be a data URL with a base64-encoded " @@ -4618,93 +5493,3 @@ msgstr "" #: awx/ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "データ URL の無効な base64 エンコードされたデータ。" - -#: awx/ui/templates/ui/index.html:31 -msgid "" -"Your session will expire in 60 seconds, would you like to " -"continue?" -msgstr "" -"セッションは 60 秒後に期限切れになります。続行しますか?" - -#: awx/ui/templates/ui/index.html:46 -msgid "CANCEL" -msgstr "取り消し" - -#: awx/ui/templates/ui/index.html:98 -msgid "Set how many days of data should be retained." -msgstr "データの保持日数を設定します。" - -#: awx/ui/templates/ui/index.html:104 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"負でない 9999 " -"より値の小さい整数を入力してください。" - -#: awx/ui/templates/ui/index.html:109 -msgid "" -"For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.\n" -"
\n" -"
CAUTION: Setting both numerical variables to \"0\" will delete all facts.\n" -"
\n" -"
" -msgstr "" -"指定された期間の前に収集されたファクトについては、時間枠 (頻度) ごとに 1 つのファクトスキャン (スナップショット) を保存します。たとえば、30 日間の前のファクトは削除され、1 つの週次ファクトは保持されます。\n" -"
\n" -"
注意: どちらの数値変数も「0」に設定すると、すべてのファクトが削除されます。\n" -"
\n" -"
" - -#: awx/ui/templates/ui/index.html:118 -msgid "Select a time period after which to remove old facts" -msgstr "古いファクトを削除するまでの期間を選択" - -#: awx/ui/templates/ui/index.html:132 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"負でない 9999 " -"より値の小さい整数を入力してください。" - -#: awx/ui/templates/ui/index.html:137 -msgid "Select a frequency for snapshot retention" -msgstr "スナップショットの保持頻度を選択" - -#: awx/ui/templates/ui/index.html:151 -msgid "" -"Please enter an integer that is not" -" negative that is " -"lower than 9999." -msgstr "" -"負でない 9999 " -"よりも値の小さい整数を入力してください。" - -#: awx/ui/templates/ui/index.html:157 -msgid "working..." -msgstr "実行中..." diff --git a/awx/locale/nl/LC_MESSAGES/django.po b/awx/locale/nl/LC_MESSAGES/django.po index 24b5ca542b..d7c7484133 100644 --- a/awx/locale/nl/LC_MESSAGES/django.po +++ b/awx/locale/nl/LC_MESSAGES/django.po @@ -1,12 +1,13 @@ # helena , 2017. #zanata # helena01 , 2017. #zanata # helena02 , 2017. #zanata +# helena , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-30 20:23+0000\n" -"PO-Revision-Date: 2017-12-06 02:18+0000\n" +"POT-Creation-Date: 2018-06-14 18:30+0000\n" +"PO-Revision-Date: 2018-06-15 12:29+0000\n" "Last-Translator: helena \n" "Language-Team: Dutch\n" "MIME-Version: 1.0\n" @@ -14,29 +15,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"X-Generator: Zanata 4.3.2\n" +"X-Generator: Zanata 4.6.0\n" -#: awx/api/authentication.py:67 -msgid "Invalid token header. No credentials provided." -msgstr "Ongeldige tokenheader. Geen referenties verschaft." - -#: awx/api/authentication.py:70 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "Ongeldige tokenheader. De tokentekenreeks kan geen spaties bevatten." - -#: awx/api/authentication.py:105 -msgid "User inactive or deleted" -msgstr "De gebruiker is niet actief of is verwijderd" - -#: awx/api/authentication.py:161 -msgid "Invalid task token" -msgstr "Ongeldig taaktoken" - -#: awx/api/conf.py:12 +#: awx/api/conf.py:15 msgid "Idle Time Force Log Out" msgstr "Niet-actieve tijd voor forceren van afmelding" -#: awx/api/conf.py:13 +#: awx/api/conf.py:16 msgid "" "Number of seconds that a user is inactive before they will need to login " "again." @@ -44,67 +29,101 @@ msgstr "" "Maximumaantal seconden dat een gebruiker niet-actief is voordat deze zich " "opnieuw moet aanmelden." -#: awx/api/conf.py:14 awx/api/conf.py:24 awx/api/conf.py:33 awx/sso/conf.py:85 -#: awx/sso/conf.py:96 awx/sso/conf.py:108 awx/sso/conf.py:123 +#: awx/api/conf.py:17 awx/api/conf.py:26 awx/api/conf.py:34 awx/api/conf.py:47 +#: awx/sso/conf.py:85 awx/sso/conf.py:96 awx/sso/conf.py:108 +#: awx/sso/conf.py:123 msgid "Authentication" msgstr "Authenticatie" -#: awx/api/conf.py:22 -msgid "Maximum number of simultaneous logins" -msgstr "Maximumaantal gelijktijdige aanmeldingen" +#: awx/api/conf.py:24 +msgid "Maximum number of simultaneous logged in sessions" +msgstr "Maximumaantal gelijktijdige aangemelde sessies" -#: awx/api/conf.py:23 +#: awx/api/conf.py:25 msgid "" -"Maximum number of simultaneous logins a user may have. To disable enter -1." +"Maximum number of simultaneous logged in sessions a user may have. To " +"disable enter -1." msgstr "" -"Maximumaantal gelijktijdige aanmeldingen dat een gebruiker kan hebben. Voer " -"-1 in om dit uit te schakelen." +"Maximumaantal gelijktijdige aangemelde sessies dat een gebruiker kan hebben." +" Voer -1 in om dit uit te schakelen." -#: awx/api/conf.py:31 +#: awx/api/conf.py:32 msgid "Enable HTTP Basic Auth" msgstr "HTTP-basisauthenticatie inschakelen" -#: awx/api/conf.py:32 +#: awx/api/conf.py:33 msgid "Enable HTTP Basic Auth for the API Browser." msgstr "Schakel HTTP-basisauthenticatie voor de API-browser in." -#: awx/api/filters.py:129 +#: awx/api/conf.py:42 +msgid "OAuth 2 Timeout Settings" +msgstr "Instellingen OAuth 2-time-out" + +#: awx/api/conf.py:43 +msgid "" +"Dictionary for customizing OAuth 2 timeouts, available items are " +"`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number " +"of seconds, and `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of " +"authorization grants in the number of seconds." +msgstr "" +"Woordenlijst voor het aanpassen van OAuth 2-time-outs. Beschikbare items " +"zijn 'ACCESS_TOKEN_EXPIRE_SECONDS', de tijdsduur van toegangstokens in het " +"aantal seconden, en 'AUTHORIZATION_CODE_EXPIRE_SECONDS', de tijdsduur van de" +" toekenning van machtigingen in het aantal seconden." + +#: awx/api/exceptions.py:20 +msgid "Resource is being used by running jobs." +msgstr "Bron wordt gebruikt om taken uit te voeren." + +#: awx/api/fields.py:81 +#, python-brace-format +msgid "Invalid key names: {invalid_key_names}" +msgstr "Ongeldige sleutelnamen: {invalid_key_names}" + +#: awx/api/fields.py:107 +msgid "Credential {} does not exist" +msgstr "Toegangsgegeven {} bestaat niet" + +#: awx/api/filters.py:96 +msgid "No related model for field {}." +msgstr "Geen verwant model voor veld {}." + +#: awx/api/filters.py:113 msgid "Filtering on password fields is not allowed." msgstr "Filteren op wachtwoordvelden is niet toegestaan." -#: awx/api/filters.py:141 awx/api/filters.py:143 +#: awx/api/filters.py:125 awx/api/filters.py:127 #, python-format msgid "Filtering on %s is not allowed." msgstr "Filteren op %s is niet toegestaan." -#: awx/api/filters.py:146 +#: awx/api/filters.py:130 msgid "Loops not allowed in filters, detected on field {}." msgstr "Lussen zijn niet toegestaan in filters, gedetecteerd in veld {}." -#: awx/api/filters.py:171 +#: awx/api/filters.py:159 +msgid "Query string field name not provided." +msgstr "Veldnaam voor queryreeks niet opgegeven." + +#: awx/api/filters.py:186 #, python-brace-format msgid "Invalid {field_name} id: {field_id}" msgstr "Ongeldig {field_name} id: {field_id}" -#: awx/api/filters.py:302 +#: awx/api/filters.py:319 #, python-format msgid "cannot filter on kind %s" msgstr "kan niet filteren op soort %s" -#: awx/api/filters.py:409 -#, python-format -msgid "cannot order by field %s" -msgstr "Kan niet ordenen op veld %s" - -#: awx/api/generics.py:550 awx/api/generics.py:612 +#: awx/api/generics.py:620 awx/api/generics.py:682 msgid "\"id\" field must be an integer." msgstr "'Id'-veld moet een geheel getal zijn." -#: awx/api/generics.py:609 +#: awx/api/generics.py:679 msgid "\"id\" is required to disassociate" msgstr "'id' is vereist om los te koppelen" -#: awx/api/generics.py:660 +#: awx/api/generics.py:730 msgid "{} 'id' field is missing." msgstr "{} 'id'-veld ontbreekt." @@ -144,11 +163,11 @@ msgstr "Tijdstempel toen dit {} werd gemaakt." msgid "Timestamp when this {} was last modified." msgstr "Tijdstempel van de laatste wijziging van dit {}." -#: awx/api/parsers.py:64 +#: awx/api/parsers.py:33 msgid "JSON parse error - not a JSON object" msgstr "JSON-parseerfout - is geen JSON-object" -#: awx/api/parsers.py:67 +#: awx/api/parsers.py:36 #, python-format msgid "" "JSON parse error - %s\n" @@ -157,76 +176,121 @@ msgstr "" "JSON-parseerfout - %s\n" "Mogelijke oorzaak: navolgende komma." -#: awx/api/serializers.py:268 +#: awx/api/serializers.py:153 +msgid "" +"The original object is already named {}, a copy from it cannot have the same" +" name." +msgstr "" +"Het oorspronkelijke object heet al {}, een kopie hiervan kan niet dezelfde " +"naam hebben." + +#: awx/api/serializers.py:295 msgid "Playbook Run" msgstr "Draaiboek uitvoering" -#: awx/api/serializers.py:269 +#: awx/api/serializers.py:296 msgid "Command" msgstr "Opdracht" -#: awx/api/serializers.py:270 awx/main/models/unified_jobs.py:435 +#: awx/api/serializers.py:297 awx/main/models/unified_jobs.py:526 msgid "SCM Update" msgstr "SCM-update" -#: awx/api/serializers.py:271 +#: awx/api/serializers.py:298 msgid "Inventory Sync" msgstr "Inventarissynchronisatie" -#: awx/api/serializers.py:272 +#: awx/api/serializers.py:299 msgid "Management Job" msgstr "Beheertaak" -#: awx/api/serializers.py:273 +#: awx/api/serializers.py:300 msgid "Workflow Job" msgstr "Workflowtaak" -#: awx/api/serializers.py:274 +#: awx/api/serializers.py:301 msgid "Workflow Template" msgstr "Workflowsjabloon" -#: awx/api/serializers.py:701 awx/api/serializers.py:759 awx/api/views.py:4365 -#, python-format -msgid "" -"Standard Output too large to display (%(text_size)d bytes), only download " -"supported for sizes over %(supported_size)d bytes" -msgstr "" -"De standaardoutput is te groot om weer te geven (%(text_size)d bytes), " -"alleen download ondersteund voor groottes van meer dan %(supported_size)d " -"bytes" +#: awx/api/serializers.py:302 +msgid "Job Template" +msgstr "Taaksjabloon" -#: awx/api/serializers.py:774 +#: awx/api/serializers.py:697 +msgid "" +"Indicates whether all of the events generated by this unified job have been " +"saved to the database." +msgstr "" +"Geeft aan of alle evenementen die aangemaakt zijn door deze " +"gemeenschappelijke taak opgeslagen zijn in de database." + +#: awx/api/serializers.py:854 msgid "Write-only field used to change the password." msgstr "Een alleen-schrijven-veld is gebruikt om het wachtwoord te wijzigen." -#: awx/api/serializers.py:776 +#: awx/api/serializers.py:856 msgid "Set if the account is managed by an external service" msgstr "Instellen als de account wordt beheerd door een externe service" -#: awx/api/serializers.py:800 +#: awx/api/serializers.py:880 msgid "Password required for new User." msgstr "Wachtwoord vereist voor een nieuwe gebruiker." -#: awx/api/serializers.py:886 +#: awx/api/serializers.py:971 #, python-format msgid "Unable to change %s on user managed by LDAP." msgstr "Kan %s niet wijzigen voor gebruiker die wordt beheerd met LDAP." -#: awx/api/serializers.py:1050 +#: awx/api/serializers.py:1057 +msgid "Must be a simple space-separated string with allowed scopes {}." +msgstr "" +"Moet een reeks zijn die gescheiden is met enkele spaties en die toegestane " +"bereiken heeft {}." + +#: awx/api/serializers.py:1151 +msgid "Authorization Grant Type" +msgstr "Soort toekenning van machtiging" + +#: awx/api/serializers.py:1153 awx/main/models/credential/__init__.py:1064 +msgid "Client Secret" +msgstr "Klant-geheim" + +#: awx/api/serializers.py:1156 +msgid "Client Type" +msgstr "Soort klant" + +#: awx/api/serializers.py:1159 +msgid "Redirect URIs" +msgstr "URI's doorverwijzen" + +#: awx/api/serializers.py:1162 +msgid "Skip Authorization" +msgstr "Autorisatie overslaan" + +#: awx/api/serializers.py:1264 +msgid "This path is already being used by another manual project." +msgstr "Dit pad wordt al gebruikt door een ander handmatig project." + +#: awx/api/serializers.py:1290 +msgid "This field has been deprecated and will be removed in a future release" +msgstr "" +"Dit veld is afgeschaft en zal worden verwijderd in een toekomstige versie" + +#: awx/api/serializers.py:1349 msgid "Organization is missing" msgstr "Organisatie ontbreekt" -#: awx/api/serializers.py:1054 +#: awx/api/serializers.py:1353 msgid "Update options must be set to false for manual projects." msgstr "" "De update-opties moeten voor handmatige projecten worden ingesteld op " "onwaar." -#: awx/api/serializers.py:1060 +#: awx/api/serializers.py:1359 msgid "Array of playbooks available within this project." msgstr "Er is binnen dit project een draaiboekenmatrix beschikbaar." -#: awx/api/serializers.py:1079 +#: awx/api/serializers.py:1378 msgid "" "Array of inventory files and directories available within this project, not " "comprehensive." @@ -234,72 +298,78 @@ msgstr "" "Er is binnen dit project een niet-volledige matrix met " "inventarisatiebestanden en -mappen beschikbaar." -#: awx/api/serializers.py:1201 +#: awx/api/serializers.py:1426 awx/api/serializers.py:3194 +msgid "A count of hosts uniquely assigned to each status." +msgstr "" +"Een telling van de unieke hosts die toegewezen zijn aan iedere status." + +#: awx/api/serializers.py:1429 awx/api/serializers.py:3197 +msgid "A count of all plays and tasks for the job run." +msgstr "" +"Een telling van alle draaiboekuitvoeringen en taken voor het uitvoeren van " +"de taak." + +#: awx/api/serializers.py:1554 msgid "Smart inventories must specify host_filter" msgstr "Smart-inventaris moet hostfilter specificeren" -#: awx/api/serializers.py:1303 +#: awx/api/serializers.py:1658 #, python-format msgid "Invalid port specification: %s" msgstr "Ongeldige poortspecificatie: %s" -#: awx/api/serializers.py:1314 +#: awx/api/serializers.py:1669 msgid "Cannot create Host for Smart Inventory" msgstr "Kan geen host aanmaken voor Smart-inventaris" -#: awx/api/serializers.py:1336 awx/api/serializers.py:3321 -#: awx/api/serializers.py:3406 awx/main/validators.py:198 -msgid "Must be valid JSON or YAML." -msgstr "Moet geldig JSON of YAML zijn." - -#: awx/api/serializers.py:1432 +#: awx/api/serializers.py:1781 msgid "Invalid group name." msgstr "Ongeldige groepsnaam." -#: awx/api/serializers.py:1437 +#: awx/api/serializers.py:1786 msgid "Cannot create Group for Smart Inventory" msgstr "Kan geen groep aanmaken voor Smart-inventaris" -#: awx/api/serializers.py:1509 +#: awx/api/serializers.py:1861 msgid "" "Script must begin with a hashbang sequence: i.e.... #!/usr/bin/env python" msgstr "" "Script moet beginnen met een hashbang-reeks, bijvoorbeeld ... #!/usr/bin/env" " python" -#: awx/api/serializers.py:1555 +#: awx/api/serializers.py:1910 msgid "`{}` is a prohibited environment variable" msgstr "`{}` is niet toegestaan als omgevingsvariabele" -#: awx/api/serializers.py:1566 +#: awx/api/serializers.py:1921 msgid "If 'source' is 'custom', 'source_script' must be provided." msgstr "Als 'bron' 'aangepast' is, moet 'source_script' worden geleverd." -#: awx/api/serializers.py:1572 +#: awx/api/serializers.py:1927 msgid "Must provide an inventory." msgstr "Moet een inventaris verschaffen." -#: awx/api/serializers.py:1576 +#: awx/api/serializers.py:1931 msgid "" "The 'source_script' does not belong to the same organization as the " "inventory." msgstr "" "Het 'source_script' behoort niet tot dezelfde categorie als de inventaris." -#: awx/api/serializers.py:1578 +#: awx/api/serializers.py:1933 msgid "'source_script' doesn't exist." msgstr "'source_script' bestaat niet." -#: awx/api/serializers.py:1602 +#: awx/api/serializers.py:1967 msgid "Automatic group relationship, will be removed in 3.3" msgstr "Automatische groepsrelatie, wordt verwijderd in 3.3" -#: awx/api/serializers.py:1679 +#: awx/api/serializers.py:2053 msgid "Cannot use manual project for SCM-based inventory." msgstr "" "Kan geen handmatig project gebruiken voor een SCM-gebaseerde inventaris." -#: awx/api/serializers.py:1685 +#: awx/api/serializers.py:2059 msgid "" "Manual inventory sources are created automatically when a group is created " "in the v1 API." @@ -307,50 +377,50 @@ msgstr "" "Handmatige inventarisbronnen worden automatisch gemaakt wanneer er een groep" " wordt gemaakt in de v1 API." -#: awx/api/serializers.py:1690 +#: awx/api/serializers.py:2064 msgid "Setting not compatible with existing schedules." msgstr "Instelling is niet compatibel met bestaande schema's." -#: awx/api/serializers.py:1695 +#: awx/api/serializers.py:2069 msgid "Cannot create Inventory Source for Smart Inventory" msgstr "Kan geen inventarisbron aanmaken voor Smart-inventaris" -#: awx/api/serializers.py:1709 +#: awx/api/serializers.py:2120 #, python-format msgid "Cannot set %s if not SCM type." msgstr "Kan %s niet instellen als het geen SCM-type is." -#: awx/api/serializers.py:1950 +#: awx/api/serializers.py:2388 msgid "Modifications not allowed for managed credential types" msgstr "Wijzigingen zijn niet toegestaan voor beheerde referentietypen" -#: awx/api/serializers.py:1955 +#: awx/api/serializers.py:2393 msgid "" "Modifications to inputs are not allowed for credential types that are in use" msgstr "" "Wijzigingen in inputs zijn niet toegestaan voor referentietypen die in " "gebruik zijn" -#: awx/api/serializers.py:1961 +#: awx/api/serializers.py:2399 #, python-format msgid "Must be 'cloud' or 'net', not %s" msgstr "Moet 'cloud' of 'net' zijn, niet %s" -#: awx/api/serializers.py:1967 +#: awx/api/serializers.py:2405 msgid "'ask_at_runtime' is not supported for custom credentials." msgstr "'ask_at_runtime' wordt niet ondersteund voor aangepaste referenties." -#: awx/api/serializers.py:2140 +#: awx/api/serializers.py:2585 #, python-format msgid "\"%s\" is not a valid choice" msgstr "\"%s\" is geen geldige keuze" -#: awx/api/serializers.py:2159 -#, python-format -msgid "'%s' is not a valid field for %s" -msgstr "'%s' is geen geldig veld voor %s" +#: awx/api/serializers.py:2604 +#, python-brace-format +msgid "'{field_name}' is not a valid field for {credential_type_name}" +msgstr "'{field_name}' is geen geldig veld voor {credential_type_name}" -#: awx/api/serializers.py:2180 +#: awx/api/serializers.py:2625 msgid "" "You cannot change the credential type of the credential, as it may break the" " functionality of the resources using it." @@ -358,7 +428,7 @@ msgstr "" "U kunt het soort toegangsgegevens niet wijzigen, omdat dan de bronnen die " "deze gebruiken niet langer werken." -#: awx/api/serializers.py:2191 +#: awx/api/serializers.py:2637 msgid "" "Write-only field used to add user to owner role. If provided, do not give " "either team or organization. Only valid for creation." @@ -367,7 +437,7 @@ msgstr "" "de eigenaarrol. Indien verschaft, geef geen team of organisatie op. Alleen " "geldig voor maken." -#: awx/api/serializers.py:2196 +#: awx/api/serializers.py:2642 msgid "" "Write-only field used to add team to owner role. If provided, do not give " "either user or organization. Only valid for creation." @@ -376,7 +446,7 @@ msgstr "" "te voegen. Indien verschaft, geef geen gebruiker of organisatie op. Alleen " "geldig voor maken." -#: awx/api/serializers.py:2201 +#: awx/api/serializers.py:2647 msgid "" "Inherit permissions from organization roles. If provided on creation, do not" " give either user or team." @@ -384,200 +454,314 @@ msgstr "" "Neem machtigingen over van organisatierollen. Indien verschaft bij maken, " "geef geen gebruiker of team op." -#: awx/api/serializers.py:2217 +#: awx/api/serializers.py:2663 msgid "Missing 'user', 'team', or 'organization'." msgstr "'gebruiker', 'team' of 'organisatie' ontbreekt." -#: awx/api/serializers.py:2257 +#: awx/api/serializers.py:2703 msgid "" "Credential organization must be set and match before assigning to a team" msgstr "" "Referentieorganisatie moet worden ingesteld en moet overeenkomen vóór " "toewijzing aan een team" -#: awx/api/serializers.py:2424 +#: awx/api/serializers.py:2904 msgid "You must provide a cloud credential." msgstr "U moet een cloudreferentie opgeven." -#: awx/api/serializers.py:2425 +#: awx/api/serializers.py:2905 msgid "You must provide a network credential." msgstr "U moet een netwerkreferentie opgeven." -#: awx/api/serializers.py:2441 +#: awx/api/serializers.py:2906 awx/main/models/jobs.py:155 +msgid "You must provide an SSH credential." +msgstr "U moet een SSH-referentie opgeven." + +#: awx/api/serializers.py:2907 +msgid "You must provide a vault credential." +msgstr "U dient een kluistoegangsgegeven op te geven." + +#: awx/api/serializers.py:2926 msgid "This field is required." msgstr "Dit veld is vereist." -#: awx/api/serializers.py:2443 awx/api/serializers.py:2445 +#: awx/api/serializers.py:2928 awx/api/serializers.py:2930 msgid "Playbook not found for project." msgstr "Draaiboek is niet gevonden voor project." -#: awx/api/serializers.py:2447 +#: awx/api/serializers.py:2932 msgid "Must select playbook for project." msgstr "Moet een draaiboek selecteren voor het project." -#: awx/api/serializers.py:2522 +#: awx/api/serializers.py:3013 +msgid "Cannot enable provisioning callback without an inventory set." +msgstr "" +"Kan provisioning-terugkoppeling niet inschakelen zonder ingesteld " +"inventaris." + +#: awx/api/serializers.py:3016 msgid "Must either set a default value or ask to prompt on launch." msgstr "" "Moet een standaardwaarde instellen of hierom laten vragen bij het opstarten." -#: awx/api/serializers.py:2524 awx/main/models/jobs.py:326 +#: awx/api/serializers.py:3018 awx/main/models/jobs.py:310 msgid "Job types 'run' and 'check' must have assigned a project." msgstr "" "Aan de taaktypen 'uitvoeren' en 'controleren' moet een project zijn " "toegewezen." -#: awx/api/serializers.py:2611 +#: awx/api/serializers.py:3134 msgid "Invalid job template." msgstr "Ongeldige taaksjabloon." -#: awx/api/serializers.py:2708 -msgid "Neither credential nor vault credential provided." -msgstr "Geen toegangsgegevens of toegangsgegevens voor de kluis ingevoerd." +#: awx/api/serializers.py:3249 +msgid "No change to job limit" +msgstr "Geen wijzigingen in de taaklimiet" -#: awx/api/serializers.py:2711 +#: awx/api/serializers.py:3250 +msgid "All failed and unreachable hosts" +msgstr "Alle mislukte en onbereikbare hosts" + +#: awx/api/serializers.py:3265 +msgid "Missing passwords needed to start: {}" +msgstr "Ontbrekende wachtwoorden die nodig zijn om op te starten: {}" + +#: awx/api/serializers.py:3284 +msgid "Relaunch by host status not available until job finishes running." +msgstr "" +"Opnieuw opstarten met hoststatus niet beschikbaar tot taak volledig " +"uitgevoerd is." + +#: awx/api/serializers.py:3298 msgid "Job Template Project is missing or undefined." msgstr "Het taaksjabloonproject ontbreekt of is niet gedefinieerd." -#: awx/api/serializers.py:2713 +#: awx/api/serializers.py:3300 msgid "Job Template Inventory is missing or undefined." msgstr "De taaksjablooninventaris ontbreekt of is niet gedefinieerd." -#: awx/api/serializers.py:2782 awx/main/tasks.py:2186 +#: awx/api/serializers.py:3338 +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:3405 awx/main/tasks.py:2268 msgid "{} are prohibited from use in ad hoc commands." msgstr "{} kunnen niet worden gebruikt in ad-hocopdrachten." -#: awx/api/serializers.py:3008 -#, python-format -msgid "%(job_type)s is not a valid job type. The choices are %(choices)s." -msgstr "%(job_type)s is geen geldig taaktype. De keuzes zijn %(choices)s." +#: awx/api/serializers.py:3474 awx/api/views.py:4843 +#, python-brace-format +msgid "" +"Standard Output too large to display ({text_size} bytes), only download " +"supported for sizes over {supported_size} bytes." +msgstr "" +"De standaardoutput is te groot om weer te geven ({text_size} bytes), " +"download wordt alleen ondersteund voor groottes van meer dan " +"{supported_size} bytes." -#: awx/api/serializers.py:3013 -msgid "Workflow job template is missing during creation." -msgstr "De taaksjabloon voor de workflow ontbreekt tijdens het maken." +#: awx/api/serializers.py:3671 +msgid "Provided variable {} has no database value to replace with." +msgstr "Opgegeven variabele {} heeft geen databasewaarde om mee te vervangen." -#: awx/api/serializers.py:3018 +#: awx/api/serializers.py:3747 #, python-format msgid "Cannot nest a %s inside a WorkflowJobTemplate" msgstr "Kan geen a %s nesten in een WorkflowJobTemplate" -#: awx/api/serializers.py:3291 -#, python-format -msgid "Job Template '%s' is missing or undefined." -msgstr "Taaksjabloon '%s' ontbreekt of is niet gedefinieerd." +#: awx/api/serializers.py:3754 awx/api/views.py:783 +msgid "Related template is not configured to accept credentials on launch." +msgstr "" +"Verwante sjabloon is niet ingesteld om toegangsgegevens bij opstarten te " +"accepteren." -#: awx/api/serializers.py:3294 +#: awx/api/serializers.py:4211 msgid "The inventory associated with this Job Template is being deleted." msgstr "De aan deze taaksjabloon gekoppelde inventaris wordt verwijderd." -#: awx/api/serializers.py:3335 awx/api/views.py:3023 -#, python-format -msgid "Cannot assign multiple %s credentials." -msgstr "Kan niet meerdere referenties voor %s toewijzen." +#: awx/api/serializers.py:4213 +msgid "The provided inventory is being deleted." +msgstr "Opgegeven inventaris wordt verwijderd." -#: awx/api/serializers.py:3337 awx/api/views.py:3026 -msgid "Extra credentials must be network or cloud." -msgstr "Extra referenties moeten netwerk of cloud zijn." +#: awx/api/serializers.py:4221 +msgid "Cannot assign multiple {} credentials." +msgstr "Kan niet meerdere toegangsgegevens voor {} toewijzen." -#: awx/api/serializers.py:3474 +#: awx/api/serializers.py:4234 +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:4360 msgid "" "Missing required fields for Notification Configuration: notification_type" msgstr "" "Ontbrekende vereiste velden voor kennisgevingsconfiguratie: " "notification_type" -#: awx/api/serializers.py:3497 +#: awx/api/serializers.py:4383 msgid "No values specified for field '{}'" msgstr "Geen waarden opgegeven voor veld '{}'" -#: awx/api/serializers.py:3502 +#: awx/api/serializers.py:4388 msgid "Missing required fields for Notification Configuration: {}." msgstr "Ontbrekende vereiste velden voor kennisgevingsconfiguratie: {}." -#: awx/api/serializers.py:3505 +#: awx/api/serializers.py:4391 msgid "Configuration field '{}' incorrect type, expected {}." msgstr "Configuratieveld '{}' onjuist type, {} verwacht." -#: awx/api/serializers.py:3558 -msgid "Inventory Source must be a cloud resource." -msgstr "Inventarisbron moet een cloudresource zijn." - -#: awx/api/serializers.py:3560 -msgid "Manual Project cannot have a schedule set." -msgstr "Handmatig project kan geen ingesteld schema hebben." - -#: awx/api/serializers.py:3563 +#: awx/api/serializers.py:4453 msgid "" -"Inventory sources with `update_on_project_update` cannot be scheduled. " -"Schedule its source project `{}` instead." +"Valid DTSTART required in rrule. Value should start with: " +"DTSTART:YYYYMMDDTHHMMSSZ" msgstr "" -"Inventarisbronnen met `update_on_project_update` kunnen niet worden gepland." -" Plan in plaats daarvan het bijbehorende bronproject `{}`." - -#: awx/api/serializers.py:3582 -msgid "Projects and inventory updates cannot accept extra variables." -msgstr "" -"Projecten en inventarisupdates kunnen geen extra variabelen accepteren." - -#: awx/api/serializers.py:3604 -msgid "" -"DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ" -msgstr "" -"DTSTART vereist in rrule. De waarde moet overeenkomen: " +"Geldige DTSTART vereist in rrule. De waarde moet beginnen met: " "DTSTART:YYYYMMDDTHHMMSSZ" -#: awx/api/serializers.py:3606 +#: awx/api/serializers.py:4455 +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:4457 msgid "Multiple DTSTART is not supported." msgstr "Meervoudige DTSTART wordt niet ondersteund." -#: awx/api/serializers.py:3608 -msgid "RRULE require in rrule." +#: awx/api/serializers.py:4459 +msgid "RRULE required in rrule." msgstr "RRULE vereist in rrule." -#: awx/api/serializers.py:3610 +#: awx/api/serializers.py:4461 msgid "Multiple RRULE is not supported." msgstr "Meervoudige RRULE wordt niet ondersteund." -#: awx/api/serializers.py:3612 +#: awx/api/serializers.py:4463 msgid "INTERVAL required in rrule." msgstr "INTERVAL is vereist in rrule." -#: awx/api/serializers.py:3614 -msgid "TZID is not supported." -msgstr "TZID wordt niet ondersteund." - -#: awx/api/serializers.py:3616 +#: awx/api/serializers.py:4465 msgid "SECONDLY is not supported." msgstr "SECONDLY wordt niet ondersteund." -#: awx/api/serializers.py:3618 +#: awx/api/serializers.py:4467 msgid "Multiple BYMONTHDAYs not supported." msgstr "Meerdere BYMONTHDAY's worden niet ondersteund." -#: awx/api/serializers.py:3620 +#: awx/api/serializers.py:4469 msgid "Multiple BYMONTHs not supported." msgstr "Meerdere BYMONTH's worden niet ondersteund." -#: awx/api/serializers.py:3622 +#: awx/api/serializers.py:4471 msgid "BYDAY with numeric prefix not supported." msgstr "BYDAY met numeriek voorvoegsel wordt niet ondersteund." -#: awx/api/serializers.py:3624 +#: awx/api/serializers.py:4473 msgid "BYYEARDAY not supported." msgstr "BYYEARDAY wordt niet ondersteund." -#: awx/api/serializers.py:3626 +#: awx/api/serializers.py:4475 msgid "BYWEEKNO not supported." msgstr "BYWEEKNO wordt niet ondersteund." -#: awx/api/serializers.py:3630 +#: awx/api/serializers.py:4477 +msgid "RRULE may not contain both COUNT and UNTIL" +msgstr "RRULE mag niet zowel COUNT als UNTIL bevatten" + +#: awx/api/serializers.py:4481 msgid "COUNT > 999 is unsupported." msgstr "COUNT > 999 wordt niet ondersteund." -#: awx/api/serializers.py:3634 -msgid "rrule parsing failed validation." -msgstr "De validering van rrule-parsering is mislukt." +#: awx/api/serializers.py:4485 +msgid "rrule parsing failed validation: {}" +msgstr "de validering van rrule-parsering is mislukt: {}" -#: awx/api/serializers.py:3760 +#: awx/api/serializers.py:4526 +msgid "Inventory Source must be a cloud resource." +msgstr "Inventarisbron moet een cloudresource zijn." + +#: awx/api/serializers.py:4528 +msgid "Manual Project cannot have a schedule set." +msgstr "Handmatig project kan geen ingesteld schema hebben." + +#: awx/api/serializers.py:4541 +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:4546 +msgid "Count of all jobs that target this instance" +msgstr "Aantal taken die deze instantie als doel hebben" + +#: awx/api/serializers.py:4579 +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:4584 +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:4592 +msgid "Policy Instance Percentage" +msgstr "Beleid instantiepercentage" + +#: awx/api/serializers.py:4593 +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:4598 +msgid "Policy Instance Minimum" +msgstr "Beleid instantieminimum" + +#: awx/api/serializers.py:4599 +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:4604 +msgid "Policy Instance List" +msgstr "Beleid instantielijst" + +#: awx/api/serializers.py:4605 +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:4627 +msgid "Duplicate entry {}." +msgstr "Dubbele invoer {}." + +#: awx/api/serializers.py:4629 +msgid "{} is not a valid hostname of an existing instance." +msgstr "{} is geen geldige hostnaam voor een bestaande instantie." + +#: awx/api/serializers.py:4634 +msgid "tower instance group name may not be changed." +msgstr "Naam van de tower-instantiegroep mag niet gewijzigd worden." + +#: awx/api/serializers.py:4704 msgid "" "A summary of the new and changed values when an object is created, updated, " "or deleted" @@ -585,7 +769,7 @@ msgstr "" "Een overzicht van de nieuwe en gewijzigde waarden wanneer een object wordt " "gemaakt, bijgewerkt of verwijderd" -#: awx/api/serializers.py:3762 +#: awx/api/serializers.py:4706 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 " @@ -595,7 +779,7 @@ msgstr "" "objecttype. Voor koppel- en ontkoppel-gebeurtenissen is dit het objecttype " "dat wordt gekoppeld aan of ontkoppeld van object2." -#: awx/api/serializers.py:3765 +#: awx/api/serializers.py:4709 msgid "" "Unpopulated for create, update, and delete events. For associate and " "disassociate events this is the object type that object1 is being associated" @@ -605,165 +789,213 @@ msgstr "" "en ontkoppel-gebeurtenissen is dit het objecttype waaraan object1 wordt " "gekoppeld." -#: awx/api/serializers.py:3768 +#: awx/api/serializers.py:4712 msgid "The action taken with respect to the given object(s)." msgstr "De actie ondernomen met betrekking tot de gegeven objecten." -#: awx/api/serializers.py:3885 -msgid "Unable to login with provided credentials." -msgstr "Kan niet aanmelden met de verschafte referenties." - -#: awx/api/serializers.py:3887 -msgid "Must include \"username\" and \"password\"." -msgstr "Moet \"gebruikersnaam\" en \"wachtwoord\" bevatten." - -#: awx/api/views.py:108 +#: awx/api/views.py:118 msgid "Your license does not allow use of the activity stream." msgstr "Uw licentie staat het gebruik van de activiteitenstroom niet toe." -#: awx/api/views.py:118 +#: awx/api/views.py:128 msgid "Your license does not permit use of system tracking." msgstr "Uw licentie staat het gebruik van systeemtracking niet toe." -#: awx/api/views.py:128 +#: awx/api/views.py:138 msgid "Your license does not allow use of workflows." msgstr "Uw licentie staat het gebruik van workflows niet toe." -#: awx/api/views.py:142 +#: awx/api/views.py:152 msgid "Cannot delete job resource when associated workflow job is running." msgstr "" "Kan taakresource niet verwijderen wanneer een gekoppelde workflowtaak wordt " "uitgevoerd." -#: awx/api/views.py:146 +#: awx/api/views.py:157 msgid "Cannot delete running job resource." msgstr "Kan geen taakbron in uitvoering verwijderen." -#: awx/api/views.py:155 awx/templates/rest_framework/api.html:28 +#: awx/api/views.py:162 +msgid "Job has not finished processing events." +msgstr "Taken die nog niet klaar zijn met het verwerken van gebeurtenissen." + +#: awx/api/views.py:221 +msgid "Related job {} is still processing events." +msgstr "Verwante taak {} is nog bezig met het verwerken van gebeurtenissen." + +#: awx/api/views.py:228 awx/templates/rest_framework/api.html:28 msgid "REST API" msgstr "REST API" -#: awx/api/views.py:164 awx/templates/rest_framework/api.html:4 +#: awx/api/views.py:238 awx/templates/rest_framework/api.html:4 msgid "AWX REST API" msgstr "AWX REST API" -#: awx/api/views.py:226 +#: awx/api/views.py:252 +msgid "API OAuth 2 Authorization Root" +msgstr "Oorsprong API OAuth 2-machtiging" + +#: awx/api/views.py:317 msgid "Version 1" msgstr "Versie 1" -#: awx/api/views.py:230 +#: awx/api/views.py:321 msgid "Version 2" msgstr "Versie 2" -#: awx/api/views.py:241 +#: awx/api/views.py:330 msgid "Ping" msgstr "Ping" -#: awx/api/views.py:272 awx/conf/apps.py:12 +#: awx/api/views.py:361 awx/conf/apps.py:10 msgid "Configuration" msgstr "Configuratie" -#: awx/api/views.py:325 +#: awx/api/views.py:418 msgid "Invalid license data" msgstr "Ongeldige licentiegegevens" -#: awx/api/views.py:327 +#: awx/api/views.py:420 msgid "Missing 'eula_accepted' property" msgstr "Ontbrekende eigenschap 'eula_accepted'" -#: awx/api/views.py:331 +#: awx/api/views.py:424 msgid "'eula_accepted' value is invalid" msgstr "Waarde 'eula_accepted' is ongeldig" -#: awx/api/views.py:334 +#: awx/api/views.py:427 msgid "'eula_accepted' must be True" msgstr "'eula_accepted' moet True zijn" -#: awx/api/views.py:341 +#: awx/api/views.py:434 msgid "Invalid JSON" msgstr "Ongeldig JSON" -#: awx/api/views.py:349 +#: awx/api/views.py:442 msgid "Invalid License" msgstr "Ongeldige licentie" -#: awx/api/views.py:359 +#: awx/api/views.py:452 msgid "Invalid license" msgstr "Ongeldige licentie" -#: awx/api/views.py:367 +#: awx/api/views.py:460 #, python-format msgid "Failed to remove license (%s)" msgstr "Kan licentie niet verwijderen (%s)" -#: awx/api/views.py:372 +#: awx/api/views.py:465 msgid "Dashboard" msgstr "Dashboard" -#: awx/api/views.py:471 +#: awx/api/views.py:564 msgid "Dashboard Jobs Graphs" msgstr "Dashboardtaakgrafieken" -#: awx/api/views.py:507 +#: awx/api/views.py:600 #, python-format msgid "Unknown period \"%s\"" msgstr "Onbekende periode \"%s\"" -#: awx/api/views.py:521 +#: awx/api/views.py:614 msgid "Instances" msgstr "Instanties" -#: awx/api/views.py:529 +#: awx/api/views.py:622 msgid "Instance Detail" msgstr "Instantiedetails" -#: awx/api/views.py:537 -msgid "Instance Running Jobs" -msgstr "Taken in uitvoering van instantie" +#: awx/api/views.py:643 +msgid "Instance Jobs" +msgstr "Instantietaken" -#: awx/api/views.py:552 +#: awx/api/views.py:657 msgid "Instance's Instance Groups" msgstr "Instantiegroepen van instantie" -#: awx/api/views.py:562 +#: awx/api/views.py:666 msgid "Instance Groups" msgstr "Instantiegroepen" -#: awx/api/views.py:570 +#: awx/api/views.py:674 msgid "Instance Group Detail" msgstr "Details van instantiegroep" -#: awx/api/views.py:578 +#: awx/api/views.py:682 +msgid "Isolated Groups can not be removed from the API" +msgstr "Geïsoleerde groepen kunnen niet verwijderd worden van de API" + +#: awx/api/views.py:684 +msgid "" +"Instance Groups acting as a controller for an Isolated Group can not be " +"removed from the API" +msgstr "" +"Instantiegroepen die dienen als controller voor een geïsoleerde groep kunnen" +" niet verwijderd worden van de API" + +#: awx/api/views.py:690 msgid "Instance Group Running Jobs" msgstr "Taken in uitvoering van instantiegroep" -#: awx/api/views.py:588 +#: awx/api/views.py:699 msgid "Instance Group's Instances" msgstr "Instanties van instantiegroep" -#: awx/api/views.py:598 +#: awx/api/views.py:709 msgid "Schedules" msgstr "Schema's" -#: awx/api/views.py:617 +#: awx/api/views.py:723 +msgid "Schedule Recurrence Rule Preview" +msgstr "Voorvertoning herhalingsregel inplannen" + +#: awx/api/views.py:770 +msgid "Cannot assign credential when related template is null." +msgstr "Kan geen toegangsgegevens toewijzen wanneer verwant sjabloon nul is." + +#: awx/api/views.py:775 +msgid "Related template cannot accept {} on launch." +msgstr "Verwant sjabloon kan {} niet accepteren bij opstarten." + +#: awx/api/views.py:777 +msgid "" +"Credential that requires user input on launch cannot be used in saved launch" +" configuration." +msgstr "" +"Toegangsgegevens die input van de gebruiker nodig hebben bij het opstarten, " +"kunnen niet gebruikt worden in opgeslagen opstartconfiguratie." + +#: awx/api/views.py:785 +#, python-brace-format +msgid "" +"This launch configuration already provides a {credential_type} credential." +msgstr "" +"Deze opstartconfiguratie levert al {credential_type}-toegangsgegevens." + +#: awx/api/views.py:788 +#, python-brace-format +msgid "Related template already uses {credential_type} credential." +msgstr "Verwant sjabloon gebruikt al {credential_type}-toegangsgegevens." + +#: awx/api/views.py:806 msgid "Schedule Jobs List" msgstr "Schema takenlijst" -#: awx/api/views.py:843 +#: awx/api/views.py:961 msgid "Your license only permits a single organization to exist." msgstr "Uw licentie staat het gebruik van maar één organisatie toe." -#: awx/api/views.py:1079 awx/api/views.py:4666 +#: awx/api/views.py:1193 awx/api/views.py:5056 msgid "You cannot assign an Organization role as a child role for a Team." msgstr "" "U kunt een organisatierol niet toewijzen als een onderliggende rol voor een " "team." -#: awx/api/views.py:1083 awx/api/views.py:4680 +#: awx/api/views.py:1197 awx/api/views.py:5070 msgid "You cannot grant system-level permissions to a team." msgstr "U kunt een team geen rechten op systeemniveau verlenen." -#: awx/api/views.py:1090 awx/api/views.py:4672 +#: awx/api/views.py:1204 awx/api/views.py:5062 msgid "" "You cannot grant credential access to a team when the Organization field " "isn't set, or belongs to a different organization" @@ -771,36 +1003,71 @@ msgstr "" "U kunt een team geen referentietoegang verlenen wanneer het veld Organisatie" " niet is ingesteld of behoort tot een andere organisatie" -#: awx/api/views.py:1180 -msgid "Cannot delete project." -msgstr "Kan project niet verwijderen." - -#: awx/api/views.py:1215 +#: awx/api/views.py:1318 msgid "Project Schedules" msgstr "Projectschema's" -#: awx/api/views.py:1227 +#: awx/api/views.py:1329 msgid "Project SCM Inventory Sources" msgstr "SCM-inventarisbronnen van project" -#: awx/api/views.py:1354 +#: awx/api/views.py:1430 +msgid "Project Update Events List" +msgstr "Lijst met projectupdategebeurtenissen" + +#: awx/api/views.py:1444 +msgid "System Job Events List" +msgstr "Lijst met systeemtaakgebeurtenissen" + +#: awx/api/views.py:1458 +msgid "Inventory Update Events List" +msgstr "Lijst met inventarisupdategebeurtenissen" + +#: awx/api/views.py:1492 msgid "Project Update SCM Inventory Updates" msgstr "SCM-inventarisupdates van projectupdate" -#: awx/api/views.py:1409 +#: awx/api/views.py:1551 msgid "Me" msgstr "Mij" -#: awx/api/views.py:1453 awx/api/views.py:4623 -msgid "You may not perform any action with your own admin_role." -msgstr "U kunt geen actie uitvoeren met uw eigen admin_role." +#: awx/api/views.py:1559 +msgid "OAuth 2 Applications" +msgstr "OAuth 2-toepassingen" -#: awx/api/views.py:1459 awx/api/views.py:4627 -msgid "You may not change the membership of a users admin_role" -msgstr "" -"U kunt het lidmaatschap van de admin_role van een gebruiker niet wijzigen" +#: awx/api/views.py:1568 +msgid "OAuth 2 Application Detail" +msgstr "Details OAuth 2-toepassing" -#: awx/api/views.py:1464 awx/api/views.py:4632 +#: awx/api/views.py:1577 +msgid "OAuth 2 Application Tokens" +msgstr "Tokens OAuth 2-toepassing" + +#: awx/api/views.py:1599 +msgid "OAuth2 Tokens" +msgstr "OAuth 2-tokens" + +#: awx/api/views.py:1608 +msgid "OAuth2 User Tokens" +msgstr "OAuth2-gebruikerstokens" + +#: awx/api/views.py:1620 +msgid "OAuth2 User Authorized Access Tokens" +msgstr "OAuth 2-gebruikerstokens gemachtigde toegang" + +#: awx/api/views.py:1635 +msgid "Organization OAuth2 Applications" +msgstr "Organisatie OAuth2-toepassingen" + +#: awx/api/views.py:1647 +msgid "OAuth2 Personal Access Tokens" +msgstr "OAuth2-tokens persoonlijke toegang" + +#: awx/api/views.py:1662 +msgid "OAuth Token Detail" +msgstr "Details OAuth-token" + +#: awx/api/views.py:1722 awx/api/views.py:5023 msgid "" "You cannot grant credential access to a user not in the credentials' " "organization" @@ -808,53 +1075,53 @@ msgstr "" "U kunt geen referentietoegang verlenen aan een gebruiker die niet tot de " "organisatie van de referenties behoort" -#: awx/api/views.py:1468 awx/api/views.py:4636 +#: awx/api/views.py:1726 awx/api/views.py:5027 msgid "You cannot grant private credential access to another user" msgstr "U kunt geen privéreferentietoegang verlenen aan een andere gebruiker" -#: awx/api/views.py:1566 +#: awx/api/views.py:1824 #, python-format msgid "Cannot change %s." msgstr "Kan %s niet wijzigen." -#: awx/api/views.py:1572 +#: awx/api/views.py:1830 msgid "Cannot delete user." msgstr "Kan gebruiker niet verwijderen." -#: awx/api/views.py:1601 +#: awx/api/views.py:1854 msgid "Deletion not allowed for managed credential types" msgstr "Verwijdering is niet toegestaan voor beheerde referentietypen" -#: awx/api/views.py:1603 +#: awx/api/views.py:1856 msgid "Credential types that are in use cannot be deleted" msgstr "Referentietypen die in gebruik zijn, kunnen niet worden verwijderd" -#: awx/api/views.py:1781 +#: awx/api/views.py:2031 msgid "Cannot delete inventory script." msgstr "Kan inventarisscript niet verwijderen." -#: awx/api/views.py:1866 +#: awx/api/views.py:2122 #, python-brace-format msgid "{0}" msgstr "{0}" -#: awx/api/views.py:2101 +#: awx/api/views.py:2353 msgid "Fact not found." msgstr "Feit niet gevonden." -#: awx/api/views.py:2125 +#: awx/api/views.py:2375 msgid "SSLError while trying to connect to {}" msgstr "SSLError tijdens poging om verbinding te maken met {}" -#: awx/api/views.py:2127 +#: awx/api/views.py:2377 msgid "Request to {} timed out." msgstr "Er is een time-out opgetreden voor de aanvraag naar {}" -#: awx/api/views.py:2129 -msgid "Unkown exception {} while trying to GET {}" +#: awx/api/views.py:2379 +msgid "Unknown exception {} while trying to GET {}" msgstr "Onbekende uitzondering {} tijdens poging tot OPHALEN {}" -#: awx/api/views.py:2132 +#: awx/api/views.py:2382 msgid "" "Unauthorized access. Please check your Insights Credential username and " "password." @@ -862,7 +1129,7 @@ msgstr "" "Geen toegang. Controleer uw Insights Credential gebruikersnaam en " "wachtwoord." -#: awx/api/views.py:2134 +#: awx/api/views.py:2385 msgid "" "Failed to gather reports and maintenance plans from Insights API at URL {}. " "Server responded with {} status code and message {}" @@ -870,216 +1137,268 @@ msgstr "" "Kan rapporten en onderhoudsplannen niet verzamelen uit Insights API op URL " "{}. Server heeft gereageerd met statuscode {} en bericht {}" -#: awx/api/views.py:2140 +#: awx/api/views.py:2392 msgid "Expected JSON response from Insights but instead got {}" msgstr "Verwachtte JSON-reactie van Insights, maar kreeg in plaats daarvan {}" -#: awx/api/views.py:2147 +#: awx/api/views.py:2399 msgid "This host is not recognized as an Insights host." msgstr "Deze host wordt niet herkend als een Insights-host." -#: awx/api/views.py:2152 +#: awx/api/views.py:2404 msgid "The Insights Credential for \"{}\" was not found." msgstr "De Insights-referentie voor \"{}\" is niet gevonden." -#: awx/api/views.py:2221 +#: awx/api/views.py:2472 msgid "Cyclical Group association." msgstr "Cyclische groepskoppeling." -#: awx/api/views.py:2499 +#: awx/api/views.py:2686 msgid "Inventory Source List" msgstr "Lijst met inventarisbronnen" -#: awx/api/views.py:2512 +#: awx/api/views.py:2698 msgid "Inventory Sources Update" msgstr "Update van inventarisbronnen" -#: awx/api/views.py:2542 +#: awx/api/views.py:2731 msgid "Could not start because `can_update` returned False" msgstr "Kan niet starten omdat 'can_update' False heeft geretourneerd" -#: awx/api/views.py:2550 +#: awx/api/views.py:2739 msgid "No inventory sources to update." msgstr "Er zijn geen inventarisbronnen om bij te werken." -#: awx/api/views.py:2582 -msgid "Cannot delete inventory source." -msgstr "Kan inventarisbron niet verwijderen." - -#: awx/api/views.py:2590 +#: awx/api/views.py:2768 msgid "Inventory Source Schedules" msgstr "Inventarisbronschema's" -#: awx/api/views.py:2620 +#: awx/api/views.py:2796 msgid "Notification Templates can only be assigned when source is one of {}." msgstr "" "Berichtsjablonen kunnen alleen worden toegewezen wanneer de bron een van {} " "is." #: awx/api/views.py:2851 +msgid "Vault credentials are not yet supported for inventory sources." +msgstr "" +"Kluistoegangsgegevens zijn nog niet toegestaan voor inventarisbronnen." + +#: awx/api/views.py:2856 +msgid "Source already has cloud credential assigned." +msgstr "Bron heeft al toegewezen cloudtoegangsgegevens." + +#: awx/api/views.py:3016 +msgid "" +"'credentials' cannot be used in combination with 'credential', " +"'vault_credential', or 'extra_credentials'." +msgstr "" +"'Toegangsgegevens' kunnen niet gebruikt worden in combinatie met " +"'referentie', 'kluistoegangsgegevens' of 'extra toegangsgegevens'." + +#: awx/api/views.py:3128 msgid "Job Template Schedules" msgstr "Taaksjabloonschema's" -#: awx/api/views.py:2871 awx/api/views.py:2882 +#: awx/api/views.py:3146 awx/api/views.py:3157 msgid "Your license does not allow adding surveys." msgstr "Uw licentie staat toevoeging van enquêtes niet toe." -#: awx/api/views.py:2889 -msgid "'name' missing from survey spec." -msgstr "'name' ontbreekt in de enquêtespecificaties." +#: awx/api/views.py:3176 +msgid "Field '{}' is missing from survey spec." +msgstr "Veld '{}' ontbreekt in de enquêtespecificaties." -#: awx/api/views.py:2891 -msgid "'description' missing from survey spec." -msgstr "'description' ontbreekt in de enquêtespecificaties." +#: awx/api/views.py:3178 +msgid "Expected {} for field '{}', received {} type." +msgstr "{} verwacht voor veld '{}', {}-soort ontvangen." -#: awx/api/views.py:2893 -msgid "'spec' missing from survey spec." -msgstr "'spec' ontbreekt in de enquêtespecificaties." - -#: awx/api/views.py:2895 -msgid "'spec' must be a list of items." -msgstr "'spec' moet een lijst met items zijn." - -#: awx/api/views.py:2897 +#: awx/api/views.py:3182 msgid "'spec' doesn't contain any items." msgstr "'spec' bevat geen items." -#: awx/api/views.py:2903 +#: awx/api/views.py:3191 #, python-format msgid "Survey question %s is not a json object." msgstr "Enquêtevraag %s is geen json-object." -#: awx/api/views.py:2905 +#: awx/api/views.py:3193 #, python-format msgid "'type' missing from survey question %s." msgstr "'type' ontbreekt in enquêtevraag %s." -#: awx/api/views.py:2907 +#: awx/api/views.py:3195 #, python-format msgid "'question_name' missing from survey question %s." msgstr "'question_name' ontbreekt in enquêtevraag %s." -#: awx/api/views.py:2909 +#: awx/api/views.py:3197 #, python-format msgid "'variable' missing from survey question %s." msgstr "'variable' ontbreekt in enquêtevraag %s." -#: awx/api/views.py:2911 +#: awx/api/views.py:3199 #, python-format msgid "'variable' '%(item)s' duplicated in survey question %(survey)s." msgstr "'variable' '%(item)s' gedupliceerd in enquêtevraag %(survey)s." -#: awx/api/views.py:2916 +#: awx/api/views.py:3204 #, python-format msgid "'required' missing from survey question %s." msgstr "'required' ontbreekt in enquêtevraag %s." -#: awx/api/views.py:2921 +#: awx/api/views.py:3209 #, python-brace-format msgid "" "Value {question_default} for '{variable_name}' expected to be a string." msgstr "" "Waarde {question_default} voor '{variable_name}' zou een string moeten zijn." -#: awx/api/views.py:2928 +#: awx/api/views.py:3219 #, python-brace-format msgid "" -"$encrypted$ is reserved keyword for password questions and may not be used " -"as a default for '{variable_name}' in survey question {question_position}." +"$encrypted$ is a reserved keyword for password question defaults, survey " +"question {question_position} is type {question_type}." msgstr "" -"$encrypted$ is een gereserveerd sleutelwoord voor het wachtwoord en mag niet" -" als standaardwaarde worden gebruikt voor '{variable_name}' in enquêtevraag " -"{question_position}." +"$encrypted$ is een gereserveerd sleutelwoord voor standaard\n" +"wachtwoordvragen, enquêtevraag {question_position} is van het soort {question_type}." -#: awx/api/views.py:3049 +#: awx/api/views.py:3235 +#, python-brace-format +msgid "" +"$encrypted$ is a reserved keyword, may not be used for new default in " +"position {question_position}." +msgstr "" +"$encrypted$ is een gereserveerd sleutelwoord. Het mag niet gebruikt worden " +"als nieuwe standaard op de positie {question_position}." + +#: awx/api/views.py:3309 +#, python-brace-format +msgid "Cannot assign multiple {credential_type} credentials." +msgstr "Kan niet meerdere {credential_type}-toegangsgegevens toewijzen." + +#: awx/api/views.py:3327 +msgid "Extra credentials must be network or cloud." +msgstr "Extra referenties moeten netwerk of cloud zijn." + +#: awx/api/views.py:3349 msgid "Maximum number of labels for {} reached." msgstr "Het maximumaantal labels voor {} is bereikt." -#: awx/api/views.py:3170 +#: awx/api/views.py:3472 msgid "No matching host could be found!" msgstr "Er is geen overeenkomende host gevonden." -#: awx/api/views.py:3173 +#: awx/api/views.py:3475 msgid "Multiple hosts matched the request!" msgstr "Meerdere hosts kwamen overeen met de aanvraag." -#: awx/api/views.py:3178 +#: awx/api/views.py:3480 msgid "Cannot start automatically, user input required!" msgstr "Kan niet automatisch starten. Gebruikersinput is vereist." -#: awx/api/views.py:3185 +#: awx/api/views.py:3487 msgid "Host callback job already pending." msgstr "Er is al een hostterugkoppelingstaak in afwachting." -#: awx/api/views.py:3199 +#: awx/api/views.py:3502 awx/api/views.py:4284 msgid "Error starting job!" msgstr "Fout bij starten taak." -#: awx/api/views.py:3306 +#: awx/api/views.py:3622 #, python-brace-format msgid "Cannot associate {0} when {1} have been associated." msgstr "Kan {0} niet koppelen wanneer {1} zijn gekoppeld." -#: awx/api/views.py:3331 +#: awx/api/views.py:3647 msgid "Multiple parent relationship not allowed." msgstr "Relatie met meerdere bovenliggende elementen is niet toegestaan." -#: awx/api/views.py:3336 +#: awx/api/views.py:3652 msgid "Cycle detected." msgstr "Cyclus gedetecteerd." -#: awx/api/views.py:3540 +#: awx/api/views.py:3850 msgid "Workflow Job Template Schedules" msgstr "Taaksjabloonschema's voor workflows" -#: awx/api/views.py:3685 awx/api/views.py:4268 +#: awx/api/views.py:3986 awx/api/views.py:4690 msgid "Superuser privileges needed." msgstr "Supergebruikersbevoegdheden vereist." -#: awx/api/views.py:3717 +#: awx/api/views.py:4018 msgid "System Job Template Schedules" msgstr "Taaksjabloonschema's voor systeem" -#: awx/api/views.py:3780 +#: awx/api/views.py:4076 msgid "POST not allowed for Job launching in version 2 of the api" msgstr "" "POST is niet toegestaan voor het openen van taken in versie 2 van de API" -#: awx/api/views.py:3942 +#: awx/api/views.py:4100 awx/api/views.py:4106 +msgid "PUT not allowed for Job Details in version 2 of the API" +msgstr "PLAATS niet toegestaan voor taakinformatie in versie 2 van de API" + +#: awx/api/views.py:4267 +#, python-brace-format +msgid "Wait until job finishes before retrying on {status_value} hosts." +msgstr "" +"U dient te wachten tot de taak afgerond is voordat u het opnieuw probeert " +"met {status_value}-hosts." + +#: awx/api/views.py:4272 +#, python-brace-format +msgid "Cannot retry on {status_value} hosts, playbook stats not available." +msgstr "" +"Kan niet opnieuw proberen met {status_value}-hosts, draaiboek niet " +"beschikbaar." + +#: awx/api/views.py:4277 +#, python-brace-format +msgid "Cannot relaunch because previous job had 0 {status_value} hosts." +msgstr "" +"Kan niet opnieuw opstarten omdat vorige taak 0 {status_value}-hosts had." + +#: awx/api/views.py:4306 +msgid "Cannot create schedule because job requires credential passwords." +msgstr "" +"Kan geen schema aanmaken omdat taak toegangsgegevens met wachtwoorden " +"vereist." + +#: awx/api/views.py:4311 +msgid "Cannot create schedule because job was launched by legacy method." +msgstr "" +"Kan geen schema aanmaken omdat taak opgestart is volgens verouderde methode." + +#: awx/api/views.py:4313 +msgid "Cannot create schedule because a related resource is missing." +msgstr "Kan geen schema aanmaken omdat een verwante hulpbron ontbreekt." + +#: awx/api/views.py:4368 msgid "Job Host Summaries List" msgstr "Lijst met taakhostoverzichten" -#: awx/api/views.py:3989 +#: awx/api/views.py:4417 msgid "Job Event Children List" msgstr "Lijst met onderliggende taakgebeurteniselementen" -#: awx/api/views.py:3998 +#: awx/api/views.py:4427 msgid "Job Event Hosts List" msgstr "Lijst met taakgebeurtenishosts" -#: awx/api/views.py:4008 +#: awx/api/views.py:4436 msgid "Job Events List" msgstr "Lijst met taakgebeurtenissen" -#: awx/api/views.py:4222 +#: awx/api/views.py:4647 msgid "Ad Hoc Command Events List" msgstr "Lijst met ad-hoc-opdrachtgebeurtenissen" -#: awx/api/views.py:4437 -msgid "Error generating stdout download file: {}" -msgstr "Fout bij genereren stdout-downloadbestand: {}" - -#: awx/api/views.py:4450 -#, python-format -msgid "Error generating stdout download file: %s" -msgstr "Fout bij genereren stdout-downloadbestand: %s" - -#: awx/api/views.py:4495 +#: awx/api/views.py:4889 msgid "Delete not allowed while there are pending notifications" msgstr "" "Verwijderen is niet toegestaan wanneer er berichten in afwachting zijn" -#: awx/api/views.py:4502 +#: awx/api/views.py:4897 msgid "Notification Template Test" msgstr "Berichtsjabloon" @@ -1232,19 +1551,36 @@ 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:95 awx/conf/registry.py:85 awx/conf/views.py:56 +#: awx/conf/conf.py:95 awx/conf/registry.py:85 awx/conf/views.py:55 msgid "User" msgstr "Gebruiker" -#: awx/conf/fields.py:63 +#: awx/conf/fields.py:60 awx/sso/fields.py:595 +#, python-brace-format +msgid "" +"Expected None, True, False, a string or list of strings but got {input_type}" +" instead." +msgstr "" +"Verwachtte None, True, False, een tekenreeks of een lijst met tekenreeksen, " +"maar kreeg in plaats daarvan {input_type}." + +#: awx/conf/fields.py:104 msgid "Enter a valid URL" msgstr "Geef een geldige URL op" -#: awx/conf/fields.py:95 +#: awx/conf/fields.py:136 #, python-brace-format msgid "\"{input}\" is not a valid string." msgstr "\"{input} is geen geldige tekenreeks." +#: awx/conf/fields.py:151 +#, python-brace-format +msgid "" +"Expected a list of tuples of max length 2 but got {input_type} instead." +msgstr "" +"Verwachtte een lijst van tupels met maximale lengte 2 maar kreeg in plaats " +"daarvan {input_type}." + #: awx/conf/license.py:22 msgid "Your Tower license does not allow that." msgstr "Uw Tower-licentie staat dat niet toe." @@ -1342,9 +1678,9 @@ msgstr "Deze waarde is handmatig ingesteld in een instellingenbestand." #: awx/conf/tests/unit/test_settings.py:411 #: awx/conf/tests/unit/test_settings.py:430 #: awx/conf/tests/unit/test_settings.py:466 awx/main/conf.py:22 -#: awx/main/conf.py:32 awx/main/conf.py:42 awx/main/conf.py:51 -#: awx/main/conf.py:63 awx/main/conf.py:81 awx/main/conf.py:96 -#: awx/main/conf.py:121 +#: awx/main/conf.py:32 awx/main/conf.py:43 awx/main/conf.py:53 +#: awx/main/conf.py:62 awx/main/conf.py:74 awx/main/conf.py:87 +#: awx/main/conf.py:100 awx/main/conf.py:125 msgid "System" msgstr "Systeem" @@ -1356,104 +1692,111 @@ msgstr "Systeem" msgid "OtherSystem" msgstr "OtherSystem" -#: awx/conf/views.py:48 +#: awx/conf/views.py:47 msgid "Setting Categories" msgstr "Instellingscategorieën" -#: awx/conf/views.py:73 +#: awx/conf/views.py:71 msgid "Setting Detail" msgstr "Instellingsdetail" -#: awx/conf/views.py:168 +#: awx/conf/views.py:166 msgid "Logging Connectivity Test" msgstr "Connectiviteitstest logboekregistratie" -#: awx/main/access.py:44 -msgid "Resource is being used by running jobs." -msgstr "Bron wordt gebruikt om taken uit te voeren." +#: awx/main/access.py:57 +#, python-format +msgid "Required related field %s for permission check." +msgstr "Verwant veld %s vereist voor machtigingscontrole." -#: awx/main/access.py:237 +#: awx/main/access.py:73 #, python-format msgid "Bad data found in related field %s." msgstr "Ongeldige gegevens gevonden in gerelateerd veld %s." -#: awx/main/access.py:281 +#: awx/main/access.py:293 msgid "License is missing." msgstr "Licentie ontbreekt." -#: awx/main/access.py:283 +#: awx/main/access.py:295 msgid "License has expired." msgstr "Licentie is verlopen." -#: awx/main/access.py:291 +#: awx/main/access.py:303 #, python-format msgid "License count of %s instances has been reached." msgstr "Het aantal licenties van %s instanties is bereikt." -#: awx/main/access.py:293 +#: awx/main/access.py:305 #, python-format msgid "License count of %s instances has been exceeded." msgstr "Het aantal licenties van %s instanties is overschreden." -#: awx/main/access.py:295 +#: awx/main/access.py:307 msgid "Host count exceeds available instances." msgstr "Het aantal hosts is groter dan het aantal instanties." -#: awx/main/access.py:299 +#: awx/main/access.py:311 #, python-format msgid "Feature %s is not enabled in the active license." msgstr "De functie %s is niet ingeschakeld in de actieve licentie." -#: awx/main/access.py:301 +#: awx/main/access.py:313 msgid "Features not found in active license." msgstr "Functies niet gevonden in actieve licentie." -#: awx/main/access.py:707 +#: awx/main/access.py:818 msgid "Unable to change inventory on a host." msgstr "Kan inventaris op een host niet wijzigen." -#: awx/main/access.py:724 awx/main/access.py:769 +#: awx/main/access.py:835 awx/main/access.py:880 msgid "Cannot associate two items from different inventories." msgstr "Kan twee items uit verschillende inventarissen niet koppelen." -#: awx/main/access.py:757 +#: awx/main/access.py:868 msgid "Unable to change inventory on a group." msgstr "Kan inventaris van een groep niet wijzigen." -#: awx/main/access.py:1017 +#: awx/main/access.py:1129 msgid "Unable to change organization on a team." msgstr "Kan organisatie van een team niet wijzigen." -#: awx/main/access.py:1030 +#: awx/main/access.py:1146 msgid "The {} role cannot be assigned to a team" msgstr "De rol {} kan niet worden toegewezen aan een team" -#: awx/main/access.py:1032 +#: awx/main/access.py:1148 msgid "The admin_role for a User cannot be assigned to a team" msgstr "" "De admin_role voor een gebruiker kan niet worden toegewezen aan een team" -#: awx/main/access.py:1479 +#: awx/main/access.py:1502 awx/main/access.py:1936 +msgid "Job was launched with prompts provided by another user." +msgstr "" +"Taak is opgestart met meldingen die aangeleverd zijn door een andere " +"gebruiker." + +#: awx/main/access.py:1522 msgid "Job has been orphaned from its job template." msgstr "De taak is verwijderd uit zijn taaksjabloon." -#: awx/main/access.py:1481 -msgid "You do not have execute permission to related job template." -msgstr "U hebt geen uitvoermachtiging voor de gerelateerde taaksjabloon." +#: awx/main/access.py:1524 +msgid "Job was launched with unknown prompted fields." +msgstr "Taak is opgestart met onbekende invoervelden." -#: awx/main/access.py:1484 +#: awx/main/access.py:1526 msgid "Job was launched with prompted fields." msgstr "De taak is gestart met invoervelden." -#: awx/main/access.py:1486 +#: awx/main/access.py:1528 msgid " Organization level permissions required." msgstr "Organisatieniveaumachtigingen zijn vereist." -#: awx/main/access.py:1488 +#: awx/main/access.py:1530 msgid " You do not have permission to related resources." msgstr "U hebt geen machtiging voor gerelateerde resources." -#: awx/main/access.py:1833 +#: awx/main/access.py:1950 msgid "" "You do not have permission to the workflow job resources required for " "relaunch." @@ -1491,27 +1834,41 @@ msgstr "Alle gebruikers zichtbaar voor organisatiebeheerders" #: awx/main/conf.py:41 msgid "" -"Controls whether any Organization Admin can view all users, even those not " -"associated with their Organization." +"Controls whether any Organization Admin can view all users and teams, even " +"those not associated with their Organization." msgstr "" -"Regelt of organisatiebeheerders alle gebruikers kunnen weergeven, zelfs " -"gebruikers die niet aan hun organisatie zijn gekoppeld." +"Regelt of een organisatiebeheerder alle gebruikers en teams kan weergeven, " +"zelfs gebruikers en teams die niet aan hun organisatie zijn gekoppeld." -#: awx/main/conf.py:49 +#: awx/main/conf.py:50 +msgid "Organization Admins Can Manage Users and Teams" +msgstr "Organisatiebeheerders kunnen gebruikers en teams beheren" + +#: awx/main/conf.py:51 +msgid "" +"Controls whether any Organization Admin has the privileges to create and " +"manage users and teams. You may want to disable this ability if you are " +"using an LDAP or SAML integration." +msgstr "" +"Regelt of een organisatiebeheerder gemachtigd is om gebruikers en teams aan " +"te maken en te beheren. Als u een LDAP- of SAML-integratie gebruikt, wilt u " +"deze mogelijkheid wellicht uitschakelen." + +#: awx/main/conf.py:60 msgid "Enable Administrator Alerts" msgstr "Beheerderssignalen inschakelen" -#: awx/main/conf.py:50 +#: awx/main/conf.py:61 msgid "Email Admin users for system events that may require attention." msgstr "" "E-mails naar gebruikers met beheerdersrechten sturen over gebeurtenissen die" " mogelijk aandacht nodig hebben." -#: awx/main/conf.py:60 +#: awx/main/conf.py:71 msgid "Base URL of the Tower host" msgstr "Basis-URL van de Tower-host" -#: awx/main/conf.py:61 +#: awx/main/conf.py:72 msgid "" "This setting is used by services like notifications to render a valid url to" " the Tower host." @@ -1519,51 +1876,46 @@ msgstr "" "Deze instelling wordt gebruikt door services zoals berichten om een geldige " "URL voor de Tower-host weer te geven." -#: awx/main/conf.py:70 +#: awx/main/conf.py:81 msgid "Remote Host Headers" msgstr "Externe hostheaders" -#: awx/main/conf.py:71 +#: awx/main/conf.py:82 msgid "" -"HTTP headers and meta keys to search to determine remote host name or IP. Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if behind a reverse proxy.\n" -"\n" -"Note: The headers will be searched in order and the first found remote host name or IP will be used.\n" -"\n" -"In the below example 8.8.8.7 would be the chosen IP address.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"HTTP headers and meta keys to search to determine remote host name or IP. " +"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if " +"behind a reverse proxy. See the \"Proxy Support\" section of the " +"Adminstrator guide for more details." msgstr "" -"HTTP-headers en metasleutels om te zoeken om de naam of het IP-adres van de externe host te bepalen. Voeg aan deze lijst een extra item toe, zoals \"HTTP_X_FORWARDED_FOR\" indien achter een omgekeerde proxy.\n" -"\n" -"Opmerking: er wordt op volgorde naar de headers gezocht en de eerst gevonden naam of het eerst gevonden IP-adres van de externe host wordt gebruikt.\n" -"\n" -"In het onderstaande voorbeeld wordt 8.8.8.7 het gekozen IP-adres.\n" -"X-Forwarded-For: 8.8.8.7, 192.168.2.1, 127.0.0.1\n" -"Host: 127.0.0.1\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', 'REMOTE_HOST']" +"HTTP-headers en metasleutels om te zoeken om de naam of het IP-adres van de " +"externe host te bepalen. Voeg aan deze lijst extra items toe, zoals " +"\"HTTP_X_FORWARDED_FOR\", wanneer achter een omgekeerde proxy. Zie de sectie" +" 'proxy-ondersteuning' in de handleiding voor beheerders voor meer " +"informatie." -#: awx/main/conf.py:88 +#: awx/main/conf.py:94 msgid "Proxy IP Whitelist" msgstr "Whitelist met proxy-IP's" -#: awx/main/conf.py:89 +#: awx/main/conf.py:95 msgid "" -"If Tower is behind a reverse proxy/load balancer, use this setting to whitelist the proxy IP addresses from which Tower should trust custom REMOTE_HOST_HEADERS header values\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"If this setting is an empty list (the default), the headers specified by REMOTE_HOST_HEADERS will be trusted unconditionally')" +"If Tower is behind a reverse proxy/load balancer, use this setting to " +"whitelist the proxy IP addresses from which Tower should trust custom " +"REMOTE_HOST_HEADERS header values. If this setting is an empty list (the " +"default), the headers specified by REMOTE_HOST_HEADERS will be trusted " +"unconditionally')" msgstr "" -"Als Tower zich achter een omgekeerde proxy/load balancer bevindt, gebruikt u deze instelling om een whitelist te maken met de proxy-IP-adressen vanwaar Tower aangepaste REMOTE_HOST_HEADERS-headerwaarden moet vertrouwen\n" -"REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']\n" -"PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']\n" -"Als deze instelling een lege lijst is (de standaardinstelling), dan worden de door REMOTE_HOST_HEADERS opgegeven headers onvoorwaardelijk vertrouwd')" +"Als Tower zich achter een omgekeerde proxy/load balancer bevindt, gebruikt u" +" deze instelling om een whitelist te maken met de proxy-IP-adressen vanwaar " +"Tower aangepaste REMOTE_HOST_HEADERS-headerwaarden moet vertrouwen. Als deze" +" instelling een lege lijst is (de standaardinstelling), dan worden de door " +"REMOTE_HOST_HEADERS opgegeven headers onvoorwaardelijk vertrouwd')" -#: awx/main/conf.py:117 +#: awx/main/conf.py:121 msgid "License" msgstr "Licentie" -#: awx/main/conf.py:118 +#: awx/main/conf.py:122 msgid "" "The license controls which features and functionality are enabled. Use " "/api/v1/config/ to update or change the license." @@ -1571,29 +1923,60 @@ msgstr "" "De licentie bepaalt welke kenmerken en functionaliteiten zijn ingeschakeld. " "Gebruik /api/v1/config/ om de licentie bij te werken of te wijzigen." -#: awx/main/conf.py:128 +#: awx/main/conf.py:132 msgid "Ansible Modules Allowed for Ad Hoc Jobs" msgstr "Ansible-modules toegestaan voor ad-hoctaken" -#: awx/main/conf.py:129 +#: awx/main/conf.py:133 msgid "List of modules allowed to be used by ad-hoc jobs." msgstr "Lijst met modules toegestaan voor gebruik met ad-hoctaken." -#: awx/main/conf.py:130 awx/main/conf.py:140 awx/main/conf.py:151 -#: awx/main/conf.py:161 awx/main/conf.py:171 awx/main/conf.py:181 -#: awx/main/conf.py:192 awx/main/conf.py:204 awx/main/conf.py:216 -#: awx/main/conf.py:229 awx/main/conf.py:241 awx/main/conf.py:251 -#: awx/main/conf.py:262 awx/main/conf.py:272 awx/main/conf.py:282 -#: awx/main/conf.py:292 awx/main/conf.py:304 awx/main/conf.py:316 -#: awx/main/conf.py:328 awx/main/conf.py:341 +#: awx/main/conf.py:134 awx/main/conf.py:156 awx/main/conf.py:165 +#: awx/main/conf.py:176 awx/main/conf.py:186 awx/main/conf.py:196 +#: awx/main/conf.py:206 awx/main/conf.py:217 awx/main/conf.py:229 +#: awx/main/conf.py:241 awx/main/conf.py:254 awx/main/conf.py:266 +#: awx/main/conf.py:276 awx/main/conf.py:287 awx/main/conf.py:298 +#: awx/main/conf.py:308 awx/main/conf.py:318 awx/main/conf.py:330 +#: awx/main/conf.py:342 awx/main/conf.py:354 awx/main/conf.py:368 msgid "Jobs" msgstr "Taken" -#: awx/main/conf.py:138 +#: awx/main/conf.py:143 +msgid "Always" +msgstr "Altijd" + +#: awx/main/conf.py:144 +msgid "Never" +msgstr "Nooit" + +#: awx/main/conf.py:145 +msgid "Only On Job Template Definitions" +msgstr "Alleen volgens definities taaksjabloon" + +#: awx/main/conf.py:148 +msgid "When can extra variables contain Jinja templates?" +msgstr "Wanneer kunnen extra variabelen Jinja-sjablonen bevatten?" + +#: awx/main/conf.py:150 +msgid "" +"Ansible allows variable substitution via the Jinja2 templating language for " +"--extra-vars. This poses a potential security risk where Tower users with " +"the ability to specify extra vars at job launch time can use Jinja2 " +"templates to run arbitrary Python. It is recommended that this value be set" +" to \"template\" or \"never\"." +msgstr "" +"Ansible maakt het mogelijk variabelen te vervangen door --extra-vars via de " +"Jinja2-sjabloontaal. Dit brengt een mogelijk veiligheidsrisico met zich mee," +" omdat Tower-gebruikers die extra variabelen kunnen specificeren wanneer een" +" taak wordt opgestart, in staat zijn Jinja2-sjablonen te gebruiken om " +"willekeurige Python uit te voeren. Wij raden u aan deze waarde in te stellen" +" op 'sjabloon' of 'nooit'." + +#: awx/main/conf.py:163 msgid "Enable job isolation" msgstr "Taakisolatie inschakelen" -#: awx/main/conf.py:139 +#: awx/main/conf.py:164 msgid "" "Isolates an Ansible job from protected parts of the system to prevent " "exposing sensitive information." @@ -1601,11 +1984,11 @@ msgstr "" "Isoleert een Ansible-taak van beschermde gedeeltes van het systeem om " "blootstelling van gevoelige informatie te voorkomen." -#: awx/main/conf.py:147 +#: awx/main/conf.py:172 msgid "Job execution path" msgstr "Taakuitvoerpad" -#: awx/main/conf.py:148 +#: awx/main/conf.py:173 msgid "" "The directory in which Tower will create new temporary directories for job " "execution and isolation (such as credential files and custom inventory " @@ -1615,22 +1998,22 @@ msgstr "" "isolatie van taken (zoals referentiebestanden en aangepaste " "inventarisscripts)." -#: awx/main/conf.py:159 +#: awx/main/conf.py:184 msgid "Paths to hide from isolated jobs" msgstr "Paden die moeten worden verborgen voor geïsoleerde taken" -#: awx/main/conf.py:160 +#: awx/main/conf.py:185 msgid "" "Additional paths to hide from isolated processes. Enter one path per line." msgstr "" "Extra paden die moeten worden verborgen voor geïsoleerde processen. Geef één" " pad per regel op." -#: awx/main/conf.py:169 +#: awx/main/conf.py:194 msgid "Paths to expose to isolated jobs" msgstr "Paden die kunnen worden blootgesteld aan geïsoleerde taken" -#: awx/main/conf.py:170 +#: awx/main/conf.py:195 msgid "" "Whitelist of paths that would otherwise be hidden to expose to isolated " "jobs. Enter one path per line." @@ -1638,11 +2021,11 @@ msgstr "" "Whitelist met paden die anders zouden zijn verborgen voor blootstelling aan " "geïsoleerde taken. Geef één pad per regel op." -#: awx/main/conf.py:179 +#: awx/main/conf.py:204 msgid "Isolated status check interval" msgstr "Controle-interval voor isolatiestatus" -#: awx/main/conf.py:180 +#: awx/main/conf.py:205 msgid "" "The number of seconds to sleep between status checks for jobs running on " "isolated instances." @@ -1650,11 +2033,11 @@ msgstr "" "Het aantal seconden rust tussen statuscontroles voor taken die worden " "uitgevoerd op geïsoleerde instanties." -#: awx/main/conf.py:189 +#: awx/main/conf.py:214 msgid "Isolated launch timeout" msgstr "Time-out geïsoleerd opstartproces" -#: awx/main/conf.py:190 +#: awx/main/conf.py:215 msgid "" "The timeout (in seconds) for launching jobs on isolated instances. This " "includes the time needed to copy source control files (playbooks) to the " @@ -1664,11 +2047,11 @@ msgstr "" "instanties. Dit is met inbegrip van de tijd vereist om de controlebestanden " "van de bron (draaiboeken) te kopiëren naar de geïsoleerde instantie." -#: awx/main/conf.py:201 +#: awx/main/conf.py:226 msgid "Isolated connection timeout" msgstr "Time-out geïsoleerde verbinding" -#: awx/main/conf.py:202 +#: awx/main/conf.py:227 msgid "" "Ansible SSH connection timeout (in seconds) to use when communicating with " "isolated instances. Value should be substantially greater than expected " @@ -1678,11 +2061,11 @@ msgstr "" "communicatie met geïsoleerde instanties. De waarde moet aanzienlijk groter " "zijn dan de verwachte netwerklatentie." -#: awx/main/conf.py:212 +#: awx/main/conf.py:237 msgid "Generate RSA keys for isolated instances" msgstr "RSA-sleutels aanmaken voor afzonderlijke instanties" -#: awx/main/conf.py:213 +#: awx/main/conf.py:238 msgid "" "If set, a random RSA key will be generated and distributed to isolated " "instances. To disable this behavior and manage authentication for isolated " @@ -1693,19 +2076,19 @@ msgstr "" "schakelen en authenticatie voor afzonderlijke instanties buiten Tower te " "beheren kunt u deze instelling uitschakelen." -#: awx/main/conf.py:227 awx/main/conf.py:228 +#: awx/main/conf.py:252 awx/main/conf.py:253 msgid "The RSA private key for SSH traffic to isolated instances" msgstr "De RSA-privésleutel voor SSH-verkeer naar geïsoleerde instanties" -#: awx/main/conf.py:239 awx/main/conf.py:240 +#: awx/main/conf.py:264 awx/main/conf.py:265 msgid "The RSA public key for SSH traffic to isolated instances" msgstr "De openbare RSA-sleutel voor SSH-verkeer naar geïsoleerde instanties" -#: awx/main/conf.py:249 +#: awx/main/conf.py:274 msgid "Extra Environment Variables" msgstr "Extra omgevingsvariabelen" -#: awx/main/conf.py:250 +#: awx/main/conf.py:275 msgid "" "Additional environment variables set for playbook runs, inventory updates, " "project updates, and notification sending." @@ -1713,11 +2096,11 @@ msgstr "" "Extra omgevingsvariabelen ingesteld voor draaiboekuitvoeringen, " "inventarisupdates, projectupdates en berichtverzending." -#: awx/main/conf.py:260 +#: awx/main/conf.py:285 msgid "Standard Output Maximum Display Size" msgstr "Maximale weergavegrootte voor standaardoutput" -#: awx/main/conf.py:261 +#: awx/main/conf.py:286 msgid "" "Maximum Size of Standard Output in bytes to display before requiring the " "output be downloaded." @@ -1725,11 +2108,11 @@ msgstr "" "De maximale weergavegrootte van standaardoutput in bytes voordat wordt " "vereist dat de output wordt gedownload." -#: awx/main/conf.py:270 +#: awx/main/conf.py:295 msgid "Job Event Standard Output Maximum Display Size" msgstr "Maximale weergavegrootte voor standaardoutput van taakgebeurtenissen" -#: awx/main/conf.py:271 +#: awx/main/conf.py:297 msgid "" "Maximum Size of Standard Output in bytes to display for a single job or ad " "hoc command event. `stdout` will end with `…` when truncated." @@ -1737,11 +2120,11 @@ msgstr "" "De maximale weergavegrootte van standaardoutput in bytes voor één taak of " "ad-hoc-opdrachtgebeurtenis. `stdout` eindigt op `…` indien afgekapt." -#: awx/main/conf.py:280 +#: awx/main/conf.py:306 msgid "Maximum Scheduled Jobs" msgstr "Maximumaantal geplande taken" -#: awx/main/conf.py:281 +#: awx/main/conf.py:307 msgid "" "Maximum number of the same job template that can be waiting to run when " "launching from a schedule before no more are created." @@ -1750,11 +2133,11 @@ msgstr "" "wanneer wordt gestart vanuit een schema voordat er geen andere meer kunnen " "worden gemaakt." -#: awx/main/conf.py:290 +#: awx/main/conf.py:316 msgid "Ansible Callback Plugins" msgstr "Ansible-terugkoppelingsplugins" -#: awx/main/conf.py:291 +#: awx/main/conf.py:317 msgid "" "List of paths to search for extra callback plugins to be used when running " "jobs. Enter one path per line." @@ -1762,11 +2145,11 @@ msgstr "" "Lijst met paden om te zoeken naar extra terugkoppelingsplugins voor gebruik " "bij het uitvoeren van taken. Geef één pad per regel op." -#: awx/main/conf.py:301 +#: awx/main/conf.py:327 msgid "Default Job Timeout" msgstr "Standaardtime-out voor taken" -#: awx/main/conf.py:302 +#: awx/main/conf.py:328 msgid "" "Maximum time in seconds to allow jobs to run. Use value of 0 to indicate " "that no timeout should be imposed. A timeout set on an individual job " @@ -1777,11 +2160,11 @@ msgstr "" "in een individuele taaksjabloon een time-out is ingesteld, heeft deze " "voorrang." -#: awx/main/conf.py:313 +#: awx/main/conf.py:339 msgid "Default Inventory Update Timeout" msgstr "Standaardtime-out voor inventarisupdates" -#: awx/main/conf.py:314 +#: awx/main/conf.py:340 msgid "" "Maximum time in seconds to allow inventory updates to run. Use value of 0 to" " indicate that no timeout should be imposed. A timeout set on an individual " @@ -1792,11 +2175,11 @@ msgstr "" "in een individuele inventarisbron een time-out is ingesteld, heeft deze " "voorrang." -#: awx/main/conf.py:325 +#: awx/main/conf.py:351 msgid "Default Project Update Timeout" msgstr "Standaardtime-out voor projectupdates" -#: awx/main/conf.py:326 +#: awx/main/conf.py:352 msgid "" "Maximum time in seconds to allow project updates to run. Use value of 0 to " "indicate that no timeout should be imposed. A timeout set on an individual " @@ -1806,43 +2189,45 @@ msgstr "" " van 0 om aan te geven dat geen time-out mag worden opgelegd. Als er in een " "individueel project een time-out is ingesteld, heeft deze voorrang." -#: awx/main/conf.py:337 +#: awx/main/conf.py:363 msgid "Per-Host Ansible Fact Cache Timeout" msgstr "Time-out voor feitcache per-host Ansible" -#: awx/main/conf.py:338 +#: awx/main/conf.py:364 msgid "" "Maximum time, in seconds, that stored Ansible facts are considered valid " "since the last time they were modified. Only valid, non-stale, facts will be" " accessible by a playbook. Note, this does not influence the deletion of " -"ansible_facts from the database." +"ansible_facts from the database. Use a value of 0 to indicate that no " +"timeout should be imposed." msgstr "" "Maximale tijd in seconden dat opgeslagen Ansible-feiten als geldig worden " "beschouwd sinds ze voor het laatst zijn gewijzigd. Alleen geldige, niet-" "verlopen feiten zijn toegankelijk voor een draaiboek. Merk op dat dit geen " -"invloed heeft op de verwijdering van ansible_facts uit de database." +"invloed heeft op de verwijdering van ansible_facts uit de database. Gebruik " +"een waarde van 0 om aan te geven dat er geen time-out mag worden opgelegd." -#: awx/main/conf.py:350 +#: awx/main/conf.py:377 msgid "Logging Aggregator" msgstr "Aggregator logboekregistraties" -#: awx/main/conf.py:351 +#: awx/main/conf.py:378 msgid "Hostname/IP where external logs will be sent to." msgstr "Hostnaam/IP-adres waarnaar externe logboeken worden verzonden." -#: awx/main/conf.py:352 awx/main/conf.py:363 awx/main/conf.py:375 -#: awx/main/conf.py:385 awx/main/conf.py:397 awx/main/conf.py:412 -#: awx/main/conf.py:424 awx/main/conf.py:433 awx/main/conf.py:443 -#: awx/main/conf.py:453 awx/main/conf.py:464 awx/main/conf.py:476 -#: awx/main/conf.py:489 +#: awx/main/conf.py:379 awx/main/conf.py:390 awx/main/conf.py:402 +#: awx/main/conf.py:412 awx/main/conf.py:424 awx/main/conf.py:439 +#: awx/main/conf.py:451 awx/main/conf.py:460 awx/main/conf.py:470 +#: awx/main/conf.py:480 awx/main/conf.py:491 awx/main/conf.py:503 +#: awx/main/conf.py:516 msgid "Logging" msgstr "Logboekregistratie" -#: awx/main/conf.py:360 +#: awx/main/conf.py:387 msgid "Logging Aggregator Port" msgstr "Aggregator logboekregistraties" -#: awx/main/conf.py:361 +#: awx/main/conf.py:388 msgid "" "Port on Logging Aggregator to send logs to (if required and not provided in " "Logging Aggregator)." @@ -1850,39 +2235,39 @@ msgstr "" "Poort van aggregator logboekregistraties waarnaar logboeken worden verzonden" " (indien vereist en niet geleverd in de aggregator logboekregistraties)." -#: awx/main/conf.py:373 +#: awx/main/conf.py:400 msgid "Logging Aggregator Type" msgstr "Type aggregator logboekregistraties" -#: awx/main/conf.py:374 +#: awx/main/conf.py:401 msgid "Format messages for the chosen log aggregator." msgstr "Maak berichten op voor de gekozen log aggregator." -#: awx/main/conf.py:383 +#: awx/main/conf.py:410 msgid "Logging Aggregator Username" msgstr "Gebruikersnaam aggregator logboekregistraties" -#: awx/main/conf.py:384 +#: awx/main/conf.py:411 msgid "Username for external log aggregator (if required)." msgstr "Gebruikersnaam voor externe log aggregator (indien vereist)" -#: awx/main/conf.py:395 +#: awx/main/conf.py:422 msgid "Logging Aggregator Password/Token" msgstr "Wachtwoord/token voor aggregator logboekregistraties" -#: awx/main/conf.py:396 +#: awx/main/conf.py:423 msgid "" "Password or authentication token for external log aggregator (if required)." msgstr "" "Wachtwoord of authenticatietoken voor externe log aggregator(indien " "vereist)." -#: awx/main/conf.py:405 +#: awx/main/conf.py:432 msgid "Loggers Sending Data to Log Aggregator Form" msgstr "" "Logboekverzamelingen die gegevens verzenden naar log aggregator-formulier" -#: awx/main/conf.py:406 +#: awx/main/conf.py:433 msgid "" "List of loggers that will send HTTP logs to the collector, these can include any or all of: \n" "awx - service logs\n" @@ -1896,15 +2281,15 @@ msgstr "" "job_events - terugkoppelgegevens van Ansible-taakgebeurtenissen\n" "system_tracking - feiten verzameld uit scantaken." -#: awx/main/conf.py:419 +#: awx/main/conf.py:446 msgid "Log System Tracking Facts Individually" msgstr "Logboeksysteem dat feiten individueel bijhoudt" -#: awx/main/conf.py:420 +#: awx/main/conf.py:447 msgid "" -"If set, system tracking facts will be sent for each package, service, " -"orother item found in a scan, allowing for greater search query granularity." -" If unset, facts will be sent as a single dictionary, allowing for greater " +"If set, system tracking facts will be sent for each package, service, or " +"other item found in a scan, allowing for greater search query granularity. " +"If unset, facts will be sent as a single dictionary, allowing for greater " "efficiency in fact processing." msgstr "" "Indien ingesteld, worden systeemtrackingfeiten verzonden voor alle " @@ -1913,36 +2298,36 @@ msgstr "" "feiten verzonden als één woordenlijst, waardoor feiten sneller kunnen worden" " verwerkt." -#: awx/main/conf.py:431 +#: awx/main/conf.py:458 msgid "Enable External Logging" msgstr "Externe logboekregistratie inschakelen" -#: awx/main/conf.py:432 +#: awx/main/conf.py:459 msgid "Enable sending logs to external log aggregator." msgstr "" "Schakel de verzending in van logboeken naar een externe log aggregator." -#: awx/main/conf.py:441 +#: awx/main/conf.py:468 msgid "Cluster-wide Tower unique identifier." msgstr "Clusterbrede, unieke Tower-id" -#: awx/main/conf.py:442 +#: awx/main/conf.py:469 msgid "Useful to uniquely identify Tower instances." msgstr "Handig om Tower-instanties uniek te identificeren." -#: awx/main/conf.py:451 +#: awx/main/conf.py:478 msgid "Logging Aggregator Protocol" msgstr "Protocol aggregator logboekregistraties" -#: awx/main/conf.py:452 +#: awx/main/conf.py:479 msgid "Protocol used to communicate with log aggregator." msgstr "Protocol gebruikt om te communiceren met de log aggregator." -#: awx/main/conf.py:460 +#: awx/main/conf.py:487 msgid "TCP Connection Timeout" msgstr "Time-out van TCP-verbinding" -#: awx/main/conf.py:461 +#: awx/main/conf.py:488 msgid "" "Number of seconds for a TCP connection to external log aggregator to " "timeout. Applies to HTTPS and TCP log aggregator protocols." @@ -1951,11 +2336,11 @@ msgstr "" " een externe log aggregator. Geldt voor HTTPS en TCP log aggregator-" "protocollen." -#: awx/main/conf.py:471 +#: awx/main/conf.py:498 msgid "Enable/disable HTTPS certificate verification" msgstr "HTTPS-certificaatcontrole in-/uitschakelen" -#: awx/main/conf.py:472 +#: awx/main/conf.py:499 msgid "" "Flag to control enable/disable of certificate verification when " "LOG_AGGREGATOR_PROTOCOL is \"https\". If enabled, Tower's log handler will " @@ -1967,11 +2352,11 @@ msgstr "" "controleert de logboekhandler van Tower het certificaat verzonden door de " "externe log aggregator voordat de verbinding tot stand wordt gebracht." -#: awx/main/conf.py:484 +#: awx/main/conf.py:511 msgid "Logging Aggregator Level Threshold" msgstr "Drempelwaarde aggregator logboekregistraties" -#: awx/main/conf.py:485 +#: awx/main/conf.py:512 msgid "" "Level threshold used by log handler. Severities from lowest to highest are " "DEBUG, INFO, WARNING, ERROR, CRITICAL. Messages less severe than the " @@ -1983,90 +2368,153 @@ msgstr "" " zijn dan de drempelwaarde, worden genegeerd door de logboekhandler. (deze " "instelling wordt genegeerd door berichten onder de categorie awx.anlytics)" -#: awx/main/conf.py:508 awx/sso/conf.py:1105 +#: awx/main/conf.py:535 awx/sso/conf.py:1262 msgid "\n" msgstr "\n" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Sudo" msgstr "Sudo" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Su" msgstr "Su" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pbrun" msgstr "Pbrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:17 msgid "Pfexec" msgstr "Pfexec" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "DZDO" msgstr "DZDO" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Pmrun" msgstr "Pmrun" -#: awx/main/constants.py:10 +#: awx/main/constants.py:18 msgid "Runas" msgstr "Runas" -#: awx/main/fields.py:57 -#, python-format -msgid "'%s' is not one of ['%s']" -msgstr "'%s' is niet een van ['%s']" +#: awx/main/constants.py:19 +msgid "Enable" +msgstr "Inschakelen" -#: awx/main/fields.py:533 +#: awx/main/constants.py:19 +msgid "Doas" +msgstr "Doas" + +#: awx/main/constants.py:21 +msgid "None" +msgstr "Geen" + +#: awx/main/fields.py:62 +#, python-brace-format +msgid "'{value}' is not one of ['{allowed_values}']" +msgstr "'{value}' behoort niet tot ['{allowed_values}']" + +#: awx/main/fields.py:421 +#, python-brace-format +msgid "{type} provided in relative path {path}, expected {expected_type}" +msgstr "" +"{type} opgegeven in relatief pad {path}, terwijl {expected_type} verwacht " +"werd" + +#: awx/main/fields.py:426 +#, python-brace-format +msgid "{type} provided, expected {expected_type}" +msgstr "{type} opgegeven, terwijl {expected_type} verwacht werd" + +#: awx/main/fields.py:431 +#, python-brace-format +msgid "Schema validation error in relative path {path} ({error})" +msgstr "Schemavalideringsfout in relatief pad {path} ({error})" + +#: awx/main/fields.py:552 +msgid "secret values must be of type string, not {}" +msgstr "Geheime waarden moeten van het soort reeks zijn, niet {}" + +#: awx/main/fields.py:587 #, python-format msgid "cannot be set unless \"%s\" is set" msgstr "kan niet ingesteld worden, tenzij '%s' ingesteld is" -#: awx/main/fields.py:549 +#: awx/main/fields.py:603 #, python-format msgid "required for %s" msgstr "vereist voor %s" -#: awx/main/fields.py:573 +#: awx/main/fields.py:627 msgid "must be set when SSH key is encrypted." msgstr "moet worden ingesteld wanneer SSH-sleutel wordt versleuteld." -#: awx/main/fields.py:579 +#: awx/main/fields.py:633 msgid "should not be set when SSH key is not encrypted." msgstr "mag niet worden ingesteld wanneer SSH-sleutel niet is gecodeerd." -#: awx/main/fields.py:637 +#: awx/main/fields.py:691 msgid "'dependencies' is not supported for custom credentials." msgstr "" "'afhankelijkheden' is niet ondersteund voor aangepaste toegangsgegevens." -#: awx/main/fields.py:651 +#: awx/main/fields.py:705 msgid "\"tower\" is a reserved field name" msgstr "\"tower\" is een gereserveerde veldnaam" -#: awx/main/fields.py:658 +#: awx/main/fields.py:712 #, python-format msgid "field IDs must be unique (%s)" msgstr "veld-id's moeten uniek zijn (%s)" -#: awx/main/fields.py:671 -#, python-format -msgid "%s not allowed for %s type (%s)" -msgstr "%s niet toegestaan voor type %s (%s)" +#: awx/main/fields.py:725 +msgid "become_method is a reserved type name" +msgstr "become_method is een gereserveerde soortnaam" -#: awx/main/fields.py:755 -#, python-format -msgid "%s uses an undefined field (%s)" -msgstr "%s gebruikt een niet-gedefinieerd veld (%s)" +#: awx/main/fields.py:736 +#, python-brace-format +msgid "{sub_key} not allowed for {element_type} type ({element_id})" +msgstr "{sub_key} niet toegestaan voor {element_type}-soort ({element_id})" -#: awx/main/middleware.py:157 +#: awx/main/fields.py:810 +msgid "" +"Must define unnamed file injector in order to reference `tower.filename`." +msgstr "" +"Bestandsinjector zonder naam moet gedefinieerd worden om te kunnen verwijzen" +" naar 'tower.filename'." + +#: awx/main/fields.py:817 +msgid "Cannot directly reference reserved `tower` namespace container." +msgstr "" +"Kan niet direct verwijzen naar gereserveerde 'tower'-naamruimtehouder." + +#: awx/main/fields.py:827 +msgid "Must use multi-file syntax when injecting multiple files" +msgstr "" +"Syntaxis voor meerdere bestanden moet gebruikt worden als meerdere bestanden" +" ingevoerd worden" + +#: awx/main/fields.py:844 +#, python-brace-format +msgid "{sub_key} uses an undefined field ({error_msg})" +msgstr "{sub_key} maakt gebruik van een niet-gedefinieerd veld ({error_msg})" + +#: awx/main/fields.py:851 +#, python-brace-format +msgid "" +"Syntax error rendering template for {sub_key} inside of {type} ({error_msg})" +msgstr "" +"Syntaxfout bij het weergeven van de sjabloon voor {sub_key} binnen {type} " +"({error_msg})" + +#: awx/main/middleware.py:146 msgid "Formats of all available named urls" msgstr "Indelingen van alle beschikbare, genoemde url's" -#: awx/main/middleware.py:158 +#: awx/main/middleware.py:147 msgid "" "Read-only list of key-value pairs that shows the standard format of all " "available named URLs." @@ -2074,15 +2522,15 @@ msgstr "" "Alleen-lezen-lijst met sleutelwaardeparen die de standaardindeling van alle " "beschikbare, genoemde URL's toont." -#: awx/main/middleware.py:160 awx/main/middleware.py:170 +#: awx/main/middleware.py:149 awx/main/middleware.py:159 msgid "Named URL" msgstr "Genoemde URL" -#: awx/main/middleware.py:167 +#: awx/main/middleware.py:156 msgid "List of all named url graph nodes." msgstr "Lijst met alle grafische knooppunten van genoemde URL's." -#: awx/main/middleware.py:168 +#: awx/main/middleware.py:157 msgid "" "Read-only list of key-value pairs that exposes named URL graph topology. Use" " this list to programmatically generate named URLs for resources" @@ -2091,31 +2539,35 @@ msgstr "" "genoemde URL's duidelijk maakt. Gebruik deze lijst om programmatische " "genoemde URL's voor resources te genereren." -#: awx/main/migrations/_reencrypt.py:25 awx/main/models/notifications.py:33 +#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:35 msgid "Email" msgstr "E-mail" -#: awx/main/migrations/_reencrypt.py:26 awx/main/models/notifications.py:34 +#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:36 msgid "Slack" msgstr "Slack" -#: awx/main/migrations/_reencrypt.py:27 awx/main/models/notifications.py:35 +#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:37 msgid "Twilio" msgstr "Twilio" -#: awx/main/migrations/_reencrypt.py:28 awx/main/models/notifications.py:36 +#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:38 msgid "Pagerduty" msgstr "Pagerduty" -#: awx/main/migrations/_reencrypt.py:29 awx/main/models/notifications.py:37 +#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:39 msgid "HipChat" msgstr "HipChat" -#: awx/main/migrations/_reencrypt.py:30 awx/main/models/notifications.py:38 +#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:41 +msgid "Mattermost" +msgstr "Mattermost" + +#: awx/main/migrations/_reencrypt.py:32 awx/main/models/notifications.py:40 msgid "Webhook" msgstr "Webhook" -#: awx/main/migrations/_reencrypt.py:31 awx/main/models/notifications.py:39 +#: awx/main/migrations/_reencrypt.py:33 awx/main/models/notifications.py:43 msgid "IRC" msgstr "IRC" @@ -2139,104 +2591,84 @@ msgstr "Entiteit gekoppeld aan een andere entiteit" msgid "Entity was Disassociated with another Entity" msgstr "Entiteit is losgekoppeld van een andere entiteit" -#: awx/main/models/ad_hoc_commands.py:100 +#: awx/main/models/ad_hoc_commands.py:95 msgid "No valid inventory." msgstr "Geen geldige inventaris." -#: awx/main/models/ad_hoc_commands.py:107 +#: awx/main/models/ad_hoc_commands.py:102 msgid "You must provide a machine / SSH credential." msgstr "U moet een machine / SSH-referentie verschaffen." -#: awx/main/models/ad_hoc_commands.py:118 -#: awx/main/models/ad_hoc_commands.py:126 +#: awx/main/models/ad_hoc_commands.py:113 +#: awx/main/models/ad_hoc_commands.py:121 msgid "Invalid type for ad hoc command" msgstr "Ongeldig type voor ad-hocopdracht" -#: awx/main/models/ad_hoc_commands.py:121 +#: awx/main/models/ad_hoc_commands.py:116 msgid "Unsupported module for ad hoc commands." msgstr "Niet-ondersteunde module voor ad-hocopdrachten." -#: awx/main/models/ad_hoc_commands.py:129 +#: awx/main/models/ad_hoc_commands.py:124 #, python-format msgid "No argument passed to %s module." msgstr "Geen argument doorgegeven aan module %s." -#: awx/main/models/ad_hoc_commands.py:245 awx/main/models/jobs.py:911 -msgid "Host Failed" -msgstr "Host is mislukt" - -#: awx/main/models/ad_hoc_commands.py:246 awx/main/models/jobs.py:912 -msgid "Host OK" -msgstr "Host OK" - -#: awx/main/models/ad_hoc_commands.py:247 awx/main/models/jobs.py:915 -msgid "Host Unreachable" -msgstr "Host onbereikbaar" - -#: awx/main/models/ad_hoc_commands.py:252 awx/main/models/jobs.py:914 -msgid "Host Skipped" -msgstr "Host overgeslagen" - -#: awx/main/models/ad_hoc_commands.py:262 awx/main/models/jobs.py:942 -msgid "Debug" -msgstr "Foutopsporing" - -#: awx/main/models/ad_hoc_commands.py:263 awx/main/models/jobs.py:943 -msgid "Verbose" -msgstr "Uitgebreid" - -#: awx/main/models/ad_hoc_commands.py:264 awx/main/models/jobs.py:944 -msgid "Deprecated" -msgstr "Afgeschaft" - -#: awx/main/models/ad_hoc_commands.py:265 awx/main/models/jobs.py:945 -msgid "Warning" -msgstr "Waarschuwing" - -#: awx/main/models/ad_hoc_commands.py:266 awx/main/models/jobs.py:946 -msgid "System Warning" -msgstr "Systeemwaarschuwing" - -#: awx/main/models/ad_hoc_commands.py:267 awx/main/models/jobs.py:947 -#: awx/main/models/unified_jobs.py:64 -msgid "Error" -msgstr "Fout" - -#: awx/main/models/base.py:40 awx/main/models/base.py:46 -#: awx/main/models/base.py:51 +#: awx/main/models/base.py:33 awx/main/models/base.py:39 +#: awx/main/models/base.py:44 awx/main/models/base.py:49 msgid "Run" msgstr "Uitvoeren" -#: awx/main/models/base.py:41 awx/main/models/base.py:47 -#: awx/main/models/base.py:52 +#: awx/main/models/base.py:34 awx/main/models/base.py:40 +#: awx/main/models/base.py:45 awx/main/models/base.py:50 msgid "Check" msgstr "Controleren" -#: awx/main/models/base.py:42 +#: awx/main/models/base.py:35 msgid "Scan" msgstr "Scannen" -#: awx/main/models/credential.py:86 +#: awx/main/models/credential/__init__.py:110 msgid "Host" msgstr "Host" -#: awx/main/models/credential.py:87 +#: awx/main/models/credential/__init__.py:111 msgid "The hostname or IP address to use." msgstr "Te gebruiken hostnaam of IP-adres" -#: awx/main/models/credential.py:93 +#: awx/main/models/credential/__init__.py:117 +#: awx/main/models/credential/__init__.py:686 +#: awx/main/models/credential/__init__.py:741 +#: awx/main/models/credential/__init__.py:806 +#: awx/main/models/credential/__init__.py:884 +#: awx/main/models/credential/__init__.py:930 +#: awx/main/models/credential/__init__.py:958 +#: awx/main/models/credential/__init__.py:987 +#: awx/main/models/credential/__init__.py:1051 +#: awx/main/models/credential/__init__.py:1092 +#: awx/main/models/credential/__init__.py:1125 +#: awx/main/models/credential/__init__.py:1177 msgid "Username" msgstr "Gebruikersnaam" -#: awx/main/models/credential.py:94 +#: awx/main/models/credential/__init__.py:118 msgid "Username for this credential." msgstr "Gebruikersnaam voor deze referentie." -#: awx/main/models/credential.py:100 +#: awx/main/models/credential/__init__.py:124 +#: awx/main/models/credential/__init__.py:690 +#: awx/main/models/credential/__init__.py:745 +#: awx/main/models/credential/__init__.py:810 +#: awx/main/models/credential/__init__.py:934 +#: awx/main/models/credential/__init__.py:962 +#: awx/main/models/credential/__init__.py:991 +#: awx/main/models/credential/__init__.py:1055 +#: awx/main/models/credential/__init__.py:1096 +#: awx/main/models/credential/__init__.py:1129 +#: awx/main/models/credential/__init__.py:1181 msgid "Password" msgstr "Wachtwoord" -#: awx/main/models/credential.py:101 +#: awx/main/models/credential/__init__.py:125 msgid "" "Password for this credential (or \"ASK\" to prompt the user for machine " "credentials)." @@ -2244,43 +2676,43 @@ msgstr "" "Wachtwoord voor deze referentie (of \"ASK\" om de gebruiker om de " "machinereferenties te vragen)." -#: awx/main/models/credential.py:108 +#: awx/main/models/credential/__init__.py:132 msgid "Security Token" msgstr "Beveiligingstoken" -#: awx/main/models/credential.py:109 +#: awx/main/models/credential/__init__.py:133 msgid "Security Token for this credential" msgstr "Beveiligingstoken voor deze referentie" -#: awx/main/models/credential.py:115 +#: awx/main/models/credential/__init__.py:139 msgid "Project" msgstr "Project" -#: awx/main/models/credential.py:116 +#: awx/main/models/credential/__init__.py:140 msgid "The identifier for the project." msgstr "De id voor het project." -#: awx/main/models/credential.py:122 +#: awx/main/models/credential/__init__.py:146 msgid "Domain" msgstr "Domein" -#: awx/main/models/credential.py:123 +#: awx/main/models/credential/__init__.py:147 msgid "The identifier for the domain." msgstr "De id voor het domein." -#: awx/main/models/credential.py:128 +#: awx/main/models/credential/__init__.py:152 msgid "SSH private key" msgstr "SSH-privésleutel" -#: awx/main/models/credential.py:129 +#: awx/main/models/credential/__init__.py:153 msgid "RSA or DSA private key to be used instead of password." msgstr "RSA- of DSA-privésleutel te gebruiken in plaats van wachtwoord." -#: awx/main/models/credential.py:135 +#: awx/main/models/credential/__init__.py:159 msgid "SSH key unlock" msgstr "SSH-sleutelontgrendeling" -#: awx/main/models/credential.py:136 +#: awx/main/models/credential/__init__.py:160 msgid "" "Passphrase to unlock SSH private key if encrypted (or \"ASK\" to prompt the " "user for machine credentials)." @@ -2288,51 +2720,47 @@ msgstr "" "Wachtwoordzin om SSH-privésleutel te ontgrendelen indien versleuteld (of " "\"ASK\" om de gebruiker om de machinereferenties te vragen)." -#: awx/main/models/credential.py:143 -msgid "None" -msgstr "Geen" - -#: awx/main/models/credential.py:144 +#: awx/main/models/credential/__init__.py:168 msgid "Privilege escalation method." msgstr "Methode voor verhoging van rechten." -#: awx/main/models/credential.py:150 +#: awx/main/models/credential/__init__.py:174 msgid "Privilege escalation username." msgstr "Gebruikersnaam voor verhoging van rechten." -#: awx/main/models/credential.py:156 +#: awx/main/models/credential/__init__.py:180 msgid "Password for privilege escalation method." msgstr "Wachtwoord voor methode voor verhoging van rechten." -#: awx/main/models/credential.py:162 +#: awx/main/models/credential/__init__.py:186 msgid "Vault password (or \"ASK\" to prompt the user)." msgstr "Wachtwoord voor kluis (of \"ASK\" om de gebruiker te vragen)." -#: awx/main/models/credential.py:166 +#: awx/main/models/credential/__init__.py:190 msgid "Whether to use the authorize mechanism." msgstr "Of het autorisatiemechanisme mag worden gebruikt." -#: awx/main/models/credential.py:172 +#: awx/main/models/credential/__init__.py:196 msgid "Password used by the authorize mechanism." msgstr "Wachtwoord gebruikt door het autorisatiemechanisme." -#: awx/main/models/credential.py:178 +#: awx/main/models/credential/__init__.py:202 msgid "Client Id or Application Id for the credential" msgstr "Klant-id voor toepassings-id voor de referentie" -#: awx/main/models/credential.py:184 +#: awx/main/models/credential/__init__.py:208 msgid "Secret Token for this credential" msgstr "Geheim token voor deze referentie" -#: awx/main/models/credential.py:190 +#: awx/main/models/credential/__init__.py:214 msgid "Subscription identifier for this credential" msgstr "Abonnements-id voor deze referentie" -#: awx/main/models/credential.py:196 +#: awx/main/models/credential/__init__.py:220 msgid "Tenant identifier for this credential" msgstr "Huurder-id voor deze referentie" -#: awx/main/models/credential.py:220 +#: awx/main/models/credential/__init__.py:244 msgid "" "Specify the type of credential you want to create. Refer to the Ansible " "Tower documentation for details on each type." @@ -2340,7 +2768,8 @@ msgstr "" "Geef het type referentie op dat u wilt maken. Raadpleeg de documentatie voor" " Ansible Tower voor details over elk type." -#: awx/main/models/credential.py:234 awx/main/models/credential.py:420 +#: awx/main/models/credential/__init__.py:258 +#: awx/main/models/credential/__init__.py:476 msgid "" "Enter inputs using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2350,31 +2779,36 @@ msgstr "" "wisselen tussen de twee. Raadpleeg de documentatie voor Ansible Tower voor " "voorbeeldsyntaxis." -#: awx/main/models/credential.py:401 +#: awx/main/models/credential/__init__.py:457 +#: awx/main/models/credential/__init__.py:681 msgid "Machine" msgstr "Machine" -#: awx/main/models/credential.py:402 +#: awx/main/models/credential/__init__.py:458 +#: awx/main/models/credential/__init__.py:772 msgid "Vault" msgstr "Kluis" -#: awx/main/models/credential.py:403 +#: awx/main/models/credential/__init__.py:459 +#: awx/main/models/credential/__init__.py:801 msgid "Network" msgstr "Netwerk" -#: awx/main/models/credential.py:404 +#: awx/main/models/credential/__init__.py:460 +#: awx/main/models/credential/__init__.py:736 msgid "Source Control" msgstr "Broncontrole" -#: awx/main/models/credential.py:405 +#: awx/main/models/credential/__init__.py:461 msgid "Cloud" msgstr "Cloud" -#: awx/main/models/credential.py:406 +#: awx/main/models/credential/__init__.py:462 +#: awx/main/models/credential/__init__.py:1087 msgid "Insights" msgstr "Inzichten" -#: awx/main/models/credential.py:427 +#: awx/main/models/credential/__init__.py:483 msgid "" "Enter injectors using either JSON or YAML syntax. Use the radio button to " "toggle between the two. Refer to the Ansible Tower documentation for example" @@ -2384,11 +2818,414 @@ msgstr "" " wisselen tussen de twee. Raadpleeg de documentatie voor Ansible Tower voor " "voorbeeldsyntaxis." -#: awx/main/models/credential.py:478 +#: awx/main/models/credential/__init__.py:534 #, python-format msgid "adding %s credential type" msgstr "%s soort toegangsgegevens toevoegen" +#: awx/main/models/credential/__init__.py:696 +#: awx/main/models/credential/__init__.py:815 +msgid "SSH Private Key" +msgstr "SSH-privésleutel" + +#: awx/main/models/credential/__init__.py:703 +#: awx/main/models/credential/__init__.py:757 +#: awx/main/models/credential/__init__.py:822 +msgid "Private Key Passphrase" +msgstr "Privésleutel wachtwoordzin" + +#: awx/main/models/credential/__init__.py:709 +msgid "Privilege Escalation Method" +msgstr "Methode voor verhoging van rechten" + +#: awx/main/models/credential/__init__.py:711 +msgid "" +"Specify a method for \"become\" operations. This is equivalent to specifying" +" the --become-method Ansible parameter." +msgstr "" +"Specificeer een methode voor 'become'-operaties. Dit staat gelijk aan het " +"specificeren van de Ansible-parameter voor de --become-method" + +#: awx/main/models/credential/__init__.py:716 +msgid "Privilege Escalation Username" +msgstr "Gebruikersnaam verhoging van rechten" + +#: awx/main/models/credential/__init__.py:720 +msgid "Privilege Escalation Password" +msgstr "Wachtwoord verhoging van rechten" + +#: awx/main/models/credential/__init__.py:750 +msgid "SCM Private Key" +msgstr "SCM-privésleutel" + +#: awx/main/models/credential/__init__.py:777 +msgid "Vault Password" +msgstr "Wachtwoord kluis" + +#: awx/main/models/credential/__init__.py:783 +msgid "Vault Identifier" +msgstr "Id kluis" + +#: awx/main/models/credential/__init__.py:786 +msgid "" +"Specify an (optional) Vault ID. This is equivalent to specifying the " +"--vault-id Ansible parameter for providing multiple Vault passwords. Note: " +"this feature only works in Ansible 2.4+." +msgstr "" +"Specificeer een (optioneel) kluis-id. Dit staat gelijk aan het specificeren " +"van de Ansible-parameter voor de --vault-id voor het opgeven van meerdere " +"kluiswachtwoorden. Let op: deze functie werkt alleen in Ansible 2.4+." + +#: awx/main/models/credential/__init__.py:827 +msgid "Authorize" +msgstr "Autoriseren" + +#: awx/main/models/credential/__init__.py:831 +msgid "Authorize Password" +msgstr "Wachtwoord autoriseren" + +#: awx/main/models/credential/__init__.py:848 +msgid "Amazon Web Services" +msgstr "Amazon webservices" + +#: awx/main/models/credential/__init__.py:853 +msgid "Access Key" +msgstr "Toegangssleutel" + +#: awx/main/models/credential/__init__.py:857 +msgid "Secret Key" +msgstr "Geheime sleutel" + +#: awx/main/models/credential/__init__.py:862 +msgid "STS Token" +msgstr "STS-token" + +#: awx/main/models/credential/__init__.py:865 +msgid "" +"Security Token Service (STS) is a web service that enables you to request " +"temporary, limited-privilege credentials for AWS Identity and Access " +"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:879 awx/main/models/inventory.py:990 +msgid "OpenStack" +msgstr "OpenStack" + +#: awx/main/models/credential/__init__.py:888 +msgid "Password (API Key)" +msgstr "Wachtwoord (API-sleutel)" + +#: awx/main/models/credential/__init__.py:893 +#: awx/main/models/credential/__init__.py:1120 +msgid "Host (Authentication URL)" +msgstr "Host (authenticatie-URL)" + +#: awx/main/models/credential/__init__.py:895 +msgid "" +"The host to authenticate with. For example, " +"https://openstack.business.com/v2.0/" +msgstr "" +"De host waarmee geauthenticeerd moet worden. Bijvoorbeeld " +"https://openstack.business.com/v2.0/" + +#: awx/main/models/credential/__init__.py:899 +msgid "Project (Tenant Name)" +msgstr "Projecten (naam huurder)" + +#: awx/main/models/credential/__init__.py:903 +msgid "Domain Name" +msgstr "Domeinnaam" + +#: awx/main/models/credential/__init__.py:905 +msgid "" +"OpenStack domains define administrative boundaries. It is only needed for " +"Keystone v3 authentication URLs. Refer to Ansible Tower documentation for " +"common scenarios." +msgstr "" +"Domeinen van OpenStack bepalen administratieve grenzen. Het is alleen nodig " +"voor Keystone v3 authenticatie-URL's. Raadpleeg documentatie van Ansible " +"Tower voor veel voorkomende scenario's." + +#: awx/main/models/credential/__init__.py:919 awx/main/models/inventory.py:987 +msgid "VMware vCenter" +msgstr "VMware vCenter" + +#: awx/main/models/credential/__init__.py:924 +msgid "VCenter Host" +msgstr "VCenter-host" + +#: awx/main/models/credential/__init__.py:926 +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:947 awx/main/models/inventory.py:988 +msgid "Red Hat Satellite 6" +msgstr "Red Hat Satellite 6" + +#: awx/main/models/credential/__init__.py:952 +msgid "Satellite 6 URL" +msgstr "Satellite 6-URL" + +#: awx/main/models/credential/__init__.py:954 +msgid "" +"Enter the URL that corresponds to your Red Hat Satellite 6 server. For " +"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:975 awx/main/models/inventory.py:989 +msgid "Red Hat CloudForms" +msgstr "Red Hat CloudForms" + +#: awx/main/models/credential/__init__.py:980 +msgid "CloudForms URL" +msgstr "CloudForms-URL" + +#: awx/main/models/credential/__init__.py:982 +msgid "" +"Enter the URL for the virtual machine that corresponds to your CloudForm " +"instance. For example, https://cloudforms.example.org" +msgstr "" +"Voer de URL in voor de virtuele machine die overeenkomt met uw CloudForm-" +"instantie. Bijvoorbeeld https://cloudforms.example.org" + +#: awx/main/models/credential/__init__.py:1004 +#: awx/main/models/inventory.py:985 +msgid "Google Compute Engine" +msgstr "Google Compute Engine" + +#: awx/main/models/credential/__init__.py:1009 +msgid "Service Account Email Address" +msgstr "E-mailadres service-account" + +#: awx/main/models/credential/__init__.py:1011 +msgid "" +"The email address assigned to the Google Compute Engine service account." +msgstr "" +"Het e-mailadres dat toegewezen is aan het Google Compute Engine-" +"serviceaccount." + +#: awx/main/models/credential/__init__.py:1017 +msgid "" +"The Project ID is the GCE assigned identification. It is often constructed " +"as three words or two words followed by a three-digit number. Examples: " +"project-id-000 and another-project-id" +msgstr "" +"Het project-ID is de toegewezen GCE-identificatie. Dit bestaat vaak uit drie" +" woorden of uit twee woorden, gevolgd door drie getallen. Bijvoorbeeld: " +"project-id-000 of another-project-id" + +#: awx/main/models/credential/__init__.py:1023 +msgid "RSA Private Key" +msgstr "RSA-privésleutel" + +#: awx/main/models/credential/__init__.py:1028 +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:1040 +#: awx/main/models/inventory.py:986 +msgid "Microsoft Azure Resource Manager" +msgstr "Microsoft Azure Resource Manager" + +#: awx/main/models/credential/__init__.py:1045 +msgid "Subscription ID" +msgstr "Abonnement-ID" + +#: awx/main/models/credential/__init__.py:1047 +msgid "Subscription ID is an Azure construct, which is mapped to a username." +msgstr "" +"Abonnement-ID is een concept van Azure en is gelinkt aan een gebruikersnaam." + +#: awx/main/models/credential/__init__.py:1060 +msgid "Client ID" +msgstr "Klant-ID" + +#: awx/main/models/credential/__init__.py:1069 +msgid "Tenant ID" +msgstr "Huurder-ID" + +#: awx/main/models/credential/__init__.py:1073 +msgid "Azure Cloud Environment" +msgstr "Azure-cloudomgeving" + +#: awx/main/models/credential/__init__.py:1075 +msgid "" +"Environment variable AZURE_CLOUD_ENVIRONMENT when using Azure GovCloud or " +"Azure stack." +msgstr "" +"Omgevingsvariabele AZURE_CLOUD_OMGEVING wanneer u Azure GovCloud of Azure " +"stack gebruikt." + +#: awx/main/models/credential/__init__.py:1115 +#: awx/main/models/inventory.py:991 +msgid "Red Hat Virtualization" +msgstr "Red Hat-virtualizering" + +#: awx/main/models/credential/__init__.py:1122 +msgid "The host to authenticate with." +msgstr "De host waarmee geauthenticeerd moet worden." + +#: awx/main/models/credential/__init__.py:1134 +msgid "CA File" +msgstr "CA-bestand" + +#: awx/main/models/credential/__init__.py:1136 +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:1167 +#: awx/main/models/inventory.py:992 +msgid "Ansible Tower" +msgstr "Ansible Tower" + +#: awx/main/models/credential/__init__.py:1172 +msgid "Ansible Tower Hostname" +msgstr "Hostnaam Ansible Tower" + +#: awx/main/models/credential/__init__.py:1174 +msgid "The Ansible Tower base URL to authenticate with." +msgstr "De basis-URL van Ansible Tower waarmee geautenticeerd moet worden." + +#: awx/main/models/credential/__init__.py:1186 +msgid "Verify SSL" +msgstr "SSL verifiëren" + +#: awx/main/models/events.py:89 awx/main/models/events.py:608 +msgid "Host Failed" +msgstr "Host is mislukt" + +#: awx/main/models/events.py:90 awx/main/models/events.py:609 +msgid "Host OK" +msgstr "Host OK" + +#: awx/main/models/events.py:91 +msgid "Host Failure" +msgstr "Hostmislukking" + +#: awx/main/models/events.py:92 awx/main/models/events.py:615 +msgid "Host Skipped" +msgstr "Host overgeslagen" + +#: awx/main/models/events.py:93 awx/main/models/events.py:610 +msgid "Host Unreachable" +msgstr "Host onbereikbaar" + +#: awx/main/models/events.py:94 awx/main/models/events.py:108 +msgid "No Hosts Remaining" +msgstr "Geen resterende hosts" + +#: awx/main/models/events.py:95 +msgid "Host Polling" +msgstr "Hostpolling" + +#: awx/main/models/events.py:96 +msgid "Host Async OK" +msgstr "Host Async OK" + +#: awx/main/models/events.py:97 +msgid "Host Async Failure" +msgstr "Host Async mislukking" + +#: awx/main/models/events.py:98 +msgid "Item OK" +msgstr "Item OK" + +#: awx/main/models/events.py:99 +msgid "Item Failed" +msgstr "Item mislukt" + +#: awx/main/models/events.py:100 +msgid "Item Skipped" +msgstr "Item overgeslagen" + +#: awx/main/models/events.py:101 +msgid "Host Retry" +msgstr "Host opnieuw proberen" + +#: awx/main/models/events.py:103 +msgid "File Difference" +msgstr "Bestandsverschil" + +#: awx/main/models/events.py:104 +msgid "Playbook Started" +msgstr "Draaiboek gestart" + +#: awx/main/models/events.py:105 +msgid "Running Handlers" +msgstr "Handlers die worden uitgevoerd" + +#: awx/main/models/events.py:106 +msgid "Including File" +msgstr "Inclusief bestand" + +#: awx/main/models/events.py:107 +msgid "No Hosts Matched" +msgstr "Geen overeenkomende hosts" + +#: awx/main/models/events.py:109 +msgid "Task Started" +msgstr "Taak gestart" + +#: awx/main/models/events.py:111 +msgid "Variables Prompted" +msgstr "Variabelen gevraagd" + +#: awx/main/models/events.py:112 +msgid "Gathering Facts" +msgstr "Feiten verzamelen" + +#: awx/main/models/events.py:113 +msgid "internal: on Import for Host" +msgstr "intern: bij importeren voor host" + +#: awx/main/models/events.py:114 +msgid "internal: on Not Import for Host" +msgstr "intern: niet bij importeren voor host" + +#: awx/main/models/events.py:115 +msgid "Play Started" +msgstr "Afspelen gestart" + +#: awx/main/models/events.py:116 +msgid "Playbook Complete" +msgstr "Draaiboek voltooid" + +#: awx/main/models/events.py:120 awx/main/models/events.py:625 +msgid "Debug" +msgstr "Foutopsporing" + +#: awx/main/models/events.py:121 awx/main/models/events.py:626 +msgid "Verbose" +msgstr "Uitgebreid" + +#: awx/main/models/events.py:122 awx/main/models/events.py:627 +msgid "Deprecated" +msgstr "Afgeschaft" + +#: awx/main/models/events.py:123 awx/main/models/events.py:628 +msgid "Warning" +msgstr "Waarschuwing" + +#: awx/main/models/events.py:124 awx/main/models/events.py:629 +msgid "System Warning" +msgstr "Systeemwaarschuwing" + +#: awx/main/models/events.py:125 awx/main/models/events.py:630 +#: awx/main/models/unified_jobs.py:67 +msgid "Error" +msgstr "Fout" + #: awx/main/models/fact.py:25 msgid "Host for the facts that the fact scan captured." msgstr "Host voor de feiten die de feitenscan heeft vastgelegd." @@ -2405,79 +3242,100 @@ msgstr "" "Willekeurige JSON-structuur van modulefeiten vastgelegd bij de tijdstempel " "voor één host." -#: awx/main/models/ha.py:78 +#: awx/main/models/ha.py:153 msgid "Instances that are members of this InstanceGroup" msgstr "Instanties die lid zijn van deze InstanceGroup" -#: awx/main/models/ha.py:83 +#: awx/main/models/ha.py:158 msgid "Instance Group to remotely control this group." msgstr "Instantiegroep om deze groep extern te regelen." -#: awx/main/models/inventory.py:52 +#: awx/main/models/ha.py:165 +msgid "Percentage of Instances to automatically assign to this group" +msgstr "" +"Percentage van instanties die automatisch aan deze groep toegewezen moeten " +"worden" + +#: awx/main/models/ha.py:169 +msgid "" +"Static minimum number of Instances to automatically assign to this group" +msgstr "" +"Statistisch minimumaantal instanties dat automatisch toegewezen moet worden " +"aan deze groep" + +#: awx/main/models/ha.py:174 +msgid "" +"List of exact-match Instances that will always be automatically assigned to " +"this group" +msgstr "" +"Lijst van exact overeenkomende instanties die altijd automatisch worden " +"toegewezen aan deze groep" + +#: awx/main/models/inventory.py:61 msgid "Hosts have a direct link to this inventory." msgstr "Hosts hebben een directe koppeling naar deze inventaris." -#: awx/main/models/inventory.py:53 +#: awx/main/models/inventory.py:62 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:58 +#: awx/main/models/inventory.py:67 msgid "inventories" msgstr "inventarissen" -#: awx/main/models/inventory.py:65 +#: awx/main/models/inventory.py:74 msgid "Organization containing this inventory." msgstr "Organisatie die deze inventaris bevat." -#: awx/main/models/inventory.py:72 +#: awx/main/models/inventory.py:81 msgid "Inventory variables in JSON or YAML format." msgstr "Inventarisvariabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:77 +#: awx/main/models/inventory.py:86 msgid "Flag indicating whether any hosts in this inventory have failed." msgstr "Vlag die aangeeft of hosts in deze inventaris zijn mislukt." -#: awx/main/models/inventory.py:82 +#: awx/main/models/inventory.py:91 msgid "Total number of hosts in this inventory." msgstr "Totaal aantal hosts in deze inventaris." -#: awx/main/models/inventory.py:87 +#: awx/main/models/inventory.py:96 msgid "Number of hosts in this inventory with active failures." msgstr "Aantal hosts in deze inventaris met actieve mislukkingen." -#: awx/main/models/inventory.py:92 +#: awx/main/models/inventory.py:101 msgid "Total number of groups in this inventory." msgstr "Totaal aantal groepen in deze inventaris." -#: awx/main/models/inventory.py:97 +#: awx/main/models/inventory.py:106 msgid "Number of groups in this inventory with active failures." msgstr "Aantal groepen in deze inventaris met actieve mislukkingen." -#: awx/main/models/inventory.py:102 +#: awx/main/models/inventory.py:111 msgid "" "Flag indicating whether this inventory has any external inventory sources." msgstr "Vlag die aangeeft of deze inventaris externe inventarisbronnen heeft." -#: awx/main/models/inventory.py:107 +#: awx/main/models/inventory.py:116 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:112 +#: awx/main/models/inventory.py:121 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:119 +#: awx/main/models/inventory.py:128 msgid "Kind of inventory being represented." msgstr "Soort inventaris dat wordt voorgesteld." -#: awx/main/models/inventory.py:125 +#: awx/main/models/inventory.py:134 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:152 +#: awx/main/models/inventory.py:161 msgid "" "Credentials to be used by hosts belonging to this inventory when accessing " "Red Hat Insights API." @@ -2485,38 +3343,38 @@ 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:161 +#: awx/main/models/inventory.py:170 msgid "Flag indicating the inventory is being deleted." msgstr "Vlag die aangeeft dat de inventaris wordt verwijderd." -#: awx/main/models/inventory.py:374 +#: awx/main/models/inventory.py:459 msgid "Assignment not allowed for Smart Inventory" msgstr "Toewijzing niet toegestaan voor Smart-inventaris" -#: awx/main/models/inventory.py:376 awx/main/models/projects.py:148 +#: awx/main/models/inventory.py:461 awx/main/models/projects.py:159 msgid "Credential kind must be 'insights'." msgstr "Referentiesoort moet 'insights' zijn." -#: awx/main/models/inventory.py:443 +#: awx/main/models/inventory.py:546 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:449 +#: awx/main/models/inventory.py:552 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:454 +#: awx/main/models/inventory.py:557 msgid "Host variables in JSON or YAML format." msgstr "Hostvariabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:476 +#: awx/main/models/inventory.py:579 msgid "Flag indicating whether the last job failed for this host." msgstr "Vlag die aangeeft of de laatste taak voor deze host is mislukt." -#: awx/main/models/inventory.py:481 +#: awx/main/models/inventory.py:584 msgid "" "Flag indicating whether this host was created/updated from any external " "inventory sources." @@ -2524,52 +3382,52 @@ msgstr "" "Vlag die aangeeft of deze host is gemaakt/bijgewerkt op grond van externe " "inventarisbronnen." -#: awx/main/models/inventory.py:487 +#: awx/main/models/inventory.py:590 msgid "Inventory source(s) that created or modified this host." msgstr "Inventarisbronnen die deze host hebben gemaakt of gewijzigd." -#: awx/main/models/inventory.py:492 +#: awx/main/models/inventory.py:595 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:498 +#: awx/main/models/inventory.py:601 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:505 +#: awx/main/models/inventory.py:608 msgid "Red Hat Insights host unique identifier." msgstr "Unieke id van Red Hat Insights-host." -#: awx/main/models/inventory.py:633 +#: awx/main/models/inventory.py:743 msgid "Group variables in JSON or YAML format." msgstr "Groepeer variabelen in JSON- of YAML-indeling." -#: awx/main/models/inventory.py:639 +#: awx/main/models/inventory.py:749 msgid "Hosts associated directly with this group." msgstr "Hosts direct gekoppeld aan deze groep." -#: awx/main/models/inventory.py:644 +#: awx/main/models/inventory.py:754 msgid "Total number of hosts directly or indirectly in this group." msgstr "Totaal aantal hosts dat direct of indirect in deze groep is." -#: awx/main/models/inventory.py:649 +#: awx/main/models/inventory.py:759 msgid "Flag indicating whether this group has any hosts with active failures." msgstr "Vlag die aangeeft of deze groep hosts met actieve mislukkingen heeft." -#: awx/main/models/inventory.py:654 +#: awx/main/models/inventory.py:764 msgid "Number of hosts in this group with active failures." msgstr "Aantal hosts in deze groep met actieve mislukkingen." -#: awx/main/models/inventory.py:659 +#: awx/main/models/inventory.py:769 msgid "Total number of child groups contained within this group." msgstr "Totaal aantal onderliggende groepen binnen deze groep." -#: awx/main/models/inventory.py:664 +#: awx/main/models/inventory.py:774 msgid "Number of child groups within this group that have active failures." msgstr "Aantal onderliggende groepen in deze groep met actieve mislukkingen." -#: awx/main/models/inventory.py:669 +#: awx/main/models/inventory.py:779 msgid "" "Flag indicating whether this group was created/updated from any external " "inventory sources." @@ -2577,68 +3435,36 @@ msgstr "" "Vlag die aangeeft of deze groep is gemaakt/bijgewerkt op grond van externe " "inventarisbronnen." -#: awx/main/models/inventory.py:675 +#: awx/main/models/inventory.py:785 msgid "Inventory source(s) that created or modified this group." msgstr "Inventarisbronnen die deze groep hebben gemaakt of gewijzigd." -#: awx/main/models/inventory.py:865 awx/main/models/projects.py:42 -#: awx/main/models/unified_jobs.py:428 +#: awx/main/models/inventory.py:981 awx/main/models/projects.py:53 +#: awx/main/models/unified_jobs.py:519 msgid "Manual" msgstr "Handmatig" -#: awx/main/models/inventory.py:866 +#: awx/main/models/inventory.py:982 msgid "File, Directory or Script" msgstr "Bestand, map of script" -#: awx/main/models/inventory.py:867 +#: awx/main/models/inventory.py:983 msgid "Sourced from a Project" msgstr "Afkomstig uit een project" -#: awx/main/models/inventory.py:868 +#: awx/main/models/inventory.py:984 msgid "Amazon EC2" msgstr "Amazon EC2" -#: awx/main/models/inventory.py:869 -msgid "Google Compute Engine" -msgstr "Google Compute Engine" - -#: awx/main/models/inventory.py:870 -msgid "Microsoft Azure Resource Manager" -msgstr "Microsoft Azure Resource Manager" - -#: awx/main/models/inventory.py:871 -msgid "VMware vCenter" -msgstr "VMware vCenter" - -#: awx/main/models/inventory.py:872 -msgid "Red Hat Satellite 6" -msgstr "Red Hat Satellite 6" - -#: awx/main/models/inventory.py:873 -msgid "Red Hat CloudForms" -msgstr "Red Hat CloudForms" - -#: awx/main/models/inventory.py:874 -msgid "OpenStack" -msgstr "OpenStack" - -#: awx/main/models/inventory.py:875 -msgid "oVirt4" -msgstr "oVirt4" - -#: awx/main/models/inventory.py:876 -msgid "Ansible Tower" -msgstr "Ansible Tower" - -#: awx/main/models/inventory.py:877 +#: awx/main/models/inventory.py:993 msgid "Custom Script" msgstr "Aangepast script" -#: awx/main/models/inventory.py:994 +#: awx/main/models/inventory.py:1110 msgid "Inventory source variables in YAML or JSON format." msgstr "Bronvariabelen inventaris in YAML- of JSON-indeling." -#: awx/main/models/inventory.py:1013 +#: awx/main/models/inventory.py:1121 msgid "" "Comma-separated list of filter expressions (EC2 only). Hosts are imported " "when ANY of the filters match." @@ -2646,77 +3472,81 @@ 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:1019 +#: awx/main/models/inventory.py:1127 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:1023 +#: awx/main/models/inventory.py:1131 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:1027 +#: awx/main/models/inventory.py:1135 msgid "Overwrite local variables from remote inventory source." msgstr "Overschrijf lokale variabelen op grond van externe inventarisbron." -#: awx/main/models/inventory.py:1032 awx/main/models/jobs.py:160 -#: awx/main/models/projects.py:117 +#: awx/main/models/inventory.py:1140 awx/main/models/jobs.py:140 +#: awx/main/models/projects.py:128 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:1065 +#: awx/main/models/inventory.py:1173 msgid "Image ID" msgstr "Image-id" -#: awx/main/models/inventory.py:1066 +#: awx/main/models/inventory.py:1174 msgid "Availability Zone" msgstr "Beschikbaarheidszone" -#: awx/main/models/inventory.py:1067 +#: awx/main/models/inventory.py:1175 msgid "Account" msgstr "Account" -#: awx/main/models/inventory.py:1068 +#: awx/main/models/inventory.py:1176 msgid "Instance ID" msgstr "Instantie-id" -#: awx/main/models/inventory.py:1069 +#: awx/main/models/inventory.py:1177 msgid "Instance State" msgstr "Instantiestaat" -#: awx/main/models/inventory.py:1070 +#: awx/main/models/inventory.py:1178 +msgid "Platform" +msgstr "Platform" + +#: awx/main/models/inventory.py:1179 msgid "Instance Type" msgstr "Instantietype" -#: awx/main/models/inventory.py:1071 +#: awx/main/models/inventory.py:1180 msgid "Key Name" msgstr "Sleutelnaam" -#: awx/main/models/inventory.py:1072 +#: awx/main/models/inventory.py:1181 msgid "Region" msgstr "Regio" -#: awx/main/models/inventory.py:1073 +#: awx/main/models/inventory.py:1182 msgid "Security Group" msgstr "Beveiligingsgroep" -#: awx/main/models/inventory.py:1074 +#: awx/main/models/inventory.py:1183 msgid "Tags" msgstr "Tags" -#: awx/main/models/inventory.py:1075 +#: awx/main/models/inventory.py:1184 msgid "Tag None" msgstr "Tag geen" -#: awx/main/models/inventory.py:1076 +#: awx/main/models/inventory.py:1185 msgid "VPC ID" msgstr "VPC ID" -#: awx/main/models/inventory.py:1145 +#: awx/main/models/inventory.py:1253 #, python-format msgid "" "Cloud-based inventory sources (such as %s) require credentials for the " @@ -2725,11 +3555,11 @@ msgstr "" "Cloudgebaseerde inventarisbronnen (zoals %s) vereisen referenties voor de " "overeenkomende cloudservice." -#: awx/main/models/inventory.py:1152 +#: awx/main/models/inventory.py:1259 msgid "Credential is required for a cloud source." msgstr "Referentie is vereist voor een cloudbron." -#: awx/main/models/inventory.py:1155 +#: awx/main/models/inventory.py:1262 msgid "" "Credentials of type machine, source control, insights and vault are " "disallowed for custom inventory sources." @@ -2737,26 +3567,26 @@ msgstr "" "Toegangsgegevens van soort machine, bronbeheer, inzichten en kluis zijn niet" " toegestaan voor aangepaste inventarisbronnen." -#: awx/main/models/inventory.py:1179 +#: awx/main/models/inventory.py:1314 #, python-format msgid "Invalid %(source)s region: %(region)s" msgstr "Ongeldige %(source)s regio: %(region)s" -#: awx/main/models/inventory.py:1203 +#: awx/main/models/inventory.py:1338 #, python-format msgid "Invalid filter expression: %(filter)s" msgstr "Ongeldige filterexpressie: %(filter)s" -#: awx/main/models/inventory.py:1224 +#: awx/main/models/inventory.py:1359 #, python-format msgid "Invalid group by choice: %(choice)s" msgstr "Ongeldige groep op keuze: %(choice)s" -#: awx/main/models/inventory.py:1259 +#: awx/main/models/inventory.py:1394 msgid "Project containing inventory file used as source." msgstr "Project met inventarisbestand dat wordt gebruikt als bron." -#: awx/main/models/inventory.py:1407 +#: awx/main/models/inventory.py:1555 #, python-format msgid "" "Unable to configure this item for cloud sync. It is already managed by %s." @@ -2764,7 +3594,7 @@ msgstr "" "Kan dit item niet configureren voor cloudsynchronisatie. Wordt al beheerd " "door %s." -#: awx/main/models/inventory.py:1417 +#: awx/main/models/inventory.py:1565 msgid "" "More than one SCM-based inventory source with update on project update per-" "inventory not allowed." @@ -2772,7 +3602,7 @@ 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:1424 +#: awx/main/models/inventory.py:1572 msgid "" "Cannot update SCM-based inventory source on launch if set to update on " "project update. Instead, configure the corresponding source project to " @@ -2782,26 +3612,29 @@ msgstr "" "ingesteld op bijwerken bij projectupdate. Configureer in plaats daarvan het " "overeenkomstige bronproject om bij te werken bij opstarten." -#: awx/main/models/inventory.py:1430 -msgid "SCM type sources must set `overwrite_vars` to `true`." -msgstr "SCM-typebronnen moeten 'overwrite_vars' instellen op 'true'." +#: awx/main/models/inventory.py:1579 +msgid "" +"SCM type sources must set `overwrite_vars` to `true` until Ansible 2.5." +msgstr "" +"SCM-typebronnen moeten 'overwrite_vars' instellen op 'true' tot aan Ansible " +"2.5." -#: awx/main/models/inventory.py:1435 +#: awx/main/models/inventory.py:1584 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:1460 +#: awx/main/models/inventory.py:1615 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:1573 +#: awx/main/models/inventory.py:1725 msgid "Inventory script contents" msgstr "Inhoud inventarisscript" -#: awx/main/models/inventory.py:1578 +#: awx/main/models/inventory.py:1730 msgid "Organization owning this inventory script" msgstr "Organisatie die eigenaar is van deze inventarisscript" @@ -2813,7 +3646,7 @@ msgstr "" "Indien ingeschakeld, worden tekstwijzigingen aangebracht in " "sjabloonbestanden op de host weergegeven in de standaardoutput" -#: awx/main/models/jobs.py:164 +#: awx/main/models/jobs.py:145 msgid "" "If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts" " at the end of a playbook run to the database and caching facts for use by " @@ -2824,37 +3657,38 @@ msgstr "" "database en worden feiten voor gebruik door Ansible in het cachegeheugen " "opgeslagen." -#: awx/main/models/jobs.py:173 -msgid "You must provide an SSH credential." -msgstr "U moet een SSH-referentie opgeven." - -#: awx/main/models/jobs.py:181 +#: awx/main/models/jobs.py:163 msgid "You must provide a Vault credential." msgstr "U moet een kluisreferentie opgeven." -#: awx/main/models/jobs.py:317 +#: awx/main/models/jobs.py:308 msgid "Job Template must provide 'inventory' or allow prompting for it." msgstr "Taaksjabloon moet 'inventory' verschaffen of toestaan erom te vragen." -#: awx/main/models/jobs.py:321 -msgid "Job Template must provide 'credential' or allow prompting for it." +#: awx/main/models/jobs.py:403 +msgid "Field is not configured to prompt on launch." +msgstr "Veld is niet ingesteld om een melding te sturen bij opstarten." + +#: awx/main/models/jobs.py:409 +msgid "Saved launch configurations cannot provide passwords needed to start." msgstr "" -"Taaksjabloon moet 'credential' verschaffen of toestaan erom te vragen." +"Opgeslagen instellingen voor bij opstarten kunnen geen wachtwoorden die " +"nodig zijn voor opstarten opgeven." -#: awx/main/models/jobs.py:427 -msgid "Cannot override job_type to or from a scan job." -msgstr "Kan job_type niet vervangen naar of vanuit een scantaak." +#: awx/main/models/jobs.py:417 +msgid "Job Template {} is missing or undefined." +msgstr "Taaksjabloon {} ontbreekt of is niet gedefinieerd." -#: awx/main/models/jobs.py:493 awx/main/models/projects.py:263 +#: awx/main/models/jobs.py:498 awx/main/models/projects.py:277 msgid "SCM Revision" msgstr "SCM-revisie" -#: awx/main/models/jobs.py:494 +#: awx/main/models/jobs.py:499 msgid "The SCM Revision from the Project used for this job, if available" msgstr "" "De SCM-revisie uit het project gebruikt voor deze taak, indien beschikbaar" -#: awx/main/models/jobs.py:502 +#: awx/main/models/jobs.py:507 msgid "" "The SCM Refresh task used to make sure the playbooks were available for the " "job run" @@ -2862,168 +3696,178 @@ msgstr "" "De taak SCM vernieuwen gebruik om te verzekeren dat de draaiboeken " "beschikbaar waren om de taak uit te voeren" -#: awx/main/models/jobs.py:809 +#: awx/main/models/jobs.py:634 +#, python-brace-format +msgid "{status_value} is not a valid status option." +msgstr "{status_value} is geen geldige statusoptie." + +#: awx/main/models/jobs.py:999 msgid "job host summaries" msgstr "taakhostoverzichten" -#: awx/main/models/jobs.py:913 -msgid "Host Failure" -msgstr "Hostmislukking" - -#: awx/main/models/jobs.py:916 awx/main/models/jobs.py:930 -msgid "No Hosts Remaining" -msgstr "Geen resterende hosts" - -#: awx/main/models/jobs.py:917 -msgid "Host Polling" -msgstr "Hostpolling" - -#: awx/main/models/jobs.py:918 -msgid "Host Async OK" -msgstr "Host Async OK" - -#: awx/main/models/jobs.py:919 -msgid "Host Async Failure" -msgstr "Host Async mislukking" - -#: awx/main/models/jobs.py:920 -msgid "Item OK" -msgstr "Item OK" - -#: awx/main/models/jobs.py:921 -msgid "Item Failed" -msgstr "Item mislukt" - -#: awx/main/models/jobs.py:922 -msgid "Item Skipped" -msgstr "Item overgeslagen" - -#: awx/main/models/jobs.py:923 -msgid "Host Retry" -msgstr "Host opnieuw proberen" - -#: awx/main/models/jobs.py:925 -msgid "File Difference" -msgstr "Bestandsverschil" - -#: awx/main/models/jobs.py:926 -msgid "Playbook Started" -msgstr "Draaiboek gestart" - -#: awx/main/models/jobs.py:927 -msgid "Running Handlers" -msgstr "Handlers die worden uitgevoerd" - -#: awx/main/models/jobs.py:928 -msgid "Including File" -msgstr "Inclusief bestand" - -#: awx/main/models/jobs.py:929 -msgid "No Hosts Matched" -msgstr "Geen overeenkomende hosts" - -#: awx/main/models/jobs.py:931 -msgid "Task Started" -msgstr "Taak gestart" - -#: awx/main/models/jobs.py:933 -msgid "Variables Prompted" -msgstr "Variabelen gevraagd" - -#: awx/main/models/jobs.py:934 -msgid "Gathering Facts" -msgstr "Feiten verzamelen" - -#: awx/main/models/jobs.py:935 -msgid "internal: on Import for Host" -msgstr "intern: bij importeren voor host" - -#: awx/main/models/jobs.py:936 -msgid "internal: on Not Import for Host" -msgstr "intern: niet bij importeren voor host" - -#: awx/main/models/jobs.py:937 -msgid "Play Started" -msgstr "Afspelen gestart" - -#: awx/main/models/jobs.py:938 -msgid "Playbook Complete" -msgstr "Draaiboek voltooid" - -#: awx/main/models/jobs.py:1351 +#: awx/main/models/jobs.py:1070 msgid "Remove jobs older than a certain number of days" msgstr "Taken ouder dan een bepaald aantal dagen verwijderen" -#: awx/main/models/jobs.py:1352 +#: awx/main/models/jobs.py:1071 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:1353 +#: awx/main/models/jobs.py:1072 msgid "Purge and/or reduce the granularity of system tracking data" msgstr "" "Granulariteit van systeemtrackinggegevens verwijderen en/of verminderen" +#: awx/main/models/jobs.py:1142 +#, 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:1157 +msgid "days must be a positive integer." +msgstr "dagen moet een positief geheel getal zijn." + #: awx/main/models/label.py:29 msgid "Organization this label belongs to." msgstr "Organisatie waartoe dit label behoort." -#: awx/main/models/notifications.py:138 awx/main/models/unified_jobs.py:59 +#: awx/main/models/mixins.py:309 +#, python-brace-format +msgid "" +"Variables {list_of_keys} are not allowed on launch. Check the Prompt on " +"Launch setting on the Job Template to include Extra Variables." +msgstr "" +"Variabelen {list_of_keys} zijn niet toegestaan bij opstarten. Ga naar de " +"instelling Melding bij opstarten op het taaksjabloon om extra variabelen toe" +" te voegen." + +#: awx/main/models/mixins.py:440 +msgid "Local absolute file path containing a custom Python virtualenv to use" +msgstr "" +"Plaatselijk absoluut bestandspad dat een aangepaste Python virtualenv bevat " +"om te gebruiken" + +#: awx/main/models/mixins.py:447 +msgid "{} is not a valid virtualenv in {}" +msgstr "{} is geen geldige virtualenv in {}" + +#: awx/main/models/notifications.py:42 +msgid "Rocket.Chat" +msgstr "Rocket.Chat" + +#: awx/main/models/notifications.py:142 awx/main/models/unified_jobs.py:62 msgid "Pending" msgstr "In afwachting" -#: awx/main/models/notifications.py:139 awx/main/models/unified_jobs.py:62 +#: awx/main/models/notifications.py:143 awx/main/models/unified_jobs.py:65 msgid "Successful" msgstr "Geslaagd" -#: awx/main/models/notifications.py:140 awx/main/models/unified_jobs.py:63 +#: awx/main/models/notifications.py:144 awx/main/models/unified_jobs.py:66 msgid "Failed" msgstr "Mislukt" -#: awx/main/models/organization.py:132 -msgid "Token not invalidated" -msgstr "Validatie van token is niet ongeldig gemaakt" +#: awx/main/models/notifications.py:218 +msgid "status_str must be either succeeded or failed" +msgstr "status_str must moet geslaagd of mislukt zijn" -#: awx/main/models/organization.py:133 -msgid "Token is expired" -msgstr "Token is verlopen" +#: awx/main/models/oauth.py:27 +msgid "application" +msgstr "toepassing" -#: awx/main/models/organization.py:134 +#: awx/main/models/oauth.py:32 +msgid "Confidential" +msgstr "Vertrouwelijk" + +#: awx/main/models/oauth.py:33 +msgid "Public" +msgstr "Openbaar" + +#: awx/main/models/oauth.py:41 +msgid "Authorization code" +msgstr "Machtigingscode" + +#: awx/main/models/oauth.py:42 +msgid "Implicit" +msgstr "Impliciet" + +#: awx/main/models/oauth.py:43 +msgid "Resource owner password-based" +msgstr "Eigenaar hulpbron op basis van wachtwoord" + +#: awx/main/models/oauth.py:44 +msgid "Client credentials" +msgstr "Toegangsgegevens klant" + +#: awx/main/models/oauth.py:59 +msgid "Organization containing this application." +msgstr "Organisatie die deze toepassing bevat." + +#: awx/main/models/oauth.py:68 msgid "" -"The maximum number of allowed sessions for this user has been exceeded." +"Used for more stringent verification of access to an application when " +"creating a token." msgstr "" -"Het maximum aantal toegestane sessies voor deze gebruiker is overschreden." +"Gebruikt voor strengere toegangscontrole voor een toepassing bij het " +"aanmaken van een token." -#: awx/main/models/organization.py:137 -msgid "Invalid token" -msgstr "Ongeldig token" +#: awx/main/models/oauth.py:73 +msgid "" +"Set to Public or Confidential depending on how secure the client device is." +msgstr "" +"Ingesteld op openbaar of vertrouwelijk, afhankelijk van de beveiliging van " +"het toestel van de klant." -#: awx/main/models/organization.py:155 -msgid "Reason the auth token was invalidated." -msgstr "Reden waarom het verificatietoken ongeldig is gemaakt." +#: awx/main/models/oauth.py:77 +msgid "" +"Set True to skip authorization step for completely trusted applications." +msgstr "" +"Stel in op True om de autorisatie over te slaan voor volledig vertrouwde " +"toepassingen." -#: awx/main/models/organization.py:194 -msgid "Invalid reason specified" -msgstr "Ongeldige reden opgegeven" +#: awx/main/models/oauth.py:82 +msgid "" +"The Grant type the user must use for acquire tokens for this application." +msgstr "" +"Het soort toekenning dat de gebruiker moet gebruiken om tokens te verkrijgen" +" voor deze toepassing." -#: awx/main/models/projects.py:43 +#: awx/main/models/oauth.py:90 +msgid "access token" +msgstr "toegangstoken" + +#: awx/main/models/oauth.py:98 +msgid "The user representing the token owner" +msgstr "De gebruiker die de tokeneigenaar vertegenwoordigt" + +#: awx/main/models/oauth.py:113 +msgid "" +"Allowed scopes, further restricts user's permissions. Must be a simple " +"space-separated string with allowed scopes ['read', 'write']." +msgstr "" +"Toegestane bereiken, beperkt de machtigingen van de gebruiker verder. Moet " +"een reeks zijn die gescheiden is met enkele spaties en die toegestane " +"bereiken heeft ['lezen', 'schrijven']." + +#: awx/main/models/projects.py:54 msgid "Git" msgstr "Git" -#: awx/main/models/projects.py:44 +#: awx/main/models/projects.py:55 msgid "Mercurial" msgstr "Mercurial" -#: awx/main/models/projects.py:45 +#: awx/main/models/projects.py:56 msgid "Subversion" msgstr "Subversie" -#: awx/main/models/projects.py:46 +#: awx/main/models/projects.py:57 msgid "Red Hat Insights" msgstr "Red Hat Insights" -#: awx/main/models/projects.py:72 +#: awx/main/models/projects.py:83 msgid "" "Local path (relative to PROJECTS_ROOT) containing playbooks and related " "files for this project." @@ -3031,67 +3875,67 @@ msgstr "" "Lokaal pad (ten opzichte van PROJECTS_ROOT) met draaiboeken en gerelateerde " "bestanden voor dit project." -#: awx/main/models/projects.py:81 +#: awx/main/models/projects.py:92 msgid "SCM Type" msgstr "Type SCM" -#: awx/main/models/projects.py:82 +#: awx/main/models/projects.py:93 msgid "Specifies the source control system used to store the project." msgstr "" "Specificeert het broncontrolesysteem gebruikt om het project op te slaan." -#: awx/main/models/projects.py:88 +#: awx/main/models/projects.py:99 msgid "SCM URL" msgstr "SCM URL" -#: awx/main/models/projects.py:89 +#: awx/main/models/projects.py:100 msgid "The location where the project is stored." msgstr "De locatie waar het project is opgeslagen." -#: awx/main/models/projects.py:95 +#: awx/main/models/projects.py:106 msgid "SCM Branch" msgstr "SCM-vertakking" -#: awx/main/models/projects.py:96 +#: awx/main/models/projects.py:107 msgid "Specific branch, tag or commit to checkout." msgstr "Specifieke vertakking, tag of toewijzing om uit te checken." -#: awx/main/models/projects.py:100 +#: awx/main/models/projects.py:111 msgid "Discard any local changes before syncing the project." msgstr "" "Verwijder alle lokale wijzigingen voordat u het project synchroniseert." -#: awx/main/models/projects.py:104 +#: awx/main/models/projects.py:115 msgid "Delete the project before syncing." msgstr "Verwijder het project alvorens te synchroniseren." -#: awx/main/models/projects.py:133 +#: awx/main/models/projects.py:144 msgid "Invalid SCM URL." msgstr "Ongeldige SCM URL." -#: awx/main/models/projects.py:136 +#: awx/main/models/projects.py:147 msgid "SCM URL is required." msgstr "SCM URL is vereist." -#: awx/main/models/projects.py:144 +#: awx/main/models/projects.py:155 msgid "Insights Credential is required for an Insights Project." msgstr "Insights-referentie is vereist voor een Insights-project." -#: awx/main/models/projects.py:150 +#: awx/main/models/projects.py:161 msgid "Credential kind must be 'scm'." msgstr "Referentie moet 'scm' zijn." -#: awx/main/models/projects.py:167 +#: awx/main/models/projects.py:178 msgid "Invalid credential." msgstr "Ongeldige referentie." -#: awx/main/models/projects.py:249 +#: awx/main/models/projects.py:263 msgid "Update the project when a job is launched that uses the project." msgstr "" "Werk het project bij wanneer een taak wordt gestart waarin het project wordt" " gebruikt." -#: awx/main/models/projects.py:254 +#: awx/main/models/projects.py:268 msgid "" "The number of seconds after the last project update ran that a newproject " "update will be launched as a job dependency." @@ -3099,23 +3943,23 @@ msgstr "" "Het aantal seconden na uitvoering van de laatste projectupdate dat een " "nieuwe projectupdate wordt gestart als een taakafhankelijkheid." -#: awx/main/models/projects.py:264 +#: awx/main/models/projects.py:278 msgid "The last revision fetched by a project update" msgstr "De laatste revisie opgehaald door een projectupdate" -#: awx/main/models/projects.py:271 +#: awx/main/models/projects.py:285 msgid "Playbook Files" msgstr "Draaiboekbestanden" -#: awx/main/models/projects.py:272 +#: awx/main/models/projects.py:286 msgid "List of playbooks found in the project" msgstr "Lijst met draaiboekbestanden aangetroffen in het project" -#: awx/main/models/projects.py:279 +#: awx/main/models/projects.py:293 msgid "Inventory Files" msgstr "Inventarisbestanden" -#: awx/main/models/projects.py:280 +#: awx/main/models/projects.py:294 msgid "" "Suggested list of content that could be Ansible inventory in the project" msgstr "" @@ -3139,67 +3983,116 @@ msgid "Admin" msgstr "Beheerder" #: awx/main/models/rbac.py:40 +msgid "Project Admin" +msgstr "Projectbeheerder" + +#: awx/main/models/rbac.py:41 +msgid "Inventory Admin" +msgstr "Inventarisbeheerder" + +#: awx/main/models/rbac.py:42 +msgid "Credential Admin" +msgstr "Toegangsgegevensbeheerder" + +#: awx/main/models/rbac.py:43 +msgid "Workflow Admin" +msgstr "Workflowbeheerder" + +#: awx/main/models/rbac.py:44 +msgid "Notification Admin" +msgstr "Meldingbeheerder" + +#: awx/main/models/rbac.py:45 msgid "Auditor" msgstr "Controleur" -#: awx/main/models/rbac.py:41 +#: awx/main/models/rbac.py:46 msgid "Execute" msgstr "Uitvoeren" -#: awx/main/models/rbac.py:42 +#: awx/main/models/rbac.py:47 msgid "Member" msgstr "Lid" -#: awx/main/models/rbac.py:43 +#: awx/main/models/rbac.py:48 msgid "Read" msgstr "Lezen" -#: awx/main/models/rbac.py:44 +#: awx/main/models/rbac.py:49 msgid "Update" msgstr "Bijwerken" -#: awx/main/models/rbac.py:45 +#: awx/main/models/rbac.py:50 msgid "Use" msgstr "Gebruiken" -#: awx/main/models/rbac.py:49 +#: awx/main/models/rbac.py:54 msgid "Can manage all aspects of the system" msgstr "Kan alle aspecten van het systeem beheren" -#: awx/main/models/rbac.py:50 +#: awx/main/models/rbac.py:55 msgid "Can view all settings on the system" msgstr "Kan alle instellingen van het systeem weergeven" -#: awx/main/models/rbac.py:51 +#: awx/main/models/rbac.py:56 msgid "May run ad hoc commands on an inventory" msgstr "Kan ad-hocopdrachten in een inventaris uitvoeren" -#: awx/main/models/rbac.py:52 +#: awx/main/models/rbac.py:57 #, python-format msgid "Can manage all aspects of the %s" msgstr "Kan alle aspecten van %s beheren" -#: awx/main/models/rbac.py:53 +#: awx/main/models/rbac.py:58 +#, python-format +msgid "Can manage all projects of the %s" +msgstr "Kan alle projecten van de %s beheren" + +#: awx/main/models/rbac.py:59 +#, python-format +msgid "Can manage all inventories of the %s" +msgstr "Kan alle inventarissen van de %s beheren" + +#: awx/main/models/rbac.py:60 +#, python-format +msgid "Can manage all credentials of the %s" +msgstr "Kan alle toegangsgegevens van de %s beheren" + +#: awx/main/models/rbac.py:61 +#, python-format +msgid "Can manage all workflows of the %s" +msgstr "Kan alle workflows van de %s beheren" + +#: awx/main/models/rbac.py:62 +#, python-format +msgid "Can manage all notifications of the %s" +msgstr "Kan alle meldingen van de %s beheren" + +#: awx/main/models/rbac.py:63 #, python-format msgid "Can view all settings for the %s" msgstr "Kan alle instellingen voor %s weergeven" -#: awx/main/models/rbac.py:54 +#: awx/main/models/rbac.py:65 +msgid "May run any executable resources in the organization" +msgstr "Kan alle uitvoerbare hulpbronnen in de organisatie uitvoeren" + +#: awx/main/models/rbac.py:66 #, python-format msgid "May run the %s" msgstr "Kan %s uitvoeren" -#: awx/main/models/rbac.py:55 +#: awx/main/models/rbac.py:68 #, python-format msgid "User is a member of the %s" msgstr "Gebruiker is lid van %s" -#: awx/main/models/rbac.py:56 +#: awx/main/models/rbac.py:69 #, python-format msgid "May view settings for the %s" msgstr "Kan instellingen voor %s weergeven" -#: awx/main/models/rbac.py:57 +#: awx/main/models/rbac.py:70 msgid "" "May update project or inventory or group using the configured source update " "system" @@ -3207,28 +4100,28 @@ msgstr "" "Kan project of inventarisgroep bijwerken met het geconfigureerde " "bronupdatesysteem" -#: awx/main/models/rbac.py:58 +#: awx/main/models/rbac.py:71 #, python-format msgid "Can use the %s in a job template" msgstr "Kan %s gebruiken in een taaksjabloon" -#: awx/main/models/rbac.py:122 +#: awx/main/models/rbac.py:135 msgid "roles" msgstr "rollen" -#: awx/main/models/rbac.py:434 +#: awx/main/models/rbac.py:441 msgid "role_ancestors" msgstr "role_ancestors" -#: awx/main/models/schedules.py:71 +#: awx/main/models/schedules.py:79 msgid "Enables processing of this schedule." msgstr "Maakt de verwerking van dit schema mogelijk." -#: awx/main/models/schedules.py:77 +#: awx/main/models/schedules.py:85 msgid "The first occurrence of the schedule occurs on or after this time." msgstr "Het eerste voorkomen van het schema treedt op of na deze tijd op." -#: awx/main/models/schedules.py:83 +#: awx/main/models/schedules.py:91 msgid "" "The last occurrence of the schedule occurs before this time, aftewards the " "schedule expires." @@ -3236,102 +4129,110 @@ msgstr "" "Het laatste voorkomen van het schema treedt voor deze tijd op, nadat het " "schema is verlopen." -#: awx/main/models/schedules.py:87 +#: awx/main/models/schedules.py:95 msgid "A value representing the schedules iCal recurrence rule." msgstr "Een waarde die de iCal-herhalingsregel van het schema voorstelt." -#: awx/main/models/schedules.py:93 +#: awx/main/models/schedules.py:101 msgid "The next time that the scheduled action will run." msgstr "De volgende keer dat de geplande actie wordt uitgevoerd." -#: awx/main/models/schedules.py:109 -msgid "Expected JSON" -msgstr "JSON verwacht" - -#: awx/main/models/schedules.py:121 -msgid "days must be a positive integer." -msgstr "dagen moet een positief geheel getal zijn." - -#: awx/main/models/unified_jobs.py:58 +#: awx/main/models/unified_jobs.py:61 msgid "New" msgstr "Nieuw" -#: awx/main/models/unified_jobs.py:60 +#: awx/main/models/unified_jobs.py:63 msgid "Waiting" msgstr "Wachten" -#: awx/main/models/unified_jobs.py:61 +#: awx/main/models/unified_jobs.py:64 msgid "Running" msgstr "Uitvoeren" -#: awx/main/models/unified_jobs.py:65 +#: awx/main/models/unified_jobs.py:68 msgid "Canceled" msgstr "Geannuleerd" -#: awx/main/models/unified_jobs.py:69 +#: awx/main/models/unified_jobs.py:72 msgid "Never Updated" msgstr "Nooit bijgewerkt" -#: awx/main/models/unified_jobs.py:73 awx/ui/templates/ui/index.html:67 -#: awx/ui/templates/ui/index.html.py:86 +#: awx/main/models/unified_jobs.py:76 msgid "OK" msgstr "OK" -#: awx/main/models/unified_jobs.py:74 +#: awx/main/models/unified_jobs.py:77 msgid "Missing" msgstr "Ontbrekend" -#: awx/main/models/unified_jobs.py:78 +#: awx/main/models/unified_jobs.py:81 msgid "No External Source" msgstr "Geen externe bron" -#: awx/main/models/unified_jobs.py:85 +#: awx/main/models/unified_jobs.py:88 msgid "Updating" msgstr "Bijwerken" -#: awx/main/models/unified_jobs.py:429 +#: awx/main/models/unified_jobs.py:427 +msgid "Field is not allowed on launch." +msgstr "Veld is niet toegestaan bij opstarten." + +#: awx/main/models/unified_jobs.py:455 +#, python-brace-format +msgid "" +"Variables {list_of_keys} provided, but this template cannot accept " +"variables." +msgstr "" +"Variabelen {list_of_keys} opgegeven, maar deze sjabloon kan geen variabelen " +"accepteren." + +#: awx/main/models/unified_jobs.py:520 msgid "Relaunch" msgstr "Opnieuw starten" -#: awx/main/models/unified_jobs.py:430 +#: awx/main/models/unified_jobs.py:521 msgid "Callback" msgstr "Terugkoppelen" -#: awx/main/models/unified_jobs.py:431 +#: awx/main/models/unified_jobs.py:522 msgid "Scheduled" msgstr "Gepland" -#: awx/main/models/unified_jobs.py:432 +#: awx/main/models/unified_jobs.py:523 msgid "Dependency" msgstr "Afhankelijkheid" -#: awx/main/models/unified_jobs.py:433 +#: awx/main/models/unified_jobs.py:524 msgid "Workflow" msgstr "Workflow" -#: awx/main/models/unified_jobs.py:434 +#: awx/main/models/unified_jobs.py:525 msgid "Sync" msgstr "Synchroniseren" -#: awx/main/models/unified_jobs.py:481 +#: awx/main/models/unified_jobs.py:573 msgid "The node the job executed on." msgstr "Het knooppunt waarop de taak is uitgevoerd." -#: awx/main/models/unified_jobs.py:507 +#: awx/main/models/unified_jobs.py:579 +msgid "The instance that managed the isolated execution environment." +msgstr "De instantie die de geïsoleerde uitvoeringsomgeving beheerd heeft." + +#: awx/main/models/unified_jobs.py:605 msgid "The date and time the job was queued for starting." msgstr "" "De datum en tijd waarop de taak in de wachtrij is gezet om te worden " "gestart." -#: awx/main/models/unified_jobs.py:513 +#: awx/main/models/unified_jobs.py:611 msgid "The date and time the job finished execution." msgstr "De datum en tijd waarop de taak de uitvoering heeft beëindigd." -#: awx/main/models/unified_jobs.py:519 +#: awx/main/models/unified_jobs.py:617 msgid "Elapsed time in seconds that the job ran." msgstr "Verstreken tijd in seconden dat de taak is uitgevoerd." -#: awx/main/models/unified_jobs.py:541 +#: awx/main/models/unified_jobs.py:639 msgid "" "A status field to indicate the state of the job if it wasn't able to run and" " capture stdout" @@ -3339,10 +4240,23 @@ msgstr "" "Een statusveld om de status van de taak aan te geven als deze niet kon " "worden uitgevoerd en stdout niet kon worden vastgelegd" -#: awx/main/models/unified_jobs.py:580 +#: awx/main/models/unified_jobs.py:668 msgid "The Rampart/Instance group the job was run under" msgstr "De Rampart-/instantiegroep waaronder de taak werd uitgevoerd" +#: awx/main/models/workflow.py:203 +#, python-brace-format +msgid "" +"Bad launch configuration starting template {template_pk} as part of workflow {workflow_pk}. Errors:\n" +"{error_text}" +msgstr "" +"Slechte opstartconfiguratie met opstartsjabloon {template_pk} als onderdeel " +"van workflow {workflow_pk}. Fouten: {error_text}" + +#: awx/main/models/workflow.py:388 +msgid "Field is not allowed for use in workflows." +msgstr "Gebruik van veld in workflows is niet toegestaan." + #: awx/main/notifications/base.py:17 #: awx/main/notifications/email_backend.py:28 msgid "" @@ -3352,11 +4266,11 @@ msgstr "" "{} #{} had status {}, geef details weer op {}\n" "\n" -#: awx/main/notifications/hipchat_backend.py:47 +#: awx/main/notifications/hipchat_backend.py:48 msgid "Error sending messages: {}" msgstr "Fout bij verzending van berichten: {}" -#: awx/main/notifications/hipchat_backend.py:49 +#: awx/main/notifications/hipchat_backend.py:50 msgid "Error sending message to hipchat: {}" msgstr "Fout bij verzending van bericht naar hipchat: {}" @@ -3364,16 +4278,27 @@ msgstr "Fout bij verzending van bericht naar hipchat: {}" msgid "Exception connecting to irc server: {}" msgstr "Uitzondering bij het maken van de verbinding met de irc-server: {}" +#: awx/main/notifications/mattermost_backend.py:48 +#: awx/main/notifications/mattermost_backend.py:50 +msgid "Error sending notification mattermost: {}" +msgstr "Fout bij verzending bericht mattermost: {}" + #: awx/main/notifications/pagerduty_backend.py:39 msgid "Exception connecting to PagerDuty: {}" msgstr "Uitzondering bij het maken van de verbinding met PagerDuty: {}" #: awx/main/notifications/pagerduty_backend.py:48 -#: awx/main/notifications/slack_backend.py:52 +#: awx/main/notifications/slack_backend.py:82 +#: awx/main/notifications/slack_backend.py:99 #: awx/main/notifications/twilio_backend.py:46 msgid "Exception sending messages: {}" msgstr "Uitzondering bij het verzenden van berichten: {}" +#: awx/main/notifications/rocketchat_backend.py:46 +#: awx/main/notifications/rocketchat_backend.py:49 +msgid "Error sending notification rocket.chat: {}" +msgstr "Fout bij verzending bericht rocket.chat: {}" + #: awx/main/notifications/twilio_backend.py:36 msgid "Exception connecting to Twilio: {}" msgstr "Uitzondering bij het maken van de verbinding met Twilio: {}" @@ -3383,7 +4308,7 @@ msgstr "Uitzondering bij het maken van de verbinding met Twilio: {}" msgid "Error sending notification webhook: {}" msgstr "Fout bij verzending bericht webhook: {}" -#: awx/main/scheduler/task_manager.py:197 +#: awx/main/scheduler/task_manager.py:201 msgid "" "Job spawned from workflow could not start because it was not in the right " "state or required manual credentials" @@ -3391,7 +4316,7 @@ msgstr "" "Taak voortgebracht vanuit workflow kon niet starten omdat deze niet de " "juiste status of vereiste handmatige referenties had" -#: awx/main/scheduler/task_manager.py:201 +#: awx/main/scheduler/task_manager.py:205 msgid "" "Job spawned from workflow could not start because it was missing a related " "resource such as project or inventory" @@ -3399,91 +4324,109 @@ msgstr "" "Taak voortgebracht vanuit workflow kon niet starten omdat een gerelateerde " "resource zoals project of inventaris ontbreekt" -#: awx/main/tasks.py:184 +#: awx/main/signals.py:616 +msgid "limit_reached" +msgstr "limit_reached" + +#: awx/main/tasks.py:282 msgid "Ansible Tower host usage over 90%" msgstr "Ansible Tower-hostgebruik meer dan 90%" -#: awx/main/tasks.py:189 +#: awx/main/tasks.py:287 msgid "Ansible Tower license will expire soon" msgstr "De licentie voor Ansible Tower verloopt binnenkort" -#: awx/main/tasks.py:318 -msgid "status_str must be either succeeded or failed" -msgstr "status_str must moet geslaagd of mislukt zijn" +#: awx/main/tasks.py:1335 +msgid "Job could not start because it does not have a valid inventory." +msgstr "Taak kon niet gestart worden omdat deze geen geldig inventaris heeft." -#: awx/main/tasks.py:1549 -msgid "Dependent inventory update {} was canceled." -msgstr "De afhankelijke inventarisupdate {} is geannuleerd." - -#: awx/main/utils/common.py:89 +#: awx/main/utils/common.py:97 #, python-format msgid "Unable to convert \"%s\" to boolean" msgstr "Kan \"%s\" niet converteren in boolean" -#: awx/main/utils/common.py:235 +#: awx/main/utils/common.py:254 #, python-format msgid "Unsupported SCM type \"%s\"" msgstr "Niet-ondersteund SCM-type \"%s\"" -#: awx/main/utils/common.py:242 awx/main/utils/common.py:254 -#: awx/main/utils/common.py:273 +#: awx/main/utils/common.py:261 awx/main/utils/common.py:273 +#: awx/main/utils/common.py:292 #, python-format msgid "Invalid %s URL" msgstr "Ongeldige %s URL" -#: awx/main/utils/common.py:244 awx/main/utils/common.py:283 +#: awx/main/utils/common.py:263 awx/main/utils/common.py:302 #, python-format msgid "Unsupported %s URL" msgstr "Niet-ondersteunde %s URL" -#: awx/main/utils/common.py:285 +#: awx/main/utils/common.py:304 #, python-format msgid "Unsupported host \"%s\" for file:// URL" msgstr "Niet-ondersteunde host \"%s\" voor bestand:// URL" -#: awx/main/utils/common.py:287 +#: awx/main/utils/common.py:306 #, python-format msgid "Host is required for %s URL" msgstr "Host is vereist voor %s URL" -#: awx/main/utils/common.py:305 +#: awx/main/utils/common.py:324 #, 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:311 +#: awx/main/utils/common.py:330 #, python-format msgid "Username must be \"hg\" for SSH access to %s." msgstr "Gebruikersnaam moet \"hg\" zijn voor SSH-toegang tot %s." -#: awx/main/validators.py:60 +#: awx/main/utils/common.py:611 +#, 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:644 +#, 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:650 +#, python-brace-format +msgid "" +"Cannot parse as JSON (error: {json_error}) or YAML (error: {yaml_error})." +msgstr "" +"Kan niet parseren als JSON (fout: {json_error}) of YAML (fout: " +"{yaml_error})." + +#: awx/main/validators.py:67 #, python-format msgid "Invalid certificate or key: %s..." msgstr "Ongeldig certificaat of ongeldige sleutel: %s..." -#: awx/main/validators.py:74 +#: awx/main/validators.py:83 #, python-format msgid "Invalid private key: unsupported type \"%s\"" msgstr "Ongeldige privésleutel: niet-ondersteund type \"%s\"" -#: awx/main/validators.py:78 +#: awx/main/validators.py:87 #, python-format msgid "Unsupported PEM object type: \"%s\"" msgstr "Niet-ondersteund PEM-objecttype \"%s\"" -#: awx/main/validators.py:103 +#: awx/main/validators.py:112 msgid "Invalid base64-encoded data" msgstr "Ongeldige base64-versleutelde gegevens" -#: awx/main/validators.py:122 +#: awx/main/validators.py:131 msgid "Exactly one private key is required." msgstr "Precies één privésleutel is vereist." -#: awx/main/validators.py:124 +#: awx/main/validators.py:133 msgid "At least one private key is required." msgstr "Ten minste één privésleutel is vereist." -#: awx/main/validators.py:126 +#: awx/main/validators.py:135 #, python-format msgid "" "At least %(min_keys)d private keys are required, only %(key_count)d " @@ -3492,12 +4435,12 @@ msgstr "" "Ten minste %(min_keys)d privésleutels zijn vereist, niet meer dan " "%(key_count)d geleverd." -#: awx/main/validators.py:129 +#: awx/main/validators.py:138 #, python-format msgid "Only one private key is allowed, %(key_count)d provided." msgstr "Maar één privésleutel is toegestaan, %(key_count)d geleverd." -#: awx/main/validators.py:131 +#: awx/main/validators.py:140 #, python-format msgid "" "No more than %(max_keys)d private keys are allowed, %(key_count)d provided." @@ -3505,15 +4448,15 @@ msgstr "" "Niet meer dan %(max_keys)d privésleutels zijn toegestaan, %(key_count)d " "geleverd." -#: awx/main/validators.py:136 +#: awx/main/validators.py:145 msgid "Exactly one certificate is required." msgstr "Precies één certificaat is vereist." -#: awx/main/validators.py:138 +#: awx/main/validators.py:147 msgid "At least one certificate is required." msgstr "Ten minste één certificaat is vereist." -#: awx/main/validators.py:140 +#: awx/main/validators.py:149 #, python-format msgid "" "At least %(min_certs)d certificates are required, only %(cert_count)d " @@ -3522,12 +4465,12 @@ msgstr "" "Ten minste %(min_certs)d certificaten zijn vereist, niet meer dan " "%(cert_count)d geleverd." -#: awx/main/validators.py:143 +#: awx/main/validators.py:152 #, python-format msgid "Only one certificate is allowed, %(cert_count)d provided." msgstr "Maar één certificaat is toegestaan, %(cert_count)d geleverd." -#: awx/main/validators.py:145 +#: awx/main/validators.py:154 #, python-format msgid "" "No more than %(max_certs)d certificates are allowed, %(cert_count)d " @@ -3572,287 +4515,287 @@ msgstr "Serverfout" msgid "A server error has occurred." msgstr "Er is een serverfout opgetreden" -#: awx/settings/defaults.py:665 +#: awx/settings/defaults.py:721 msgid "US East (Northern Virginia)" msgstr "VS Oosten (Northern Virginia)" -#: awx/settings/defaults.py:666 +#: awx/settings/defaults.py:722 msgid "US East (Ohio)" msgstr "VS Oosten (Ohio)" -#: awx/settings/defaults.py:667 +#: awx/settings/defaults.py:723 msgid "US West (Oregon)" msgstr "VS Westen (Oregon)" -#: awx/settings/defaults.py:668 +#: awx/settings/defaults.py:724 msgid "US West (Northern California)" msgstr "VS Westen (Northern California)" -#: awx/settings/defaults.py:669 +#: awx/settings/defaults.py:725 msgid "Canada (Central)" msgstr "Canada (Midden)" -#: awx/settings/defaults.py:670 +#: awx/settings/defaults.py:726 msgid "EU (Frankfurt)" msgstr "EU (Frankfurt)" -#: awx/settings/defaults.py:671 +#: awx/settings/defaults.py:727 msgid "EU (Ireland)" msgstr "EU (Ierland)" -#: awx/settings/defaults.py:672 +#: awx/settings/defaults.py:728 msgid "EU (London)" msgstr "EU (Londen)" -#: awx/settings/defaults.py:673 +#: awx/settings/defaults.py:729 msgid "Asia Pacific (Singapore)" msgstr "Azië-Pacific (Singapore)" -#: awx/settings/defaults.py:674 +#: awx/settings/defaults.py:730 msgid "Asia Pacific (Sydney)" msgstr "Azië-Pacific (Sydney)" -#: awx/settings/defaults.py:675 +#: awx/settings/defaults.py:731 msgid "Asia Pacific (Tokyo)" msgstr "Azië-Pacific (Tokyo)" -#: awx/settings/defaults.py:676 +#: awx/settings/defaults.py:732 msgid "Asia Pacific (Seoul)" msgstr "Azië-Pacific (Seoul)" -#: awx/settings/defaults.py:677 +#: awx/settings/defaults.py:733 msgid "Asia Pacific (Mumbai)" msgstr "Azië-Pacific (Mumbai)" -#: awx/settings/defaults.py:678 +#: awx/settings/defaults.py:734 msgid "South America (Sao Paulo)" msgstr "Zuid-Amerika (Sao Paulo)" -#: awx/settings/defaults.py:679 +#: awx/settings/defaults.py:735 msgid "US West (GovCloud)" msgstr "VS Westen (GovCloud)" -#: awx/settings/defaults.py:680 +#: awx/settings/defaults.py:736 msgid "China (Beijing)" msgstr "China (Beijing)" -#: awx/settings/defaults.py:729 +#: awx/settings/defaults.py:785 msgid "US East 1 (B)" msgstr "VS Oosten 1 (B)" -#: awx/settings/defaults.py:730 +#: awx/settings/defaults.py:786 msgid "US East 1 (C)" msgstr "VS Oosten 1 (C)" -#: awx/settings/defaults.py:731 +#: awx/settings/defaults.py:787 msgid "US East 1 (D)" msgstr "VS Oosten 1 (D)" -#: awx/settings/defaults.py:732 +#: awx/settings/defaults.py:788 msgid "US East 4 (A)" msgstr "VS Oosten 4 (A)" -#: awx/settings/defaults.py:733 +#: awx/settings/defaults.py:789 msgid "US East 4 (B)" msgstr "VS Oosten 4 (B)" -#: awx/settings/defaults.py:734 +#: awx/settings/defaults.py:790 msgid "US East 4 (C)" msgstr "VS Oosten 4 (C)" -#: awx/settings/defaults.py:735 +#: awx/settings/defaults.py:791 msgid "US Central (A)" msgstr "VS Midden (A)" -#: awx/settings/defaults.py:736 +#: awx/settings/defaults.py:792 msgid "US Central (B)" msgstr "VS Midden (B)" -#: awx/settings/defaults.py:737 +#: awx/settings/defaults.py:793 msgid "US Central (C)" msgstr "VS Midden (C)" -#: awx/settings/defaults.py:738 +#: awx/settings/defaults.py:794 msgid "US Central (F)" msgstr "VS Midden (F)" -#: awx/settings/defaults.py:739 +#: awx/settings/defaults.py:795 msgid "US West (A)" msgstr "VS Westen (A)" -#: awx/settings/defaults.py:740 +#: awx/settings/defaults.py:796 msgid "US West (B)" msgstr "VS Westen (B)" -#: awx/settings/defaults.py:741 +#: awx/settings/defaults.py:797 msgid "US West (C)" msgstr "VS Westen (C)" -#: awx/settings/defaults.py:742 +#: awx/settings/defaults.py:798 msgid "Europe West 1 (B)" msgstr "Europa Westen 1 (B)" -#: awx/settings/defaults.py:743 +#: awx/settings/defaults.py:799 msgid "Europe West 1 (C)" msgstr "Europa Westen 1 (C)" -#: awx/settings/defaults.py:744 +#: awx/settings/defaults.py:800 msgid "Europe West 1 (D)" msgstr "Europa Westen 1 (D)" -#: awx/settings/defaults.py:745 +#: awx/settings/defaults.py:801 msgid "Europe West 2 (A)" msgstr "Europa Westen 2 (A)" -#: awx/settings/defaults.py:746 +#: awx/settings/defaults.py:802 msgid "Europe West 2 (B)" msgstr "Europa Westen 2 (B)" -#: awx/settings/defaults.py:747 +#: awx/settings/defaults.py:803 msgid "Europe West 2 (C)" msgstr "Europa Westen 2 (C)" -#: awx/settings/defaults.py:748 +#: awx/settings/defaults.py:804 msgid "Asia East (A)" msgstr "Azië Oosten (A)" -#: awx/settings/defaults.py:749 +#: awx/settings/defaults.py:805 msgid "Asia East (B)" msgstr "Azië Oosten (B)" -#: awx/settings/defaults.py:750 +#: awx/settings/defaults.py:806 msgid "Asia East (C)" msgstr "Azië Oosten (C)" -#: awx/settings/defaults.py:751 +#: awx/settings/defaults.py:807 msgid "Asia Southeast (A)" msgstr "Azië (Zuidoost) (A)" -#: awx/settings/defaults.py:752 +#: awx/settings/defaults.py:808 msgid "Asia Southeast (B)" msgstr "Azië (Zuidoost) (B)" -#: awx/settings/defaults.py:753 +#: awx/settings/defaults.py:809 msgid "Asia Northeast (A)" msgstr "Azië (Noordoost) (A)" -#: awx/settings/defaults.py:754 +#: awx/settings/defaults.py:810 msgid "Asia Northeast (B)" msgstr "Azië (Noordoost) (B)" -#: awx/settings/defaults.py:755 +#: awx/settings/defaults.py:811 msgid "Asia Northeast (C)" msgstr "Azië (Noordoost) (C)" -#: awx/settings/defaults.py:756 +#: awx/settings/defaults.py:812 msgid "Australia Southeast (A)" msgstr "Australië Zuidoost (A)" -#: awx/settings/defaults.py:757 +#: awx/settings/defaults.py:813 msgid "Australia Southeast (B)" msgstr "Australië Zuidoost (B)" -#: awx/settings/defaults.py:758 +#: awx/settings/defaults.py:814 msgid "Australia Southeast (C)" msgstr "Australië Zuidoost (C)" -#: awx/settings/defaults.py:780 +#: awx/settings/defaults.py:836 msgid "US East" msgstr "VS Oosten" -#: awx/settings/defaults.py:781 +#: awx/settings/defaults.py:837 msgid "US East 2" msgstr "VS Oosten 2" -#: awx/settings/defaults.py:782 +#: awx/settings/defaults.py:838 msgid "US Central" msgstr "VS Midden" -#: awx/settings/defaults.py:783 +#: awx/settings/defaults.py:839 msgid "US North Central" msgstr "VS Noord Midden" -#: awx/settings/defaults.py:784 +#: awx/settings/defaults.py:840 msgid "US South Central" msgstr "VS Zuid MIdden" -#: awx/settings/defaults.py:785 +#: awx/settings/defaults.py:841 msgid "US West Central" msgstr "VS West Midden" -#: awx/settings/defaults.py:786 +#: awx/settings/defaults.py:842 msgid "US West" msgstr "VS Westen" -#: awx/settings/defaults.py:787 +#: awx/settings/defaults.py:843 msgid "US West 2" msgstr "VS Westen 2" -#: awx/settings/defaults.py:788 +#: awx/settings/defaults.py:844 msgid "Canada East" msgstr "Canada Oosten" -#: awx/settings/defaults.py:789 +#: awx/settings/defaults.py:845 msgid "Canada Central" msgstr "Canada Midden" -#: awx/settings/defaults.py:790 +#: awx/settings/defaults.py:846 msgid "Brazil South" msgstr "Brazilië Zuiden" -#: awx/settings/defaults.py:791 +#: awx/settings/defaults.py:847 msgid "Europe North" msgstr "Europa Noorden" -#: awx/settings/defaults.py:792 +#: awx/settings/defaults.py:848 msgid "Europe West" msgstr "Europa Westen" -#: awx/settings/defaults.py:793 +#: awx/settings/defaults.py:849 msgid "UK West" msgstr "VK Westen" -#: awx/settings/defaults.py:794 +#: awx/settings/defaults.py:850 msgid "UK South" msgstr "VK Zuiden" -#: awx/settings/defaults.py:795 +#: awx/settings/defaults.py:851 msgid "Asia East" msgstr "Azië Oosten" -#: awx/settings/defaults.py:796 +#: awx/settings/defaults.py:852 msgid "Asia Southeast" msgstr "Azië Zuidoosten" -#: awx/settings/defaults.py:797 +#: awx/settings/defaults.py:853 msgid "Australia East" msgstr "Australië Oosten" -#: awx/settings/defaults.py:798 +#: awx/settings/defaults.py:854 msgid "Australia Southeast" msgstr "Australië Zuidoosten" -#: awx/settings/defaults.py:799 +#: awx/settings/defaults.py:855 msgid "India West" msgstr "India Westen" -#: awx/settings/defaults.py:800 +#: awx/settings/defaults.py:856 msgid "India South" msgstr "India Zuiden" -#: awx/settings/defaults.py:801 +#: awx/settings/defaults.py:857 msgid "Japan East" msgstr "Japan Oosten" -#: awx/settings/defaults.py:802 +#: awx/settings/defaults.py:858 msgid "Japan West" msgstr "Japan Westen" -#: awx/settings/defaults.py:803 +#: awx/settings/defaults.py:859 msgid "Korea Central" msgstr "Korea Midden" -#: awx/settings/defaults.py:804 +#: awx/settings/defaults.py:860 msgid "Korea South" msgstr "Korea Zuiden" @@ -3865,7 +4808,7 @@ msgid "" "Mapping to organization admins/users from social auth accounts. This setting\n" "controls which users are placed into which Tower organizations based on their\n" "username and email address. Configuration details are available in the Ansible\n" -"Tower documentation.'" +"Tower documentation." msgstr "" "Toewijzing aan organisatiebeheerders/-gebruikers vanuit sociale " "verificatieaccounts. Deze instelling bepaalt welke gebruikers in welke " @@ -3916,11 +4859,11 @@ msgstr "" " aangemeld met sociale verificatie of een gebruikersaccount hebben met een " "overeenkomend e-mailadres, kunnen zich aanmelden." -#: awx/sso/conf.py:137 +#: awx/sso/conf.py:141 msgid "LDAP Server URI" msgstr "URI LDAP-server" -#: awx/sso/conf.py:138 +#: awx/sso/conf.py:142 msgid "" "URI to connect to LDAP server, such as \"ldap://ldap.example.com:389\" (non-" "SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be" @@ -3933,19 +4876,20 @@ msgstr "" "opgegeven door ze van elkaar te scheiden met komma's. LDAP-authenticatie is " "uitgeschakeld als deze parameter leeg is." -#: awx/sso/conf.py:142 awx/sso/conf.py:158 awx/sso/conf.py:170 -#: awx/sso/conf.py:182 awx/sso/conf.py:198 awx/sso/conf.py:218 -#: awx/sso/conf.py:240 awx/sso/conf.py:255 awx/sso/conf.py:273 -#: awx/sso/conf.py:290 awx/sso/conf.py:307 awx/sso/conf.py:323 -#: awx/sso/conf.py:337 awx/sso/conf.py:354 awx/sso/conf.py:380 +#: awx/sso/conf.py:146 awx/sso/conf.py:162 awx/sso/conf.py:174 +#: awx/sso/conf.py:186 awx/sso/conf.py:202 awx/sso/conf.py:222 +#: awx/sso/conf.py:244 awx/sso/conf.py:259 awx/sso/conf.py:277 +#: awx/sso/conf.py:294 awx/sso/conf.py:306 awx/sso/conf.py:332 +#: awx/sso/conf.py:348 awx/sso/conf.py:362 awx/sso/conf.py:380 +#: awx/sso/conf.py:406 msgid "LDAP" msgstr "LDAP" -#: awx/sso/conf.py:154 +#: awx/sso/conf.py:158 msgid "LDAP Bind DN" msgstr "DN LDAP-binding" -#: awx/sso/conf.py:155 +#: awx/sso/conf.py:159 msgid "" "DN (Distinguished Name) of user to bind for all search queries. This is the " "system user account we will use to login to query LDAP for other user " @@ -3956,29 +4900,29 @@ msgstr "" "ondervragen voor andere gebruikersinformatie. Raadpleeg de documentatie van " "Ansible Tower voor voorbeeldsyntaxis." -#: awx/sso/conf.py:168 +#: awx/sso/conf.py:172 msgid "LDAP Bind Password" msgstr "Wachtwoord voor LDAP-binding" -#: awx/sso/conf.py:169 +#: awx/sso/conf.py:173 msgid "Password used to bind LDAP user account." msgstr "Wachtwoord gebruikt om LDAP-gebruikersaccount te binden." -#: awx/sso/conf.py:180 +#: awx/sso/conf.py:184 msgid "LDAP Start TLS" msgstr "TLS voor starten LDAP" -#: awx/sso/conf.py:181 +#: awx/sso/conf.py:185 msgid "Whether to enable TLS when the LDAP connection is not using SSL." msgstr "" "Of TLS moet worden ingeschakeld wanneer de LDAP-verbinding geen SSL " "gebruikt." -#: awx/sso/conf.py:191 +#: awx/sso/conf.py:195 msgid "LDAP Connection Options" msgstr "Opties voor LDAP-verbinding" -#: awx/sso/conf.py:192 +#: awx/sso/conf.py:196 msgid "" "Additional options to set for the LDAP connection. LDAP referrals are " "disabled by default (to prevent certain LDAP queries from hanging with AD). " @@ -3992,11 +4936,11 @@ msgstr "" "https://www.python-ldap.org/doc/html/ldap.html#options voor de opties en " "waarden die u kunt instellen." -#: awx/sso/conf.py:211 +#: awx/sso/conf.py:215 msgid "LDAP User Search" msgstr "Gebruikers zoeken met LDAP" -#: awx/sso/conf.py:212 +#: awx/sso/conf.py:216 msgid "" "LDAP search query to find users. Any user that matches the given pattern " "will be able to login to Tower. The user should also be mapped into a Tower" @@ -4011,11 +4955,11 @@ msgstr "" " ondersteund, kan \"LDAPUnion\" worden gebruikt. Zie de Tower-documentatie " "voor details." -#: awx/sso/conf.py:234 +#: awx/sso/conf.py:238 msgid "LDAP User DN Template" msgstr "Sjabloon voor LDAP-gebruikers-DN" -#: awx/sso/conf.py:235 +#: awx/sso/conf.py:239 msgid "" "Alternative to user search, if user DNs are all of the same format. This " "approach is more efficient for user lookups than searching if it is usable " @@ -4028,27 +4972,27 @@ msgstr "" "organisatie. Als deze instelling een waarde heeft, wordt die gebruikt in " "plaats van AUTH_LDAP_USER_SEARCH." -#: awx/sso/conf.py:250 +#: awx/sso/conf.py:254 msgid "LDAP User Attribute Map" msgstr "Kenmerkentoewijzing van LDAP-gebruikers" -#: awx/sso/conf.py:251 +#: awx/sso/conf.py:255 msgid "" "Mapping of LDAP user schema to Tower API user attributes. The default " "setting is valid for ActiveDirectory but users with other LDAP " "configurations may need to change the values. Refer to the Ansible Tower " -"documentation for additonal details." +"documentation for additional details." msgstr "" "Toewijzing van LDAP-gebruikersschema aan gebruikerskenmerken van Tower API. " "De standaardinstelling is geldig voor ActiveDirectory, maar gebruikers met " "andere LDAP-configuraties moeten mogelijk de waarden veranderen. Raadpleeg " "de documentatie van Ansible Tower voor meer informatie." -#: awx/sso/conf.py:269 +#: awx/sso/conf.py:273 msgid "LDAP Group Search" msgstr "LDAP-groep zoeken" -#: awx/sso/conf.py:270 +#: awx/sso/conf.py:274 msgid "" "Users are mapped to organizations based on their membership in LDAP groups. " "This setting defines the LDAP search query to find groups. Unlike the user " @@ -4059,25 +5003,35 @@ msgstr "" " vinden. Anders dan het zoeken naar gebruikers biedt het zoeken naar groepen" " geen ondersteuning voor LDAPSearchUnion." -#: awx/sso/conf.py:286 +#: awx/sso/conf.py:290 msgid "LDAP Group Type" msgstr "Type LDAP-groep" -#: awx/sso/conf.py:287 +#: awx/sso/conf.py:291 msgid "" "The group type may need to be changed based on the type of the LDAP server." -" Values are listed at: http://pythonhosted.org/django-auth-ldap/groups.html" -"#types-of-groups" +" Values are listed at: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" msgstr "" "Mogelijk moet het groepstype worden gewijzigd op grond van het type LDAP-" -"server. Waarden worden vermeld op: http://pythonhosted.org/django-auth-" -"ldap/groups.html#types-of-groups" +"server. Waarden worden vermeld op: https://django-auth-" +"ldap.readthedocs.io/en/stable/groups.html#types-of-groups" -#: awx/sso/conf.py:302 +#: awx/sso/conf.py:304 +msgid "LDAP Group Type Parameters" +msgstr "Parameters LDAP-groepstype" + +#: awx/sso/conf.py:305 +msgid "Key value parameters to send the chosen group type init method." +msgstr "" +"Parameters sleutelwaarde om de gekozen init.-methode van de groepssoort te " +"verzenden." + +#: awx/sso/conf.py:327 msgid "LDAP Require Group" msgstr "LDAP-vereist-groep" -#: awx/sso/conf.py:303 +#: awx/sso/conf.py:328 msgid "" "Group DN required to login. If specified, user must be a member of this " "group to login via LDAP. If not set, everyone in LDAP that matches the user " @@ -4088,11 +5042,11 @@ msgstr "" "kan iedereen in LDAP die overeenkomt met de gebruikerszoekopdracht zich " "aanmelden via Tower. Maar één vereiste groep wordt ondersteund." -#: awx/sso/conf.py:319 +#: awx/sso/conf.py:344 msgid "LDAP Deny Group" msgstr "LDAP-weiger-groep" -#: awx/sso/conf.py:320 +#: awx/sso/conf.py:345 msgid "" "Group DN denied from login. If specified, user will not be allowed to login " "if a member of this group. Only one deny group is supported." @@ -4101,11 +5055,11 @@ msgstr "" "zich niet aanmelden als deze lid is van deze groep. Maar één weiger-groep " "wordt ondersteund." -#: awx/sso/conf.py:333 +#: awx/sso/conf.py:358 msgid "LDAP User Flags By Group" msgstr "LDAP-gebruikersvlaggen op groep" -#: awx/sso/conf.py:334 +#: awx/sso/conf.py:359 msgid "" "Retrieve users from a given group. At this time, superuser and system " "auditors are the only groups supported. Refer to the Ansible Tower " @@ -4115,11 +5069,11 @@ msgstr "" "ondersteunde groepen supergebruikers en systeemauditors. Raadpleeg de " "documentatie van Ansible Tower voor meer informatie." -#: awx/sso/conf.py:349 +#: awx/sso/conf.py:375 msgid "LDAP Organization Map" msgstr "Toewijzing LDAP-organisaties" -#: awx/sso/conf.py:350 +#: awx/sso/conf.py:376 msgid "" "Mapping between organization admins/users and LDAP groups. This controls " "which users are placed into which Tower organizations relative to their LDAP" @@ -4131,11 +5085,11 @@ msgstr "" "opzichte van hun LDAP-groepslidmaatschappen. Configuratiedetails zijn " "beschikbaar in de documentatie van Ansible Tower." -#: awx/sso/conf.py:377 +#: awx/sso/conf.py:403 msgid "LDAP Team Map" msgstr "LDAP-teamtoewijzing" -#: awx/sso/conf.py:378 +#: awx/sso/conf.py:404 msgid "" "Mapping between team members (users) and LDAP groups. Configuration details " "are available in the Ansible Tower documentation." @@ -4143,11 +5097,11 @@ msgstr "" "Toewijzing tussen teamleden (gebruikers) en LDAP-groepen. " "Configuratiedetails zijn beschikbaar in de documentatie van Ansible Tower." -#: awx/sso/conf.py:406 +#: awx/sso/conf.py:440 msgid "RADIUS Server" msgstr "RADIUS-server" -#: awx/sso/conf.py:407 +#: awx/sso/conf.py:441 msgid "" "Hostname/IP of RADIUS server. RADIUS authentication is disabled if this " "setting is empty." @@ -4155,79 +5109,79 @@ msgstr "" "Hostnaam/IP-adres van RADIUS-server. RADIUS-authenticatie wordt " "uitgeschakeld als deze instelling leeg is." -#: awx/sso/conf.py:409 awx/sso/conf.py:423 awx/sso/conf.py:435 +#: awx/sso/conf.py:443 awx/sso/conf.py:457 awx/sso/conf.py:469 #: awx/sso/models.py:14 msgid "RADIUS" msgstr "RADIUS" -#: awx/sso/conf.py:421 +#: awx/sso/conf.py:455 msgid "RADIUS Port" msgstr "RADIUS-poort" -#: awx/sso/conf.py:422 +#: awx/sso/conf.py:456 msgid "Port of RADIUS server." msgstr "Poort van RADIUS-server." -#: awx/sso/conf.py:433 +#: awx/sso/conf.py:467 msgid "RADIUS Secret" msgstr "RADIUS-geheim" -#: awx/sso/conf.py:434 +#: awx/sso/conf.py:468 msgid "Shared secret for authenticating to RADIUS server." msgstr "Gedeeld geheim voor authenticatie naar RADIUS-server." -#: awx/sso/conf.py:450 +#: awx/sso/conf.py:484 msgid "TACACS+ Server" msgstr "TACACS+ server" -#: awx/sso/conf.py:451 +#: awx/sso/conf.py:485 msgid "Hostname of TACACS+ server." msgstr "Hostnaam van TACACS+ server." -#: awx/sso/conf.py:452 awx/sso/conf.py:465 awx/sso/conf.py:478 -#: awx/sso/conf.py:491 awx/sso/conf.py:503 awx/sso/models.py:15 +#: awx/sso/conf.py:486 awx/sso/conf.py:499 awx/sso/conf.py:512 +#: awx/sso/conf.py:525 awx/sso/conf.py:537 awx/sso/models.py:15 msgid "TACACS+" msgstr "TACACS+" -#: awx/sso/conf.py:463 +#: awx/sso/conf.py:497 msgid "TACACS+ Port" msgstr "TACACS+ poort" -#: awx/sso/conf.py:464 +#: awx/sso/conf.py:498 msgid "Port number of TACACS+ server." msgstr "Poortnummer van TACACS+ server." -#: awx/sso/conf.py:476 +#: awx/sso/conf.py:510 msgid "TACACS+ Secret" msgstr "TACACS+ geheim" -#: awx/sso/conf.py:477 +#: awx/sso/conf.py:511 msgid "Shared secret for authenticating to TACACS+ server." msgstr "Gedeeld geheim voor authenticatie naar TACACS+ server." -#: awx/sso/conf.py:489 +#: awx/sso/conf.py:523 msgid "TACACS+ Auth Session Timeout" msgstr "Time-out TACACS+ authenticatiesessie" -#: awx/sso/conf.py:490 +#: awx/sso/conf.py:524 msgid "TACACS+ session timeout value in seconds, 0 disables timeout." msgstr "" "Time-outwaarde TACACS+ sessie in seconden, 0 schakelt de time-out uit." -#: awx/sso/conf.py:501 +#: awx/sso/conf.py:535 msgid "TACACS+ Authentication Protocol" msgstr "TACACS+ authenticatieprotocol" -#: awx/sso/conf.py:502 +#: awx/sso/conf.py:536 msgid "Choose the authentication protocol used by TACACS+ client." msgstr "" "Kies het authenticatieprotocol dat wordt gebruikt door de TACACS+ client." -#: awx/sso/conf.py:517 +#: awx/sso/conf.py:551 msgid "Google OAuth2 Callback URL" msgstr "Terugkoppelings-URL voor Google OAuth2" -#: awx/sso/conf.py:518 awx/sso/conf.py:611 awx/sso/conf.py:676 +#: awx/sso/conf.py:552 awx/sso/conf.py:645 awx/sso/conf.py:710 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4237,33 +5191,33 @@ msgstr "" " van uw registratieproces. Raadpleeg de documentatie van Ansible Tower voor " "meer informatie." -#: awx/sso/conf.py:521 awx/sso/conf.py:533 awx/sso/conf.py:545 -#: awx/sso/conf.py:558 awx/sso/conf.py:572 awx/sso/conf.py:584 -#: awx/sso/conf.py:596 +#: awx/sso/conf.py:555 awx/sso/conf.py:567 awx/sso/conf.py:579 +#: awx/sso/conf.py:592 awx/sso/conf.py:606 awx/sso/conf.py:618 +#: awx/sso/conf.py:630 msgid "Google OAuth2" msgstr "Google OAuth2" -#: awx/sso/conf.py:531 +#: awx/sso/conf.py:565 msgid "Google OAuth2 Key" msgstr "Google OAuth2-sleutel" -#: awx/sso/conf.py:532 +#: awx/sso/conf.py:566 msgid "The OAuth2 key from your web application." msgstr "De OAuth2-sleutel van uw webtoepassing." -#: awx/sso/conf.py:543 +#: awx/sso/conf.py:577 msgid "Google OAuth2 Secret" msgstr "Google OAuth2-geheim" -#: awx/sso/conf.py:544 +#: awx/sso/conf.py:578 msgid "The OAuth2 secret from your web application." msgstr "Het OAuth2-geheim van uw webtoepassing." -#: awx/sso/conf.py:555 +#: awx/sso/conf.py:589 msgid "Google OAuth2 Whitelisted Domains" msgstr "In whitelist opgenomen Google OAuth2-domeinen" -#: awx/sso/conf.py:556 +#: awx/sso/conf.py:590 msgid "" "Update this setting to restrict the domains who are allowed to login using " "Google OAuth2." @@ -4271,11 +5225,11 @@ msgstr "" "Werk deze instelling bij om te beperken welke domeinen zich mogen aanmelden " "met Google OAuth2." -#: awx/sso/conf.py:567 +#: awx/sso/conf.py:601 msgid "Google OAuth2 Extra Arguments" msgstr "Extra argumenten Google OAuth2" -#: awx/sso/conf.py:568 +#: awx/sso/conf.py:602 msgid "" "Extra arguments for Google OAuth2 login. You can restrict it to only allow a" " single domain to authenticate, even if the user is logged in with multple " @@ -4286,81 +5240,81 @@ msgstr "" "domein, zelfs als de gebruiker aangemeld is met meerdere Google-accounts. " "Raadpleeg de documentatie van Ansible Tower voor meer informatie." -#: awx/sso/conf.py:582 +#: awx/sso/conf.py:616 msgid "Google OAuth2 Organization Map" msgstr "Organisatietoewijzing Google OAuth2" -#: awx/sso/conf.py:594 +#: awx/sso/conf.py:628 msgid "Google OAuth2 Team Map" msgstr "Teamtoewijzing Google OAuth2" -#: awx/sso/conf.py:610 +#: awx/sso/conf.py:644 msgid "GitHub OAuth2 Callback URL" msgstr "Terugkoppelings-URL GitHub OAuth2" -#: awx/sso/conf.py:614 awx/sso/conf.py:626 awx/sso/conf.py:637 -#: awx/sso/conf.py:649 awx/sso/conf.py:661 +#: awx/sso/conf.py:648 awx/sso/conf.py:660 awx/sso/conf.py:671 +#: awx/sso/conf.py:683 awx/sso/conf.py:695 msgid "GitHub OAuth2" msgstr "GitHub OAuth2" -#: awx/sso/conf.py:624 +#: awx/sso/conf.py:658 msgid "GitHub OAuth2 Key" msgstr "GitHub OAuth2-sleutel" -#: awx/sso/conf.py:625 +#: awx/sso/conf.py:659 msgid "The OAuth2 key (Client ID) from your GitHub developer application." msgstr "De OAuth2-sleutel (Client-id) van uw GitHub-ontwikkelaarstoepassing." -#: awx/sso/conf.py:635 +#: awx/sso/conf.py:669 msgid "GitHub OAuth2 Secret" msgstr "GitHub OAuth2-geheim" -#: awx/sso/conf.py:636 +#: awx/sso/conf.py:670 msgid "" "The OAuth2 secret (Client Secret) from your GitHub developer application." msgstr "" "Het OAuth2-geheim (Client-geheim) van uw GitHub-ontwikkelaarstoepassing." -#: awx/sso/conf.py:647 +#: awx/sso/conf.py:681 msgid "GitHub OAuth2 Organization Map" msgstr "GitHub OAuth2-organisatietoewijzing" -#: awx/sso/conf.py:659 +#: awx/sso/conf.py:693 msgid "GitHub OAuth2 Team Map" msgstr "GitHub OAuth2-teamtoewijzing" -#: awx/sso/conf.py:675 +#: awx/sso/conf.py:709 msgid "GitHub Organization OAuth2 Callback URL" msgstr "OAuth2-terugkoppelings-URL GitHub-organisatie" -#: awx/sso/conf.py:679 awx/sso/conf.py:691 awx/sso/conf.py:702 -#: awx/sso/conf.py:715 awx/sso/conf.py:726 awx/sso/conf.py:738 +#: awx/sso/conf.py:713 awx/sso/conf.py:725 awx/sso/conf.py:736 +#: awx/sso/conf.py:749 awx/sso/conf.py:760 awx/sso/conf.py:772 msgid "GitHub Organization OAuth2" msgstr "Organization OAuth2 van GitHub-organisatie" -#: awx/sso/conf.py:689 +#: awx/sso/conf.py:723 msgid "GitHub Organization OAuth2 Key" msgstr " OAuth2-sleutel van GitHub-organisatie" -#: awx/sso/conf.py:690 awx/sso/conf.py:768 +#: awx/sso/conf.py:724 awx/sso/conf.py:802 msgid "The OAuth2 key (Client ID) from your GitHub organization application." msgstr "De OAuth2-sleutel (Client-id) van uw GitHub-organisatietoepassing." -#: awx/sso/conf.py:700 +#: awx/sso/conf.py:734 msgid "GitHub Organization OAuth2 Secret" msgstr " OAuth2-geheim van GitHub-organisatie" -#: awx/sso/conf.py:701 awx/sso/conf.py:779 +#: awx/sso/conf.py:735 awx/sso/conf.py:813 msgid "" "The OAuth2 secret (Client Secret) from your GitHub organization application." msgstr "" "Het OAuth2-geheim (Client-geheim) van uw GitHub-organisatietoepassing." -#: awx/sso/conf.py:712 +#: awx/sso/conf.py:746 msgid "GitHub Organization Name" msgstr "Naam van GitHub-organisatie" -#: awx/sso/conf.py:713 +#: awx/sso/conf.py:747 msgid "" "The name of your GitHub organization, as used in your organization's URL: " "https://github.com//." @@ -4368,19 +5322,19 @@ msgstr "" "De naam van uw GitHub-organisatie zoals gebruikt in de URL van uw " "organisatie: https://github.com//." -#: awx/sso/conf.py:724 +#: awx/sso/conf.py:758 msgid "GitHub Organization OAuth2 Organization Map" msgstr "OAuth2-organisatietoewijzing van GitHub-organisatie" -#: awx/sso/conf.py:736 +#: awx/sso/conf.py:770 msgid "GitHub Organization OAuth2 Team Map" msgstr "OAuth2-teamtoewijzing van GitHub-organisatie" -#: awx/sso/conf.py:752 +#: awx/sso/conf.py:786 msgid "GitHub Team OAuth2 Callback URL" msgstr "OAuth2-terugkoppelings-URL GitHub-team" -#: awx/sso/conf.py:753 +#: awx/sso/conf.py:787 msgid "" "Create an organization-owned application at " "https://github.com/organizations//settings/applications and obtain " @@ -4392,24 +5346,24 @@ msgstr "" " een OAuth2-sleutel (Client-id) en -geheim (Client-geheim). Lever deze URL " "als de terugkoppelings-URL voor uw toepassing." -#: awx/sso/conf.py:757 awx/sso/conf.py:769 awx/sso/conf.py:780 -#: awx/sso/conf.py:793 awx/sso/conf.py:804 awx/sso/conf.py:816 +#: awx/sso/conf.py:791 awx/sso/conf.py:803 awx/sso/conf.py:814 +#: awx/sso/conf.py:827 awx/sso/conf.py:838 awx/sso/conf.py:850 msgid "GitHub Team OAuth2" msgstr "OAuth2 van GitHub-team" -#: awx/sso/conf.py:767 +#: awx/sso/conf.py:801 msgid "GitHub Team OAuth2 Key" msgstr "OAuth2-sleutel GitHub-team" -#: awx/sso/conf.py:778 +#: awx/sso/conf.py:812 msgid "GitHub Team OAuth2 Secret" msgstr "OAuth2-geheim GitHub-team" -#: awx/sso/conf.py:790 +#: awx/sso/conf.py:824 msgid "GitHub Team ID" msgstr "Id GitHub-team" -#: awx/sso/conf.py:791 +#: awx/sso/conf.py:825 msgid "" "Find the numeric team ID using the Github API: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." @@ -4417,19 +5371,19 @@ msgstr "" "Zoek de numerieke team-id op met de Github API: http://fabian-" "kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/." -#: awx/sso/conf.py:802 +#: awx/sso/conf.py:836 msgid "GitHub Team OAuth2 Organization Map" msgstr "OAuth2-organisatietoewijzing van GitHub-team" -#: awx/sso/conf.py:814 +#: awx/sso/conf.py:848 msgid "GitHub Team OAuth2 Team Map" msgstr "OAuth2-teamtoewijzing van GitHub-organisatie" -#: awx/sso/conf.py:830 +#: awx/sso/conf.py:864 msgid "Azure AD OAuth2 Callback URL" msgstr "Terugkoppelings-URL voor Azure AD OAuth2" -#: awx/sso/conf.py:831 +#: awx/sso/conf.py:865 msgid "" "Provide this URL as the callback URL for your application as part of your " "registration process. Refer to the Ansible Tower documentation for more " @@ -4439,40 +5393,40 @@ msgstr "" " van uw registratieproces. Raadpleeg de documentatie van Ansible Tower voor " "meer informatie." -#: awx/sso/conf.py:834 awx/sso/conf.py:846 awx/sso/conf.py:857 -#: awx/sso/conf.py:869 awx/sso/conf.py:881 +#: awx/sso/conf.py:868 awx/sso/conf.py:880 awx/sso/conf.py:891 +#: awx/sso/conf.py:903 awx/sso/conf.py:915 msgid "Azure AD OAuth2" msgstr "Azure AD OAuth2" -#: awx/sso/conf.py:844 +#: awx/sso/conf.py:878 msgid "Azure AD OAuth2 Key" msgstr "Azure AD OAuth2-sleutel" -#: awx/sso/conf.py:845 +#: awx/sso/conf.py:879 msgid "The OAuth2 key (Client ID) from your Azure AD application." msgstr "De OAuth2-sleutel (Client-id) van uw Azure AD-toepassing." -#: awx/sso/conf.py:855 +#: awx/sso/conf.py:889 msgid "Azure AD OAuth2 Secret" msgstr "Azure AD OAuth2-geheim" -#: awx/sso/conf.py:856 +#: awx/sso/conf.py:890 msgid "The OAuth2 secret (Client Secret) from your Azure AD application." msgstr "Het OAuth2-geheim (Client-geheim) van uw Azure AD-toepassing." -#: awx/sso/conf.py:867 +#: awx/sso/conf.py:901 msgid "Azure AD OAuth2 Organization Map" msgstr "Azure AD OAuth2-organisatietoewijzing" -#: awx/sso/conf.py:879 +#: awx/sso/conf.py:913 msgid "Azure AD OAuth2 Team Map" msgstr "Azure AD OAuth2-teamtoewijzing" -#: awx/sso/conf.py:904 +#: awx/sso/conf.py:938 msgid "SAML Assertion Consumer Service (ACS) URL" msgstr "URL SAML Assertion Consumer Service (ACS)" -#: awx/sso/conf.py:905 +#: awx/sso/conf.py:939 msgid "" "Register Tower as a service provider (SP) with each identity provider (IdP) " "you have configured. Provide your SP Entity ID and this ACS URL for your " @@ -4482,18 +5436,20 @@ msgstr "" " die u hebt geconfigureerd. Lever uw SP-entiteit-id en deze ACS URL voor uw " "toepassing." -#: awx/sso/conf.py:908 awx/sso/conf.py:922 awx/sso/conf.py:936 -#: awx/sso/conf.py:951 awx/sso/conf.py:965 awx/sso/conf.py:978 -#: awx/sso/conf.py:999 awx/sso/conf.py:1017 awx/sso/conf.py:1036 -#: awx/sso/conf.py:1070 awx/sso/conf.py:1083 awx/sso/models.py:16 +#: awx/sso/conf.py:942 awx/sso/conf.py:956 awx/sso/conf.py:970 +#: awx/sso/conf.py:985 awx/sso/conf.py:999 awx/sso/conf.py:1012 +#: awx/sso/conf.py:1033 awx/sso/conf.py:1051 awx/sso/conf.py:1070 +#: awx/sso/conf.py:1106 awx/sso/conf.py:1138 awx/sso/conf.py:1152 +#: awx/sso/conf.py:1169 awx/sso/conf.py:1182 awx/sso/conf.py:1195 +#: awx/sso/conf.py:1211 awx/sso/models.py:16 msgid "SAML" msgstr "SAML" -#: awx/sso/conf.py:919 +#: awx/sso/conf.py:953 msgid "SAML Service Provider Metadata URL" msgstr "URL voor metagegevens van SAML-serviceprovider" -#: awx/sso/conf.py:920 +#: awx/sso/conf.py:954 msgid "" "If your identity provider (IdP) allows uploading an XML metadata file, you " "can download one from this URL." @@ -4501,11 +5457,11 @@ msgstr "" "Als uw identiteitsprovider (IdP) toestaat een XML-gegevensbestand te " "uploaden, kunt u er een uploaden vanaf deze URL." -#: awx/sso/conf.py:932 +#: awx/sso/conf.py:966 msgid "SAML Service Provider Entity ID" msgstr "Entiteit-id van SAML-serviceprovider" -#: awx/sso/conf.py:933 +#: awx/sso/conf.py:967 msgid "" "The application-defined unique identifier used as the audience of the SAML " "service provider (SP) configuration. This is usually the URL for Tower." @@ -4513,11 +5469,11 @@ msgstr "" "De toepassingsgedefinieerde unieke id gebruikt als doelgroep van de SAML-" "serviceprovider (SP)-configuratie. Dit is gewoonlijk de URL voor Tower." -#: awx/sso/conf.py:948 +#: awx/sso/conf.py:982 msgid "SAML Service Provider Public Certificate" msgstr "Openbaar certificaat SAML-serviceprovider" -#: awx/sso/conf.py:949 +#: awx/sso/conf.py:983 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " certificate content here." @@ -4525,11 +5481,11 @@ msgstr "" "Maak een sleutelpaar voor Tower om dit te gebruiken als serviceprovider (SP)" " en neem de certificaatinhoud hier op." -#: awx/sso/conf.py:962 +#: awx/sso/conf.py:996 msgid "SAML Service Provider Private Key" msgstr "Privésleutel SAML-serviceprovider" -#: awx/sso/conf.py:963 +#: awx/sso/conf.py:997 msgid "" "Create a keypair for Tower to use as a service provider (SP) and include the" " private key content here." @@ -4537,11 +5493,11 @@ msgstr "" "Maak een sleutelpaar voor Tower om dit te gebruiken als serviceprovider (SP)" " en neem de inhoud van de privésleutel hier op." -#: awx/sso/conf.py:975 +#: awx/sso/conf.py:1009 msgid "SAML Service Provider Organization Info" msgstr "Organisatie-informatie SAML-serviceprovider" -#: awx/sso/conf.py:976 +#: awx/sso/conf.py:1010 msgid "" "Provide the URL, display name, and the name of your app. Refer to the " "Ansible Tower documentation for example syntax." @@ -4549,11 +5505,11 @@ msgstr "" "Geef de URL, weergavenaam en de naam van uw toepassing op. Raadpleeg de " "documentatie van Ansible Tower voor voorbeeldsyntaxis." -#: awx/sso/conf.py:995 +#: awx/sso/conf.py:1029 msgid "SAML Service Provider Technical Contact" msgstr "Technisch contactpersoon SAML-serviceprovider" -#: awx/sso/conf.py:996 +#: awx/sso/conf.py:1030 msgid "" "Provide the name and email address of the technical contact for your service" " provider. Refer to the Ansible Tower documentation for example syntax." @@ -4562,11 +5518,11 @@ msgstr "" "serviceprovider op. Raadpleeg de documentatie van Ansible Tower voor " "voorbeeldsyntaxis." -#: awx/sso/conf.py:1013 +#: awx/sso/conf.py:1047 msgid "SAML Service Provider Support Contact" msgstr "Ondersteuningscontactpersoon SAML-serviceprovider" -#: awx/sso/conf.py:1014 +#: awx/sso/conf.py:1048 msgid "" "Provide the name and email address of the support contact for your service " "provider. Refer to the Ansible Tower documentation for example syntax." @@ -4575,11 +5531,11 @@ msgstr "" "serviceprovider op. Raadpleeg de documentatie van Ansible Tower voor " "voorbeeldsyntaxis." -#: awx/sso/conf.py:1030 +#: awx/sso/conf.py:1064 msgid "SAML Enabled Identity Providers" msgstr "Id-providers met SAML-mogelijkheden" -#: awx/sso/conf.py:1031 +#: awx/sso/conf.py:1065 msgid "" "Configure the Entity ID, SSO URL and certificate for each identity provider " "(IdP) in use. Multiple SAML IdPs are supported. Some IdPs may provide user " @@ -4594,44 +5550,99 @@ msgstr "" "voor elke IdP. Raadpleeg de documentatie van Ansible voor meer informatie en" " syntaxis." -#: awx/sso/conf.py:1068 +#: awx/sso/conf.py:1102 +msgid "SAML Security Config" +msgstr "SAML-beveiligingsconfiguratie" + +#: awx/sso/conf.py:1103 +msgid "" +"A dict of key value pairs that are passed to the underlying python-saml " +"security setting https://github.com/onelogin/python-saml#settings" +msgstr "" +"Een dict van sleutelwaardeparen die doorgegeven worden aan de onderliggende " +"python-saml-beveiligingsinstelling https://github.com/onelogin/python-" +"saml#settings" + +#: awx/sso/conf.py:1135 +msgid "SAML Service Provider extra configuration data" +msgstr "SAML-serviceprovider extra configuratiegegevens" + +#: awx/sso/conf.py:1136 +msgid "" +"A dict of key value pairs to be passed to the underlying python-saml Service" +" Provider configuration setting." +msgstr "" +"Een dict van sleutelwaardeparen die doorgegeven moeten worden aan de " +"onderliggende configuratie-instelling van de python-saml-serviceprovider." + +#: awx/sso/conf.py:1149 +msgid "SAML IDP to extra_data attribute mapping" +msgstr "SAML IDP voor extra_data kenmerkentoewijzing" + +#: awx/sso/conf.py:1150 +msgid "" +"A list of tuples that maps IDP attributes to extra_attributes. Each " +"attribute will be a list of values, even if only 1 value." +msgstr "" +"Een lijst van tuples die IDP-kenmerken toewijst aan extra_attributes. Ieder " +"kenmerk is een lijst van variabelen, zelfs als de lijst maar één variabele " +"bevat." + +#: awx/sso/conf.py:1167 msgid "SAML Organization Map" msgstr "SAML-organisatietoewijzing" -#: awx/sso/conf.py:1081 +#: awx/sso/conf.py:1180 msgid "SAML Team Map" msgstr "SAML-teamtoewijzing" -#: awx/sso/fields.py:123 +#: awx/sso/conf.py:1193 +msgid "SAML Organization Attribute Mapping" +msgstr "Kenmerktoewijzing SAML-organisatie" + +#: awx/sso/conf.py:1194 +msgid "Used to translate user organization membership into Tower." +msgstr "" +"Gebruikt om organisatielidmaatschap van gebruikers om te zetten in Tower." + +#: awx/sso/conf.py:1209 +msgid "SAML Team Attribute Mapping" +msgstr "Kenmerktoewijzing SAML-team" + +#: awx/sso/conf.py:1210 +msgid "Used to translate user team membership into Tower." +msgstr "Gebruikt om teamlidmaatschap van gebruikers om te zetten in Tower." + +#: awx/sso/fields.py:183 #, python-brace-format msgid "Invalid connection option(s): {invalid_options}." msgstr "Ongeldige verbindingsoptie(s): {invalid_options}." -#: awx/sso/fields.py:194 +#: awx/sso/fields.py:266 msgid "Base" msgstr "Basis" -#: awx/sso/fields.py:195 +#: awx/sso/fields.py:267 msgid "One Level" msgstr "Eén niveau" -#: awx/sso/fields.py:196 +#: awx/sso/fields.py:268 msgid "Subtree" msgstr "Substructuur" -#: awx/sso/fields.py:214 +#: awx/sso/fields.py:286 #, python-brace-format msgid "Expected a list of three items but got {length} instead." msgstr "Verwachtte een lijst met drie items, maar kreeg er {length}." -#: awx/sso/fields.py:215 +#: awx/sso/fields.py:287 #, python-brace-format msgid "Expected an instance of LDAPSearch but got {input_type} instead." msgstr "" "Verwachtte een instantie van LDAPSearch, maar kreeg in plaats daarvan " "{input_type}." -#: awx/sso/fields.py:251 +#: awx/sso/fields.py:323 #, python-brace-format msgid "" "Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} " @@ -4640,88 +5651,79 @@ msgstr "" "Verwachtte een instantie van LDAPSearch of LDAPSearchUnion, maar kreeg in " "plaats daarvan {input_type}." -#: awx/sso/fields.py:289 +#: awx/sso/fields.py:361 #, python-brace-format msgid "Invalid user attribute(s): {invalid_attrs}." msgstr "Ongeldig(e) gebruikerskenmerk(en): {invalid_attrs}." -#: awx/sso/fields.py:306 +#: awx/sso/fields.py:378 #, python-brace-format msgid "Expected an instance of LDAPGroupType but got {input_type} instead." msgstr "" "Verwachtte een instantie van LDAPGroupType, maar kreeg in plaats daarvan " "{input_type}." -#: awx/sso/fields.py:334 -#, python-brace-format -msgid "Invalid user flag: \"{invalid_flag}\"." -msgstr "Ongeldige gebruikersvlag: \"{invalid_flag}\"." - -#: awx/sso/fields.py:350 awx/sso/fields.py:517 -#, python-brace-format -msgid "" -"Expected None, True, False, a string or list of strings but got {input_type}" -" instead." -msgstr "" -"Verwachtte None, True, False, een tekenreeks of een lijst met tekenreeksen, " -"maar kreeg in plaats daarvan {input_type}." - -#: awx/sso/fields.py:386 -#, python-brace-format -msgid "Missing key(s): {missing_keys}." -msgstr "Ontbrekende sleutel(s): {missing_keys}." - -#: awx/sso/fields.py:387 +#: awx/sso/fields.py:418 awx/sso/fields.py:465 #, python-brace-format msgid "Invalid key(s): {invalid_keys}." msgstr "Ongeldige sleutel(s): {invalid_keys}." -#: awx/sso/fields.py:436 awx/sso/fields.py:553 +#: awx/sso/fields.py:443 +#, python-brace-format +msgid "Invalid user flag: \"{invalid_flag}\"." +msgstr "Ongeldige gebruikersvlag: \"{invalid_flag}\"." + +#: awx/sso/fields.py:464 +#, python-brace-format +msgid "Missing key(s): {missing_keys}." +msgstr "Ontbrekende sleutel(s): {missing_keys}." + +#: awx/sso/fields.py:514 awx/sso/fields.py:631 #, python-brace-format msgid "Invalid key(s) for organization map: {invalid_keys}." msgstr "Ongeldige sleutel(s) voor organisatietoewijzing: {invalid_keys}." -#: awx/sso/fields.py:454 +#: awx/sso/fields.py:532 #, python-brace-format msgid "Missing required key for team map: {invalid_keys}." msgstr "Ontbrekende vereiste sleutel voor teamtoewijzing: {invalid_keys}." -#: awx/sso/fields.py:455 awx/sso/fields.py:572 +#: awx/sso/fields.py:533 awx/sso/fields.py:650 #, python-brace-format msgid "Invalid key(s) for team map: {invalid_keys}." msgstr "Ongeldige sleutel(s) voor teamtoewijzing: {invalid_keys}." -#: awx/sso/fields.py:571 +#: awx/sso/fields.py:649 #, python-brace-format msgid "Missing required key for team map: {missing_keys}." msgstr "Ontbrekende vereiste sleutel voor teamtoewijzing: {missing_keys}." -#: awx/sso/fields.py:589 +#: awx/sso/fields.py:667 #, python-brace-format msgid "Missing required key(s) for org info record: {missing_keys}." msgstr "Ontbrekende vereiste sleutel(s) voor org info record: {missing_keys}." -#: awx/sso/fields.py:602 +#: awx/sso/fields.py:680 #, python-brace-format msgid "Invalid language code(s) for org info: {invalid_lang_codes}." msgstr "Ongeldige taalcode(s) voor org info: {invalid_lang_codes}." -#: awx/sso/fields.py:621 +#: awx/sso/fields.py:699 #, python-brace-format msgid "Missing required key(s) for contact: {missing_keys}." msgstr "Ontbrekende vereiste sleutel(s) voor contactpersoon: {missing_keys}." -#: awx/sso/fields.py:633 +#: awx/sso/fields.py:711 #, python-brace-format msgid "Missing required key(s) for IdP: {missing_keys}." msgstr "Ontbrekende vereiste sleutel(s) voor IdP: {missing_keys}." -#: awx/sso/pipeline.py:24 +#: awx/sso/pipeline.py:31 #, python-brace-format msgid "An account cannot be found for {0}" msgstr "Een account kan niet worden gevonden voor {0}" -#: awx/sso/pipeline.py:30 +#: awx/sso/pipeline.py:37 msgid "Your account is inactive" msgstr "Uw account is inactief" @@ -4748,70 +5750,48 @@ msgstr "TACACS+ geheim staat geen niet-ascii-tekens toe" msgid "AWX" msgstr "AWX" -#: awx/templates/rest_framework/api.html:39 +#: awx/templates/rest_framework/api.html:42 msgid "Ansible Tower API Guide" msgstr "Gebruikersgids Ansible Tower API" -#: awx/templates/rest_framework/api.html:40 +#: awx/templates/rest_framework/api.html:43 msgid "Back to Ansible Tower" msgstr "Terug naar Ansible Tower" -#: awx/templates/rest_framework/api.html:41 +#: awx/templates/rest_framework/api.html:44 msgid "Resize" msgstr "Groter/kleiner maken" +#: awx/templates/rest_framework/base.html:37 +msgid "navbar" +msgstr "navbar" + +#: awx/templates/rest_framework/base.html:75 +msgid "content" +msgstr "inhoud" + #: awx/templates/rest_framework/base.html:78 -#: awx/templates/rest_framework/base.html:92 -#, python-format -msgid "Make a GET request on the %(name)s resource" -msgstr "Maak een OPHAAL-aanvraag voor de resource van %(name)s" +msgid "request form" +msgstr "aanvraagformulier" -#: awx/templates/rest_framework/base.html:80 -msgid "Specify a format for the GET request" -msgstr "Geef een indeling op voor de OPHAAL-aanvraag" - -#: awx/templates/rest_framework/base.html:86 -#, python-format -msgid "" -"Make a GET request on the %(name)s resource with the format set to " -"`%(format)s`" -msgstr "" -"Maak een OPHAAL-aanvraag voor de resource van %(name)s met de indeling " -"ingesteld op `%(format)s`" - -#: awx/templates/rest_framework/base.html:100 -#, python-format -msgid "Make an OPTIONS request on the %(name)s resource" -msgstr "Maak een OPTIES-aanvraag voor de resource van %(name)s" - -#: awx/templates/rest_framework/base.html:106 -#, python-format -msgid "Make a DELETE request on the %(name)s resource" -msgstr "Maak een VERWIJDER-aanvraag voor de resource van %(name)s" - -#: awx/templates/rest_framework/base.html:113 +#: awx/templates/rest_framework/base.html:134 msgid "Filters" msgstr "Filters" -#: awx/templates/rest_framework/base.html:172 -#: awx/templates/rest_framework/base.html:186 -#, python-format -msgid "Make a POST request on the %(name)s resource" -msgstr "Maak een POST-aanvraag voor de resource van %(name)s" +#: awx/templates/rest_framework/base.html:139 +msgid "main content" +msgstr "hoofdinhoud" -#: awx/templates/rest_framework/base.html:216 -#: awx/templates/rest_framework/base.html:230 -#, python-format -msgid "Make a PUT request on the %(name)s resource" -msgstr "Maak een PLAATS-aanvraag voor de resource van %(name)s" +#: awx/templates/rest_framework/base.html:155 +msgid "request info" +msgstr "aanvraaginformatie" -#: awx/templates/rest_framework/base.html:233 -#, python-format -msgid "Make a PATCH request on the %(name)s resource" -msgstr "Maak een PATCH-aanvraag voor de resource van %(name)s" +#: awx/templates/rest_framework/base.html:159 +msgid "response info" +msgstr "reactie-informatie" #: awx/ui/apps.py:9 awx/ui/conf.py:22 awx/ui/conf.py:36 awx/ui/conf.py:51 -#: awx/ui/conf.py:63 +#: awx/ui/conf.py:63 awx/ui/conf.py:73 msgid "UI" msgstr "Gebruikersinterface" @@ -4867,14 +5847,28 @@ msgstr "" "ondersteund." #: awx/ui/conf.py:60 -msgid "Max Job Events Retreived by UI" -msgstr "Het maximumaantal taakgebeurtenissen dat is opgehaald door de UI" +msgid "Max Job Events Retrieved by UI" +msgstr "Maximumaantal taakgebeurtenissen dat de UI ophaalt" #: awx/ui/conf.py:61 msgid "" -"Maximum number of job events for the UI to retreive within a single request." +"Maximum number of job events for the UI to retrieve within a single request." msgstr "" -"Het maximumaantal taakgebeurtenissen dat de UI kan ophalen met één verzoek." +"Maximumaantal taakgebeurtenissen dat de UI op kan halen met een enkele " +"aanvraag." + +#: awx/ui/conf.py:70 +msgid "Enable Live Updates in the UI" +msgstr "Live-updates in de UI inschakelen" + +#: awx/ui/conf.py:71 +msgid "" +"If disabled, the page will not refresh when events are received. Reloading " +"the page will be required to get the latest details." +msgstr "" +"Indien dit uitgeschakeld is, wordt de pagina niet ververst wanneer " +"gebeurtenissen binnenkomen. De pagina moet opnieuw geladen worden om de " +"nieuwste informatie op te halen." #: awx/ui/fields.py:29 msgid "" @@ -4887,96 +5881,3 @@ msgstr "" #: awx/ui/fields.py:30 msgid "Invalid base64-encoded data in data URL." msgstr "Ongeldige base64-versleutelde gegevens in gegevens-URL." - -#: awx/ui/templates/ui/index.html:31 -msgid "" -"Your session will expire in 60 seconds, would you like to " -"continue?" -msgstr "" -"Uw sessie verloopt over 60 seconden. Wilt u doorgaan?" - -#: awx/ui/templates/ui/index.html:46 -msgid "CANCEL" -msgstr "ANNULEREN" - -#: awx/ui/templates/ui/index.html:98 -msgid "Set how many days of data should be retained." -msgstr "Stel in hoeveel dagen aan gegevens er moet worden bewaard." - -#: awx/ui/templates/ui/index.html:104 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Geef een geheel getal opdat niet negatief " -"is dat kleiner is dan " -"9999." - -#: awx/ui/templates/ui/index.html:109 -msgid "" -"For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.\n" -"
\n" -"
CAUTION: Setting both numerical variables to \"0\" will delete all facts.\n" -"
\n" -"
" -msgstr "" -"Voor verzamelde feiten die ouder zijn dan de opgegeven periode, slaat u één feitenscan (momentopname) per tijdvenster (frequentie) op. Bijvoorbeeld feiten ouder dan 30 dagen worden verwijderd, terwijl één wekelijkse feitenscan wordt bewaard.\n" -"
\n" -"
LET OP: als u beide numerieke variabelen instelt op \"0\", worden alle feiten verwijderd.\n" -"
\n" -"
" - -#: awx/ui/templates/ui/index.html:118 -msgid "Select a time period after which to remove old facts" -msgstr "Selecteer een tijdsperiode waarna oude feiten worden verwijderd." - -#: awx/ui/templates/ui/index.html:132 -msgid "" -"Please enter an integer that is not " -"negative that is lower than " -"9999." -msgstr "" -"Geef een geheel getal op dat niet negatief " -"is dat kleiner is dan " -"9999." - -#: awx/ui/templates/ui/index.html:137 -msgid "Select a frequency for snapshot retention" -msgstr "Selecteer een frequentie voor het behoud van momentopnames" - -#: awx/ui/templates/ui/index.html:151 -msgid "" -"Please enter an integer that is not" -" negative that is " -"lower than 9999." -msgstr "" -"Geef een geheel getal op dat niet " -"negatief is dat kleiner" -" is dan 9999." - -#: awx/ui/templates/ui/index.html:157 -msgid "working..." -msgstr "bezig..." diff --git a/awx/main/access.py b/awx/main/access.py index 5bf2076898..c053058f04 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -5,6 +5,8 @@ import os import sys import logging +import six +from functools import reduce # Django from django.conf import settings @@ -96,8 +98,6 @@ def check_user_access(user, model_class, action, *args, **kwargs): Return True if user can perform action against model_class with the provided parameters. ''' - if 'write' not in getattr(user, 'oauth_scopes', ['write']) and action != 'read': - return False access_class = access_registry[model_class] access_instance = access_class(user) access_method = getattr(access_instance, 'can_%s' % action) @@ -217,6 +217,15 @@ class BaseAccess(object): def can_copy(self, obj): return self.can_add({'reference_obj': obj}) + def can_copy_related(self, obj): + ''' + can_copy_related() should only be used to check if the user have access to related + many to many credentials in when copying the object. It does not check if the user + has permission for any other related objects. Therefore, when checking if the user + can copy an object, it should always be used in conjunction with can_add() + ''' + return True + def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): if skip_sub_obj_read_check: @@ -391,21 +400,24 @@ class BaseAccess(object): return user_capabilities def get_method_capability(self, method, obj, parent_obj): - if method in ['change']: # 3 args - return self.can_change(obj, {}) - elif method in ['delete', 'run_ad_hoc_commands', 'copy']: - access_method = getattr(self, "can_%s" % method) - return access_method(obj) - elif method in ['start']: - return self.can_start(obj, validate_license=False) - elif method in ['attach', 'unattach']: # parent/sub-object call - access_method = getattr(self, "can_%s" % method) - if type(parent_obj) == Team: - relationship = 'parents' - parent_obj = parent_obj.member_role - else: - relationship = 'members' - return access_method(obj, parent_obj, relationship, skip_sub_obj_read_check=True, data={}) + try: + if method in ['change']: # 3 args + return self.can_change(obj, {}) + elif method in ['delete', 'run_ad_hoc_commands', 'copy']: + access_method = getattr(self, "can_%s" % method) + return access_method(obj) + elif method in ['start']: + return self.can_start(obj, validate_license=False) + elif method in ['attach', 'unattach']: # parent/sub-object call + access_method = getattr(self, "can_%s" % method) + if type(parent_obj) == Team: + relationship = 'parents' + parent_obj = parent_obj.member_role + else: + relationship = 'members' + return access_method(obj, parent_obj, relationship, skip_sub_obj_read_check=True, data={}) + except (ParseError, ObjectDoesNotExist): + return False return False @@ -516,29 +528,28 @@ class UserAccess(BaseAccess): return False return bool(self.user == obj or self.can_admin(obj, data)) - def user_membership_roles(self, u): - return Role.objects.filter( - content_type=ContentType.objects.get_for_model(Organization), - role_field__in=[ - 'admin_role', 'member_role', - 'execute_role', 'project_admin_role', 'inventory_admin_role', - 'credential_admin_role', 'workflow_admin_role', - 'notification_admin_role' - ], - members=u - ) + @staticmethod + def user_organizations(u): + ''' + Returns all organizations that count `u` as a member + ''' + return Organization.accessible_objects(u, 'member_role') def is_all_org_admin(self, u): - return not self.user_membership_roles(u).exclude( - ancestors__in=self.user.roles.filter(role_field='admin_role') + ''' + returns True if `u` is member of any organization that is + not also an organization that `self.user` admins + ''' + return not self.user_organizations(u).exclude( + pk__in=Organization.accessible_pk_qs(self.user, 'admin_role') ).exists() def user_is_orphaned(self, u): - return not self.user_membership_roles(u).exists() + return not self.user_organizations(u).exists() @check_superuser - def can_admin(self, obj, data, allow_orphans=False): - if not settings.MANAGE_ORGANIZATION_AUTH: + def can_admin(self, obj, data, allow_orphans=False, check_setting=True): + if check_setting and (not settings.MANAGE_ORGANIZATION_AUTH): return False if obj.is_superuser or obj.is_system_auditor: # must be superuser to admin users with system roles @@ -742,12 +753,13 @@ class InventoryAccess(BaseAccess): # If no data is specified, just checking for generic add permission? if not data: return Organization.accessible_objects(self.user, 'inventory_admin_role').exists() - - return self.check_related('organization', Organization, data, role_field='inventory_admin_role') + return (self.check_related('organization', Organization, data, role_field='inventory_admin_role') and + self.check_related('insights_credential', Credential, data, role_field='use_role')) @check_superuser def can_change(self, obj, data): - return self.can_admin(obj, data) + return (self.can_admin(obj, data) and + self.check_related('insights_credential', Credential, data, obj=obj, role_field='use_role')) @check_superuser def can_admin(self, obj, data): @@ -1071,7 +1083,7 @@ class CredentialAccess(BaseAccess): return True if data and data.get('user', None): user_obj = get_object_from_data('user', User, data) - return check_user_access(self.user, User, 'change', user_obj, None) + return bool(self.user == user_obj or UserAccess(self.user).can_admin(user_obj, None, check_setting=False)) if data and data.get('team', None): team_obj = get_object_from_data('team', Team, data) return check_user_access(self.user, Team, 'change', team_obj, None) @@ -1114,6 +1126,9 @@ class TeamAccess(BaseAccess): select_related = ('created_by', 'modified_by', 'organization',) def filtered_queryset(self): + if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and \ + (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()): + return self.model.objects.all() return self.model.accessible_objects(self.user, 'read_role') @check_superuser @@ -1197,14 +1212,15 @@ class ProjectAccess(BaseAccess): @check_superuser def can_add(self, data): if not data: # So the browseable API will work - return Organization.accessible_objects(self.user, 'project_admin_role').exists() - return self.check_related('organization', Organization, data, role_field='project_admin_role', mandatory=True) + return Organization.accessible_objects(self.user, 'admin_role').exists() + return (self.check_related('organization', Organization, data, role_field='project_admin_role', mandatory=True) and + self.check_related('credential', Credential, data, role_field='use_role')) @check_superuser def can_change(self, obj, data): - if not self.check_related('organization', Organization, data, obj=obj, role_field='project_admin_role'): - return False - return self.user in obj.admin_role + return (self.check_related('organization', Organization, data, obj=obj, role_field='project_admin_role') and + self.user in obj.admin_role and + self.check_related('credential', Credential, data, obj=obj, role_field='use_role')) @check_superuser def can_start(self, obj, validate_license=True): @@ -1320,6 +1336,17 @@ class JobTemplateAccess(BaseAccess): return self.user in project.use_role else: return False + + @check_superuser + def can_copy_related(self, obj): + ''' + Check if we have access to all the credentials related to Job Templates. + Does not verify the user's permission for any other related fields (projects, inventories, etc). + ''' + + # obj.credentials.all() is accessible ONLY when object is saved (has valid id) + credential_manager = getattr(obj, 'credentials', None) if getattr(obj, 'id', False) else Credentials.objects.none() + return reduce(lambda prev, cred: prev and self.user in cred.use_role, credential_manager.all(), True) def can_start(self, obj, validate_license=True): # Check license. @@ -1488,7 +1515,7 @@ class JobAccess(BaseAccess): # Obtain prompts used to start original job JobLaunchConfig = obj._meta.get_field('launch_config').related_model try: - config = obj.launch_config + config = JobLaunchConfig.objects.prefetch_related('credentials').get(job=obj) except JobLaunchConfig.DoesNotExist: config = None @@ -1496,6 +1523,12 @@ class JobAccess(BaseAccess): if obj.job_template is not None: if config is None: prompts_access = False + elif not config.has_user_prompts(obj.job_template): + prompts_access = True + elif obj.created_by_id != self.user.pk: + prompts_access = False + if self.save_messages: + self.messages['detail'] = _('Job was launched with prompts provided by another user.') else: prompts_access = ( JobLaunchConfigAccess(self.user).can_add({'reference_obj': config}) and @@ -1507,13 +1540,13 @@ class JobAccess(BaseAccess): elif not jt_access: return False - org_access = obj.inventory and self.user in obj.inventory.organization.inventory_admin_role + org_access = bool(obj.inventory) and self.user in obj.inventory.organization.inventory_admin_role project_access = obj.project is None or self.user in obj.project.admin_role credential_access = all([self.user in cred.use_role for cred in obj.credentials.all()]) # job can be relaunched if user could make an equivalent JT ret = org_access and credential_access and project_access - if not ret and self.save_messages: + if not ret and self.save_messages and not self.messages: if not obj.job_template: pretext = _('Job has been orphaned from its job template.') elif config is None: @@ -1918,12 +1951,22 @@ class WorkflowJobAccess(BaseAccess): if not wfjt: return False - # execute permission to WFJT is mandatory for any relaunch - if self.user not in wfjt.execute_role: - return False + # If job was launched by another user, it could have survey passwords + if obj.created_by_id != self.user.pk: + # Obtain prompts used to start original job + JobLaunchConfig = obj._meta.get_field('launch_config').related_model + try: + config = JobLaunchConfig.objects.get(job=obj) + except JobLaunchConfig.DoesNotExist: + config = None - # user's WFJT access doesn't guarentee permission to launch, introspect nodes - return self.can_recreate(obj) + if config is None or config.prompts_dict(): + if self.save_messages: + self.messages['detail'] = _('Job was launched with prompts provided by another user.') + return False + + # execute permission to WFJT is mandatory for any relaunch + return (self.user in wfjt.execute_role) def can_recreate(self, obj): node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credentials', 'unified_job_template') @@ -2342,9 +2385,7 @@ class LabelAccess(BaseAccess): prefetch_related = ('modified_by', 'created_by', 'organization',) def filtered_queryset(self): - return self.model.objects.filter( - organization__in=Organization.accessible_pk_qs(self.user, 'read_role') - ) + return self.model.objects.all() @check_superuser def can_read(self, obj): @@ -2531,7 +2572,11 @@ class RoleAccess(BaseAccess): # administrators of that Organization the ability to edit that user. To prevent # unwanted escalations lets ensure that the Organization administartor has the abilty # to admin the user being added to the role. - if isinstance(obj.content_object, Organization) and obj.role_field in ['member_role', 'admin_role']: + if (isinstance(obj.content_object, Organization) and + obj.role_field in (Organization.member_role.field.parent_role + ['member_role'])): + if not isinstance(sub_obj, User): + logger.error(six.text_type('Unexpected attempt to associate {} with organization role.').format(sub_obj)) + return False if not UserAccess(self.user).can_admin(sub_obj, None, allow_orphans=True): return False diff --git a/awx/main/conf.py b/awx/main/conf.py index a9802f9289..5008d72dc2 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -38,7 +38,8 @@ register( 'ORG_ADMINS_CAN_SEE_ALL_USERS', field_class=fields.BooleanField, label=_('All Users Visible to Organization Admins'), - help_text=_('Controls whether any Organization Admin can view all users, even those not associated with their Organization.'), + help_text=_('Controls whether any Organization Admin can view all users and teams, ' + 'even those not associated with their Organization.'), category=_('System'), category_slug='system', ) @@ -81,7 +82,7 @@ register( help_text=_('HTTP headers and meta keys to search to determine remote host ' 'name or IP. Add additional items to this list, such as ' '"HTTP_X_FORWARDED_FOR", if behind a reverse proxy. ' - 'See the "Proxy Support" section of the Adminstrator guide for' + 'See the "Proxy Support" section of the Adminstrator guide for ' 'more details.'), category=_('System'), category_slug='system', @@ -482,10 +483,12 @@ register( register( 'LOG_AGGREGATOR_PROTOCOL', field_class=fields.ChoiceField, - choices=[('https', 'HTTPS'), ('tcp', 'TCP'), ('udp', 'UDP')], + choices=[('https', 'HTTPS/HTTP'), ('tcp', 'TCP'), ('udp', 'UDP')], default='https', label=_('Logging Aggregator Protocol'), - help_text=_('Protocol used to communicate with log aggregator.'), + help_text=_('Protocol used to communicate with log aggregator. ' + 'HTTPS/HTTP assumes HTTPS unless http:// is explicitly used in ' + 'the Logging Aggregator hostname.'), category=_('Logging'), category_slug='logging', ) diff --git a/awx/main/constants.py b/awx/main/constants.py index e7c8a943fc..3a92dfc18f 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ __all__ = [ 'CLOUD_PROVIDERS', 'SCHEDULEABLE_PROVIDERS', 'PRIVILEGE_ESCALATION_METHODS', - 'ANSI_SGR_PATTERN', 'CAN_CANCEL', 'ACTIVE_STATES' + 'ANSI_SGR_PATTERN', 'CAN_CANCEL', 'ACTIVE_STATES', 'STANDARD_INVENTORY_UPDATE_ENV' ] @@ -20,6 +20,12 @@ PRIVILEGE_ESCALATION_METHODS = [ ] CHOICES_PRIVILEGE_ESCALATION_METHODS = [('', _('None'))] + PRIVILEGE_ESCALATION_METHODS ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m') +STANDARD_INVENTORY_UPDATE_ENV = { + # Failure to parse inventory should always be fatal + 'ANSIBLE_INVENTORY_UNPARSED_FAILED': 'True', + # Always use the --export option for ansible-inventory + 'ANSIBLE_INVENTORY_EXPORT': 'True' +} CAN_CANCEL = ('new', 'pending', 'waiting', 'running') ACTIVE_STATES = CAN_CANCEL -TOKEN_CENSOR = '************' +CENSOR_VALUE = '************' diff --git a/awx/main/consumers.py b/awx/main/consumers.py index bc79a5c000..eb2afe9ab0 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -4,10 +4,12 @@ import logging from channels import Group from channels.auth import channel_session_user_from_http, channel_session_user +from django.http.cookie import parse_cookie from django.core.serializers.json import DjangoJSONEncoder logger = logging.getLogger('awx.main.consumers') +XRF_KEY = '_auth_user_xrf' def discard_groups(message): @@ -18,12 +20,20 @@ def discard_groups(message): @channel_session_user_from_http def ws_connect(message): + headers = dict(message.content.get('headers', '')) message.reply_channel.send({"accept": True}) message.content['method'] = 'FAKE' if message.user.is_authenticated(): message.reply_channel.send( {"text": json.dumps({"accept": True, "user": message.user.id})} ) + # store the valid CSRF token from the cookie so we can compare it later + # on ws_receive + cookie_token = parse_cookie( + headers.get('cookie') + ).get('csrftoken') + if cookie_token: + message.channel_session[XRF_KEY] = cookie_token else: logger.error("Request user is not authenticated to use websocket.") message.reply_channel.send({"close": True}) @@ -42,6 +52,20 @@ def ws_receive(message): raw_data = message.content['text'] data = json.loads(raw_data) + xrftoken = data.get('xrftoken') + if ( + not xrftoken or + XRF_KEY not in message.channel_session or + xrftoken != message.channel_session[XRF_KEY] + ): + logger.error( + "access denied to channel, XRF mismatch for {}".format(user.username) + ) + message.reply_channel.send({ + "text": json.dumps({"error": "access denied to channel"}) + }) + return + if 'groups' in data: discard_groups(message) groups = data['groups'] diff --git a/awx/main/expect/isolated_manager.py b/awx/main/expect/isolated_manager.py index 71ce262fef..9fa5c4af45 100644 --- a/awx/main/expect/isolated_manager.py +++ b/awx/main/expect/isolated_manager.py @@ -318,7 +318,7 @@ class IsolatedManager(object): path = self.path_to('artifacts', 'stdout') if os.path.exists(path): - with codecs.open(path, 'r', encoding='utf-8') as f: + with open(path, 'r') as f: f.seek(seek) for line in f: self.stdout_handle.write(line) @@ -434,6 +434,7 @@ class IsolatedManager(object): task_result = {} if 'capacity_cpu' in task_result and 'capacity_mem' in task_result: cls.update_capacity(instance, task_result, awx_application_version) + logger.debug('Isolated instance {} successful heartbeat'.format(instance.hostname)) elif instance.capacity == 0: logger.debug('Isolated instance {} previously marked as lost, could not re-join.'.format( instance.hostname)) @@ -468,13 +469,11 @@ class IsolatedManager(object): return OutputEventFilter(job_event_callback) - def run(self, instance, host, private_data_dir, proot_temp_dir): + def run(self, instance, private_data_dir, proot_temp_dir): """ Run a job on an isolated host. :param instance: a `model.Job` instance - :param host: the hostname (or IP address) to run the - isolated job on :param private_data_dir: an absolute path on the local file system where job-specific data should be written (i.e., `/tmp/ansible_awx_xyz/`) @@ -486,14 +485,11 @@ class IsolatedManager(object): `ansible-playbook` run. """ self.instance = instance - self.host = host + self.host = instance.execution_node self.private_data_dir = private_data_dir self.proot_temp_dir = proot_temp_dir status, rc = self.dispatch() if status == 'successful': status, rc = self.check() - else: - # If dispatch fails, attempt to consume artifacts that *might* exist - self.check() self.cleanup() return status, rc diff --git a/awx/main/expect/run.py b/awx/main/expect/run.py index 0c8881a85c..679cf709f3 100755 --- a/awx/main/expect/run.py +++ b/awx/main/expect/run.py @@ -4,7 +4,7 @@ import argparse import base64 import codecs import collections -import cStringIO +import StringIO import logging import json import os @@ -18,6 +18,7 @@ import time import pexpect import psutil +import six logger = logging.getLogger('awx.main.utils.expect') @@ -99,6 +100,12 @@ def run_pexpect(args, cwd, env, logfile, password_patterns = expect_passwords.keys() password_values = expect_passwords.values() + # pexpect needs all env vars to be utf-8 encoded strings + # https://github.com/pexpect/pexpect/issues/512 + for k, v in env.items(): + if isinstance(v, six.text_type): + env[k] = v.encode('utf-8') + child = pexpect.spawn( args[0], args[1:], cwd=cwd, env=env, ignore_sighup=True, encoding='utf-8', echo=False, use_poll=True @@ -240,7 +247,7 @@ def handle_termination(pid, args, proot_cmd, is_cancel=True): def __run__(private_data_dir): - buff = cStringIO.StringIO() + buff = StringIO.StringIO() with open(os.path.join(private_data_dir, 'env'), 'r') as f: for line in f: buff.write(line) diff --git a/awx/main/fields.py b/awx/main/fields.py index d63eb54002..0747a32b4d 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -218,6 +218,7 @@ class ImplicitRoleField(models.ForeignKey): kwargs.setdefault('to', 'Role') kwargs.setdefault('related_name', '+') kwargs.setdefault('null', 'True') + kwargs.setdefault('editable', False) super(ImplicitRoleField, self).__init__(*args, **kwargs) def deconstruct(self): diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index 842816cabe..d171f20544 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -4,6 +4,7 @@ from django.core.management.base import BaseCommand from crum import impersonate from awx.main.models import User, Organization, Project, Inventory, CredentialType, Credential, Host, JobTemplate +from awx.main.signals import disable_computed_fields class Command(BaseCommand): @@ -22,33 +23,34 @@ class Command(BaseCommand): except IndexError: superuser = None with impersonate(superuser): - o = Organization.objects.create(name='Default') - p = Project(name='Demo Project', - scm_type='git', - scm_url='https://github.com/ansible/ansible-tower-samples', - scm_update_on_launch=True, - scm_update_cache_timeout=0, - organization=o) - p.save(skip_update=True) - ssh_type = CredentialType.from_v1_kind('ssh') - c = Credential.objects.create(credential_type=ssh_type, - name='Demo Credential', - inputs={ - 'username': superuser.username - }, - created_by=superuser) - c.admin_role.members.add(superuser) - i = Inventory.objects.create(name='Demo Inventory', - organization=o, - created_by=superuser) - Host.objects.create(name='localhost', - inventory=i, - variables="ansible_connection: local", - created_by=superuser) - jt = JobTemplate.objects.create(name='Demo Job Template', - playbook='hello_world.yml', - project=p, - inventory=i) - jt.credentials.add(c) + with disable_computed_fields(): + o = Organization.objects.create(name='Default') + p = Project(name='Demo Project', + scm_type='git', + scm_url='https://github.com/ansible/ansible-tower-samples', + scm_update_on_launch=True, + scm_update_cache_timeout=0, + organization=o) + p.save(skip_update=True) + ssh_type = CredentialType.from_v1_kind('ssh') + c = Credential.objects.create(credential_type=ssh_type, + name='Demo Credential', + inputs={ + 'username': superuser.username + }, + created_by=superuser) + c.admin_role.members.add(superuser) + i = Inventory.objects.create(name='Demo Inventory', + organization=o, + created_by=superuser) + Host.objects.create(name='localhost', + inventory=i, + variables="ansible_connection: local", + created_by=superuser) + jt = JobTemplate.objects.create(name='Demo Job Template', + playbook='hello_world.yml', + project=p, + inventory=i) + jt.credentials.add(c) print('Default organization added.') print('Demo Credential, Inventory, and Job Template added.') diff --git a/awx/main/management/commands/expire_sessions.py b/awx/main/management/commands/expire_sessions.py new file mode 100644 index 0000000000..2053b483f6 --- /dev/null +++ b/awx/main/management/commands/expire_sessions.py @@ -0,0 +1,37 @@ +# Python +from importlib import import_module + +# Django +from django.utils import timezone +from django.conf import settings +from django.contrib.auth import logout +from django.http import HttpRequest +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from django.contrib.sessions.models import Session +from django.core.exceptions import ObjectDoesNotExist + + +class Command(BaseCommand): + """Expire Django auth sessions for a user/all users""" + help='Expire Django auth sessions. Will expire all auth sessions if --user option is not supplied.' + + def add_arguments(self, parser): + parser.add_argument('--user', dest='user', type=str) + + def handle(self, *args, **options): + # Try to see if the user exist + try: + user = User.objects.get(username=options['user']) if options['user'] else None + except ObjectDoesNotExist: + raise CommandError('The user does not exist.') + # We use the following hack to filter out sessions that are still active, + # with consideration for timezones. + start = timezone.now() + sessions = Session.objects.filter(expire_date__gte=start).iterator() + request = HttpRequest() + for session in sessions: + user_id = session.get_decoded().get('_auth_user_id') + if (user is None) or (user_id and user.id == int(user_id)): + request.session = import_module(settings.SESSION_ENGINE).SessionStore(session.session_key) + logout(request) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 021064a46b..d3ac4960d2 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -30,6 +30,7 @@ from awx.main.utils import ( ) from awx.main.utils.mem_inventory import MemInventory, dict_to_mem_data from awx.main.signals import disable_activity_stream +from awx.main.constants import STANDARD_INVENTORY_UPDATE_ENV logger = logging.getLogger('awx.main.commands.inventory_import') @@ -82,7 +83,10 @@ class AnsibleInventoryLoader(object): env = dict(os.environ.items()) env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_PATH env['PATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH'] - env['ANSIBLE_INVENTORY_UNPARSED_FAILED'] = '1' + # Set configuration items that should always be used for updates + for key, value in STANDARD_INVENTORY_UPDATE_ENV.items(): + if key not in env: + env[key] = value venv_libdir = os.path.join(settings.ANSIBLE_VENV_PATH, "lib") env.pop('PYTHONPATH', None) # default to none if no python_ver matches if os.path.isdir(os.path.join(venv_libdir, "python2.7")): @@ -1001,37 +1005,43 @@ class Command(BaseCommand): self.all_group.debug_tree() with batch_role_ancestor_rebuilding(): - # Ensure that this is managed as an atomic SQL transaction, - # and thus properly rolled back if there is an issue. - with transaction.atomic(): - # Merge/overwrite inventory into database. - if settings.SQL_DEBUG: - logger.warning('loading into database...') - with ignore_inventory_computed_fields(): - if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): - self.load_into_database() - else: - with disable_activity_stream(): + # If using with transaction.atomic() with try ... catch, + # with transaction.atomic() must be inside the try section of the code as per Django docs + try: + # Ensure that this is managed as an atomic SQL transaction, + # and thus properly rolled back if there is an issue. + with transaction.atomic(): + # Merge/overwrite inventory into database. + if settings.SQL_DEBUG: + logger.warning('loading into database...') + with ignore_inventory_computed_fields(): + if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): self.load_into_database() - if settings.SQL_DEBUG: - queries_before2 = len(connection.queries) - self.inventory.update_computed_fields() - if settings.SQL_DEBUG: - logger.warning('update computed fields took %d queries', - len(connection.queries) - queries_before2) - try: + else: + with disable_activity_stream(): + self.load_into_database() + if settings.SQL_DEBUG: + queries_before2 = len(connection.queries) + self.inventory.update_computed_fields() + if settings.SQL_DEBUG: + logger.warning('update computed fields took %d queries', + len(connection.queries) - queries_before2) + # Check if the license is valid. + # If the license is not valid, a CommandError will be thrown, + # and inventory update will be marked as invalid. + # with transaction.atomic() will roll back the changes. self.check_license() - except CommandError as e: - self.mark_license_failure(save=True) - raise e + except CommandError as e: + self.mark_license_failure() + raise e - if settings.SQL_DEBUG: - logger.warning('Inventory import completed for %s in %0.1fs', - self.inventory_source.name, time.time() - begin) - else: - logger.info('Inventory import completed for %s in %0.1fs', - self.inventory_source.name, time.time() - begin) - status = 'successful' + if settings.SQL_DEBUG: + logger.warning('Inventory import completed for %s in %0.1fs', + self.inventory_source.name, time.time() - begin) + else: + logger.info('Inventory import completed for %s in %0.1fs', + self.inventory_source.name, time.time() - begin) + status = 'successful' # If we're in debug mode, then log the queries and time # used to do the operation. @@ -1058,6 +1068,8 @@ class Command(BaseCommand): self.inventory_update.result_traceback = tb self.inventory_update.status = status self.inventory_update.save(update_fields=['status', 'result_traceback']) + self.inventory_source.status = status + self.inventory_source.save(update_fields=['status']) if exc and isinstance(exc, CommandError): sys.exit(1) diff --git a/awx/main/management/commands/register_queue.py b/awx/main/management/commands/register_queue.py index 5f252a4637..2894ecba97 100644 --- a/awx/main/management/commands/register_queue.py +++ b/awx/main/management/commands/register_queue.py @@ -22,7 +22,7 @@ class Command(BaseCommand): parser.add_argument('--queuename', dest='queuename', type=lambda s: six.text_type(s, 'utf8'), help='Queue to create/update') parser.add_argument('--hostnames', dest='hostnames', type=lambda s: six.text_type(s, 'utf8'), - help='Comma-Delimited Hosts to add to the Queue') + help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)') parser.add_argument('--controller', dest='controller', type=lambda s: six.text_type(s, 'utf8'), default='', help='The controlling group (makes this an isolated group)') parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0, @@ -44,6 +44,9 @@ class Command(BaseCommand): ig.policy_instance_minimum = instance_min changed = True + if changed: + ig.save() + return (ig, created, changed) def update_instance_group_controller(self, ig, controller): @@ -72,16 +75,16 @@ class Command(BaseCommand): else: raise InstanceNotFound(six.text_type("Instance does not exist: {}").format(inst_name), changed) - ig.instances = instances + ig.instances.add(*instances) - instance_list_before = set(ig.policy_instance_list) - instance_list_after = set(instance_list_unique) - if len(instance_list_before) != len(instance_list_after) or \ - len(set(instance_list_before) - set(instance_list_after)) != 0: + instance_list_before = ig.policy_instance_list + instance_list_after = instance_list_unique + new_instances = set(instance_list_after) - set(instance_list_before) + if new_instances: changed = True + ig.policy_instance_list = ig.policy_instance_list + list(new_instances) + ig.save() - ig.policy_instance_list = list(instance_list_unique) - ig.save() return (instances, changed) def handle(self, **options): @@ -97,25 +100,27 @@ class Command(BaseCommand): hostname_list = options.get('hostnames').split(",") with advisory_lock(six.text_type('instance_group_registration_{}').format(queuename)): - (ig, created, changed) = self.get_create_update_instance_group(queuename, inst_per, inst_min) + changed2 = False + changed3 = False + (ig, created, changed1) = self.get_create_update_instance_group(queuename, inst_per, inst_min) if created: print(six.text_type("Creating instance group {}".format(ig.name))) elif not created: print(six.text_type("Instance Group already registered {}").format(ig.name)) if ctrl: - (ig_ctrl, changed) = self.update_instance_group_controller(ig, ctrl) - if changed: + (ig_ctrl, changed2) = self.update_instance_group_controller(ig, ctrl) + if changed2: print(six.text_type("Set controller group {} on {}.").format(ctrl, queuename)) try: - (instances, changed) = self.add_instances_to_group(ig, hostname_list) + (instances, changed3) = self.add_instances_to_group(ig, hostname_list) for i in instances: print(six.text_type("Added instance {} to {}").format(i.hostname, ig.name)) except InstanceNotFound as e: instance_not_found_err = e - if changed: + if any([changed1, changed2, changed3]): print('(changed: True)') if instance_not_found_err: diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index 7bb33b033c..14e44496bb 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -162,6 +162,7 @@ class CallbackBrokerWorker(ConsumerMixin): if body.get('event') == 'EOF': try: + final_counter = body.get('final_counter', 0) logger.info('Event processing is finished for Job {}, sending notifications'.format(job_identifier)) # EOF events are sent when stdout for the running task is # closed. don't actually persist them to the database; we @@ -169,7 +170,7 @@ class CallbackBrokerWorker(ConsumerMixin): # approximation for when a job is "done" emit_channel_notification( 'jobs-summary', - dict(group_name='jobs', unified_job_id=job_identifier) + dict(group_name='jobs', unified_job_id=job_identifier, final_counter=final_counter) ) # Additionally, when we've processed all events, we should # have all the data we need to send out success/failure diff --git a/awx/main/management/commands/test_isolated_connection.py b/awx/main/management/commands/test_isolated_connection.py index 23bd20bf50..efaf881535 100644 --- a/awx/main/management/commands/test_isolated_connection.py +++ b/awx/main/management/commands/test_isolated_connection.py @@ -3,7 +3,6 @@ import shutil import subprocess import sys import tempfile -from optparse import make_option from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -15,10 +14,9 @@ class Command(BaseCommand): """Tests SSH connectivity between a controller and target isolated node""" help = 'Tests SSH connectivity between a controller and target isolated node' - option_list = BaseCommand.option_list + ( - make_option('--hostname', dest='hostname', type='string', - help='Hostname of an isolated node'), - ) + def add_arguments(self, parser): + parser.add_argument('--hostname', dest='hostname', type=str, + help='Hostname of an isolated node') def handle(self, *args, **options): hostname = options.get('hostname') @@ -30,7 +28,7 @@ class Command(BaseCommand): args = [ 'ansible', 'all', '-i', '{},'.format(hostname), '-u', settings.AWX_ISOLATED_USERNAME, '-T5', '-m', 'shell', - '-a', 'hostname', '-vvv' + '-a', 'awx-expect -h', '-vvv' ] if all([ getattr(settings, 'AWX_ISOLATED_KEY_GENERATION', False) is True, diff --git a/awx/main/management/commands/watch_celery.py b/awx/main/management/commands/watch_celery.py new file mode 100644 index 0000000000..bd45f06803 --- /dev/null +++ b/awx/main/management/commands/watch_celery.py @@ -0,0 +1,66 @@ +import datetime +import os +import signal +import subprocess +import sys +import time + +from celery import Celery +from django.core.management.base import BaseCommand +from django.conf import settings + + +class Command(BaseCommand): + """Watch local celery workers""" + help=("Sends a periodic ping to the local celery process over AMQP to ensure " + "it's responsive; this command is only intended to run in an environment " + "where celeryd is running") + + # + # Just because celery is _running_ doesn't mean it's _working_; it's + # imperative that celery workers are _actually_ handling AMQP messages on + # their appropriate queues for awx to function. Unfortunately, we've been + # plagued by a variety of bugs in celery that cause it to hang and become + # an unresponsive zombie, such as: + # + # https://github.com/celery/celery/issues/4185 + # https://github.com/celery/celery/issues/4457 + # + # The goal of this code is periodically send a broadcast AMQP message to + # the celery process on the local host via celery.app.control.ping; + # If that _fails_, we attempt to determine the pid of the celery process + # and send SIGHUP (which tends to resolve these sorts of issues for us). + # + + INTERVAL = 60 + + def _log(self, msg): + sys.stderr.write(datetime.datetime.utcnow().isoformat()) + sys.stderr.write(' ') + sys.stderr.write(msg) + sys.stderr.write('\n') + + def handle(self, **options): + app = Celery('awx') + app.config_from_object('django.conf:settings') + while True: + try: + pongs = app.control.ping(['celery@{}'.format(settings.CLUSTER_HOST_ID)], timeout=30) + except Exception: + pongs = [] + if not pongs: + self._log('celery is not responsive to ping over local AMQP') + pid = self.getpid() + if pid: + self._log('sending SIGHUP to {}'.format(pid)) + os.kill(pid, signal.SIGHUP) + time.sleep(self.INTERVAL) + + def getpid(self): + cmd = 'supervisorctl pid tower-processes:awx-celeryd' + if os.path.exists('/supervisor_task.conf'): + cmd = 'supervisorctl -c /supervisor_task.conf pid tower-processes:celery' + try: + return int(subprocess.check_output(cmd, shell=True)) + except Exception: + self._log('could not detect celery pid') diff --git a/awx/main/managers.py b/awx/main/managers.py index d2af95e2b8..f31a532572 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -96,7 +96,7 @@ class InstanceManager(models.Manager): instance = self.filter(hostname=hostname) if instance.exists(): return (False, instance[0]) - instance = self.create(uuid=uuid, hostname=hostname) + instance = self.create(uuid=uuid, hostname=hostname, capacity=0) return (True, instance) def get_or_register(self): diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 90e74af7e1..15bbf3aa15 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -119,6 +119,20 @@ class ActivityStreamMiddleware(threading.local): self.instance_ids.append(instance.id) +class SessionTimeoutMiddleware(object): + """ + Resets the session timeout for both the UI and the actual session for the API + to the value of SESSION_COOKIE_AGE on every request if there is a valid session. + """ + + def process_response(self, request, response): + req_session = getattr(request, 'session', None) + if req_session and not req_session.is_empty(): + request.session.set_expiry(request.session.get_expiry_age()) + response['Session-Timeout'] = int(settings.SESSION_COOKIE_AGE) + return response + + def _customize_graph(): from awx.main.models import Instance, Schedule, UnifiedJobTemplate for model in [Schedule, UnifiedJobTemplate]: diff --git a/awx/main/migrations/0006_v320_release.py b/awx/main/migrations/0006_v320_release.py index c0a4330e04..8902a34438 100644 --- a/awx/main/migrations/0006_v320_release.py +++ b/awx/main/migrations/0006_v320_release.py @@ -484,7 +484,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='instance', name='last_isolated_check', - field=models.DateTimeField(auto_now_add=True, null=True), + field=models.DateTimeField(editable=False, null=True), ), # Migrations that don't change db schema but simply to make Django ORM happy. # e.g. Choice updates, help_text updates, etc. diff --git a/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py b/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py index aac423cd1c..2cdf557856 100644 --- a/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py +++ b/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals # AWX +from awx.main.migrations import _migration_utils as migration_utils from awx.main.migrations import _credentialtypes as credentialtypes from django.db import migrations, models @@ -14,6 +15,7 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), migrations.RunPython(credentialtypes.create_rhv_tower_credtype), migrations.AlterField( model_name='inventorysource', diff --git a/awx/main/migrations/0012_v322_update_cred_types.py b/awx/main/migrations/0012_v322_update_cred_types.py index 86d9fd55fa..b1e77fd810 100644 --- a/awx/main/migrations/0012_v322_update_cred_types.py +++ b/awx/main/migrations/0012_v322_update_cred_types.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals # AWX +from awx.main.migrations import _migration_utils as migration_utils from awx.main.migrations import _credentialtypes as credentialtypes from django.db import migrations @@ -14,5 +15,6 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), migrations.RunPython(credentialtypes.add_azure_cloud_environment_field), ] diff --git a/awx/main/migrations/0013_v330_multi_credential.py b/awx/main/migrations/0013_v330_multi_credential.py index abc186c382..437ade51aa 100644 --- a/awx/main/migrations/0013_v330_multi_credential.py +++ b/awx/main/migrations/0013_v330_multi_credential.py @@ -5,7 +5,7 @@ from django.db import migrations, models from awx.main.migrations import _migration_utils as migration_utils from awx.main.migrations import _credentialtypes as credentialtypes -from awx.main.migrations._multi_cred import migrate_to_multi_cred +from awx.main.migrations._multi_cred import migrate_to_multi_cred, migrate_back_from_multi_cred class Migration(migrations.Migration): @@ -13,6 +13,13 @@ class Migration(migrations.Migration): dependencies = [ ('main', '0012_v322_update_cred_types'), ] + run_before = [ + # Django-vendored migrations will make reference to settings + # this migration was introduced in Django 1.11 / Tower 3.3 upgrade + # migration main-0009 changed the setting model and is not backward compatible, + # so we assure that at least all of Tower 3.2 migrations are finished before running it + ('auth', '0008_alter_user_username_max_length') + ] operations = [ migrations.AddField( @@ -25,8 +32,8 @@ class Migration(migrations.Migration): name='credentials', field=models.ManyToManyField(related_name='unifiedjobtemplates', to='main.Credential'), ), - migrations.RunPython(migration_utils.set_current_apps_for_migrations), - migrations.RunPython(migrate_to_multi_cred), + migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrate_back_from_multi_cred), + migrations.RunPython(migrate_to_multi_cred, migration_utils.set_current_apps_for_migrations), migrations.RemoveField( model_name='job', name='credential', @@ -51,5 +58,6 @@ class Migration(migrations.Migration): model_name='jobtemplate', name='vault_credential', ), - migrations.RunPython(credentialtypes.add_vault_id_field) + migrations.RunPython(migration_utils.set_current_apps_for_migrations, credentialtypes.remove_vault_id_field), + migrations.RunPython(credentialtypes.add_vault_id_field, migration_utils.set_current_apps_for_migrations) ] diff --git a/awx/main/migrations/0021_v330_declare_new_rbac_roles.py b/awx/main/migrations/0021_v330_declare_new_rbac_roles.py index e43d4cb46a..4714a0194c 100644 --- a/awx/main/migrations/0021_v330_declare_new_rbac_roles.py +++ b/awx/main/migrations/0021_v330_declare_new_rbac_roles.py @@ -20,6 +20,11 @@ class Migration(migrations.Migration): name='execute_role', field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), ), + migrations.AddField( + model_name='organization', + name='job_template_admin_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), migrations.AddField( model_name='organization', name='credential_admin_role', @@ -73,7 +78,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='jobtemplate', name='admin_role', - field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'project.organization.project_admin_role', b'inventory.organization.inventory_admin_role'], related_name='+', to='main.Role'), + field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'project.organization.job_template_admin_role', b'inventory.organization.job_template_admin_role'], related_name='+', to='main.Role'), ), migrations.AlterField( model_name='jobtemplate', @@ -83,6 +88,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role', b'credential_admin_role', b'execute_role'], related_name='+', to='main.Role'), + field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'execute_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role', b'credential_admin_role', b'job_template_admin_role'], related_name='+', to='main.Role'), ), + ] diff --git a/awx/main/migrations/0023_v330_inventory_multicred.py b/awx/main/migrations/0023_v330_inventory_multicred.py index b184abc296..fdb95e8ddc 100644 --- a/awx/main/migrations/0023_v330_inventory_multicred.py +++ b/awx/main/migrations/0023_v330_inventory_multicred.py @@ -19,8 +19,8 @@ class Migration(migrations.Migration): operations = [ # Run data migration before removing the old credential field - migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrations.RunPython.noop), - migrations.RunPython(migrate_inventory_source_cred, migrate_inventory_source_cred_reverse), + migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrate_inventory_source_cred_reverse), + migrations.RunPython(migrate_inventory_source_cred, migration_utils.set_current_apps_for_migrations), migrations.RemoveField( model_name='inventorysource', name='credential', diff --git a/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py b/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py index e7d2ef49b9..993dcc2d33 100644 --- a/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py +++ b/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py @@ -13,6 +13,12 @@ class Migration(migrations.Migration): dependencies = [ ('main', '0024_v330_create_user_session_membership'), ] + run_before = [ + # As of this migration, OAuth2Application and OAuth2AccessToken are models in main app + # Grant and RefreshToken models are still in the oauth2_provider app and reference + # the app and token models, so these must be created before the oauth2_provider models + ('oauth2_provider', '0001_initial') + ] operations = [ diff --git a/awx/main/migrations/0030_v330_modify_application.py b/awx/main/migrations/0030_v330_modify_application.py index 7725ffeaff..32b4fdd5a3 100644 --- a/awx/main/migrations/0030_v330_modify_application.py +++ b/awx/main/migrations/0030_v330_modify_application.py @@ -20,4 +20,8 @@ class Migration(migrations.Migration): name='organization', field=models.ForeignKey(help_text='Organization containing this application.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='main.Organization'), ), + migrations.AlterUniqueTogether( + name='oauth2application', + unique_together=set([('name', 'organization')]), + ), ] diff --git a/awx/main/migrations/0033_v330_oauth_help_text.py b/awx/main/migrations/0033_v330_oauth_help_text.py index 41704307b0..0b64579d65 100644 --- a/awx/main/migrations/0033_v330_oauth_help_text.py +++ b/awx/main/migrations/0033_v330_oauth_help_text.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='oauth2accesstoken', name='scope', - field=models.TextField(blank=True, help_text="Allowed scopes, further restricts user's permissions."), + field=models.TextField(blank=True, default=b'write', help_text="Allowed scopes, further restricts user's permissions."), ), migrations.AlterField( model_name='oauth2accesstoken', diff --git a/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py b/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py new file mode 100644 index 0000000000..6f79485f3f --- /dev/null +++ b/awx/main/migrations/0038_v330_add_deleted_activitystream_actor.py @@ -0,0 +1,24 @@ +#d -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-05-21 19:51 +from __future__ import unicode_literals + +import awx.main.fields +import awx.main.models.activity_stream +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_v330_remove_legacy_fact_cleanup'), + ] + + operations = [ + migrations.AddField( + model_name='activitystream', + name='deleted_actor', + field=awx.main.fields.JSONField(null=True), + ), + ] diff --git a/awx/main/migrations/0039_v330_custom_venv_help_text.py b/awx/main/migrations/0039_v330_custom_venv_help_text.py new file mode 100644 index 0000000000..ba68aa158f --- /dev/null +++ b/awx/main/migrations/0039_v330_custom_venv_help_text.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-05-23 20:17 +from __future__ import unicode_literals + +import awx.main.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0038_v330_add_deleted_activitystream_actor'), + ] + + operations = [ + migrations.AlterField( + model_name='jobtemplate', + name='custom_virtualenv', + field=models.CharField(blank=True, default=None, help_text='Local absolute file path containing a custom Python virtualenv to use', max_length=100, null=True), + ), + migrations.AlterField( + model_name='organization', + name='custom_virtualenv', + field=models.CharField(blank=True, default=None, help_text='Local absolute file path containing a custom Python virtualenv to use', max_length=100, null=True), + ), + migrations.AlterField( + model_name='project', + name='custom_virtualenv', + field=models.CharField(blank=True, default=None, help_text='Local absolute file path containing a custom Python virtualenv to use', max_length=100, null=True), + ), + ] diff --git a/awx/main/migrations/0040_v330_unifiedjob_controller_node.py b/awx/main/migrations/0040_v330_unifiedjob_controller_node.py new file mode 100644 index 0000000000..8b127dd06d --- /dev/null +++ b/awx/main/migrations/0040_v330_unifiedjob_controller_node.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-05-25 18:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0039_v330_custom_venv_help_text'), + ] + + operations = [ + migrations.AddField( + model_name='unifiedjob', + name='controller_node', + field=models.TextField(blank=True, default=b'', editable=False, help_text='The instance that managed the isolated execution environment.'), + ), + ] diff --git a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py new file mode 100644 index 0000000000..6f71563e29 --- /dev/null +++ b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-06-14 21:03 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL), + ('main', '0040_v330_unifiedjob_controller_node'), + ] + + operations = [ + migrations.AddField( + model_name='oauth2accesstoken', + name='source_refresh_token', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL), + ), + ] diff --git a/awx/main/migrations/0042_v330_org_member_role_deparent.py b/awx/main/migrations/0042_v330_org_member_role_deparent.py new file mode 100644 index 0000000000..2ae100053d --- /dev/null +++ b/awx/main/migrations/0042_v330_org_member_role_deparent.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-02 13:47 +from __future__ import unicode_literals + +import awx.main.fields +from django.db import migrations +import django.db.models.deletion +from awx.main.migrations._rbac import rebuild_role_hierarchy + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0041_v330_update_oauth_refreshtoken'), + ] + + operations = [ + migrations.AlterField( + model_name='organization', + name='member_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role'], related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='organization', + name='read_role', + field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'member_role', b'auditor_role', b'execute_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role', b'credential_admin_role', b'job_template_admin_role'], related_name='+', to='main.Role'), + ), + migrations.RunPython(rebuild_role_hierarchy), + ] diff --git a/awx/main/migrations/0043_v330_oauth2accesstoken_modified.py b/awx/main/migrations/0043_v330_oauth2accesstoken_modified.py new file mode 100644 index 0000000000..4476cbe773 --- /dev/null +++ b/awx/main/migrations/0043_v330_oauth2accesstoken_modified.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-10 14:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0042_v330_org_member_role_deparent'), + ] + + operations = [ + migrations.AddField( + model_name='oauth2accesstoken', + name='modified', + field=models.DateTimeField(editable=False), + ), + ] diff --git a/awx/main/migrations/0044_v330_add_inventory_update_inventory.py b/awx/main/migrations/0044_v330_add_inventory_update_inventory.py new file mode 100644 index 0000000000..1ec8b838ec --- /dev/null +++ b/awx/main/migrations/0044_v330_add_inventory_update_inventory.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-17 03:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0043_v330_oauth2accesstoken_modified'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryupdate', + name='inventory', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='inventory_updates', to='main.Inventory'), + ), + ] diff --git a/awx/main/migrations/0045_v330_instance_managed_by_policy.py b/awx/main/migrations/0045_v330_instance_managed_by_policy.py new file mode 100644 index 0000000000..a3f87fb3ae --- /dev/null +++ b/awx/main/migrations/0045_v330_instance_managed_by_policy.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-25 17:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0044_v330_add_inventory_update_inventory'), + ] + + operations = [ + migrations.AddField( + model_name='instance', + name='managed_by_policy', + field=models.BooleanField(default=True), + ) + ] diff --git a/awx/main/migrations/0046_v330_remove_client_credentials_grant.py b/awx/main/migrations/0046_v330_remove_client_credentials_grant.py new file mode 100644 index 0000000000..e4eca09fa8 --- /dev/null +++ b/awx/main/migrations/0046_v330_remove_client_credentials_grant.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-25 21:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0045_v330_instance_managed_by_policy'), + ] + + operations = [ + migrations.AlterField( + model_name='oauth2application', + name='authorization_grant_type', + field=models.CharField(choices=[(b'authorization-code', 'Authorization code'), (b'implicit', 'Implicit'), (b'password', 'Resource owner password-based')], help_text='The Grant type the user must use for acquire tokens for this application.', max_length=32), + ), + ] diff --git a/awx/main/migrations/0047_v330_activitystream_instance.py b/awx/main/migrations/0047_v330_activitystream_instance.py new file mode 100644 index 0000000000..0bc6b3c0bc --- /dev/null +++ b/awx/main/migrations/0047_v330_activitystream_instance.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-25 20:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0046_v330_remove_client_credentials_grant'), + ] + + operations = [ + migrations.AddField( + model_name='activitystream', + name='instance', + field=models.ManyToManyField(blank=True, to='main.Instance'), + ), + ] diff --git a/awx/main/migrations/_credentialtypes.py b/awx/main/migrations/_credentialtypes.py index fbf812e8c2..9d78cec49d 100644 --- a/awx/main/migrations/_credentialtypes.py +++ b/awx/main/migrations/_credentialtypes.py @@ -180,6 +180,17 @@ def add_vault_id_field(apps, schema_editor): vault_credtype.save() +def remove_vault_id_field(apps, schema_editor): + vault_credtype = CredentialType.objects.get(kind='vault') + idx = 0 + for i, input in enumerate(vault_credtype.inputs['fields']): + if input['id'] == 'vault_id': + idx = i + break + vault_credtype.inputs['fields'].pop(idx) + vault_credtype.save() + + def create_rhv_tower_credtype(apps, schema_editor): CredentialType.setup_tower_managed_defaults() diff --git a/awx/main/migrations/_multi_cred.py b/awx/main/migrations/_multi_cred.py index 5e4ed5ff4b..1b2a99c110 100644 --- a/awx/main/migrations/_multi_cred.py +++ b/awx/main/migrations/_multi_cred.py @@ -1,56 +1,124 @@ +import logging + + +logger = logging.getLogger('awx.main.migrations') + + def migrate_to_multi_cred(app, schema_editor): Job = app.get_model('main', 'Job') JobTemplate = app.get_model('main', 'JobTemplate') + ct = 0 for cls in (Job, JobTemplate): for j in cls.objects.iterator(): if j.credential: + ct += 1 + logger.debug('Migrating cred %s to %s %s multi-cred relation.', j.credential_id, cls, j.id) j.credentials.add(j.credential) if j.vault_credential: + ct += 1 + logger.debug('Migrating cred %s to %s %s multi-cred relation.', j.vault_credential_id, cls, j.id) j.credentials.add(j.vault_credential) for cred in j.extra_credentials.all(): + ct += 1 + logger.debug('Migrating cred %s to %s %s multi-cred relation.', cred.id, cls, j.id) j.credentials.add(cred) + if ct: + logger.info('Finished migrating %s credentials to multi-cred', ct) + + +def migrate_back_from_multi_cred(app, schema_editor): + Job = app.get_model('main', 'Job') + JobTemplate = app.get_model('main', 'JobTemplate') + CredentialType = app.get_model('main', 'CredentialType') + vault_credtype = CredentialType.objects.get(kind='vault') + ssh_credtype = CredentialType.objects.get(kind='ssh') + + ct = 0 + for cls in (Job, JobTemplate): + for j in cls.objects.iterator(): + for cred in j.credentials.iterator(): + changed = False + if cred.credential_type_id == vault_credtype.id: + changed = True + ct += 1 + logger.debug('Reverse migrating vault cred %s for %s %s', cred.id, cls, j.id) + j.vault_credential = cred + elif cred.credential_type_id == ssh_credtype.id: + changed = True + ct += 1 + logger.debug('Reverse migrating ssh cred %s for %s %s', cred.id, cls, j.id) + j.credential = cred + else: + changed = True + ct += 1 + logger.debug('Reverse migrating cloud cred %s for %s %s', cred.id, cls, j.id) + j.extra_credentials.add(cred) + if changed: + j.save() + if ct: + logger.info('Finished reverse migrating %s credentials from multi-cred', ct) def migrate_workflow_cred(app, schema_editor): WorkflowJobTemplateNode = app.get_model('main', 'WorkflowJobTemplateNode') WorkflowJobNode = app.get_model('main', 'WorkflowJobNode') + ct = 0 for cls in (WorkflowJobNode, WorkflowJobTemplateNode): for node in cls.objects.iterator(): if node.credential: - node.credentials.add(j.credential) + logger.debug('Migrating prompted credential %s for %s %s', node.credential_id, cls, node.id) + ct += 1 + node.credentials.add(node.credential) + if ct: + logger.info('Finished migrating total of %s workflow prompted credentials', ct) def migrate_workflow_cred_reverse(app, schema_editor): WorkflowJobTemplateNode = app.get_model('main', 'WorkflowJobTemplateNode') WorkflowJobNode = app.get_model('main', 'WorkflowJobNode') + ct = 0 for cls in (WorkflowJobNode, WorkflowJobTemplateNode): for node in cls.objects.iterator(): cred = node.credentials.first() if cred: node.credential = cred - node.save() + logger.debug('Reverse migrating prompted credential %s for %s %s', node.credential_id, cls, node.id) + ct += 1 + node.save(update_fields=['credential']) + if ct: + logger.info('Finished reverse migrating total of %s workflow prompted credentials', ct) def migrate_inventory_source_cred(app, schema_editor): InventoryUpdate = app.get_model('main', 'InventoryUpdate') InventorySource = app.get_model('main', 'InventorySource') + ct = 0 for cls in (InventoryUpdate, InventorySource): for obj in cls.objects.iterator(): if obj.credential: + ct += 1 + logger.debug('Migrating credential %s for %s %s', obj.credential_id, cls, obj.id) obj.credentials.add(obj.credential) + if ct: + logger.info('Finished migrating %s inventory source credentials to multi-cred', ct) def migrate_inventory_source_cred_reverse(app, schema_editor): InventoryUpdate = app.get_model('main', 'InventoryUpdate') InventorySource = app.get_model('main', 'InventorySource') + ct = 0 for cls in (InventoryUpdate, InventorySource): for obj in cls.objects.iterator(): cred = obj.credentials.first() if cred: + ct += 1 + logger.debug('Reverse migrating credential %s for %s %s', cred.id, cls, obj.id) obj.credential = cred obj.save() + if ct: + logger.info('Finished reverse migrating %s inventory source credentials from multi-cred', ct) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 45d8cbea07..766cefbc5b 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -33,6 +33,7 @@ class ActivityStream(models.Model): operation = models.CharField(max_length=13, choices=OPERATION_CHOICES) timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) + deleted_actor = JSONField(null=True) object_relationship_type = models.TextField(blank=True) object1 = models.TextField() @@ -65,6 +66,7 @@ class ActivityStream(models.Model): notification = models.ManyToManyField("Notification", blank=True) label = models.ManyToManyField("Label", blank=True) role = models.ManyToManyField("Role", blank=True) + instance = models.ManyToManyField("Instance", blank=True) instance_group = models.ManyToManyField("InstanceGroup", blank=True) o_auth2_application = models.ManyToManyField("OAuth2Application", blank=True) o_auth2_access_token = models.ManyToManyField("OAuth2AccessToken", blank=True) @@ -77,6 +79,18 @@ class ActivityStream(models.Model): return reverse('api:activity_stream_detail', kwargs={'pk': self.pk}, request=request) def save(self, *args, **kwargs): + # Store denormalized actor metadata so that we retain it for accounting + # purposes when the User row is deleted. + if self.actor: + self.deleted_actor = { + 'id': self.actor_id, + 'username': self.actor.username, + 'first_name': self.actor.first_name, + 'last_name': self.actor.last_name, + } + if 'update_fields' in kwargs and 'deleted_actor' not in kwargs['update_fields']: + kwargs['update_fields'].append('deleted_actor') + # For compatibility with Django 1.4.x, attempt to handle any calls to # save that pass update_fields. try: diff --git a/awx/main/models/base.py b/awx/main/models/base.py index fcca82474c..663711cafa 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -221,7 +221,46 @@ class PasswordFieldsModel(BaseModel): update_fields.append(field) -class PrimordialModel(CreatedModifiedModel): +class HasEditsMixin(BaseModel): + """Mixin which will keep the versions of field values from last edit + so we can tell if current model has unsaved changes. + """ + + class Meta: + abstract = True + + @classmethod + def _get_editable_fields(cls): + fds = set([]) + for field in cls._meta.concrete_fields: + if hasattr(field, 'attname'): + if field.attname == 'id': + continue + elif field.attname.endswith('ptr_id'): + # polymorphic fields should always be non-editable, see: + # https://github.com/django-polymorphic/django-polymorphic/issues/349 + continue + if getattr(field, 'editable', True): + fds.add(field.attname) + return fds + + def _get_fields_snapshot(self, fields_set=None): + new_values = {} + if fields_set is None: + fields_set = self._get_editable_fields() + for attr, val in self.__dict__.items(): + if attr in fields_set: + new_values[attr] = val + return new_values + + def _values_have_edits(self, new_values): + return any( + new_values.get(fd_name, None) != self._prior_values_store.get(fd_name, None) + for fd_name in new_values.keys() + ) + + +class PrimordialModel(HasEditsMixin, CreatedModifiedModel): ''' Common model for all object types that have these standard fields must use a subclass CommonModel or CommonModelNameNotUnique though @@ -254,9 +293,13 @@ class PrimordialModel(CreatedModifiedModel): tags = TaggableManager(blank=True) + def __init__(self, *args, **kwargs): + r = super(PrimordialModel, self).__init__(*args, **kwargs) + self._prior_values_store = self._get_fields_snapshot() + return r + def save(self, *args, **kwargs): update_fields = kwargs.get('update_fields', []) - fields_are_specified = bool(update_fields) user = get_current_user() if user and not user.id: user = None @@ -264,15 +307,14 @@ class PrimordialModel(CreatedModifiedModel): self.created_by = user if 'created_by' not in update_fields: update_fields.append('created_by') - # Update modified_by if not called with update_fields, or if any - # editable fields are present in update_fields - if ( - (not fields_are_specified) or - any(getattr(self._meta.get_field(name), 'editable', True) for name in update_fields)): + # Update modified_by if any editable fields have changed + new_values = self._get_fields_snapshot() + if (not self.pk and not self.modified_by) or self._values_have_edits(new_values): self.modified_by = user if 'modified_by' not in update_fields: update_fields.append('modified_by') super(PrimordialModel, self).save(*args, **kwargs) + self._prior_values_store = new_values def clean_description(self): # Description should always be empty string, never null. diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 48205dea1f..a31131c198 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -14,7 +14,7 @@ from jinja2 import Template # Django from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.core.exceptions import ValidationError from django.utils.encoding import force_text @@ -419,7 +419,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): else: fmt_str = six.text_type('{}_{}') return fmt_str.format(type_alias, self.inputs.get('vault_id')) - return str(type_alias) + return six.text_type(type_alias) @staticmethod def unique_dict(cred_qs): @@ -623,6 +623,11 @@ class CredentialType(CommonModelNameNotUnique): if len(value): namespace[field_name] = value + # default missing boolean fields to False + for field in self.inputs.get('fields', []): + if field['type'] == 'boolean' and field['id'] not in credential.inputs.keys(): + namespace[field['id']] = safe_namespace[field['id']] = False + file_tmpls = self.injectors.get('file', {}) # If any file templates are provided, render the files and update the # special `tower` template namespace so the filename can be @@ -673,46 +678,46 @@ class CredentialType(CommonModelNameNotUnique): def ssh(cls): return cls( kind='ssh', - name='Machine', + name=ugettext_noop('Machine'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True }, { 'id': 'ssh_key_data', - 'label': 'SSH Private Key', + 'label': ugettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True }, { 'id': 'ssh_key_unlock', - 'label': 'Private Key Passphrase', + 'label': ugettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True, 'ask_at_runtime': True }, { 'id': 'become_method', - 'label': 'Privilege Escalation Method', + 'label': ugettext_noop('Privilege Escalation Method'), 'type': 'become_method', - 'help_text': ('Specify a method for "become" operations. This is ' - 'equivalent to specifying the --become-method ' - 'Ansible parameter.') + 'help_text': ugettext_noop('Specify a method for "become" operations. This is ' + 'equivalent to specifying the --become-method ' + 'Ansible parameter.') }, { 'id': 'become_username', - 'label': 'Privilege Escalation Username', + 'label': ugettext_noop('Privilege Escalation Username'), 'type': 'string', }, { 'id': 'become_password', - 'label': 'Privilege Escalation Password', + 'label': ugettext_noop('Privilege Escalation Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True @@ -728,28 +733,28 @@ def ssh(cls): def scm(cls): return cls( kind='scm', - name='Source Control', + name=ugettext_noop('Source Control'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True }, { 'id': 'ssh_key_data', - 'label': 'SCM Private Key', + 'label': ugettext_noop('SCM Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True }, { 'id': 'ssh_key_unlock', - 'label': 'Private Key Passphrase', + 'label': ugettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True }], @@ -764,25 +769,25 @@ def scm(cls): def vault(cls): return cls( kind='vault', - name='Vault', + name=ugettext_noop('Vault'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'vault_password', - 'label': 'Vault Password', + 'label': ugettext_noop('Vault Password'), 'type': 'string', 'secret': True, 'ask_at_runtime': True }, { 'id': 'vault_id', - 'label': 'Vault Identifier', + 'label': ugettext_noop('Vault Identifier'), 'type': 'string', 'format': 'vault_id', - 'help_text': ('Specify an (optional) Vault ID. This is ' - 'equivalent to specifying the --vault-id ' - 'Ansible parameter for providing multiple Vault ' - 'passwords. Note: this feature only works in ' - 'Ansible 2.4+.') + 'help_text': ugettext_noop('Specify an (optional) Vault ID. This is ' + 'equivalent to specifying the --vault-id ' + 'Ansible parameter for providing multiple Vault ' + 'passwords. Note: this feature only works in ' + 'Ansible 2.4+.') }], 'required': ['vault_password'], } @@ -793,37 +798,37 @@ def vault(cls): def net(cls): return cls( kind='net', - name='Network', + name=ugettext_noop('Network'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }, { 'id': 'ssh_key_data', - 'label': 'SSH Private Key', + 'label': ugettext_noop('SSH Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True }, { 'id': 'ssh_key_unlock', - 'label': 'Private Key Passphrase', + 'label': ugettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True, }, { 'id': 'authorize', - 'label': 'Authorize', + 'label': ugettext_noop('Authorize'), 'type': 'boolean', }, { 'id': 'authorize_password', - 'label': 'Authorize Password', + 'label': ugettext_noop('Authorize Password'), 'type': 'string', 'secret': True, }], @@ -840,27 +845,27 @@ def net(cls): def aws(cls): return cls( kind='cloud', - name='Amazon Web Services', + name=ugettext_noop('Amazon Web Services'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Access Key', + 'label': ugettext_noop('Access Key'), 'type': 'string' }, { 'id': 'password', - 'label': 'Secret Key', + 'label': ugettext_noop('Secret Key'), 'type': 'string', 'secret': True, }, { 'id': 'security_token', - 'label': 'STS Token', + 'label': ugettext_noop('STS Token'), 'type': 'string', 'secret': True, - 'help_text': ('Security Token Service (STS) is a web service ' - 'that enables you to request temporary, ' - 'limited-privilege credentials for AWS Identity ' - 'and Access Management (IAM) users.'), + 'help_text': ugettext_noop('Security Token Service (STS) is a web service ' + 'that enables you to request temporary, ' + 'limited-privilege credentials for AWS Identity ' + 'and Access Management (IAM) users.'), }], 'required': ['username', 'password'] } @@ -871,36 +876,36 @@ def aws(cls): def openstack(cls): return cls( kind='cloud', - name='OpenStack', + name=ugettext_noop('OpenStack'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password (API Key)', + 'label': ugettext_noop('Password (API Key)'), 'type': 'string', 'secret': True, }, { 'id': 'host', - 'label': 'Host (Authentication URL)', + 'label': ugettext_noop('Host (Authentication URL)'), 'type': 'string', - 'help_text': ('The host to authenticate with. For example, ' - 'https://openstack.business.com/v2.0/') + 'help_text': ugettext_noop('The host to authenticate with. For example, ' + 'https://openstack.business.com/v2.0/') }, { 'id': 'project', - 'label': 'Project (Tenant Name)', + 'label': ugettext_noop('Project (Tenant Name)'), 'type': 'string', }, { 'id': 'domain', - 'label': 'Domain Name', + 'label': ugettext_noop('Domain Name'), 'type': 'string', - 'help_text': ('OpenStack domains define administrative boundaries. ' - 'It is only needed for Keystone v3 authentication ' - 'URLs. Refer to Ansible Tower documentation for ' - 'common scenarios.') + 'help_text': ugettext_noop('OpenStack domains define administrative boundaries. ' + 'It is only needed for Keystone v3 authentication ' + 'URLs. Refer to Ansible Tower documentation for ' + 'common scenarios.') }], 'required': ['username', 'password', 'host', 'project'] } @@ -911,22 +916,22 @@ def openstack(cls): def vmware(cls): return cls( kind='cloud', - name='VMware vCenter', + name=ugettext_noop('VMware vCenter'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'host', - 'label': 'VCenter Host', + 'label': ugettext_noop('VCenter Host'), 'type': 'string', - 'help_text': ('Enter the hostname or IP address that corresponds ' - 'to your VMware vCenter.') + 'help_text': ugettext_noop('Enter the hostname or IP address that corresponds ' + 'to your VMware vCenter.') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }], @@ -939,22 +944,22 @@ def vmware(cls): def satellite6(cls): return cls( kind='cloud', - name='Red Hat Satellite 6', + name=ugettext_noop('Red Hat Satellite 6'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'host', - 'label': 'Satellite 6 URL', + 'label': ugettext_noop('Satellite 6 URL'), 'type': 'string', - 'help_text': ('Enter the URL that corresponds to your Red Hat ' - 'Satellite 6 server. For example, https://satellite.example.org') + 'help_text': ugettext_noop('Enter the URL that corresponds to your Red Hat ' + 'Satellite 6 server. For example, https://satellite.example.org') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }], @@ -967,23 +972,23 @@ def satellite6(cls): def cloudforms(cls): return cls( kind='cloud', - name='Red Hat CloudForms', + name=ugettext_noop('Red Hat CloudForms'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'host', - 'label': 'CloudForms URL', + 'label': ugettext_noop('CloudForms URL'), 'type': 'string', - 'help_text': ('Enter the URL for the virtual machine that ' - 'corresponds to your CloudForm instance. ' - 'For example, https://cloudforms.example.org') + 'help_text': ugettext_noop('Enter the URL for the virtual machine that ' + 'corresponds to your CloudForm instance. ' + 'For example, https://cloudforms.example.org') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }], @@ -996,32 +1001,32 @@ def cloudforms(cls): def gce(cls): return cls( kind='cloud', - name='Google Compute Engine', + name=ugettext_noop('Google Compute Engine'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Service Account Email Address', + 'label': ugettext_noop('Service Account Email Address'), 'type': 'string', - 'help_text': ('The email address assigned to the Google Compute ' - 'Engine service account.') + 'help_text': ugettext_noop('The email address assigned to the Google Compute ' + 'Engine service account.') }, { 'id': 'project', 'label': 'Project', 'type': 'string', - 'help_text': ('The Project ID is the GCE assigned identification. ' - 'It is often constructed as three words or two words ' - 'followed by a three-digit number. Examples: project-id-000 ' - 'and another-project-id') + 'help_text': ugettext_noop('The Project ID is the GCE assigned identification. ' + 'It is often constructed as three words or two words ' + 'followed by a three-digit number. Examples: project-id-000 ' + 'and another-project-id') }, { 'id': 'ssh_key_data', - 'label': 'RSA Private Key', + 'label': ugettext_noop('RSA Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True, - 'help_text': ('Paste the contents of the PEM file associated ' - 'with the service account email.') + 'help_text': ugettext_noop('Paste the contents of the PEM file associated ' + 'with the service account email.') }], 'required': ['username', 'ssh_key_data'], } @@ -1032,43 +1037,43 @@ def gce(cls): def azure_rm(cls): return cls( kind='cloud', - name='Microsoft Azure Resource Manager', + name=ugettext_noop('Microsoft Azure Resource Manager'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'subscription', - 'label': 'Subscription ID', + 'label': ugettext_noop('Subscription ID'), 'type': 'string', - 'help_text': ('Subscription ID is an Azure construct, which is ' - 'mapped to a username.') + 'help_text': ugettext_noop('Subscription ID is an Azure construct, which is ' + 'mapped to a username.') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }, { 'id': 'client', - 'label': 'Client ID', + 'label': ugettext_noop('Client ID'), 'type': 'string' }, { 'id': 'secret', - 'label': 'Client Secret', + 'label': ugettext_noop('Client Secret'), 'type': 'string', 'secret': True, }, { 'id': 'tenant', - 'label': 'Tenant ID', + 'label': ugettext_noop('Tenant ID'), 'type': 'string' }, { 'id': 'cloud_environment', - 'label': 'Azure Cloud Environment', + 'label': ugettext_noop('Azure Cloud Environment'), 'type': 'string', - 'help_text': ('Environment variable AZURE_CLOUD_ENVIRONMENT when' - ' using Azure GovCloud or Azure stack.') + 'help_text': ugettext_noop('Environment variable AZURE_CLOUD_ENVIRONMENT when' + ' using Azure GovCloud or Azure stack.') }], 'required': ['subscription'], } @@ -1079,16 +1084,16 @@ def azure_rm(cls): def insights(cls): return cls( kind='insights', - name='Insights', + name=ugettext_noop('Insights'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True }], @@ -1107,28 +1112,28 @@ def insights(cls): def rhv(cls): return cls( kind='cloud', - name='Red Hat Virtualization', + name=ugettext_noop('Red Hat Virtualization'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'host', - 'label': 'Host (Authentication URL)', + 'label': ugettext_noop('Host (Authentication URL)'), 'type': 'string', - 'help_text': ('The host to authenticate with.') + 'help_text': ugettext_noop('The host to authenticate with.') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }, { 'id': 'ca_file', - 'label': 'CA File', + 'label': ugettext_noop('CA File'), 'type': 'string', - 'help_text': ('Absolute file path to the CA file to use (optional)') + 'help_text': ugettext_noop('Absolute file path to the CA file to use (optional)') }], 'required': ['host', 'username', 'password'], }, @@ -1159,26 +1164,26 @@ def rhv(cls): def tower(cls): return cls( kind='cloud', - name='Ansible Tower', + name=ugettext_noop('Ansible Tower'), managed_by_tower=True, inputs={ 'fields': [{ 'id': 'host', - 'label': 'Ansible Tower Hostname', + 'label': ugettext_noop('Ansible Tower Hostname'), 'type': 'string', - 'help_text': ('The Ansible Tower base URL to authenticate with.') + 'help_text': ugettext_noop('The Ansible Tower base URL to authenticate with.') }, { 'id': 'username', - 'label': 'Username', + 'label': ugettext_noop('Username'), 'type': 'string' }, { 'id': 'password', - 'label': 'Password', + 'label': ugettext_noop('Password'), 'type': 'string', 'secret': True, }, { 'id': 'verify_ssl', - 'label': 'Verify SSL', + 'label': ugettext_noop('Verify SSL'), 'type': 'boolean', 'secret': False }], diff --git a/awx/main/models/events.py b/awx/main/models/events.py index a6e2c67c74..4361f01853 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -1,5 +1,6 @@ import datetime import logging +from collections import defaultdict from django.conf import settings from django.db import models, DatabaseError @@ -39,6 +40,21 @@ def sanitize_event_keys(kwargs, valid_keys): kwargs[key] = Truncator(kwargs[key]).chars(1024) +def create_host_status_counts(event_data): + host_status = {} + host_status_keys = ['skipped', 'ok', 'changed', 'failures', 'dark'] + + for key in host_status_keys: + for host in event_data.get(key, {}): + host_status[host] = key + + host_status_counts = defaultdict(lambda: 0) + + for value in host_status.values(): + host_status_counts[value] += 1 + + return dict(host_status_counts) + class BasePlaybookEvent(CreatedModifiedModel): ''' @@ -194,6 +210,9 @@ class BasePlaybookEvent(CreatedModifiedModel): def event_level(self): return self.LEVEL_FOR_EVENT.get(self.event, 0) + def get_host_status_counts(self): + return create_host_status_counts(getattr(self, 'event_data', {})) + def get_event_display2(self): msg = self.get_event_display() if self.event == 'playbook_on_play_start': @@ -588,6 +607,9 @@ class BaseCommandEvent(CreatedModifiedModel): ''' return self.event + def get_host_status_counts(self): + return create_host_status_counts(getattr(self, 'event_data', {})) + class AdHocCommandEvent(BaseCommandEvent): diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 471276c560..0956ba00be 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -1,8 +1,11 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. +import six +import random from decimal import Decimal +from django.core.exceptions import ValidationError from django.db import models, connection from django.db.models.signals import post_save, post_delete from django.dispatch import receiver @@ -16,6 +19,7 @@ from awx import __version__ as awx_application_version from awx.api.versioning import reverse from awx.main.managers import InstanceManager, InstanceGroupManager from awx.main.fields import JSONField +from awx.main.models.base import BaseModel, HasEditsMixin from awx.main.models.inventory import InventoryUpdate from awx.main.models.jobs import Job from awx.main.models.projects import ProjectUpdate @@ -26,7 +30,37 @@ from awx.main.models.mixins import RelatedJobsMixin __all__ = ('Instance', 'InstanceGroup', 'JobOrigin', 'TowerScheduleState',) -class Instance(models.Model): +def validate_queuename(v): + # celery and kombu don't play nice with unicode in queue names + if v: + try: + '{}'.format(v.decode('utf-8')) + except UnicodeEncodeError: + raise ValidationError(_(six.text_type('{} contains unsupported characters')).format(v)) + + +class HasPolicyEditsMixin(HasEditsMixin): + + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + r = super(BaseModel, self).__init__(*args, **kwargs) + self._prior_values_store = self._get_fields_snapshot() + return r + + def save(self, *args, **kwargs): + super(BaseModel, self).save(*args, **kwargs) + self._prior_values_store = self._get_fields_snapshot() + + def has_policy_changes(self): + if not hasattr(self, 'POLICY_FIELDS'): + raise RuntimeError('HasPolicyEditsMixin Model needs to set POLICY_FIELDS') + new_values = self._get_fields_snapshot(fields_set=self.POLICY_FIELDS) + return self._values_have_edits(new_values) + + +class Instance(HasPolicyEditsMixin, BaseModel): """A model representing an AWX instance running against this database.""" objects = InstanceManager() @@ -37,7 +71,6 @@ class Instance(models.Model): last_isolated_check = models.DateTimeField( null=True, editable=False, - auto_now_add=True ) version = models.CharField(max_length=24, blank=True) capacity = models.PositiveIntegerField( @@ -52,6 +85,9 @@ class Instance(models.Model): enabled = models.BooleanField( default=True ) + managed_by_policy = models.BooleanField( + default=True + ) cpu = models.IntegerField( default=0, editable=False, @@ -72,6 +108,8 @@ class Instance(models.Model): class Meta: app_label = 'main' + POLICY_FIELDS = frozenset(('managed_by_policy', 'hostname', 'capacity_adjustment')) + def get_absolute_url(self, request=None): return reverse('api:instance_detail', kwargs={'pk': self.pk}, request=request) @@ -80,6 +118,10 @@ class Instance(models.Model): return sum(x.task_impact for x in UnifiedJob.objects.filter(execution_node=self.hostname, status__in=('running', 'waiting'))) + @property + def remaining_capacity(self): + return self.capacity - self.consumed_capacity + @property def role(self): # NOTE: TODO: Likely to repurpose this once standalone ramparts are a thing @@ -89,6 +131,10 @@ class Instance(models.Model): def jobs_running(self): return UnifiedJob.objects.filter(execution_node=self.hostname, status__in=('running', 'waiting',)).count() + @property + def jobs_total(self): + return UnifiedJob.objects.filter(execution_node=self.hostname).count() + def is_lost(self, ref_time=None, isolated=False): if ref_time is None: ref_time = now() @@ -100,6 +146,8 @@ class Instance(models.Model): def is_controller(self): return Instance.objects.filter(rampart_groups__controller__instances=self).exists() + def is_isolated(self): + return self.rampart_groups.filter(controller__isnull=False).exists() def refresh_capacity(self): cpu = get_cpu_capacity() @@ -113,9 +161,13 @@ class Instance(models.Model): self.save(update_fields=['capacity', 'version', 'modified', 'cpu', 'memory', 'cpu_capacity', 'mem_capacity']) + def clean_hostname(self): + validate_queuename(self.hostname) + return self.hostname + -class InstanceGroup(models.Model, RelatedJobsMixin): +class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): """A model representing a Queue/Group of AWX Instances.""" objects = InstanceGroupManager() @@ -150,6 +202,10 @@ class InstanceGroup(models.Model, RelatedJobsMixin): help_text=_("List of exact-match Instances that will always be automatically assigned to this group") ) + POLICY_FIELDS = frozenset(( + 'policy_instance_list', 'policy_instance_minimum', 'policy_instance_percentage', 'controller' + )) + def get_absolute_url(self, request=None): return reverse('api:instance_group_detail', kwargs={'pk': self.pk}, request=request) @@ -157,6 +213,15 @@ class InstanceGroup(models.Model, RelatedJobsMixin): def capacity(self): return sum([inst.capacity for inst in self.instances.all()]) + @property + def jobs_running(self): + return UnifiedJob.objects.filter(status__in=('running', 'waiting'), + instance_group=self).count() + + @property + def jobs_total(self): + return UnifiedJob.objects.filter(instance_group=self).count() + ''' RelatedJobsMixin ''' @@ -167,6 +232,37 @@ class InstanceGroup(models.Model, RelatedJobsMixin): class Meta: app_label = 'main' + def clean_name(self): + validate_queuename(self.name) + return self.name + + def fit_task_to_most_remaining_capacity_instance(self, task): + instance_most_capacity = None + for i in self.instances.filter(capacity__gt=0).order_by('hostname'): + if not i.enabled: + continue + if i.remaining_capacity >= task.task_impact and \ + (instance_most_capacity is None or + i.remaining_capacity > instance_most_capacity.remaining_capacity): + instance_most_capacity = i + return instance_most_capacity + + def find_largest_idle_instance(self): + largest_instance = None + for i in self.instances.filter(capacity__gt=0).order_by('hostname'): + if i.jobs_running == 0: + if largest_instance is None: + largest_instance = i + elif i.capacity > largest_instance.capacity: + largest_instance = i + return largest_instance + + def choose_online_controller_node(self): + return random.choice(list(self.controller + .instances + .filter(capacity__gt=0) + .values_list('hostname', flat=True))) + class TowerScheduleState(SingletonModel): schedule_last_run = models.DateTimeField(auto_now_add=True) @@ -190,29 +286,31 @@ class JobOrigin(models.Model): app_label = 'main' -@receiver(post_save, sender=InstanceGroup) -def on_instance_group_saved(sender, instance, created=False, raw=False, **kwargs): +def schedule_policy_task(): from awx.main.tasks import apply_cluster_membership_policies connection.on_commit(lambda: apply_cluster_membership_policies.apply_async()) +@receiver(post_save, sender=InstanceGroup) +def on_instance_group_saved(sender, instance, created=False, raw=False, **kwargs): + if created or instance.has_policy_changes(): + schedule_policy_task() + + @receiver(post_save, sender=Instance) def on_instance_saved(sender, instance, created=False, raw=False, **kwargs): - if created: - from awx.main.tasks import apply_cluster_membership_policies - connection.on_commit(lambda: apply_cluster_membership_policies.apply_async()) + if created or instance.has_policy_changes(): + schedule_policy_task() @receiver(post_delete, sender=InstanceGroup) def on_instance_group_deleted(sender, instance, using, **kwargs): - from awx.main.tasks import apply_cluster_membership_policies - connection.on_commit(lambda: apply_cluster_membership_policies.apply_async()) + schedule_policy_task() @receiver(post_delete, sender=Instance) def on_instance_deleted(sender, instance, using, **kwargs): - from awx.main.tasks import apply_cluster_membership_policies - connection.on_commit(lambda: apply_cluster_membership_policies.apply_async()) + schedule_policy_task() # Unfortunately, the signal can't just be connected against UnifiedJob; it diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 24d0e9a1c5..76fee71175 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1420,7 +1420,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, RelatedJobsMix @classmethod def _get_unified_job_field_names(cls): return set(f.name for f in InventorySourceOptions._meta.fields) | set( - ['name', 'description', 'schedule', 'credentials'] + ['name', 'description', 'schedule', 'credentials', 'inventory'] ) def save(self, *args, **kwargs): @@ -1599,6 +1599,13 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin, class Meta: app_label = 'main' + inventory = models.ForeignKey( + 'Inventory', + related_name='inventory_updates', + null=True, + default=None, + on_delete=models.DO_NOTHING, + ) inventory_source = models.ForeignKey( 'InventorySource', related_name='inventory_updates', diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 80280636cc..0fff829d23 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -33,7 +33,7 @@ from awx.main.models.notifications import ( NotificationTemplate, JobNotificationMixin, ) -from awx.main.utils import parse_yaml_or_json +from awx.main.utils import parse_yaml_or_json, getattr_dne from awx.main.fields import ImplicitRoleField from awx.main.models.mixins import ( ResourceMixin, @@ -278,7 +278,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour allows_field='credentials' ) admin_role = ImplicitRoleField( - parent_role=['project.organization.project_admin_role', 'inventory.organization.inventory_admin_role'] + parent_role=['project.organization.job_template_admin_role', 'inventory.organization.job_template_admin_role'] ) execute_role = ImplicitRoleField( parent_role=['admin_role', 'project.organization.execute_role', 'inventory.organization.execute_role'], @@ -343,11 +343,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour # not block a provisioning callback from creating/launching jobs. if callback_extra_vars is None: for ask_field_name in set(self.get_ask_mapping().values()): - if ask_field_name == 'ask_credential_on_launch': - # if ask_credential_on_launch is True, it just means it can - # optionally be specified at launch time, not that it's *required* - # to launch - continue if getattr(self, ask_field_name): prompting_needed = True break @@ -402,7 +397,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour if 'prompts' not in exclude_errors: errors_dict[field_name] = _('Field is not configured to prompt on launch.').format(field_name=field_name) - if 'prompts' not in exclude_errors and self.passwords_needed_to_start: + if ('prompts' not in exclude_errors and + (not getattr(self, 'ask_credential_on_launch', False)) and + self.passwords_needed_to_start): errors_dict['passwords_needed_to_start'] = _( 'Saved launch configurations cannot provide passwords needed to start.') @@ -772,9 +769,13 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana if not os.path.realpath(filepath).startswith(destination): system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name))) continue - with codecs.open(filepath, 'w', encoding='utf-8') as f: - os.chmod(f.name, 0o600) - json.dump(host.ansible_facts, f) + try: + with codecs.open(filepath, 'w', encoding='utf-8') as f: + os.chmod(f.name, 0o600) + json.dump(host.ansible_facts, f) + except IOError: + system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name))) + continue # make note of the time we wrote the file so we can check if it changed later modification_times[filepath] = os.path.getmtime(filepath) @@ -957,24 +958,37 @@ class JobLaunchConfig(LaunchTimeConfig): editable=False, ) + def has_user_prompts(self, template): + ''' + Returns True if any fields exist in the launch config that are + not permissions exclusions + (has to exist because of callback relaunch exception) + ''' + return self._has_user_prompts(template, only_unprompted=False) + def has_unprompted(self, template): ''' - returns False if the template has set ask_ fields to False after + returns True if the template has set ask_ fields to False after launching with those prompts ''' + return self._has_user_prompts(template, only_unprompted=True) + + def _has_user_prompts(self, template, only_unprompted=True): prompts = self.prompts_dict() ask_mapping = template.get_ask_mapping() if template.survey_enabled and (not template.ask_variables_on_launch): ask_mapping.pop('extra_vars') - provided_vars = set(prompts['extra_vars'].keys()) + provided_vars = set(prompts.get('extra_vars', {}).keys()) survey_vars = set( element.get('variable') for element in template.survey_spec.get('spec', {}) if 'variable' in element ) - if provided_vars - survey_vars: + if (provided_vars and not only_unprompted) or (provided_vars - survey_vars): return True for field_name, ask_field_name in ask_mapping.items(): - if field_name in prompts and not getattr(template, ask_field_name): + if field_name in prompts and not (getattr(template, ask_field_name) and only_unprompted): + if field_name == 'limit' and self.job and self.job.launch_type == 'callback': + continue # exception for relaunching callbacks return True else: return False @@ -1019,7 +1033,8 @@ class JobHostSummary(CreatedModifiedModel): failed = models.BooleanField(default=False, editable=False) def __unicode__(self): - hostname = self.host.name if self.host else 'N/A' + host = getattr_dne(self, 'host') + hostname = host.name if host else 'N/A' return '%s changed=%d dark=%d failures=%d ok=%d processed=%d skipped=%s' % \ (hostname, self.changed, self.dark, self.failures, self.ok, self.processed, self.skipped) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 7d563f1e36..7dcb560aa2 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -436,7 +436,8 @@ class CustomVirtualEnvMixin(models.Model): blank=True, null=True, default=None, - max_length=100 + max_length=100, + help_text=_('Local absolute file path containing a custom Python virtualenv to use') ) def clean_custom_virtualenv(self): @@ -465,7 +466,7 @@ class RelatedJobsMixin(object): return self._get_related_jobs().filter(status__in=ACTIVE_STATES) ''' - Returns [{'id': '1', 'type': 'job'}, {'id': 2, 'type': 'project_update'}, ...] + Returns [{'id': 1, 'type': 'job'}, {'id': 2, 'type': 'project_update'}, ...] ''' def get_active_jobs(self): UnifiedJob = apps.get_model('main', 'UnifiedJob') @@ -474,5 +475,5 @@ class RelatedJobsMixin(object): if not isinstance(jobs, QuerySet): raise RuntimeError("Programmer error. Expected _get_active_jobs() to return a QuerySet.") - return [dict(id=str(t[0]), type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')] + return [dict(id=t[0], type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')] diff --git a/awx/main/models/oauth.py b/awx/main/models/oauth.py index 45e13fc8b0..63a70ac016 100644 --- a/awx/main/models/oauth.py +++ b/awx/main/models/oauth.py @@ -11,7 +11,9 @@ from django.conf import settings # Django OAuth Toolkit from oauth2_provider.models import AbstractApplication, AbstractAccessToken from oauth2_provider.generators import generate_client_secret +from oauthlib import oauth2 +from awx.main.utils import get_external_account from awx.main.fields import OAuth2ClientSecretField @@ -25,6 +27,7 @@ class OAuth2Application(AbstractApplication): class Meta: app_label = 'main' verbose_name = _('application') + unique_together = (("name", "organization"),) CLIENT_CONFIDENTIAL = "confidential" CLIENT_PUBLIC = "public" @@ -36,12 +39,10 @@ class OAuth2Application(AbstractApplication): GRANT_AUTHORIZATION_CODE = "authorization-code" GRANT_IMPLICIT = "implicit" GRANT_PASSWORD = "password" - GRANT_CLIENT_CREDENTIALS = "client-credentials" GRANT_TYPES = ( (GRANT_AUTHORIZATION_CODE, _("Authorization code")), (GRANT_IMPLICIT, _("Implicit")), (GRANT_PASSWORD, _("Resource owner password-based")), - (GRANT_CLIENT_CREDENTIALS, _("Client credentials")), ) description = models.TextField( @@ -109,8 +110,13 @@ class OAuth2AccessToken(AbstractAccessToken): ) scope = models.TextField( blank=True, + default='write', help_text=_('Allowed scopes, further restricts user\'s permissions. Must be a simple space-separated string with allowed scopes [\'read\', \'write\'].') ) + modified = models.DateTimeField( + editable=False, + auto_now=True + ) def is_valid(self, scopes=None): valid = super(OAuth2AccessToken, self).is_valid(scopes) @@ -119,3 +125,11 @@ class OAuth2AccessToken(AbstractAccessToken): self.save(update_fields=['last_used']) return valid + def save(self, *args, **kwargs): + if self.user and settings.ALLOW_OAUTH2_FOR_EXTERNAL_USERS is False: + external_account = get_external_account(self.user) + if external_account is not None: + raise oauth2.AccessDeniedError(_( + 'OAuth2 Tokens cannot be created by users associated with an external authentication provider ({})' + ).format(external_account)) + super(OAuth2AccessToken, self).save(*args, **kwargs) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index db406fd2ed..d1373442b2 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -60,16 +60,21 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi notification_admin_role = ImplicitRoleField( parent_role='admin_role', ) + job_template_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - parent_role=['admin_role', 'execute_role', 'project_admin_role', - 'inventory_admin_role', 'workflow_admin_role', - 'notification_admin_role', 'credential_admin_role'] + parent_role=['admin_role'] ) read_role = ImplicitRoleField( - parent_role=['member_role', 'auditor_role'], + parent_role=['member_role', 'auditor_role', + 'execute_role', 'project_admin_role', + 'inventory_admin_role', 'workflow_admin_role', + 'notification_admin_role', 'credential_admin_role', + 'job_template_admin_role',], ) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 3bad19c8eb..dce47eb8dd 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -324,13 +324,9 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn ['name', 'description', 'schedule'] ) - def __init__(self, *args, **kwargs): - r = super(Project, self).__init__(*args, **kwargs) - self._prior_values_store = self._current_sensitive_fields() - return r - def save(self, *args, **kwargs): new_instance = not bool(self.pk) + pre_save_vals = getattr(self, '_prior_values_store', {}) # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) @@ -361,21 +357,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn self.save(update_fields=update_fields) # If we just created a new project with SCM, start the initial update. # also update if certain fields have changed - relevant_change = False - new_values = self._current_sensitive_fields() - if hasattr(self, '_prior_values_store') and self._prior_values_store != new_values: - relevant_change = True - self._prior_values_store = new_values + relevant_change = any( + pre_save_vals.get(fd_name, None) != self._prior_values_store.get(fd_name, None) + for fd_name in self.FIELDS_TRIGGER_UPDATE + ) if (relevant_change or new_instance) and (not skip_update) and self.scm_type: self.update() - def _current_sensitive_fields(self): - new_values = {} - for attr, val in self.__dict__.items(): - if attr in Project.FIELDS_TRIGGER_UPDATE: - new_values[attr] = val - return new_values - def _get_current_status(self): if self.scm_type: if self.current_job and self.current_job.status: diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 7ce8567e88..740aa8ebd2 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -40,6 +40,7 @@ role_names = { 'project_admin_role': _('Project Admin'), 'inventory_admin_role': _('Inventory Admin'), 'credential_admin_role': _('Credential Admin'), + 'job_template_admin_role': _('Job Template Admin'), 'workflow_admin_role': _('Workflow Admin'), 'notification_admin_role': _('Notification Admin'), 'auditor_role': _('Auditor'), @@ -58,6 +59,7 @@ role_descriptions = { 'project_admin_role': _('Can manage all projects of the %s'), 'inventory_admin_role': _('Can manage all inventories of the %s'), 'credential_admin_role': _('Can manage all credentials of the %s'), + 'job_template_admin_role': _('Can manage all job templates of the %s'), 'workflow_admin_role': _('Can manage all workflows of the %s'), 'notification_admin_role': _('Can manage all notifications of the %s'), 'auditor_role': _('Can view all settings for the %s'), diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f758a230a9..864655f60a 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -36,7 +36,7 @@ from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin from awx.main.utils import ( encrypt_dict, decrypt_field, _inventory_updates, copy_model_by_class, copy_m2m_relationships, - get_type_for_model, parse_yaml_or_json + get_type_for_model, parse_yaml_or_json, getattr_dne ) from awx.main.utils import polymorphic from awx.main.constants import ACTIVE_STATES, CAN_CANCEL @@ -507,7 +507,8 @@ class StdoutMaxBytesExceeded(Exception): self.supported = supported -class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, UnifiedJobTypeStringMixin, TaskManagerUnifiedJobMixin): +class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, + UnifiedJobTypeStringMixin, TaskManagerUnifiedJobMixin): ''' Concrete base class for unified job run by the task engine. ''' @@ -571,6 +572,12 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique editable=False, help_text=_("The node the job executed on."), ) + controller_node = models.TextField( + blank=True, + default='', + editable=False, + help_text=_("The instance that managed the isolated execution environment."), + ) notifications = models.ManyToManyField( 'Notification', editable=False, @@ -814,7 +821,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique # Done. return result - def copy_unified_job(self, limit=None): + def copy_unified_job(self, _eager_fields=None, **new_prompts): ''' Returns saved object, including related fields. Create a copy of this unified job for the purpose of relaunch @@ -824,12 +831,14 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique parent_field_name = unified_job_class._get_parent_field_name() fields = unified_jt_class._get_unified_job_field_names() | set([parent_field_name]) - create_data = {"launch_type": "relaunch"} - if limit: - create_data["limit"] = limit + create_data = {} + if _eager_fields: + create_data = _eager_fields.copy() + create_data["launch_type"] = "relaunch" prompts = self.launch_prompts() - if self.unified_job_template and prompts: + if self.unified_job_template and (prompts is not None): + prompts.update(new_prompts) prompts['_eager_fields'] = create_data unified_job = self.unified_job_template.create_unified_job(**prompts) else: @@ -1226,17 +1235,8 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique raise RuntimeError("Expected celery_task_id to be set on model.") kwargs['task_id'] = self.celery_task_id task_class = self._get_task_class() - from awx.main.models.ha import InstanceGroup - ig = InstanceGroup.objects.get(name=queue) - args = [self.pk] - if ig.controller_id: - if self.supports_isolation(): # case of jobs and ad hoc commands - isolated_instance = ig.instances.order_by('-capacity').first() - args.append(isolated_instance.hostname) - else: # proj & inv updates, system jobs run on controller - queue = ig.controller.name kwargs['queue'] = queue - task_class().apply_async(args, opts, **kwargs) + task_class().apply_async([self.pk], opts, **kwargs) def start(self, error_callback, success_callback, **kwargs): ''' @@ -1376,25 +1376,34 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique for name in ('awx', 'tower'): r['{}_job_id'.format(name)] = self.pk r['{}_job_launch_type'.format(name)] = self.launch_type - if self.created_by: - for name in ('awx', 'tower'): - r['{}_user_id'.format(name)] = self.created_by.pk - r['{}_user_name'.format(name)] = self.created_by.username - r['{}_user_email'.format(name)] = self.created_by.email - r['{}_user_first_name'.format(name)] = self.created_by.first_name - r['{}_user_last_name'.format(name)] = self.created_by.last_name - else: + + created_by = getattr_dne(self, 'created_by') + + if not created_by: wj = self.get_workflow_job() if wj: for name in ('awx', 'tower'): r['{}_workflow_job_id'.format(name)] = wj.pk r['{}_workflow_job_name'.format(name)] = wj.name - if wj.created_by: - for name in ('awx', 'tower'): - r['{}_user_id'.format(name)] = wj.created_by.pk - r['{}_user_name'.format(name)] = wj.created_by.username - if self.schedule: + created_by = getattr_dne(wj, 'created_by') + + schedule = getattr_dne(self, 'schedule') + if schedule: for name in ('awx', 'tower'): - r['{}_schedule_id'.format(name)] = self.schedule.pk - r['{}_schedule_name'.format(name)] = self.schedule.name + r['{}_schedule_id'.format(name)] = schedule.pk + r['{}_schedule_name'.format(name)] = schedule.name + + if created_by: + for name in ('awx', 'tower'): + r['{}_user_id'.format(name)] = created_by.pk + r['{}_user_name'.format(name)] = created_by.username + r['{}_user_email'.format(name)] = created_by.email + r['{}_user_first_name'.format(name)] = created_by.first_name + r['{}_user_last_name'.format(name)] = created_by.last_name return r + + def get_celery_queue_name(self): + return self.controller_node or self.execution_node or settings.CELERY_DEFAULT_QUEUE + + def is_isolated(self): + return bool(self.controller_node) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index c63bbc6f1f..198595424f 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -288,7 +288,8 @@ class WorkflowJobOptions(BaseModel): def create_relaunch_workflow_job(self): new_workflow_job = self.copy_unified_job() - new_workflow_job.copy_nodes_from_original(original=self) + if self.workflow_job_template is None: + new_workflow_job.copy_nodes_from_original(original=self) return new_workflow_job @@ -370,23 +371,28 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl return workflow_job def _accept_or_ignore_job_kwargs(self, _exclude_errors=(), **kwargs): - prompted_fields = {} - rejected_fields = {} - accepted_vars, rejected_vars, errors_dict = self.accept_or_ignore_variables(kwargs.get('extra_vars', {})) + exclude_errors = kwargs.pop('_exclude_errors', []) + prompted_data = {} + rejected_data = {} + accepted_vars, rejected_vars, errors_dict = self.accept_or_ignore_variables( + kwargs.get('extra_vars', {}), + _exclude_errors=exclude_errors, + extra_passwords=kwargs.get('survey_passwords', {})) if accepted_vars: - prompted_fields['extra_vars'] = accepted_vars + prompted_data['extra_vars'] = accepted_vars if rejected_vars: - rejected_fields['extra_vars'] = rejected_vars + rejected_data['extra_vars'] = rejected_vars # WFJTs do not behave like JTs, it can not accept inventory, credential, etc. bad_kwargs = kwargs.copy() bad_kwargs.pop('extra_vars', None) + bad_kwargs.pop('survey_passwords', None) if bad_kwargs: - rejected_fields.update(bad_kwargs) + rejected_data.update(bad_kwargs) for field in bad_kwargs: errors_dict[field] = _('Field is not allowed for use in workflows.') - return prompted_fields, rejected_fields, errors_dict + return prompted_data, rejected_data, errors_dict def can_start_without_user_input(self): return not bool(self.variables_needed_to_start) diff --git a/awx/main/routing.py b/awx/main/routing.py index 79a3c84a5a..0a49f25c6c 100644 --- a/awx/main/routing.py +++ b/awx/main/routing.py @@ -1,5 +1,4 @@ from channels.routing import route -from awx.network_ui.routing import channel_routing as network_ui_routing channel_routing = [ @@ -7,6 +6,3 @@ channel_routing = [ route("websocket.disconnect", "awx.main.consumers.ws_disconnect", path=r'^/websocket/$'), route("websocket.receive", "awx.main.consumers.ws_receive", path=r'^/websocket/$'), ] - - -channel_routing += network_ui_routing diff --git a/awx/main/scheduler/dag_workflow.py b/awx/main/scheduler/dag_workflow.py index aa1f8bd957..3f7657f571 100644 --- a/awx/main/scheduler/dag_workflow.py +++ b/awx/main/scheduler/dag_workflow.py @@ -69,6 +69,7 @@ class WorkflowDAG(SimpleDAG): job = obj.job if obj.unified_job_template is None: + is_failed = True continue elif not job: return False, False diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 943d4960e6..96d1e80812 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -7,6 +7,7 @@ import logging import uuid import json import six +import random from sets import Set # Django @@ -234,15 +235,26 @@ class TaskManager(): def get_dependent_jobs_for_inv_and_proj_update(self, job_obj): return [{'type': j.model_to_str(), 'id': j.id} for j in job_obj.dependent_jobs.all()] - def start_task(self, task, rampart_group, dependent_tasks=[]): + def start_task(self, task, rampart_group, dependent_tasks=None, instance=None): from awx.main.tasks import handle_work_error, handle_work_success + dependent_tasks = dependent_tasks or [] + task_actual = { 'type': get_type_for_model(type(task)), 'id': task.id, } dependencies = [{'type': get_type_for_model(type(t)), 'id': t.id} for t in dependent_tasks] + controller_node = None + if task.supports_isolation() and rampart_group.controller_id: + try: + controller_node = rampart_group.choose_online_controller_node() + except IndexError: + logger.debug(six.text_type("No controllers available in group {} to run {}").format( + rampart_group.name, task.log_format)) + return + error_handler = handle_work_error.s(subtasks=[task_actual] + dependencies) success_handler = handle_work_success.s(task_actual=task_actual) @@ -263,11 +275,21 @@ class TaskManager(): elif not task.supports_isolation() and rampart_group.controller_id: # non-Ansible jobs on isolated instances run on controller task.instance_group = rampart_group.controller - logger.info('Submitting isolated %s to queue %s via %s.', - task.log_format, task.instance_group_id, rampart_group.controller_id) + task.execution_node = random.choice(list(rampart_group.controller.instances.all().values_list('hostname', flat=True))) + logger.info(six.text_type('Submitting isolated {} to queue {}.').format( + task.log_format, task.instance_group.name, task.execution_node)) + elif controller_node: + task.instance_group = rampart_group + task.execution_node = instance.hostname + task.controller_node = controller_node + logger.info(six.text_type('Submitting isolated {} to queue {} controlled by {}.').format( + task.log_format, task.execution_node, controller_node)) else: task.instance_group = rampart_group - logger.info('Submitting %s to instance group %s.', task.log_format, task.instance_group_id) + if instance is not None: + task.execution_node = instance.hostname + logger.info(six.text_type('Submitting {} to <{},{}>.').format( + task.log_format, task.instance_group_id, task.execution_node)) with disable_activity_stream(): task.celery_task_id = str(uuid.uuid4()) task.save() @@ -278,11 +300,10 @@ class TaskManager(): def post_commit(): task.websocket_emit_status(task.status) if task.status != 'failed': - if rampart_group is not None: - actual_queue=rampart_group.name - else: - actual_queue=settings.CELERY_DEFAULT_QUEUE - task.start_celery_task(opts, error_callback=error_handler, success_callback=success_handler, queue=actual_queue) + task.start_celery_task(opts, + error_callback=error_handler, + success_callback=success_handler, + queue=task.get_celery_queue_name()) connection.on_commit(post_commit) @@ -431,21 +452,37 @@ class TaskManager(): continue preferred_instance_groups = task.preferred_instance_groups found_acceptable_queue = False + idle_instance_that_fits = None for rampart_group in preferred_instance_groups: + if idle_instance_that_fits is None: + idle_instance_that_fits = rampart_group.find_largest_idle_instance() if self.get_remaining_capacity(rampart_group.name) <= 0: logger.debug(six.text_type("Skipping group {} capacity <= 0").format(rampart_group.name)) continue - if not self.would_exceed_capacity(task, rampart_group.name): - logger.debug(six.text_type("Starting dependent {} in group {}").format(task.log_format, rampart_group.name)) + + execution_instance = rampart_group.fit_task_to_most_remaining_capacity_instance(task) + if execution_instance: + logger.debug(six.text_type("Starting dependent {} in group {} instance {}").format( + task.log_format, rampart_group.name, execution_instance.hostname)) + elif not execution_instance and idle_instance_that_fits: + execution_instance = idle_instance_that_fits + logger.debug(six.text_type("Starting dependent {} in group {} on idle instance {}").format( + task.log_format, rampart_group.name, execution_instance.hostname)) + if execution_instance: self.graph[rampart_group.name]['graph'].add_job(task) tasks_to_fail = filter(lambda t: t != task, dependency_tasks) tasks_to_fail += [dependent_task] - self.start_task(task, rampart_group, tasks_to_fail) + self.start_task(task, rampart_group, tasks_to_fail, execution_instance) found_acceptable_queue = True + break + else: + logger.debug(six.text_type("No instance available in group {} to run job {} w/ capacity requirement {}").format( + rampart_group.name, task.log_format, task.task_impact)) if not found_acceptable_queue: logger.debug(six.text_type("Dependent {} couldn't be scheduled on graph, waiting for next cycle").format(task.log_format)) def process_pending_tasks(self, pending_tasks): + running_workflow_templates = set([wf.workflow_job_template_id for wf in self.get_running_workflow_jobs()]) for task in pending_tasks: self.process_dependencies(task, self.generate_dependencies(task)) if self.is_job_blocked(task): @@ -453,25 +490,41 @@ class TaskManager(): continue preferred_instance_groups = task.preferred_instance_groups found_acceptable_queue = False + idle_instance_that_fits = None if isinstance(task, WorkflowJob): - self.start_task(task, None, task.get_jobs_fail_chain()) + if task.workflow_job_template_id in running_workflow_templates: + if not task.allow_simultaneous: + logger.debug(six.text_type("{} is blocked from running, workflow already running").format(task.log_format)) + continue + else: + running_workflow_templates.add(task.workflow_job_template_id) + self.start_task(task, None, task.get_jobs_fail_chain(), None) continue for rampart_group in preferred_instance_groups: + if idle_instance_that_fits is None: + idle_instance_that_fits = rampart_group.find_largest_idle_instance() remaining_capacity = self.get_remaining_capacity(rampart_group.name) if remaining_capacity <= 0: logger.debug(six.text_type("Skipping group {}, remaining_capacity {} <= 0").format( rampart_group.name, remaining_capacity)) continue - if not self.would_exceed_capacity(task, rampart_group.name): - logger.debug(six.text_type("Starting {} in group {} (remaining_capacity={})").format( - task.log_format, rampart_group.name, remaining_capacity)) + + execution_instance = rampart_group.fit_task_to_most_remaining_capacity_instance(task) + if execution_instance: + logger.debug(six.text_type("Starting {} in group {} instance {} (remaining_capacity={})").format( + task.log_format, rampart_group.name, execution_instance.hostname, remaining_capacity)) + elif not execution_instance and idle_instance_that_fits: + execution_instance = idle_instance_that_fits + logger.debug(six.text_type("Starting {} in group {} instance {} (remaining_capacity={})").format( + task.log_format, rampart_group.name, execution_instance.hostname, remaining_capacity)) + if execution_instance: self.graph[rampart_group.name]['graph'].add_job(task) - self.start_task(task, rampart_group, task.get_jobs_fail_chain()) + self.start_task(task, rampart_group, task.get_jobs_fail_chain(), execution_instance) found_acceptable_queue = True break else: - logger.debug(six.text_type("Not enough capacity to run {} on {} (remaining_capacity={})").format( - task.log_format, rampart_group.name, remaining_capacity)) + logger.debug(six.text_type("No instance available in group {} to run job {} w/ capacity requirement {}").format( + rampart_group.name, task.log_format, task.task_impact)) if not found_acceptable_queue: logger.debug(six.text_type("{} couldn't be scheduled on graph, waiting for next cycle").format(task.log_format)) diff --git a/awx/main/signals.py b/awx/main/signals.py index 86c798795d..1cd56a5697 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -6,11 +6,13 @@ import contextlib import logging import threading import json +import pkg_resources +import sys # Django from django.conf import settings from django.db.models.signals import ( - post_init, + pre_save, post_save, pre_delete, post_delete, @@ -18,6 +20,7 @@ from django.db.models.signals import ( ) from django.dispatch import receiver from django.contrib.auth import SESSION_KEY +from django.contrib.sessions.models import Session from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -29,10 +32,9 @@ import six # AWX from awx.main.models import * # noqa -from django.contrib.sessions.models import Session from awx.api.serializers import * # noqa -from awx.main.constants import TOKEN_CENSOR -from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore +from awx.main.constants import CENSOR_VALUE +from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, get_current_apps from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates from awx.main.tasks import update_inventory_computed_fields from awx.main.fields import ( @@ -52,6 +54,13 @@ logger = logging.getLogger('awx.main.signals') # when a Host-Group or Group-Group relationship is updated, or when a Job is deleted +def get_activity_stream_class(): + if 'migrate' in sys.argv: + return get_current_apps().get_model('main', 'ActivityStream') + else: + return ActivityStream + + def get_current_user_or_none(): u = get_current_user() if not isinstance(u, User): @@ -200,14 +209,6 @@ def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): l.delete() -def set_original_organization(sender, instance, **kwargs): - '''set_original_organization is used to set the original, or - pre-save organization, so we can later determine if the organization - field is dirty. - ''' - instance.__original_org_id = instance.organization_id - - def save_related_job_templates(sender, instance, **kwargs): '''save_related_job_templates loops through all of the job templates that use an Inventory or Project that have had their @@ -217,7 +218,7 @@ def save_related_job_templates(sender, instance, **kwargs): if sender not in (Project, Inventory): raise ValueError('This signal callback is only intended for use with Project or Inventory') - if instance.__original_org_id != instance.organization_id: + if instance._prior_values_store.get('organization_id') != instance.organization_id: jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance}) for jt in jtq: update_role_parentage_for_instance(jt) @@ -240,8 +241,6 @@ def connect_computed_field_signals(): connect_computed_field_signals() -post_init.connect(set_original_organization, sender=Project) -post_init.connect(set_original_organization, sender=Inventory) post_save.connect(save_related_job_templates, sender=Project) post_save.connect(save_related_job_templates, sender=Inventory) post_save.connect(emit_job_event_detail, sender=JobEvent) @@ -391,6 +390,7 @@ model_serializer_mapping = { Inventory: InventorySerializer, Host: HostSerializer, Group: GroupSerializer, + InstanceGroup: InstanceGroupSerializer, InventorySource: InventorySourceSerializer, CustomInventoryScript: CustomInventoryScriptSerializer, Credential: CredentialSerializer, @@ -428,8 +428,8 @@ def activity_stream_create(sender, instance, created, **kwargs): if 'extra_vars' in changes: changes['extra_vars'] = instance.display_extra_vars() if type(instance) == OAuth2AccessToken: - changes['token'] = TOKEN_CENSOR - activity_entry = ActivityStream( + changes['token'] = CENSOR_VALUE + activity_entry = get_activity_stream_class()( operation='create', object1=object1, changes=json.dumps(changes), @@ -439,7 +439,7 @@ def activity_stream_create(sender, instance, created, **kwargs): # we don't really use them anyway. if instance._meta.model_name != 'setting': # Is not conf.Setting instance activity_entry.save() - getattr(activity_entry, object1).add(instance) + getattr(activity_entry, object1).add(instance.pk) else: activity_entry.setting = conf_to_dict(instance) activity_entry.save() @@ -463,14 +463,14 @@ def activity_stream_update(sender, instance, **kwargs): if getattr(_type, '_deferred', False): return object1 = camelcase_to_underscore(instance.__class__.__name__) - activity_entry = ActivityStream( + activity_entry = get_activity_stream_class()( operation='update', object1=object1, changes=json.dumps(changes), actor=get_current_user_or_none()) if instance._meta.model_name != 'setting': # Is not conf.Setting instance activity_entry.save() - getattr(activity_entry, object1).add(instance) + getattr(activity_entry, object1).add(instance.pk) else: activity_entry.setting = conf_to_dict(instance) activity_entry.save() @@ -495,8 +495,8 @@ def activity_stream_delete(sender, instance, **kwargs): changes = model_to_dict(instance) object1 = camelcase_to_underscore(instance.__class__.__name__) if type(instance) == OAuth2AccessToken: - changes['token'] = TOKEN_CENSOR - activity_entry = ActivityStream( + changes['token'] = CENSOR_VALUE + activity_entry = get_activity_stream_class()( operation='delete', changes=json.dumps(changes), object1=object1, @@ -543,7 +543,7 @@ def activity_stream_associate(sender, instance, **kwargs): continue if isinstance(obj1, SystemJob) or isinstance(obj2_actual, SystemJob): continue - activity_entry = ActivityStream( + activity_entry = get_activity_stream_class()( changes=json.dumps(dict(object1=object1, object1_pk=obj1.pk, object2=object2, @@ -556,8 +556,8 @@ def activity_stream_associate(sender, instance, **kwargs): object_relationship_type=obj_rel, actor=get_current_user_or_none()) activity_entry.save() - getattr(activity_entry, object1).add(obj1) - getattr(activity_entry, object2).add(obj2_actual) + getattr(activity_entry, object1).add(obj1.pk) + getattr(activity_entry, object2).add(obj2_actual.pk) # Record the role for RBAC changes if 'role' in kwargs: @@ -603,6 +603,16 @@ def delete_inventory_for_org(sender, instance, **kwargs): @receiver(post_save, sender=Session) def save_user_session_membership(sender, **kwargs): session = kwargs.get('instance', None) + if pkg_resources.get_distribution('channels').version >= '2': + # If you get into this code block, it means we upgraded channels, but + # didn't make the settings.SESSIONS_PER_USER feature work + raise RuntimeError( + 'save_user_session_membership must be updated for channels>=2: ' + 'http://channels.readthedocs.io/en/latest/one-to-two.html#requirements' + ) + if 'runworker' in sys.argv: + # don't track user session membership for websocket per-channel sessions + return if not session: return user = session.get_decoded().get(SESSION_KEY, None) @@ -611,13 +621,15 @@ def save_user_session_membership(sender, **kwargs): user = User.objects.get(pk=user) if UserSessionMembership.objects.filter(user=user, session=session).exists(): return - UserSessionMembership.objects.create(user=user, session=session, created=timezone.now()) - for membership in UserSessionMembership.get_memberships_over_limit(user): + UserSessionMembership(user=user, session=session, created=timezone.now()).save() + expired = UserSessionMembership.get_memberships_over_limit(user) + for membership in expired: + Session.objects.filter(session_key__in=[membership.session_id]).delete() + membership.delete() + if len(expired): consumers.emit_channel_notification( - 'control-limit_reached', - dict(group_name='control', - reason=unicode(_('limit_reached')), - session_key=membership.session.session_key) + 'control-limit_reached_{}'.format(user.pk), + dict(group_name='control', reason=unicode(_('limit_reached'))) ) @@ -631,3 +643,7 @@ def create_access_token_user_if_missing(sender, **kwargs): post_save.connect(create_access_token_user_if_missing, sender=OAuth2AccessToken) +# Connect the Instance Group to Activity Stream receivers. +post_save.connect(activity_stream_create, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_create") +pre_save.connect(activity_stream_update, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_update") +pre_delete.connect(activity_stream_delete, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_delete") diff --git a/awx/main/storage.py b/awx/main/storage.py deleted file mode 100644 index bf810b21cd..0000000000 --- a/awx/main/storage.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -import base64 -from cStringIO import StringIO - -from django.core import files -from django.core.files.storage import Storage - - -class DatabaseStorage(Storage): - """A class for storing uploaded files into the database, rather than - on the filesystem. - """ - def __init__(self, model): - self.model = model - - def _open(self, name, mode='rb'): - try: - f = self.model.objects.get(filename=name) - except self.model.DoesNotExist: - return None - fh = StringIO(base64.b64decode(f.contents)) - fh.name = name - fh.mode = mode - fh.size = f.size - return files.File(fh) - - def _save(self, name, content): - try: - file_ = self.model.objects.get(filename=name) - except self.model.DoesNotExist: - file_ = self.model(filename=name) - file_.contents = base64.b64encode(content.read()) - file_.save() - return name - - def exists(self, name): - """Return True if the given file already exists in the database, - or False otherwise. - """ - return bool(self.model.objects.filter(filename=name).count()) - - def delete(self, name): - """Delete the file in the database, failing silently if the file - does not exist. - """ - self.model.objects.filter(filename=name).delete() - - def listdir(self, path=None): - """Return a full list of files stored in the database, ignoring - whatever may be sent to the `path` argument. - """ - filenames = [i.filename for i in self.model.order_by('filename')] - return ([], filenames) - - def url(self, name): - raise NotImplementedError - - def size(self, name): - """Return the size of the given file, if it exists; raise DoesNotExist - if the file is not present. - """ - file_ = self.model.objects.get(filename=name) - return len(file_.contents) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3690c52f45..328fb90ebd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -5,6 +5,7 @@ from collections import OrderedDict, namedtuple import ConfigParser import cStringIO +import errno import functools import importlib import json @@ -28,8 +29,10 @@ except Exception: psutil = None # Celery -from celery import Task, shared_task, Celery -from celery.signals import celeryd_init, worker_shutdown, worker_ready, celeryd_after_setup +from kombu import Queue, Exchange +from kombu.common import Broadcast +from celery import Task, shared_task +from celery.signals import celeryd_init, worker_shutdown, celeryd_after_setup # Django from django.conf import settings @@ -48,7 +51,7 @@ from crum import impersonate # AWX from awx import __version__ as awx_application_version -from awx.main.constants import CLOUD_PROVIDERS, PRIVILEGE_ESCALATION_METHODS +from awx.main.constants import CLOUD_PROVIDERS, PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV from awx.main.access import access_registry from awx.main.models import * # noqa from awx.main.constants import ACTIVE_STATES @@ -62,7 +65,6 @@ from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja from awx.main.utils.reload import stop_local_services from awx.main.utils.pglock import advisory_lock -from awx.main.utils.ha import register_celery_worker_queues from awx.main.consumers import emit_channel_notification from awx.conf import settings_registry @@ -106,8 +108,6 @@ def log_celery_failure(self, exc, task_id, args, kwargs, einfo): @celeryd_init.connect def celery_startup(conf=None, **kwargs): - # Re-init all schedules - # NOTE: Rework this during the Rampart work startup_logger = logging.getLogger('awx.main.tasks') startup_logger.info("Syncing Schedules") for sch in Schedule.objects.all(): @@ -119,6 +119,19 @@ def celery_startup(conf=None, **kwargs): except Exception: logger.exception(six.text_type("Failed to rebuild schedule {}.").format(sch)) + # set the queues we want to bind to dynamically at startup + queues = [] + me = Instance.objects.me() + for q in [me.hostname] + settings.AWX_CELERY_QUEUES_STATIC: + q = q.encode('utf-8') + queues.append(Queue(q, Exchange(q), routing_key=q)) + for q in settings.AWX_CELERY_BCAST_QUEUES_STATIC: + queues.append(Broadcast(q.encode('utf-8'))) + conf.CELERY_QUEUES = list(set(queues)) + + # Expedite the first hearbeat run so a node comes online quickly. + cluster_node_heartbeat.apply([]) + @worker_shutdown.connect def inform_cluster_of_shutdown(*args, **kwargs): @@ -135,52 +148,76 @@ def inform_cluster_of_shutdown(*args, **kwargs): @shared_task(bind=True, queue=settings.CELERY_DEFAULT_QUEUE) def apply_cluster_membership_policies(self): with advisory_lock('cluster_policy_lock', wait=True): - considered_instances = Instance.objects.all().order_by('id') - total_instances = considered_instances.count() - filtered_instances = [] + all_instances = list(Instance.objects.order_by('id')) + all_groups = list(InstanceGroup.objects.all()) + iso_hostnames = set([]) + for ig in all_groups: + if ig.controller_id is not None: + iso_hostnames.update(ig.policy_instance_list) + + considered_instances = [inst for inst in all_instances if inst.hostname not in iso_hostnames] + total_instances = len(considered_instances) actual_groups = [] actual_instances = [] Group = namedtuple('Group', ['obj', 'instances']) Node = namedtuple('Instance', ['obj', 'groups']) - # Process policy instance list first, these will represent manually managed instances - # that will not go through automatic policy determination - for ig in InstanceGroup.objects.all(): - logger.info(six.text_type("Applying cluster membership policies to Group {}").format(ig.name)) - ig.instances.clear() + # Process policy instance list first, these will represent manually managed memberships + instance_hostnames_map = {inst.hostname: inst for inst in all_instances} + for ig in all_groups: group_actual = Group(obj=ig, instances=[]) - for i in ig.policy_instance_list: - inst = Instance.objects.filter(hostname=i) - if not inst.exists(): + for hostname in ig.policy_instance_list: + if hostname not in instance_hostnames_map: continue - inst = inst[0] + inst = instance_hostnames_map[hostname] logger.info(six.text_type("Policy List, adding Instance {} to Group {}").format(inst.hostname, ig.name)) group_actual.instances.append(inst.id) - ig.instances.add(inst) - filtered_instances.append(inst) - actual_groups.append(group_actual) + # NOTE: arguable behavior: policy-list-group is not added to + # instance's group count for consideration in minimum-policy rules + + if ig.controller_id is None: + actual_groups.append(group_actual) + else: + # For isolated groups, _only_ apply the policy_instance_list + # do not add to in-memory list, so minimum rules not applied + logger.info('Committing instances {} to isolated group {}'.format(group_actual.instances, ig.name)) + ig.instances.set(group_actual.instances) + # Process Instance minimum policies next, since it represents a concrete lower bound to the # number of instances to make available to instance groups - actual_instances = [Node(obj=i, groups=[]) for i in filter(lambda x: x not in filtered_instances, considered_instances)] - logger.info("Total instances not directly associated: {}".format(total_instances)) + actual_instances = [Node(obj=i, groups=[]) for i in considered_instances if i.managed_by_policy] + logger.info("Total non-isolated instances:{} available for policy: {}".format( + total_instances, len(actual_instances))) for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)): for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)): if len(g.instances) >= g.obj.policy_instance_minimum: break + if i.obj.id in g.instances: + # If the instance is already _in_ the group, it was + # applied earlier via the policy list + continue logger.info(six.text_type("Policy minimum, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name)) - g.obj.instances.add(i.obj) g.instances.append(i.obj.id) i.groups.append(g.obj.id) - # Finally process instance policy percentages + + # Finally, process instance policy percentages for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)): for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)): + if i.obj.id in g.instances: + # If the instance is already _in_ the group, it was + # applied earlier via a minimum policy or policy list + continue if 100 * float(len(g.instances)) / len(actual_instances) >= g.obj.policy_instance_percentage: break logger.info(six.text_type("Policy percentage, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name)) g.instances.append(i.obj.id) - g.obj.instances.add(i.obj) i.groups.append(g.obj.id) - handle_ha_toplogy_changes.apply([]) + + # On a differential basis, apply instances to non-isolated groups + with transaction.atomic(): + for g in actual_groups: + logger.info('Committing instances {} to group {}'.format(g.instances, g.obj.name)) + g.obj.instances.set(g.instances) @shared_task(exchange='tower_broadcast_all', bind=True) @@ -196,40 +233,32 @@ def handle_setting_changes(self, setting_keys): cache.delete_many(cache_keys) -@shared_task(bind=True, exchange='tower_broadcast_all') -def handle_ha_toplogy_changes(self): - (changed, instance) = Instance.objects.get_or_register() - if changed: - logger.info(six.text_type("Registered tower node '{}'").format(instance.hostname)) - logger.debug(six.text_type("Reconfigure celeryd queues task on host {}").format(self.request.hostname)) - awx_app = Celery('awx') - awx_app.config_from_object('django.conf:settings') - instances, removed_queues, added_queues = register_celery_worker_queues(awx_app, self.request.hostname) - if len(removed_queues) + len(added_queues) > 0: - logger.info(six.text_type("Workers on tower node(s) '{}' removed from queues {} and added to queues {}") - .format([i.hostname for i in instances], removed_queues, added_queues)) - - -@worker_ready.connect -def handle_ha_toplogy_worker_ready(sender, **kwargs): - logger.debug(six.text_type("Configure celeryd queues task on host {}").format(sender.hostname)) - instances, removed_queues, added_queues = register_celery_worker_queues(sender.app, sender.hostname) - if len(removed_queues) + len(added_queues) > 0: - logger.info(six.text_type("Workers on tower node(s) '{}' removed from queues {} and added to queues {}") - .format([i.hostname for i in instances], removed_queues, added_queues)) - - # Expedite the first hearbeat run so a node comes online quickly. - cluster_node_heartbeat.apply([]) - apply_cluster_membership_policies.apply([]) - - @celeryd_after_setup.connect -def handle_update_celery_hostname(sender, instance, **kwargs): +def auto_register_ha_instance(sender, instance, **kwargs): + # + # When celeryd starts, if the instance cannot be found in the database, + # automatically register it. This is mostly useful for openshift-based + # deployments where: + # + # 2 Instances come online + # Instance B encounters a network blip, Instance A notices, and + # deprovisions it + # Instance B's connectivity is restored, celeryd starts, and it + # re-registers itself + # + # In traditional container-less deployments, instances don't get + # deprovisioned when they miss their heartbeat, so this code is mostly a + # no-op. + # + if instance.hostname != 'celery@{}'.format(settings.CLUSTER_HOST_ID): + error = six.text_type('celery -n {} does not match settings.CLUSTER_HOST_ID={}').format( + instance.hostname, settings.CLUSTER_HOST_ID + ) + logger.error(error) + raise RuntimeError(error) (changed, tower_instance) = Instance.objects.get_or_register() if changed: logger.info(six.text_type("Registered tower node '{}'").format(tower_instance.hostname)) - instance.hostname = 'celery@{}'.format(tower_instance.hostname) - logger.warn(six.text_type("Set hostname to {}").format(instance.hostname)) @shared_task(queue=settings.CELERY_DEFAULT_QUEUE) @@ -317,11 +346,9 @@ def cluster_node_heartbeat(self): logger.warning(six.text_type('Rejoining the cluster as instance {}.').format(this_inst.hostname)) if this_inst.enabled: this_inst.refresh_capacity() - handle_ha_toplogy_changes.apply_async() elif this_inst.capacity != 0 and not this_inst.enabled: this_inst.capacity = 0 this_inst.save(update_fields=['capacity']) - handle_ha_toplogy_changes.apply_async() if startup_event: return else: @@ -375,7 +402,11 @@ def awx_isolated_heartbeat(self): accept_before = nowtime - timedelta(seconds=(poll_interval - 10)) isolated_instance_qs = Instance.objects.filter( rampart_groups__controller__instances__hostname=local_hostname, + ) + isolated_instance_qs = isolated_instance_qs.filter( last_isolated_check__lt=accept_before + ) | isolated_instance_qs.filter( + last_isolated_check=None ) # Fast pass of isolated instances, claiming the nodes to update with transaction.atomic(): @@ -418,6 +449,8 @@ def awx_periodic_scheduler(self): try: job_kwargs = schedule.get_job_kwargs() new_unified_job = schedule.unified_job_template.create_unified_job(**job_kwargs) + logger.info(six.text_type('Spawned {} from schedule {}-{}.').format( + new_unified_job.log_format, schedule.name, schedule.pk)) if invalid_license: new_unified_job.status = 'failed' @@ -860,14 +893,11 @@ class BaseTask(Task): ''' @with_path_cleanup - def run(self, pk, isolated_host=None, **kwargs): + def run(self, pk, **kwargs): ''' Run the job/task and capture its output. ''' - execution_node = settings.CLUSTER_HOST_ID - if isolated_host is not None: - execution_node = isolated_host - instance = self.update_model(pk, status='running', execution_node=execution_node, + instance = self.update_model(pk, status='running', start_args='') # blank field to remove encrypted passwords instance.websocket_emit_status("running") @@ -876,8 +906,9 @@ class BaseTask(Task): extra_update_fields = {} event_ct = 0 stdout_handle = None + try: - kwargs['isolated'] = isolated_host is not None + kwargs['isolated'] = instance.is_isolated() self.pre_run_hook(instance, **kwargs) if instance.cancel_flag: instance = self.update_model(instance.pk, status='canceled') @@ -937,7 +968,7 @@ class BaseTask(Task): credential, env, safe_env, args, safe_args, kwargs['private_data_dir'] ) - if isolated_host is None: + if instance.is_isolated() is False: stdout_handle = self.get_stdout_handle(instance) else: stdout_handle = isolated_manager.IsolatedManager.get_stdout_handle( @@ -953,7 +984,7 @@ class BaseTask(Task): ssh_key_path = self.get_ssh_key_path(instance, **kwargs) # If we're executing on an isolated host, don't bother adding the # key to the agent in this environment - if ssh_key_path and isolated_host is None: + if ssh_key_path and instance.is_isolated() is False: ssh_auth_sock = os.path.join(kwargs['private_data_dir'], 'ssh_auth.sock') args = run.wrap_args_with_ssh_agent(args, ssh_key_path, ssh_auth_sock) safe_args = run.wrap_args_with_ssh_agent(safe_args, ssh_key_path, ssh_auth_sock) @@ -973,11 +1004,11 @@ class BaseTask(Task): proot_cmd=getattr(settings, 'AWX_PROOT_CMD', 'bwrap'), ) instance = self.update_model(instance.pk, output_replacements=output_replacements) - if isolated_host: + if instance.is_isolated() is True: manager_instance = isolated_manager.IsolatedManager( args, cwd, env, stdout_handle, ssh_key_path, **_kw ) - status, rc = manager_instance.run(instance, isolated_host, + status, rc = manager_instance.run(instance, kwargs['private_data_dir'], kwargs.get('proot_temp_dir')) else: @@ -995,7 +1026,7 @@ class BaseTask(Task): if stdout_handle: stdout_handle.flush() stdout_handle.close() - event_ct = getattr(stdout_handle, '_event_ct', 0) + event_ct = getattr(stdout_handle, '_counter', 0) logger.info('%s finished running, producing %s events.', instance.log_format, event_ct) except Exception: @@ -1008,6 +1039,9 @@ class BaseTask(Task): instance = self.update_model(pk) if instance.cancel_flag: status = 'canceled' + cancel_wait = (now() - instance.modified).seconds if instance.modified else 0 + if cancel_wait > 5: + logger.warn(six.text_type('Request to cancel {} took {} seconds to complete.').format(instance.log_format, cancel_wait)) instance = self.update_model(pk, status=status, result_traceback=tb, output_replacements=output_replacements, @@ -1341,7 +1375,7 @@ class RunJob(BaseTask): job_request_id = '' if self.request.id is None else self.request.id pu_ig = job.instance_group pu_en = job.execution_node - if kwargs['isolated']: + if job.is_isolated() is True: pu_ig = pu_ig.controller pu_en = settings.CLUSTER_HOST_ID local_project_sync = job.project.create_project_update( @@ -1682,29 +1716,37 @@ class RunProjectUpdate(BaseTask): logger.error(six.text_type("I/O error({0}) while trying to open lock file [{1}]: {2}").format(e.errno, lock_path, e.strerror)) raise - try: - start_time = time.time() - fcntl.flock(self.lock_fd, fcntl.LOCK_EX) - waiting_time = time.time() - start_time - if waiting_time > 1.0: - logger.info(six.text_type( - '{} spent {} waiting to acquire lock for local source tree ' - 'for path {}.').format(instance.log_format, waiting_time, lock_path)) - except IOError as e: - os.close(self.lock_fd) - logger.error(six.text_type("I/O error({0}) while trying to aquire lock on file [{1}]: {2}").format(e.errno, lock_path, e.strerror)) - raise + start_time = time.time() + while True: + try: + instance.refresh_from_db(fields=['cancel_flag']) + if instance.cancel_flag: + logger.info(six.text_type("ProjectUpdate({0}) was cancelled".format(instance.pk))) + return + fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + break + except IOError as e: + if e.errno not in (errno.EAGAIN, errno.EACCES): + os.close(self.lock_fd) + logger.error(six.text_type("I/O error({0}) while trying to aquire lock on file [{1}]: {2}").format(e.errno, lock_path, e.strerror)) + raise + else: + time.sleep(1.0) + waiting_time = time.time() - start_time + + if waiting_time > 1.0: + logger.info(six.text_type( + '{} spent {} waiting to acquire lock for local source tree ' + 'for path {}.').format(instance.log_format, waiting_time, lock_path)) def pre_run_hook(self, instance, **kwargs): # re-create root project folder if a natural disaster has destroyed it if not os.path.exists(settings.PROJECTS_ROOT): os.mkdir(settings.PROJECTS_ROOT) - if instance.launch_type == 'sync': - self.acquire_lock(instance) + self.acquire_lock(instance) def post_run_hook(self, instance, status, **kwargs): - if instance.launch_type == 'sync': - self.release_lock(instance) + self.release_lock(instance) p = instance.project if instance.job_type == 'check' and status not in ('failed', 'canceled',): fd = open(self.revision_path, 'r') @@ -1977,8 +2019,7 @@ class RunInventoryUpdate(BaseTask): # Pass inventory source ID to inventory script. env['INVENTORY_SOURCE_ID'] = str(inventory_update.inventory_source_id) env['INVENTORY_UPDATE_ID'] = str(inventory_update.pk) - # Always use the --export option for ansible-inventory - env['ANSIBLE_INVENTORY_EXPORT'] = str(True) + env.update(STANDARD_INVENTORY_UPDATE_ENV) plugin_name = inventory_update.get_inventory_plugin_name() if plugin_name is not None: env['ANSIBLE_INVENTORY_ENABLED'] = plugin_name @@ -2274,7 +2315,10 @@ class RunAdHocCommand(BaseTask): args.extend(['-e', '@%s' % (extra_vars_path)]) args.extend(['-m', ad_hoc_command.module_name]) - args.extend(['-a', sanitize_jinja(ad_hoc_command.module_args)]) + module_args = ad_hoc_command.module_args + if settings.ALLOW_JINJA_IN_EXTRA_VARS != 'always': + module_args = sanitize_jinja(module_args) + args.extend(['-a', module_args]) if ad_hoc_command.limit: args.append(ad_hoc_command.limit) @@ -2372,6 +2416,7 @@ def deep_copy_model_obj( ): logger.info(six.text_type('Deep copy {} from {} to {}.').format(model_name, obj_pk, new_obj_pk)) from awx.api.generics import CopyAPIView + from awx.main.signals import disable_activity_stream model = getattr(importlib.import_module(model_module), model_name, None) if model is None: return @@ -2382,7 +2427,7 @@ def deep_copy_model_obj( except ObjectDoesNotExist: logger.warning("Object or user no longer exists.") return - with transaction.atomic(), ignore_inventory_computed_fields(): + with transaction.atomic(), ignore_inventory_computed_fields(), disable_activity_stream(): copy_mapping = {} for sub_obj_setup in sub_obj_list: sub_model = getattr(importlib.import_module(sub_obj_setup[0]), diff --git a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py index f6466affd7..8beaddd391 100644 --- a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py +++ b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py @@ -2,7 +2,7 @@ import json import mock import pytest -from awx.main.models import Credential, Job +from awx.main.models import Credential, CredentialType, Job from awx.api.versioning import reverse @@ -151,6 +151,27 @@ def test_prevent_multiple_machine_creds(get, post, job_template, admin, machine_ assert 'Cannot assign multiple Machine credentials.' in resp.content +@pytest.mark.django_db +@pytest.mark.parametrize('kind', ['scm', 'insights']) +def test_invalid_credential_type_at_launch(get, post, job_template, admin, kind): + cred_type = CredentialType.defaults[kind]() + cred_type.save() + cred = Credential( + name='Some Cred', + credential_type=cred_type, + inputs={ + 'username': 'bob', + 'password': 'secret', + } + ) + cred.save() + url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) + + resp = post(url, {'credentials': [cred.pk]}, admin, expect=400) + assert 'Cannot assign a Credential of kind `{}`'.format(kind) in resp.data.get('credentials', []) + assert Job.objects.count() == 0 + + @pytest.mark.django_db def test_prevent_multiple_machine_creds_at_launch(get, post, job_template, admin, machine_credential): other_cred = Credential(credential_type=machine_credential.credential_type, name="Second", @@ -394,3 +415,43 @@ def test_inventory_source_invalid_deprecated_credential(patch, admin, ec2_source url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk}) resp = patch(url, {'credential': 999999}, admin, expect=400) assert 'Credential 999999 does not exist' in resp.content + + +@pytest.mark.django_db +def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template): + job_template.credentials.add(machine_credential) + starting_entries = job_template.activitystream_set.count() + # no-op patch + patch( + job_template.get_absolute_url(), + admin_user, + data={'credential': machine_credential.pk}, + expect=200 + ) + # no-op should not produce activity stream entries + assert starting_entries == job_template.activitystream_set.count() + + +@pytest.mark.django_db +def test_multi_vault_preserved_on_put(get, put, admin_user, job_template, vault_credential): + ''' + A PUT request will necessarily specify deprecated fields, but if the deprecated + field is a singleton while the `credentials` relation has many, that makes + it very easy to drop those credentials not specified in the PUT data + ''' + vault2 = Credential.objects.create( + name='second-vault', + credential_type=vault_credential.credential_type, + inputs={'vault_password': 'foo', 'vault_id': 'foo'} + ) + job_template.credentials.add(vault_credential, vault2) + assert job_template.credentials.count() == 2 # sanity check + r = get(job_template.get_absolute_url(), admin_user, expect=200) + # should be a no-op PUT request + put( + job_template.get_absolute_url(), + admin_user, + data=r.data, + expect=200 + ) + assert job_template.credentials.count() == 2 diff --git a/awx/main/tests/functional/api/test_generic.py b/awx/main/tests/functional/api/test_generic.py index 68ca294027..e1ec08ad5d 100644 --- a/awx/main/tests/functional/api/test_generic.py +++ b/awx/main/tests/functional/api/test_generic.py @@ -106,4 +106,14 @@ def test_filterable_fields(options, instance, admin_user): assert 'filterable' in filterable_info assert filterable_info['filterable'] is True - assert 'filterable' not in non_filterable_info + assert not non_filterable_info['filterable'] + + +@pytest.mark.django_db +def test_handle_content_type(post, admin): + ''' Tower should return 415 when wrong content type is in HTTP requests ''' + post(reverse('api:project_list'), + {'name': 't', 'organization': None}, + admin, + content_type='text/html', + expect=415) diff --git a/awx/main/tests/functional/api/test_host_filter.py b/awx/main/tests/functional/api/test_host_filter.py index 8a19c24e2d..45542f6325 100644 --- a/awx/main/tests/functional/api/test_host_filter.py +++ b/awx/main/tests/functional/api/test_host_filter.py @@ -47,3 +47,6 @@ def test_q1(inventory_structure, get, user): query = '(name="host1" and groups__name="g1") or (name="host3" and groups__name="g2")' evaluate_query(query, [hosts[0], hosts[2]]) + # The following test verifies if the search in host_filter is case insensitive. + query = 'search="HOST1"' + evaluate_query(query, [hosts[0]]) diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index cd78d0de33..939db63dc2 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -2,6 +2,7 @@ import pytest from awx.api.versioning import reverse from awx.main.models import ( + Instance, InstanceGroup, ProjectUpdate, ) @@ -14,6 +15,17 @@ def tower_instance_group(): return ig +@pytest.fixture +def instance(): + instance = Instance.objects.create(hostname='iso') + return instance + + +@pytest.fixture +def non_iso_instance(): + return Instance.objects.create(hostname='iamnotanisolatedinstance') + + @pytest.fixture def instance_group(job_factory): ig = InstanceGroup(name="east") @@ -22,9 +34,11 @@ def instance_group(job_factory): @pytest.fixture -def isolated_instance_group(instance_group): +def isolated_instance_group(instance_group, instance): ig = InstanceGroup(name="iso", controller=instance_group) ig.save() + ig.instances.set([instance]) + ig.save() return ig @@ -73,12 +87,12 @@ def test_delete_instance_group_jobs(delete, instance_group_jobs_successful, inst @pytest.mark.django_db def test_delete_instance_group_jobs_running(delete, instance_group_jobs_running, instance_group_jobs_successful, instance_group, admin): def sort_keys(x): - return (x['type'], x['id']) + return (x['type'], str(x['id'])) url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk}) response = delete(url, None, admin, expect=409) - expect_transformed = [dict(id=str(j.id), type=j.model_to_str()) for j in instance_group_jobs_running] + expect_transformed = [dict(id=j.id, type=j.model_to_str()) for j in instance_group_jobs_running] response_sorted = sorted(response.data['active_jobs'], key=sort_keys) expect_sorted = sorted(expect_transformed, key=sort_keys) @@ -113,3 +127,52 @@ def test_prevent_delete_iso_and_control_groups(delete, isolated_instance_group, controller_url = reverse("api:instance_group_detail", kwargs={'pk': isolated_instance_group.controller.pk}) delete(iso_url, None, admin, expect=403) delete(controller_url, None, admin, expect=403) + + +@pytest.mark.django_db +def test_prevent_isolated_instance_added_to_non_isolated_instance_group(post, admin, instance, instance_group, isolated_instance_group): + url = reverse("api:instance_group_instance_list", kwargs={'pk': instance_group.pk}) + + assert True is instance.is_isolated() + resp = post(url, {'associate': True, 'id': instance.id}, admin, expect=400) + assert u"Isolated instances may not be added or removed from instances groups via the API." == resp.data['error'] + + +@pytest.mark.django_db +def test_prevent_isolated_instance_added_to_non_isolated_instance_group_via_policy_list(patch, admin, instance, instance_group, isolated_instance_group): + url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk}) + + assert True is instance.is_isolated() + resp = patch(url, {'policy_instance_list': [instance.hostname]}, user=admin, expect=400) + assert [u"Isolated instances may not be added or removed from instances groups via the API."] == resp.data['policy_instance_list'] + assert instance_group.policy_instance_list == [] + + +@pytest.mark.django_db +def test_prevent_isolated_instance_removal_from_isolated_instance_group(post, admin, instance, instance_group, isolated_instance_group): + url = reverse("api:instance_group_instance_list", kwargs={'pk': isolated_instance_group.pk}) + + assert True is instance.is_isolated() + resp = post(url, {'disassociate': True, 'id': instance.id}, admin, expect=400) + assert u"Isolated instances may not be added or removed from instances groups via the API." == resp.data['error'] + + +@pytest.mark.django_db +def test_prevent_non_isolated_instance_added_to_isolated_instance_group( + post, admin, non_iso_instance, isolated_instance_group): + url = reverse("api:instance_group_instance_list", kwargs={'pk': isolated_instance_group.pk}) + + assert False is non_iso_instance.is_isolated() + resp = post(url, {'associate': True, 'id': non_iso_instance.id}, admin, expect=400) + assert u"Isolated instance group membership may not be managed via the API." == resp.data['error'] + + +@pytest.mark.django_db +def test_prevent_non_isolated_instance_added_to_isolated_instance_group_via_policy_list( + patch, admin, non_iso_instance, isolated_instance_group): + url = reverse("api:instance_group_detail", kwargs={'pk': isolated_instance_group.pk}) + + assert False is non_iso_instance.is_isolated() + resp = patch(url, {'policy_instance_list': [non_iso_instance.hostname]}, user=admin, expect=400) + assert [u"Isolated instance group membership may not be managed via the API."] == resp.data['policy_instance_list'] + assert isolated_instance_group.policy_instance_list == [] diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 2e4b7df63e..fb2da5f804 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -450,6 +450,17 @@ class TestInsightsCredential: {'insights_credential': insights_credential.id}, admin_user, expect=200) + def test_insights_credential_protection(self, post, patch, insights_inventory, alice, insights_credential): + insights_inventory.organization.admin_role.members.add(alice) + insights_inventory.admin_role.members.add(alice) + post(reverse('api:inventory_list'), { + "name": "test", + "organization": insights_inventory.organization.id, + "insights_credential": insights_credential.id + }, alice, expect=403) + patch(insights_inventory.get_absolute_url(), + {'insights_credential': insights_credential.id}, alice, expect=403) + def test_non_insights_credential(self, patch, insights_inventory, admin_user, scm_credential): patch(insights_inventory.get_absolute_url(), {'insights_credential': scm_credential.id}, admin_user, diff --git a/awx/main/tests/functional/api/test_job.py b/awx/main/tests/functional/api/test_job.py index db5da93c2f..8cda6a6cfe 100644 --- a/awx/main/tests/functional/api/test_job.py +++ b/awx/main/tests/functional/api/test_job.py @@ -1,15 +1,23 @@ +# Python import pytest import mock - from dateutil.parser import parse from dateutil.relativedelta import relativedelta +from crum import impersonate +# Django rest framework from rest_framework.exceptions import PermissionDenied +# AWX from awx.api.versioning import reverse from awx.api.views import RelatedJobsPreventDeleteMixin, UnifiedJobDeletionMixin - -from awx.main.models import JobTemplate, User, Job +from awx.main.models import ( + JobTemplate, + User, + Job, + AdHocCommand, + ProjectUpdate, +) @pytest.mark.django_db @@ -33,7 +41,8 @@ def test_job_relaunch_permission_denied_response( jt.credentials.add(machine_credential) jt_user = User.objects.create(username='jobtemplateuser') jt.execute_role.members.add(jt_user) - job = jt.create_unified_job() + with impersonate(jt_user): + job = jt.create_unified_job() # User capability is shown for this r = get(job.get_absolute_url(), jt_user, expect=200) @@ -46,6 +55,29 @@ def test_job_relaunch_permission_denied_response( assert 'do not have permission' in r.data['detail'] +@pytest.mark.django_db +def test_job_relaunch_permission_denied_response_other_user(get, post, inventory, project, alice, bob): + ''' + Asserts custom permission denied message corresponding to + awx/main/tests/functional/test_rbac_job.py::TestJobRelaunchAccess::test_other_user_prompts + ''' + jt = JobTemplate.objects.create( + name='testjt', inventory=inventory, project=project, + ask_credential_on_launch=True, + ask_variables_on_launch=True) + jt.execute_role.members.add(alice, bob) + with impersonate(bob): + job = jt.create_unified_job(extra_vars={'job_var': 'foo2'}) + + # User capability is shown for this + r = get(job.get_absolute_url(), alice, expect=200) + assert r.data['summary_fields']['user_capabilities']['start'] + + # Job has prompted data, launch denied w/ message + r = post(reverse('api:job_relaunch', kwargs={'pk':job.pk}), {}, alice, expect=403) + assert 'Job was launched with prompts provided by another user' in r.data['detail'] + + @pytest.mark.django_db def test_job_relaunch_without_creds(post, inventory, project, admin_user): jt = JobTemplate.objects.create( @@ -75,7 +107,7 @@ def test_job_relaunch_on_failed_hosts(post, inventory, project, machine_credenti project=project ) jt.credentials.add(machine_credential) - job = jt.create_unified_job(_eager_fields={'status': 'failed', 'limit': 'host1,host2,host3'}) + job = jt.create_unified_job(_eager_fields={'status': 'failed'}, limit='host1,host2,host3') job.job_events.create(event='playbook_on_stats') job.job_host_summaries.create(host=h1, failed=False, ok=1, changed=0, failures=0, host_name=h1.name) job.job_host_summaries.create(host=h2, failed=False, ok=0, changed=1, failures=0, host_name=h2.name) @@ -133,3 +165,68 @@ def test_block_related_unprocessed_events(mocker, organization, project, delete, with mock.patch('awx.api.views.now', lambda: time_of_request): with pytest.raises(PermissionDenied): view.perform_destroy(organization) + + +@pytest.mark.django_db +def test_disallowed_http_update_methods(put, patch, post, inventory, project, admin_user): + jt = JobTemplate.objects.create( + name='test_disallowed_methods', inventory=inventory, + project=project + ) + job = jt.create_unified_job() + post( + url=reverse('api:job_detail', kwargs={'pk': job.pk, 'version': 'v2'}), + data={}, + user=admin_user, + expect=405 + ) + put( + url=reverse('api:job_detail', kwargs={'pk': job.pk, 'version': 'v2'}), + data={}, + user=admin_user, + expect=405 + ) + patch( + url=reverse('api:job_detail', kwargs={'pk': job.pk, 'version': 'v2'}), + data={}, + user=admin_user, + expect=405 + ) + + +class TestControllerNode(): + @pytest.fixture + def project_update(self, project): + return ProjectUpdate.objects.create(project=project) + + @pytest.fixture + def job(self): + return JobTemplate.objects.create().create_unified_job() + + @pytest.fixture + def adhoc(self, inventory): + return AdHocCommand.objects.create(inventory=inventory) + + @pytest.mark.django_db + def test_field_controller_node_exists(self, sqlite_copy_expert, + admin_user, job, project_update, + inventory_update, adhoc, get, system_job_factory): + system_job = system_job_factory() + + r = get(reverse('api:unified_job_list') + '?id={}'.format(job.id), admin_user, expect=200) + assert 'controller_node' in r.data['results'][0] + + r = get(job.get_absolute_url(), admin_user, expect=200) + assert 'controller_node' in r.data + + r = get(reverse('api:ad_hoc_command_detail', kwargs={'pk': adhoc.pk}), admin_user, expect=200) + assert 'controller_node' in r.data + + r = get(reverse('api:project_update_detail', kwargs={'pk': project_update.pk}), admin_user, expect=200) + assert 'controller_node' not in r.data + + r = get(reverse('api:inventory_update_detail', kwargs={'pk': inventory_update.pk}), admin_user, expect=200) + assert 'controller_node' not in r.data + + r = get(reverse('api:system_job_detail', kwargs={'pk': system_job.pk}), admin_user, expect=200) + assert 'controller_node' not in r.data diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 0d9d1c8985..c555aa75d4 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -6,7 +6,7 @@ import pytest # AWX from awx.api.serializers import JobTemplateSerializer from awx.api.versioning import reverse -from awx.main.models.jobs import Job, JobTemplate +from awx.main.models import Job, JobTemplate, CredentialType from awx.main.migrations import _save_password_keys as save_password_keys # Django @@ -182,6 +182,27 @@ def test_extra_credential_creation(get, post, organization_factory, job_template assert response.data.get('count') == 1 +@pytest.mark.django_db +@pytest.mark.parametrize('kind', ['scm', 'insights']) +def test_invalid_credential_kind_xfail(get, post, organization_factory, job_template_factory, kind): + objs = organization_factory("org", superusers=['admin']) + jt = job_template_factory("jt", organization=objs.organization, + inventory='test_inv', project='test_proj').job_template + + url = reverse('api:job_template_credentials_list', kwargs={'version': 'v2', 'pk': jt.pk}) + cred_type = CredentialType.defaults[kind]() + cred_type.save() + response = post(url, { + 'name': 'My Cred', + 'credential_type': cred_type.pk, + 'inputs': { + 'username': 'bob', + 'password': 'secret', + } + }, objs.superusers.admin, expect=400) + assert 'Cannot assign a Credential of kind `{}`.'.format(kind) in response.data.values() + + @pytest.mark.django_db def test_extra_credential_unique_type_xfail(get, post, organization_factory, job_template_factory, credentialtype_aws): objs = organization_factory("org", superusers=['admin']) @@ -567,16 +588,9 @@ def test_v1_launch_with_extra_credentials(get, post, organization_factory, credential=machine_credential.pk, extra_credentials=[credential.pk, net_credential.pk] ), - objs.superusers.admin, expect=201 + objs.superusers.admin, expect=400 ) - job_pk = resp.data.get('id') - assert resp.data.get('ignored_fields').keys() == ['extra_credentials'] - - resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin) - assert resp.data.get('count') == 0 - - resp = get(reverse('api:job_template_extra_credentials_list', kwargs={'pk': jt.pk}), objs.superusers.admin) - assert resp.data.get('count') == 0 + assert 'Field is not allowed for use with v1 API' in resp.data.get('extra_credentials') @pytest.mark.django_db diff --git a/awx/main/tests/functional/api/test_oauth.py b/awx/main/tests/functional/api/test_oauth.py index 7e745213c8..31b1393a9c 100644 --- a/awx/main/tests/functional/api/test_oauth.py +++ b/awx/main/tests/functional/api/test_oauth.py @@ -1,13 +1,17 @@ import pytest import base64 +import json from django.db import connection +from django.test.utils import override_settings +from django.test import Client from awx.main.utils.encryption import decrypt_value, get_encryption_key from awx.api.versioning import reverse, drf_reverse from awx.main.models.oauth import (OAuth2Application as Application, OAuth2AccessToken as AccessToken, ) +from awx.sso.models import UserEnterpriseAuth from oauth2_provider.models import RefreshToken @@ -29,7 +33,50 @@ def test_personal_access_token_creation(oauth_application, post, alice): @pytest.mark.django_db -def test_oauth_application_create(admin, organization, post): +@pytest.mark.parametrize('allow_oauth, status', [(True, 201), (False, 403)]) +def test_token_creation_disabled_for_external_accounts(oauth_application, post, alice, allow_oauth, status): + UserEnterpriseAuth(user=alice, provider='radius').save() + url = drf_reverse('api:oauth_authorization_root_view') + 'token/' + + with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USERS=allow_oauth): + resp = post( + url, + data='grant_type=password&username=alice&password=alice&scope=read', + content_type='application/x-www-form-urlencoded', + HTTP_AUTHORIZATION='Basic ' + base64.b64encode(':'.join([ + oauth_application.client_id, oauth_application.client_secret + ])), + status=status + ) + if allow_oauth: + assert AccessToken.objects.count() == 1 + else: + assert 'OAuth2 Tokens cannot be created by users associated with an external authentication provider' in resp.content + assert AccessToken.objects.count() == 0 + + +@pytest.mark.django_db +def test_pat_creation_no_default_scope(oauth_application, post, admin): + # tests that the default scope is overriden + url = reverse('api:o_auth2_token_list') + response = post(url, {'description': 'test token', + 'scope': 'read', + 'application': oauth_application.pk, + }, admin) + assert response.data['scope'] == 'read' + + +@pytest.mark.django_db +def test_pat_creation_no_scope(oauth_application, post, admin): + url = reverse('api:o_auth2_token_list') + response = post(url, {'description': 'test token', + 'application': oauth_application.pk, + }, admin) + assert response.data['scope'] == 'write' + + +@pytest.mark.django_db +def test_oauth2_application_create(admin, organization, post): response = post( reverse('api:o_auth2_application_list'), { 'name': 'test app', @@ -47,7 +94,18 @@ def test_oauth_application_create(admin, organization, post): assert created_app.client_type == 'confidential' assert created_app.authorization_grant_type == 'password' assert created_app.organization == organization - + + +@pytest.mark.django_db +def test_oauth2_validator(admin, oauth_application, post): + post( + reverse('api:o_auth2_application_list'), { + 'name': 'Write App Token', + 'application': oauth_application.pk, + 'scope': 'Write', + }, admin, expect=400 + ) + @pytest.mark.django_db def test_oauth_application_update(oauth_application, organization, patch, admin, alice): @@ -94,7 +152,7 @@ def test_oauth_token_create(oauth_application, get, post, admin): reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201 ) - assert 'modified' in response.data + assert 'modified' in response.data and response.data['modified'] is not None assert 'updated' not in response.data token = AccessToken.objects.get(token=response.data['token']) refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) @@ -117,6 +175,27 @@ def test_oauth_token_create(oauth_application, get, post, admin): assert response.data['summary_fields']['tokens']['results'][0] == { 'id': token.pk, 'scope': token.scope, 'token': '************' } + # If the application is implicit grant type, no new refresb tokens should be created. + # The following tests check for that. + oauth_application.authorization_grant_type = 'implicit' + oauth_application.save() + token_count = RefreshToken.objects.count() + response = post( + reverse('api:o_auth2_token_list'), + {'scope': 'read', 'application': oauth_application.pk}, admin, expect=201 + ) + assert response.data['refresh_token'] is None + response = post( + reverse('api:user_authorized_token_list', kwargs={'pk': admin.pk}), + {'scope': 'read', 'application': oauth_application.pk}, admin, expect=201 + ) + assert response.data['refresh_token'] is None + response = post( + reverse('api:application_o_auth2_token_list', kwargs={'pk': oauth_application.pk}), + {'scope': 'read'}, admin, expect=201 + ) + assert response.data['refresh_token'] is None + assert token_count == RefreshToken.objects.count() @pytest.mark.django_db @@ -146,7 +225,7 @@ def test_oauth_token_delete(oauth_application, post, delete, get, admin): admin, expect=204 ) assert AccessToken.objects.count() == 0 - assert RefreshToken.objects.count() == 0 + assert RefreshToken.objects.count() == 1 response = get( reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), admin, expect=200 @@ -181,3 +260,57 @@ def test_oauth_list_user_tokens(oauth_application, post, get, admin, alice): post(url, {'scope': 'read'}, user, expect=201) response = get(url, admin, expect=200) assert response.data['count'] == 1 + + +@pytest.mark.django_db +def test_refresh_accesstoken(oauth_application, post, get, delete, admin): + response = post( + reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), + {'scope': 'read'}, admin, expect=201 + ) + token = AccessToken.objects.get(token=response.data['token']) + refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) + assert AccessToken.objects.count() == 1 + assert RefreshToken.objects.count() == 1 + + refresh_url = drf_reverse('api:oauth_authorization_root_view') + 'token/' + response = post( + refresh_url, + data='grant_type=refresh_token&refresh_token=' + refresh_token.token, + content_type='application/x-www-form-urlencoded', + HTTP_AUTHORIZATION='Basic ' + base64.b64encode(':'.join([ + oauth_application.client_id, oauth_application.client_secret + ])) + ) + + new_token = json.loads(response._container[0])['access_token'] + new_refresh_token = json.loads(response._container[0])['refresh_token'] + assert token not in AccessToken.objects.all() + assert AccessToken.objects.get(token=new_token) != 0 + assert RefreshToken.objects.get(token=new_refresh_token) != 0 + refresh_token = RefreshToken.objects.get(token=refresh_token) + assert refresh_token.revoked + + +@pytest.mark.django_db +def test_implicit_authorization(oauth_application, admin): + oauth_application.client_type = 'confidential' + oauth_application.authorization_grant_type = 'implicit' + oauth_application.redirect_uris = 'http://test.com' + oauth_application.save() + data = { + 'response_type': 'token', + 'client_id': oauth_application.client_id, + 'client_secret': oauth_application.client_secret, + 'scope': 'read', + 'redirect_uri': 'http://test.com', + 'allow': True + } + + request_client = Client() + request_client.force_login(admin, 'django.contrib.auth.backends.ModelBackend') + refresh_token_count = RefreshToken.objects.count() + response = request_client.post(drf_reverse('api:authorize'), data) + assert 'http://test.com' in response.url and 'access_token' in response.url + # Make sure no refresh token is created for app with implicit grant type. + assert refresh_token_count == RefreshToken.objects.count() diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py index 43a9ffb1e5..5e4793b6a5 100644 --- a/awx/main/tests/functional/api/test_organizations.py +++ b/awx/main/tests/functional/api/test_organizations.py @@ -260,12 +260,12 @@ def test_organization_delete(delete, admin, organization, organization_jobs_succ @pytest.mark.django_db def test_organization_delete_with_active_jobs(delete, admin, organization, organization_jobs_running): def sort_keys(x): - return (x['type'], x['id']) + return (x['type'], str(x['id'])) url = reverse('api:organization_detail', kwargs={'pk': organization.id}) resp = delete(url, None, user=admin, expect=409) - expect_transformed = [dict(id=str(j.id), type=j.model_to_str()) for j in organization_jobs_running] + expect_transformed = [dict(id=j.id, type=j.model_to_str()) for j in organization_jobs_running] resp_sorted = sorted(resp.data['active_jobs'], key=sort_keys) expect_sorted = sorted(expect_transformed, key=sort_keys) diff --git a/awx/main/tests/functional/api/test_role.py b/awx/main/tests/functional/api/test_role.py index af673509e7..d398263c37 100644 --- a/awx/main/tests/functional/api/test_role.py +++ b/awx/main/tests/functional/api/test_role.py @@ -12,3 +12,28 @@ def test_admin_visible_to_orphaned_users(get, alice): names.add(item['name']) assert 'System Auditor' in names assert 'System Administrator' in names + + +@pytest.mark.django_db +@pytest.mark.parametrize('role,code', [ + ('member_role', 400), + ('admin_role', 400), + ('inventory_admin_role', 204) +]) +@pytest.mark.parametrize('reversed', [ + True, False +]) +def test_org_object_role_assigned_to_team(post, team, organization, org_admin, role, code, reversed): + if reversed: + url = reverse('api:role_teams_list', kwargs={'pk': getattr(organization, role).id}) + sub_id = team.id + else: + url = reverse('api:team_roles_list', kwargs={'pk': team.id}) + sub_id = getattr(organization, role).id + + post( + url=url, + data={'id': sub_id}, + user=org_admin, + expect=code + ) diff --git a/awx/main/tests/functional/api/test_schedules.py b/awx/main/tests/functional/api/test_schedules.py index 56c35d5f94..21e3b91065 100644 --- a/awx/main/tests/functional/api/test_schedules.py +++ b/awx/main/tests/functional/api/test_schedules.py @@ -2,7 +2,8 @@ import pytest from awx.api.versioning import reverse -from awx.main.models import JobTemplate +from awx.main.models import JobTemplate, Schedule +from awx.main.utils.encryption import decrypt_value, get_encryption_key RRULE_EXAMPLE = 'DTSTART:20151117T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1' @@ -51,6 +52,50 @@ def test_valid_survey_answer(post, admin_user, project, inventory, survey_spec_f admin_user, expect=201) +@pytest.mark.django_db +def test_encrypted_survey_answer(post, patch, admin_user, project, inventory, survey_spec_factory): + job_template = JobTemplate.objects.create( + name='test-jt', + project=project, + playbook='helloworld.yml', + inventory=inventory, + ask_variables_on_launch=False, + survey_enabled=True, + survey_spec=survey_spec_factory([{'variable': 'var1', 'type': 'password'}]) + ) + + # test encrypted-on-create + url = reverse('api:job_template_schedules_list', kwargs={'pk': job_template.id}) + r = post(url, {'name': 'test sch', 'rrule': RRULE_EXAMPLE, 'extra_data': '{"var1": "foo"}'}, + admin_user, expect=201) + assert r.data['extra_data']['var1'] == "$encrypted$" + schedule = Schedule.objects.get(pk=r.data['id']) + assert schedule.extra_data['var1'].startswith('$encrypted$') + assert decrypt_value(get_encryption_key('value', pk=None), schedule.extra_data['var1']) == 'foo' + + # test a no-op change + r = patch( + schedule.get_absolute_url(), + data={'extra_data': {'var1': '$encrypted$'}}, + user=admin_user, + expect=200 + ) + assert r.data['extra_data']['var1'] == '$encrypted$' + schedule.refresh_from_db() + assert decrypt_value(get_encryption_key('value', pk=None), schedule.extra_data['var1']) == 'foo' + + # change to a different value + r = patch( + schedule.get_absolute_url(), + data={'extra_data': {'var1': 'bar'}}, + user=admin_user, + expect=200 + ) + assert r.data['extra_data']['var1'] == '$encrypted$' + schedule.refresh_from_db() + assert decrypt_value(get_encryption_key('value', pk=None), schedule.extra_data['var1']) == 'bar' + + @pytest.mark.django_db @pytest.mark.parametrize('rrule, error', [ ("", "This field may not be blank"), @@ -87,6 +132,12 @@ def test_invalid_rrules(post, admin_user, project, inventory, rrule, error): assert error in resp.content +@pytest.mark.django_db +def test_normal_users_can_preview_schedules(post, alice): + url = reverse('api:schedule_rrule') + post(url, {'rrule': get_rrule()}, alice, expect=200) + + @pytest.mark.django_db def test_utc_preview(post, admin_user): url = reverse('api:schedule_rrule') diff --git a/awx/main/tests/functional/api/test_search_filter.py b/awx/main/tests/functional/api/test_search_filter.py new file mode 100644 index 0000000000..4e67ed834a --- /dev/null +++ b/awx/main/tests/functional/api/test_search_filter.py @@ -0,0 +1,54 @@ +# Python +import pytest +import json + +# Django Rest Framework +from rest_framework.test import APIRequestFactory + +# AWX +from awx.api.views import HostList +from awx.main.models import Host, Group, Inventory +from awx.api.versioning import reverse + + +@pytest.mark.django_db +class TestSearchFilter: + def test_related_research_filter_relation(self, admin): + inv = Inventory.objects.create(name="inv") + group1 = Group.objects.create(name="g1", inventory=inv) + group2 = Group.objects.create(name="g2", inventory=inv) + host1 = Host.objects.create(name="host1", inventory=inv) + host2 = Host.objects.create(name="host2", inventory=inv) + host3 = Host.objects.create(name="host3", inventory=inv) + host1.groups.add(group1) + host2.groups.add(group1) + host2.groups.add(group2) + host3.groups.add(group2) + host1.save() + host2.save() + host3.save() + # Login the client + factory = APIRequestFactory() + # Actually test the endpoint. + host_list_url = reverse('api:host_list') + + # Test if the OR releation works. + request = factory.get(host_list_url, data={'groups__search': ['g1', 'g2']}) + request.user = admin + response = HostList.as_view()(request) + response.render() + result = json.loads(response.content) + assert result['count'] == 3 + expected_hosts = ['host1', 'host2', 'host3'] + for i in result['results']: + expected_hosts.remove(i['name']) + assert not expected_hosts + + # Test if the AND relation works. + request = factory.get(host_list_url, data={'groups__search': ['g1,g2']}) + request.user = admin + response = HostList.as_view()(request) + response.render() + result = json.loads(response.content) + assert result['count'] == 1 + assert result['results'][0]['name'] == 'host2' diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 97effe0fa3..4e6852ce85 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -101,6 +101,42 @@ def test_ldap_settings(get, put, patch, delete, admin): patch(url, user=admin, data={'AUTH_LDAP_BIND_DN': u'cn=暴力膜,dc=大新闻,dc=真的粉丝'}, expect=200) +@pytest.mark.django_db +@pytest.mark.parametrize('value', [ + None, '', 'INVALID', 1, [1], ['INVALID'], +]) +def test_ldap_user_flags_by_group_invalid_dn(get, patch, admin, value): + url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) + patch(url, user=admin, + data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': value}}, + expect=400) + + +@pytest.mark.django_db +def test_ldap_user_flags_by_group_string(get, patch, admin): + expected = 'CN=Admins,OU=Groups,DC=example,DC=com' + url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) + patch(url, user=admin, + data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, + expect=200) + resp = get(url, user=admin) + assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == [expected] + + +@pytest.mark.django_db +def test_ldap_user_flags_by_group_list(get, patch, admin): + expected = [ + 'CN=Admins,OU=Groups,DC=example,DC=com', + 'CN=Superadmins,OU=Groups,DC=example,DC=com' + ] + url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) + patch(url, user=admin, + data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, + expect=200) + resp = get(url, user=admin) + assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == expected + + @pytest.mark.parametrize('setting', [ 'AUTH_LDAP_USER_DN_TEMPLATE', 'AUTH_LDAP_REQUIRE_GROUP', diff --git a/awx/main/tests/functional/api/test_unified_jobs_stdout.py b/awx/main/tests/functional/api/test_unified_jobs_stdout.py index 3f1a5760fc..b465f92606 100644 --- a/awx/main/tests/functional/api/test_unified_jobs_stdout.py +++ b/awx/main/tests/functional/api/test_unified_jobs_stdout.py @@ -3,11 +3,8 @@ import base64 import json import re -import shutil -import tempfile from django.conf import settings -from django.db.backends.sqlite3.base import SQLiteCursorWrapper import mock import pytest @@ -31,28 +28,6 @@ def _mk_inventory_update(): return iu -@pytest.fixture(scope='function') -def sqlite_copy_expert(request): - # copy_expert is postgres-specific, and SQLite doesn't support it; mock its - # behavior to test that it writes a file that contains stdout from events - path = tempfile.mkdtemp(prefix='job-event-stdout') - - def write_stdout(self, sql, fd): - # simulate postgres copy_expert support with ORM code - parts = sql.split(' ') - tablename = parts[parts.index('from') + 1] - for cls in (JobEvent, AdHocCommandEvent, ProjectUpdateEvent, - InventoryUpdateEvent, SystemJobEvent): - if cls._meta.db_table == tablename: - for event in cls.objects.order_by('start_line').all(): - fd.write(event.stdout.encode('utf-8')) - - setattr(SQLiteCursorWrapper, 'copy_expert', write_stdout) - request.addfinalizer(lambda: shutil.rmtree(path)) - request.addfinalizer(lambda: delattr(SQLiteCursorWrapper, 'copy_expert')) - return path - - @pytest.mark.django_db @pytest.mark.parametrize('Parent, Child, relation, view', [ [Job, JobEvent, 'job', 'api:job_stdout'], diff --git a/awx/main/tests/functional/commands/test_expire_sessions.py b/awx/main/tests/functional/commands/test_expire_sessions.py new file mode 100644 index 0000000000..91e811bdcf --- /dev/null +++ b/awx/main/tests/functional/commands/test_expire_sessions.py @@ -0,0 +1,67 @@ +# Python +import pytest +import string +import random + +# Django +from django.utils import timezone +from django.test import Client +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sessions.models import Session +from django.core.management.base import CommandError + +# AWX +from awx.main.management.commands.expire_sessions import Command + + +@pytest.mark.django_db +class TestExpireSessionsCommand: + @staticmethod + def create_and_login_fake_users(): + # We already have Alice and Bob, so we are going to create Charlie and Dylan + charlie = User.objects.create_user('charlie', 'charlie@email.com', 'pass') + dylan = User.objects.create_user('dylan', 'dylan@email.com', 'word') + client_0 = Client() + client_1 = Client() + client_0.force_login(charlie, backend=settings.AUTHENTICATION_BACKENDS[0]) + client_1.force_login(dylan, backend=settings.AUTHENTICATION_BACKENDS[0]) + return charlie, dylan + + @staticmethod + def run_command(username=None): + command_obj = Command() + command_obj.handle(user=username) + + def test_expire_all_sessions(self): + charlie, dylan = self.create_and_login_fake_users() + self.run_command() + start = timezone.now() + sessions = Session.objects.filter(expire_date__gte=start) + for session in sessions: + user_id = int(session.get_decoded().get('_auth_user_id')) + if user_id == charlie.id or user_id == dylan.id: + self.fail('The user should not have active sessions.') + + def test_non_existing_user(self): + fake_username = '' + while fake_username == '' or User.objects.filter(username=fake_username).exists(): + fake_username = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)) + with pytest.raises(CommandError) as excinfo: + self.run_command(fake_username) + assert excinfo.value.message.strip() == 'The user does not exist.' + + def test_expire_one_user(self): + # alice should be logged out, but bob should not. + charlie, dylan = self.create_and_login_fake_users() + self.run_command('charlie') + start = timezone.now() + sessions = Session.objects.filter(expire_date__gte=start) + dylan_still_active = False + for session in sessions: + user_id = int(session.get_decoded().get('_auth_user_id')) + if user_id == charlie.id: + self.fail('Charlie should not have active sessions.') + elif user_id == dylan.id: + dylan_still_active = True + assert dylan_still_active diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 28d7b65564..de25bf1a7f 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -4,6 +4,8 @@ import mock import json import os import six +import tempfile +import shutil from datetime import timedelta from six.moves import xrange @@ -14,6 +16,7 @@ from django.utils import timezone from django.contrib.auth.models import User from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder +from django.db.backends.sqlite3.base import SQLiteCursorWrapper from jsonbfield.fields import JSONField # AWX @@ -44,6 +47,13 @@ from awx.main.models.notifications import ( NotificationTemplate, Notification ) +from awx.main.models.events import ( + JobEvent, + AdHocCommandEvent, + ProjectUpdateEvent, + InventoryUpdateEvent, + SystemJobEvent, +) from awx.main.models.workflow import WorkflowJobTemplate from awx.main.models.ad_hoc_commands import AdHocCommand from awx.main.models.oauth import OAuth2Application as Application @@ -553,7 +563,9 @@ def _request(verb): response.data[key] = str(value) except Exception: response.data = data_copy - assert response.status_code == expect + assert response.status_code == expect, 'Response data: {}'.format( + getattr(response, 'data', None) + ) if hasattr(response, 'render'): response.render() __SWAGGER_REQUESTS__.setdefault(request.path, {})[ @@ -672,7 +684,7 @@ def job_template_labels(organization, job_template): @pytest.fixture def workflow_job_template(organization): - wjt = WorkflowJobTemplate(name='test-workflow_job_template') + wjt = WorkflowJobTemplate(name='test-workflow_job_template', organization=organization) wjt.save() return wjt @@ -729,3 +741,26 @@ def oauth_application(admin): name='test app', user=admin, client_type='confidential', authorization_grant_type='password' ) + + +@pytest.fixture +def sqlite_copy_expert(request): + # copy_expert is postgres-specific, and SQLite doesn't support it; mock its + # behavior to test that it writes a file that contains stdout from events + path = tempfile.mkdtemp(prefix='job-event-stdout') + + def write_stdout(self, sql, fd): + # simulate postgres copy_expert support with ORM code + parts = sql.split(' ') + tablename = parts[parts.index('from') + 1] + for cls in (JobEvent, AdHocCommandEvent, ProjectUpdateEvent, + InventoryUpdateEvent, SystemJobEvent): + if cls._meta.db_table == tablename: + for event in cls.objects.order_by('start_line').all(): + fd.write(event.stdout.encode('utf-8')) + + setattr(SQLiteCursorWrapper, 'copy_expert', write_stdout) + request.addfinalizer(lambda: shutil.rmtree(path)) + request.addfinalizer(lambda: delattr(SQLiteCursorWrapper, 'copy_expert')) + return path + diff --git a/awx/main/tests/functional/models/test_activity_stream.py b/awx/main/tests/functional/models/test_activity_stream.py index f13ce48f20..eff901093e 100644 --- a/awx/main/tests/functional/models/test_activity_stream.py +++ b/awx/main/tests/functional/models/test_activity_stream.py @@ -184,6 +184,32 @@ def test_annon_user_action(): assert not entry.actor +@pytest.mark.django_db +def test_activity_stream_deleted_actor(alice, bob): + alice.first_name = 'Alice' + alice.last_name = 'Doe' + alice.save() + with impersonate(alice): + o = Organization.objects.create(name='test organization') + entry = o.activitystream_set.get(operation='create') + assert entry.actor == alice + + alice.delete() + entry = o.activitystream_set.get(operation='create') + assert entry.actor is None + deleted = entry.deleted_actor + assert deleted['username'] == 'alice' + assert deleted['first_name'] == 'Alice' + assert deleted['last_name'] == 'Doe' + + entry.actor = bob + entry.save(update_fields=['actor']) + deleted = entry.deleted_actor + + entry = ActivityStream.objects.get(id=entry.pk) + assert entry.deleted_actor['username'] == 'bob' + + @pytest.mark.django_db def test_modified_not_allowed_field(somecloud_type): ''' diff --git a/awx/main/tests/functional/models/test_job.py b/awx/main/tests/functional/models/test_job.py index ec23045fea..013f73ca39 100644 --- a/awx/main/tests/functional/models/test_job.py +++ b/awx/main/tests/functional/models/test_job.py @@ -1,6 +1,7 @@ import pytest +import six -from awx.main.models import JobTemplate, Job +from awx.main.models import JobTemplate, Job, JobHostSummary from crum import impersonate @@ -65,3 +66,18 @@ def test_update_parent_instance(job_template, alice): assert job_template.current_job == job assert job_template.status == 'pending' assert job_template.modified_by is None + + +@pytest.mark.django_db +def test_job_host_summary_representation(host): + job = Job.objects.create(name='foo') + jhs = JobHostSummary.objects.create( + host=host, job=job, + changed=1, dark=2, failures=3, ok=4, processed=5, skipped=6 + ) + assert 'single-host changed=1 dark=2 failures=3 ok=4 processed=5 skipped=6' == six.text_type(jhs) + + # Representation should be robust to deleted related items + jhs = JobHostSummary.objects.get(pk=jhs.id) + host.delete() + assert 'N/A changed=1 dark=2 failures=3 ok=4 processed=5 skipped=6' == six.text_type(jhs) diff --git a/awx/main/tests/functional/models/test_project.py b/awx/main/tests/functional/models/test_project.py index 71352ed633..f150dbe00a 100644 --- a/awx/main/tests/functional/models/test_project.py +++ b/awx/main/tests/functional/models/test_project.py @@ -2,6 +2,7 @@ import pytest import mock from awx.main.models import Project +from awx.main.models.organization import Organization @pytest.mark.django_db @@ -31,3 +32,10 @@ def test_sensitive_change_triggers_update(project): project.scm_url = 'https://foo2.invalid' project.save() mock_update.assert_called_once_with() + + +@pytest.mark.django_db +def test_foreign_key_change_changes_modified_by(project, organization): + assert project._get_fields_snapshot()['organization_id'] == organization.id + project.organization = Organization(name='foo', pk=41) + assert project._get_fields_snapshot()['organization_id'] == 41 diff --git a/awx/main/tests/functional/models/test_schedule.py b/awx/main/tests/functional/models/test_schedule.py index aa933c33c9..b03e3aab8b 100644 --- a/awx/main/tests/functional/models/test_schedule.py +++ b/awx/main/tests/functional/models/test_schedule.py @@ -7,6 +7,8 @@ import pytz from awx.main.models import JobTemplate, Schedule +from crum import impersonate + @pytest.fixture def job_template(inventory, project): @@ -18,6 +20,22 @@ def job_template(inventory, project): ) +@pytest.mark.django_db +def test_computed_fields_modified_by_retained(job_template, admin_user): + with impersonate(admin_user): + s = Schedule.objects.create( + name='Some Schedule', + rrule='DTSTART:20300112T210000Z RRULE:FREQ=DAILY;INTERVAL=1', + unified_job_template=job_template + ) + s.refresh_from_db() + assert s.created_by == admin_user + assert s.modified_by == admin_user + s.update_computed_fields() + s.save() + assert s.modified_by == admin_user + + @pytest.mark.django_db def test_repeats_forever(job_template): s = Schedule( diff --git a/awx/main/tests/functional/models/test_unified_job.py b/awx/main/tests/functional/models/test_unified_job.py index 05d5aa318a..f85cc4fe5d 100644 --- a/awx/main/tests/functional/models/test_unified_job.py +++ b/awx/main/tests/functional/models/test_unified_job.py @@ -1,3 +1,4 @@ +import itertools import pytest import mock @@ -88,7 +89,7 @@ class TestIsolatedRuns: with mock.patch.object(job, '_get_task_class') as task_class: task_class.return_value = MockTaskClass job.start_celery_task([], error_callback, success_callback, 'thepentagon') - mock_async.assert_called_with([job.id, 'iso2'], [], + mock_async.assert_called_with([job.id], [], link_error=error_callback, link=success_callback, queue='thepentagon', @@ -100,7 +101,7 @@ class TestIsolatedRuns: with mock.patch.object(job, '_get_task_class') as task_class: task_class.return_value = MockTaskClass job.start_celery_task([], error_callback, success_callback, 'thepentagon') - mock_async.assert_called_with([job.id, 'iso1'], [], + mock_async.assert_called_with([job.id], [], link_error=error_callback, link=success_callback, queue='thepentagon', @@ -113,6 +114,27 @@ class TestMetaVars: Extension of unit tests with same class name ''' + def test_deleted_user(self, admin_user): + job = Job.objects.create( + name='job', + created_by=admin_user + ) + job.save() + + user_vars = ['_'.join(x) for x in itertools.product( + ['tower', 'awx'], + ['user_name', 'user_id', 'user_email', 'user_first_name', 'user_last_name'] + )] + + for key in user_vars: + assert key in job.awx_meta_vars() + + # deleted user is hard to simulate as this test occurs within one transaction + job = Job.objects.get(pk=job.id) + job.created_by_id = 999999999 + for key in user_vars: + assert key not in job.awx_meta_vars() + def test_workflow_job_metavars(self, admin_user): workflow_job = WorkflowJob.objects.create( name='workflow-job', @@ -124,6 +146,7 @@ class TestMetaVars: ) workflow_job.workflow_nodes.create(job=job) data = job.awx_meta_vars() + assert data['awx_user_id'] == admin_user.id assert data['awx_user_name'] == admin_user.username assert data['awx_workflow_job_id'] == workflow_job.pk @@ -141,6 +164,7 @@ class TestMetaVars: ) data = job.awx_meta_vars() assert data['awx_schedule_id'] == schedule.pk + assert 'awx_user_name' not in data @pytest.mark.django_db diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index 61882f2097..0514fc8bda 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -61,6 +61,15 @@ class TestWorkflowDAGFunctional(TransactionTestCase): self.assertTrue(is_done) self.assertFalse(has_failed) + # verify that relaunched WFJ fails if a JT leaf is deleted + for jt in JobTemplate.objects.all(): + jt.delete() + relaunched = wfj.create_relaunch_workflow_job() + dag = WorkflowDAG(workflow_job=relaunched) + is_done, has_failed = dag.is_workflow_done() + self.assertTrue(is_done) + self.assertTrue(has_failed) + def test_workflow_fails_for_unfinished_node(self): wfj = self.workflow_job(states=['error', None, None, None, None]) dag = WorkflowDAG(workflow_job=wfj) diff --git a/awx/main/tests/functional/task_management/test_rampart_groups.py b/awx/main/tests/functional/task_management/test_rampart_groups.py index ce79b78003..c58869ebf3 100644 --- a/awx/main/tests/functional/task_management/test_rampart_groups.py +++ b/awx/main/tests/functional/task_management/test_rampart_groups.py @@ -31,7 +31,7 @@ def test_multi_group_basic_job_launch(instance_factory, default_instance_group, mock_task_impact.return_value = 500 with mocker.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, []), mock.call(j2, ig2, [])]) + TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, [], i1), mock.call(j2, ig2, [], i2)]) @@ -65,15 +65,18 @@ def test_multi_group_with_shared_dependency(instance_factory, default_instance_g with mocker.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() pu = p.project_updates.first() - TaskManager.start_task.assert_called_once_with(pu, default_instance_group, [j1]) + TaskManager.start_task.assert_called_once_with(pu, + default_instance_group, + [j1], + default_instance_group.instances.all()[0]) pu.finished = pu.created + timedelta(seconds=1) pu.status = "successful" pu.save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_any_call(j1, ig1, []) - TaskManager.start_task.assert_any_call(j2, ig2, []) + TaskManager.start_task.assert_any_call(j1, ig1, [], i1) + TaskManager.start_task.assert_any_call(j2, ig2, [], i2) assert TaskManager.start_task.call_count == 2 @@ -85,7 +88,7 @@ def test_workflow_job_no_instancegroup(workflow_job_template_factory, default_in wfj.save() with mocker.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(wfj, None, []) + TaskManager.start_task.assert_called_once_with(wfj, None, [], None) assert wfj.instance_group is None @@ -131,8 +134,9 @@ def test_overcapacity_blocking_other_groups_unaffected(instance_factory, default mock_task_impact.return_value = 500 with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job: tm.schedule() - mock_job.assert_has_calls([mock.call(j1, ig1, []), mock.call(j1_1, ig1, []), - mock.call(j2, ig2, [])]) + mock_job.assert_has_calls([mock.call(j1, ig1, [], i1), + mock.call(j1_1, ig1, [], i1), + mock.call(j2, ig2, [], i2)]) assert mock_job.call_count == 3 @@ -163,13 +167,16 @@ def test_failover_group_run(instance_factory, default_instance_group, mocker, mock_task_impact.return_value = 500 with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job: tm.schedule() - mock_job.assert_has_calls([mock.call(j1, ig1, []), mock.call(j1_1, ig2, [])]) + mock_job.assert_has_calls([mock.call(j1, ig1, [], i1), + mock.call(j1_1, ig2, [], i2)]) assert mock_job.call_count == 2 @pytest.mark.django_db def test_instance_group_basic_policies(instance_factory, instance_group_factory): i0 = instance_factory("i0") + i0.managed_by_policy = False + i0.save() i1 = instance_factory("i1") i2 = instance_factory("i2") i3 = instance_factory("i3") diff --git a/awx/main/tests/functional/task_management/test_scheduler.py b/awx/main/tests/functional/task_management/test_scheduler.py index 82625533c7..813adff7cf 100644 --- a/awx/main/tests/functional/task_management/test_scheduler.py +++ b/awx/main/tests/functional/task_management/test_scheduler.py @@ -18,6 +18,7 @@ from awx.main.models.notifications import JobNotificationMixin @pytest.mark.django_db def test_single_job_scheduler_launch(default_instance_group, job_template_factory, mocker): + instance = default_instance_group.instances.all()[0] objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start"]) @@ -26,11 +27,12 @@ def test_single_job_scheduler_launch(default_instance_group, job_template_factor j.save() with mocker.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j, default_instance_group, [], instance) @pytest.mark.django_db def test_single_jt_multi_job_launch_blocks_last(default_instance_group, job_template_factory, mocker): + instance = default_instance_group.instances.all()[0] objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start", "job_should_not_start"]) @@ -42,16 +44,17 @@ def test_single_jt_multi_job_launch_blocks_last(default_instance_group, job_temp j2.save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j1, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j1, default_instance_group, [], instance) j1.status = "successful" j1.save() with mocker.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j2, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j2, default_instance_group, [], instance) @pytest.mark.django_db def test_single_jt_multi_job_launch_allow_simul_allowed(default_instance_group, job_template_factory, mocker): + instance = default_instance_group.instances.all()[0] objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start", "job_should_not_start"]) @@ -68,12 +71,13 @@ def test_single_jt_multi_job_launch_allow_simul_allowed(default_instance_group, j2.save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_has_calls([mock.call(j1, default_instance_group, []), - mock.call(j2, default_instance_group, [])]) + TaskManager.start_task.assert_has_calls([mock.call(j1, default_instance_group, [], instance), + mock.call(j2, default_instance_group, [], instance)]) @pytest.mark.django_db def test_multi_jt_capacity_blocking(default_instance_group, job_template_factory, mocker): + instance = default_instance_group.instances.all()[0] objects1 = job_template_factory('jt1', organization='org1', project='proj1', inventory='inv1', credential='cred1', jobs=["job_should_start"]) @@ -91,20 +95,20 @@ def test_multi_jt_capacity_blocking(default_instance_group, job_template_factory mock_task_impact.return_value = 500 with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job: tm.schedule() - mock_job.assert_called_once_with(j1, default_instance_group, []) + mock_job.assert_called_once_with(j1, default_instance_group, [], instance) j1.status = "successful" j1.save() with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job: tm.schedule() - mock_job.assert_called_once_with(j2, default_instance_group, []) - - + mock_job.assert_called_once_with(j2, default_instance_group, [], instance) + @pytest.mark.django_db def test_single_job_dependencies_project_launch(default_instance_group, job_template_factory, mocker): objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start"]) + instance = default_instance_group.instances.all()[0] j = objects.jobs["job_should_start"] j.status = 'pending' j.save() @@ -121,12 +125,12 @@ def test_single_job_dependencies_project_launch(default_instance_group, job_temp mock_pu.assert_called_once_with(j) pu = [x for x in p.project_updates.all()] assert len(pu) == 1 - TaskManager.start_task.assert_called_once_with(pu[0], default_instance_group, [j]) + TaskManager.start_task.assert_called_once_with(pu[0], default_instance_group, [j], instance) pu[0].status = "successful" pu[0].save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j, default_instance_group, [], instance) @pytest.mark.django_db @@ -134,6 +138,7 @@ def test_single_job_dependencies_inventory_update_launch(default_instance_group, objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start"]) + instance = default_instance_group.instances.all()[0] j = objects.jobs["job_should_start"] j.status = 'pending' j.save() @@ -151,12 +156,12 @@ def test_single_job_dependencies_inventory_update_launch(default_instance_group, mock_iu.assert_called_once_with(j, ii) iu = [x for x in ii.inventory_updates.all()] assert len(iu) == 1 - TaskManager.start_task.assert_called_once_with(iu[0], default_instance_group, [j]) + TaskManager.start_task.assert_called_once_with(iu[0], default_instance_group, [j], instance) iu[0].status = "successful" iu[0].save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j, default_instance_group, [], instance) @pytest.mark.django_db @@ -164,6 +169,7 @@ def test_job_dependency_with_already_updated(default_instance_group, job_templat objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start"]) + instance = default_instance_group.instances.all()[0] j = objects.jobs["job_should_start"] j.status = 'pending' j.save() @@ -185,11 +191,12 @@ def test_job_dependency_with_already_updated(default_instance_group, job_templat mock_iu.assert_not_called() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j, default_instance_group, [], instance) @pytest.mark.django_db def test_shared_dependencies_launch(default_instance_group, job_template_factory, mocker, inventory_source_factory): + instance = default_instance_group.instances.all()[0] objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["first_job", "second_job"]) @@ -218,8 +225,8 @@ def test_shared_dependencies_launch(default_instance_group, job_template_factory TaskManager().schedule() pu = p.project_updates.first() iu = ii.inventory_updates.first() - TaskManager.start_task.assert_has_calls([mock.call(pu, default_instance_group, [iu, j1]), - mock.call(iu, default_instance_group, [pu, j1])]) + TaskManager.start_task.assert_has_calls([mock.call(pu, default_instance_group, [iu, j1], instance), + mock.call(iu, default_instance_group, [pu, j1], instance)]) pu.status = "successful" pu.finished = pu.created + timedelta(seconds=1) pu.save() @@ -228,12 +235,12 @@ def test_shared_dependencies_launch(default_instance_group, job_template_factory iu.save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j1, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j1, default_instance_group, [], instance) j1.status = "successful" j1.save() with mock.patch("awx.main.scheduler.TaskManager.start_task"): TaskManager().schedule() - TaskManager.start_task.assert_called_once_with(j2, default_instance_group, []) + TaskManager.start_task.assert_called_once_with(j2, default_instance_group, [], instance) pu = [x for x in p.project_updates.all()] iu = [x for x in ii.inventory_updates.all()] assert len(pu) == 1 diff --git a/awx/main/tests/functional/test_copy.py b/awx/main/tests/functional/test_copy.py index 99e123a8fa..1b017c8e10 100644 --- a/awx/main/tests/functional/test_copy.py +++ b/awx/main/tests/functional/test_copy.py @@ -18,6 +18,8 @@ def test_job_template_copy(post, get, project, inventory, machine_credential, va job_template_with_survey_passwords.credentials.add(machine_credential) job_template_with_survey_passwords.credentials.add(vault_credential) job_template_with_survey_passwords.admin_role.members.add(alice) + project.admin_role.members.add(alice) + inventory.admin_role.members.add(alice) assert get( reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), alice, expect=200 @@ -26,6 +28,10 @@ def test_job_template_copy(post, get, project, inventory, machine_credential, va reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), admin, expect=200 ).data['can_copy'] is True + post( + reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), + {'name': 'new jt name'}, alice, expect=403 + ) jt_copy_pk = post( reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), {'name': 'new jt name'}, admin, expect=201 @@ -52,6 +58,7 @@ def test_project_copy(post, get, project, organization, scm_credential, alice): reverse('api:project_copy', kwargs={'pk': project.pk}), alice, expect=200 ).data['can_copy'] is False project.organization.admin_role.members.add(alice) + scm_credential.use_role.members.add(alice) assert get( reverse('api:project_copy', kwargs={'pk': project.pk}), alice, expect=200 ).data['can_copy'] is True @@ -170,7 +177,7 @@ def test_credential_copy(post, get, machine_credential, credentialtype_ssh, admi @pytest.mark.django_db def test_notification_template_copy(post, get, notification_template_with_encrypt, organization, alice): - #notification_template_with_encrypt.admin_role.members.add(alice) + notification_template_with_encrypt.organization.auditor_role.members.add(alice) assert get( reverse( 'api:notification_template_copy', kwargs={'pk': notification_template_with_encrypt.pk} @@ -197,6 +204,7 @@ def test_notification_template_copy(post, get, notification_template_with_encryp @pytest.mark.django_db def test_inventory_script_copy(post, get, inventory_script, organization, alice): + inventory_script.organization.auditor_role.members.add(alice) assert get( reverse('api:inventory_script_copy', kwargs={'pk': inventory_script.pk}), alice, expect=200 ).data['can_copy'] is False diff --git a/awx/main/tests/functional/test_instances.py b/awx/main/tests/functional/test_instances.py index 91dee86b9e..ec38c0598a 100644 --- a/awx/main/tests/functional/test_instances.py +++ b/awx/main/tests/functional/test_instances.py @@ -1,16 +1,66 @@ import pytest import mock -from awx.main.models import AdHocCommand, InventoryUpdate, Job, JobTemplate, ProjectUpdate, Instance +from awx.main.models import AdHocCommand, InventoryUpdate, Job, JobTemplate, ProjectUpdate +from awx.main.models.ha import Instance, InstanceGroup from awx.main.tasks import apply_cluster_membership_policies from awx.api.versioning import reverse +from django.utils.timezone import now + @pytest.mark.django_db def test_default_tower_instance_group(default_instance_group, job_factory): assert default_instance_group in job_factory().preferred_instance_groups +@pytest.mark.django_db +class TestPolicyTaskScheduling: + """Tests make assertions about when the policy task gets scheduled""" + + @pytest.mark.parametrize('field, value, expect', [ + ('name', 'foo-bar-foo-bar', False), + ('policy_instance_percentage', 35, True), + ('policy_instance_minimum', 3, True), + ('policy_instance_list', ['bar?'], True), + ('modified', now(), False) + ]) + def test_policy_task_ran_for_ig_when_needed(self, instance_group_factory, field, value, expect): + # always run on instance group creation + with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy: + ig = InstanceGroup.objects.create(name='foo') + mock_policy.assert_called_once() + # selectively run on instance group modification + with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy: + setattr(ig, field, value) + ig.save() + if expect: + mock_policy.assert_called_once() + else: + mock_policy.assert_not_called() + + @pytest.mark.parametrize('field, value, expect', [ + ('hostname', 'foo-bar-foo-bar', True), + ('managed_by_policy', False, True), + ('enabled', False, False), + ('capacity_adjustment', 0.42, True), + ('capacity', 42, False) + ]) + def test_policy_task_ran_for_instance_when_needed(self, instance_group_factory, field, value, expect): + # always run on instance group creation + with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy: + inst = Instance.objects.create(hostname='foo') + mock_policy.assert_called_once() + # selectively run on instance group modification + with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy: + setattr(inst, field, value) + inst.save() + if expect: + mock_policy.assert_called_once() + else: + mock_policy.assert_not_called() + + @pytest.mark.django_db def test_instance_dup(org_admin, organization, project, instance_factory, instance_group_factory, get, system_auditor): i1 = instance_factory("i1") @@ -32,8 +82,7 @@ def test_instance_dup(org_admin, organization, project, instance_factory, instan @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_few_instances(mock, instance_factory, instance_group_factory): +def test_policy_instance_few_instances(instance_factory, instance_group_factory): i1 = instance_factory("i1") ig_1 = instance_group_factory("ig1", percentage=25) ig_2 = instance_group_factory("ig2", percentage=25) @@ -61,8 +110,7 @@ def test_policy_instance_few_instances(mock, instance_factory, instance_group_fa @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_distribution_round_up(mock, instance_factory, instance_group_factory): +def test_policy_instance_distribution_round_up(instance_factory, instance_group_factory): i1 = instance_factory("i1") i2 = instance_factory("i2") i3 = instance_factory("i3") @@ -76,8 +124,7 @@ def test_policy_instance_distribution_round_up(mock, instance_factory, instance_ @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_distribution_uneven(mock, instance_factory, instance_group_factory): +def test_policy_instance_distribution_uneven(instance_factory, instance_group_factory): i1 = instance_factory("i1") i2 = instance_factory("i2") i3 = instance_factory("i3") @@ -97,8 +144,7 @@ def test_policy_instance_distribution_uneven(mock, instance_factory, instance_gr @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_distribution_even(mock, instance_factory, instance_group_factory): +def test_policy_instance_distribution_even(instance_factory, instance_group_factory): i1 = instance_factory("i1") i2 = instance_factory("i2") i3 = instance_factory("i3") @@ -131,8 +177,7 @@ def test_policy_instance_distribution_even(mock, instance_factory, instance_grou @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_distribution_simultaneous(mock, instance_factory, instance_group_factory): +def test_policy_instance_distribution_simultaneous(instance_factory, instance_group_factory): i1 = instance_factory("i1") i2 = instance_factory("i2") i3 = instance_factory("i3") @@ -154,8 +199,7 @@ def test_policy_instance_distribution_simultaneous(mock, instance_factory, insta @pytest.mark.django_db -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes', return_value=None) -def test_policy_instance_list_manually_managed(mock, instance_factory, instance_group_factory): +def test_policy_instance_list_manually_assigned(instance_factory, instance_group_factory): i1 = instance_factory("i1") i2 = instance_factory("i2") ig_1 = instance_group_factory("ig1", percentage=100, minimum=2) @@ -163,13 +207,36 @@ def test_policy_instance_list_manually_managed(mock, instance_factory, instance_ ig_2.policy_instance_list = [i2.hostname] ig_2.save() apply_cluster_membership_policies() - assert len(ig_1.instances.all()) == 1 + assert len(ig_1.instances.all()) == 2 assert i1 in ig_1.instances.all() - assert i2 not in ig_1.instances.all() + assert i2 in ig_1.instances.all() assert len(ig_2.instances.all()) == 1 assert i2 in ig_2.instances.all() +@pytest.mark.django_db +def test_policy_instance_list_explicitly_pinned(instance_factory, instance_group_factory): + i1 = instance_factory("i1") + i2 = instance_factory("i2") + ig_1 = instance_group_factory("ig1", percentage=100, minimum=2) + ig_2 = instance_group_factory("ig2") + ig_2.policy_instance_list = [i2.hostname] + ig_2.save() + + # without being marked as manual, i2 will be picked up by ig_1 + apply_cluster_membership_policies() + assert set(ig_1.instances.all()) == set([i1, i2]) + assert set(ig_2.instances.all()) == set([i2]) + + i2.managed_by_policy = False + i2.save() + + # after marking as manual, i2 no longer available for ig_1 + apply_cluster_membership_policies() + assert set(ig_1.instances.all()) == set([i1]) + assert set(ig_2.instances.all()) == set([i2]) + + @pytest.mark.django_db def test_basic_instance_group_membership(instance_group_factory, default_instance_group, job_factory): j = job_factory() @@ -193,6 +260,18 @@ def test_inherited_instance_group_membership(instance_group_factory, default_ins assert default_instance_group not in j.preferred_instance_groups +@pytest.mark.django_db +def test_mixed_group_membership(instance_factory, instance_group_factory): + for i in range(5): + instance_factory("i{}".format(i)) + ig_1 = instance_group_factory("ig1", percentage=60) + ig_2 = instance_group_factory("ig2", minimum=3) + ig_3 = instance_group_factory("ig3", minimum=1, percentage=60) + apply_cluster_membership_policies() + for group in (ig_1, ig_2, ig_3): + assert len(group.instances.all()) == 3 + + @pytest.mark.django_db def test_instance_group_capacity(instance_factory, instance_group_factory): i1 = instance_factory("i1") diff --git a/awx/main/tests/functional/test_jobs.py b/awx/main/tests/functional/test_jobs.py index aa95574b36..fd8070aab7 100644 --- a/awx/main/tests/functional/test_jobs.py +++ b/awx/main/tests/functional/test_jobs.py @@ -21,7 +21,6 @@ def test_orphan_unified_job_creation(instance, inventory): @pytest.mark.django_db @mock.patch('awx.main.utils.common.get_cpu_capacity', lambda: (2,8)) @mock.patch('awx.main.utils.common.get_mem_capacity', lambda: (8000,62)) -@mock.patch('awx.main.tasks.handle_ha_toplogy_changes.apply_async', lambda: True) def test_job_capacity_and_with_inactive_node(): i = Instance.objects.create(hostname='test-1') i.refresh_capacity() diff --git a/awx/main/tests/functional/test_named_url.py b/awx/main/tests/functional/test_named_url.py index 593822c806..df315e583d 100644 --- a/awx/main/tests/functional/test_named_url.py +++ b/awx/main/tests/functional/test_named_url.py @@ -5,10 +5,10 @@ from django.core.exceptions import ImproperlyConfigured from awx.api.versioning import reverse from awx.main.middleware import URLModificationMiddleware from awx.main.models import * # noqa +from awx.conf import settings_registry -@pytest.fixture(scope='function', autouse=True) -def init_url_modification_middleware(): +def setup_module(module): # In real-world scenario, named url graph structure is populated by __init__ # of URLModificationMiddleware. The way Django bootstraps ensures the initialization # will happen *once and only once*, while the number of initialization is uncontrollable @@ -20,6 +20,12 @@ def init_url_modification_middleware(): pass +def teardown_module(module): + # settings_registry will be persistent states unless we explicitly clean them up. + settings_registry.unregister('NAMED_URL_FORMATS') + settings_registry.unregister('NAMED_URL_GRAPH_NODES') + + @pytest.mark.django_db def test_user(get, admin_user): test_user = User.objects.create(username='test_user', password='test_user', is_superuser=False) diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index 55cc484006..f8466355f1 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -209,6 +209,25 @@ def test_create_project(post, organization, org_admin, org_member, admin, rando, assert Project.objects.filter(name='Project', organization=organization).exists() +@pytest.mark.django_db +def test_project_credential_protection(post, put, project, organization, scm_credential, org_admin): + project.save() + project.admin_role.members.add(org_admin) + put( + reverse('api:project_detail', kwargs={'pk':project.id}), { + 'name': 'should not change', + 'credential': scm_credential.id + }, org_admin, expect=403 + ) + post( + reverse('api:project_list'), { + 'name': 'should not create', + 'organization':organization.id, + 'credential': scm_credential.id + }, org_admin, expect=403 + ) + + @pytest.mark.django_db() def test_create_project_null_organization(post, organization, admin): post(reverse('api:project_list'), { 'name': 't', 'organization': None}, admin, expect=201) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 783fdd9e82..2b134c18f5 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -1,5 +1,7 @@ import pytest +import mock + from awx.main.access import CredentialAccess from awx.main.models.credential import Credential from django.contrib.auth.models import User @@ -22,6 +24,21 @@ def test_credential_access_superuser(): assert access.can_delete(credential) +@pytest.mark.django_db +def test_credential_access_self(rando): + access = CredentialAccess(rando) + assert access.can_add({'user': rando.pk}) + + +@pytest.mark.django_db +@pytest.mark.parametrize('ext_auth', [True, False]) +def test_credential_access_org_user(org_member, org_admin, ext_auth): + access = CredentialAccess(org_admin) + with mock.patch('awx.main.access.settings') as settings_mock: + settings_mock.MANAGE_ORGANIZATION_AUTH = ext_auth + assert access.can_add({'user': org_member.pk}) + + @pytest.mark.django_db def test_credential_access_auditor(credential, organization_factory): objects = organization_factory("org_cred_auditor", diff --git a/awx/main/tests/functional/test_rbac_job.py b/awx/main/tests/functional/test_rbac_job.py index 55785f9c73..4f8b62a574 100644 --- a/awx/main/tests/functional/test_rbac_job.py +++ b/awx/main/tests/functional/test_rbac_job.py @@ -19,6 +19,8 @@ from awx.main.models import ( Credential ) +from crum import impersonate + @pytest.fixture def normal_job(deploy_jobtemplate): @@ -151,11 +153,14 @@ class TestJobRelaunchAccess: ask_inventory_on_launch=True, ask_credential_on_launch=True ) - job_with_links = Job.objects.create(name='existing-job', inventory=inventory, job_template=job_template) + u = user('user1', False) + job_with_links = Job.objects.create( + name='existing-job', inventory=inventory, job_template=job_template, + created_by=u + ) job_with_links.credentials.add(machine_credential) JobLaunchConfig.objects.create(job=job_with_links, inventory=inventory) job_with_links.launch_config.credentials.add(machine_credential) # credential was prompted - u = user('user1', False) job_template.execute_role.members.add(u) if inv_access: job_with_links.inventory.use_role.members.add(u) @@ -223,6 +228,32 @@ class TestJobRelaunchAccess: job.credentials.add(net_credential) assert not rando.can_access(Job, 'start', job, validate_license=False) + @pytest.mark.job_runtime_vars + def test_callback_relaunchable_by_user(self, job_template, rando): + with impersonate(rando): + job = job_template.create_unified_job( + _eager_fields={'launch_type': 'callback'}, + limit='host2' + ) + assert 'limit' in job.launch_config.prompts_dict() # sanity assertion + job_template.execute_role.members.add(rando) + can_access, messages = rando.can_access_with_errors(Job, 'start', job, validate_license=False) + assert can_access, messages + + def test_other_user_prompts(self, inventory, project, alice, bob): + jt = JobTemplate.objects.create( + name='testjt', inventory=inventory, project=project, + ask_credential_on_launch=True, + ask_variables_on_launch=True) + jt.execute_role.members.add(alice, bob) + + with impersonate(bob): + job = jt.create_unified_job(extra_vars={'job_var': 'foo2'}) + + assert 'job_var' in job.launch_config.extra_data + assert bob.can_access(Job, 'start', job, validate_license=False) + assert not alice.can_access(Job, 'start', job, validate_license=False) + @pytest.mark.django_db class TestJobAndUpdateCancels: diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index e48e7f6a7c..60c35e0803 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -87,6 +87,7 @@ class TestJobRelaunchAccess: for cred in job_with_prompts.credentials.all(): cred.use_role.members.add(rando) job_with_prompts.inventory.use_role.members.add(rando) + job_with_prompts.created_by = rando assert rando.can_access(Job, 'start', job_with_prompts) def test_no_relaunch_after_limit_change(self, inventory, machine_credential, rando): diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 0b9d0c46bd..7ccc5f4c5e 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -81,11 +81,14 @@ def test_job_template_access_use_level(jt_linked, rando): @pytest.mark.django_db -@pytest.mark.parametrize("role_names", [("admin_role",), ("inventory_admin_role", "project_admin_role")]) +@pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")]) def test_job_template_access_admin(role_names, jt_linked, rando): access = JobTemplateAccess(rando) # Appoint this user as admin of the organization #jt_linked.inventory.organization.admin_role.members.add(rando) + assert not access.can_read(jt_linked) + assert not access.can_delete(jt_linked) + for role_name in role_names: role = getattr(jt_linked.inventory.organization, role_name) role.members.add(rando) diff --git a/awx/main/tests/functional/test_rbac_oauth.py b/awx/main/tests/functional/test_rbac_oauth.py index 757c55e12b..5a53cf9108 100644 --- a/awx/main/tests/functional/test_rbac_oauth.py +++ b/awx/main/tests/functional/test_rbac_oauth.py @@ -79,7 +79,7 @@ class TestOAuth2Application: can_access_list = [True, True, False, False] for user, can_access in zip(user_list, can_access_list): app = Application.objects.create( - name='test app for {}'.format(org_admin.username), user=org_admin, + name='test app for {}'.format(user.username), user=org_admin, client_type='confidential', authorization_grant_type='password', organization=organization ) access = OAuth2ApplicationAccess(user) @@ -94,7 +94,7 @@ class TestOAuth2Application: can_access_list = [True, True, False, False] for user, can_access in zip(user_list, can_access_list): app = Application.objects.create( - name='test app for {}'.format(admin.username), user=admin, + name='test app for {}'.format(user.username), user=admin, client_type='confidential', authorization_grant_type='password', organization=organization ) access = OAuth2ApplicationAccess(user) @@ -200,7 +200,7 @@ class TestOAuth2Token: user_list = [admin, org_admin, org_member, alice] can_access_list = [True, False, True, False] response = post( - reverse('api:o_auth2_personal_token_list', kwargs={'pk': org_member.pk}), + reverse('api:user_personal_token_list', kwargs={'pk': org_member.pk}), {'scope': 'read'}, org_member, expect=201 ) token = AccessToken.objects.get(token=response.data['token']) @@ -220,7 +220,7 @@ class TestOAuth2Token: for user, can_access in zip(user_list, can_access_list): response = post( - reverse('api:o_auth2_personal_token_list', kwargs={'pk': user.pk}), + reverse('api:user_personal_token_list', kwargs={'pk': user.pk}), {'scope': 'read', 'application':None}, user, expect=201 ) token = AccessToken.objects.get(token=response.data['token']) diff --git a/awx/main/tests/functional/test_rbac_role.py b/awx/main/tests/functional/test_rbac_role.py index abaa8a4410..29598b6753 100644 --- a/awx/main/tests/functional/test_rbac_role.py +++ b/awx/main/tests/functional/test_rbac_role.py @@ -70,6 +70,43 @@ def test_org_user_role_attach(user, organization, inventory): assert not role_access.can_attach(organization.admin_role, nonmember, 'members', None) +# Permissions when adding users/teams to org special-purpose roles +@pytest.mark.django_db +def test_user_org_object_roles(organization, org_admin, org_member): + ''' + Unlike admin & member roles, the special-purpose organization roles do not + confer any permissions related to user management, + Normal rules about role delegation should apply, only admin to org needed. + ''' + assert RoleAccess(org_admin).can_attach( + organization.notification_admin_role, org_member, 'members', None + ) + assert not RoleAccess(org_member).can_attach( + organization.notification_admin_role, org_member, 'members', None + ) + + +@pytest.mark.django_db +def test_team_org_object_roles(organization, team, org_admin, org_member): + ''' + the special-purpose organization roles are not ancestors of any + team roles, and can be delegated en masse through teams, + following normal admin rules + ''' + assert RoleAccess(org_admin).can_attach( + organization.notification_admin_role, team, 'member_role.parents', {'id': 68} + ) + # Obviously team admin isn't enough to assign organization roles to the team + team.admin_role.members.add(org_member) + assert not RoleAccess(org_member).can_attach( + organization.notification_admin_role, team, 'member_role.parents', {'id': 68} + ) + # Cannot make a team member of an org + assert not RoleAccess(org_admin).can_attach( + organization.member_role, team, 'member_role.parents', {'id': 68} + ) + + # Singleton user editing restrictions @pytest.mark.django_db def test_org_superuser_role_attach(admin_user, org_admin, organization): diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index 5e7cf4ad85..0ea0851adc 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -1,7 +1,8 @@ import pytest +import mock from awx.main.access import TeamAccess -from awx.main.models import Project +from awx.main.models import Project, Organization, Team @pytest.mark.django_db @@ -105,6 +106,30 @@ def test_team_admin_member_access(team, user, project): assert len(Project.accessible_objects(u, 'use_role')) == 1 +@pytest.mark.django_db +def test_team_member_org_role_access_project(team, rando, project, organization): + team.member_role.members.add(rando) + assert rando not in project.read_role + team.member_role.children.add(organization.project_admin_role) + assert rando in project.admin_role + + +@pytest.mark.django_db +def test_team_member_org_role_access_workflow(team, rando, workflow_job_template, organization): + team.member_role.members.add(rando) + assert rando not in workflow_job_template.read_role + team.member_role.children.add(organization.workflow_admin_role) + assert rando in workflow_job_template.admin_role + + +@pytest.mark.django_db +def test_team_member_org_role_access_inventory(team, rando, inventory, organization): + team.member_role.members.add(rando) + assert rando not in inventory.read_role + team.member_role.children.add(organization.inventory_admin_role) + assert rando in inventory.admin_role + + @pytest.mark.django_db def test_org_admin_team_access(organization, team, user, project): u = user('team_admin', False) @@ -116,3 +141,14 @@ def test_org_admin_team_access(organization, team, user, project): team.member_role.children.add(project.use_role) assert len(Project.accessible_objects(u, 'use_role')) == 1 + + +@pytest.mark.django_db +@pytest.mark.parametrize('enabled', [True, False]) +def test_org_admin_view_all_teams(org_admin, enabled): + access = TeamAccess(org_admin) + other_org = Organization.objects.create(name='other-org') + other_team = Team.objects.create(name='other-team', organization=other_org) + with mock.patch('awx.main.access.settings') as settings_mock: + settings_mock.ORG_ADMINS_CAN_SEE_ALL_USERS = enabled + assert access.can_read(other_team) is enabled diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 5cd63027d2..116b9ec834 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -7,7 +7,7 @@ from awx.main.access import ( # WorkflowJobNodeAccess ) -from awx.main.models import InventorySource +from awx.main.models import InventorySource, JobLaunchConfig @pytest.fixture @@ -135,6 +135,20 @@ class TestWorkflowJobAccess: access = WorkflowJobAccess(rando) assert access.can_cancel(workflow_job) + def test_execute_role_relaunch(self, wfjt, workflow_job, rando): + wfjt.execute_role.members.add(rando) + JobLaunchConfig.objects.create(job=workflow_job) + assert WorkflowJobAccess(rando).can_start(workflow_job) + + def test_cannot_relaunch_friends_job(self, wfjt, rando, alice): + workflow_job = wfjt.workflow_jobs.create(name='foo', created_by=alice) + JobLaunchConfig.objects.create( + job=workflow_job, + extra_data={'foo': 'fooforyou'} + ) + wfjt.execute_role.members.add(alice) + assert not WorkflowJobAccess(rando).can_start(workflow_job) + @pytest.mark.django_db class TestWFJTCopyAccess: diff --git a/awx/main/tests/functional/test_session.py b/awx/main/tests/functional/test_session.py index 90f33626ea..5ce9bfffd6 100644 --- a/awx/main/tests/functional/test_session.py +++ b/awx/main/tests/functional/test_session.py @@ -1,14 +1,14 @@ +from importlib import import_module import pytest -from datetime import timedelta import re -from django.utils.timezone import now as tz_now +from django.conf import settings from django.test.utils import override_settings from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.models import Session from django.contrib.auth import SESSION_KEY +import mock -from awx.main.models import UserSessionMembership from awx.api.versioning import reverse @@ -24,6 +24,20 @@ class AlwaysPassBackend(object): return '{}.{}'.format(cls.__module__, cls.__name__) +@pytest.mark.django_db +@pytest.mark.parametrize('accept, status', [ + ['*/*', 200], + ['text/html', 200], + ['application/json', 406] +]) +def test_login_json_not_allowed(get, accept, status): + get( + '/api/login/', + HTTP_ACCEPT=accept, + expect=status + ) + + @pytest.mark.skip(reason="Needs Update - CA") @pytest.mark.django_db def test_session_create_delete(admin, post, get): @@ -49,33 +63,40 @@ def test_session_create_delete(admin, post, get): assert not Session.objects.filter(session_key=session_key).exists() -@pytest.mark.skip(reason="Needs Update - CA") @pytest.mark.django_db -def test_session_overlimit(admin, post): - AlwaysPassBackend.user = admin - with override_settings( - AUTHENTICATION_BACKENDS=(AlwaysPassBackend.get_backend_path(),), - SESSION_COOKIE_NAME='session_id', SESSIONS_PER_USER=3 - ): - sessions_to_deprecate = [] - for _ in range(5): - response = post( - '/api/login/', - data={'username': admin.username, 'password': admin.password, 'next': '/api/'}, - expect=302, middleware=SessionMiddleware(), format='multipart' - ) - session_key = re.findall( - r'session_id=[a-zA-z0-9]+', - str(response.cookies['session_id']) - )[0][len('session_id=') :] - sessions_to_deprecate.append(Session.objects.get(session_key=session_key)) - sessions_to_deprecate[0].expire_date = tz_now() - timedelta(seconds=1000) - sessions_to_deprecate[0].save() - sessions_overlimit = [x.session for x in UserSessionMembership.get_memberships_over_limit(admin)] - assert sessions_to_deprecate[0] not in sessions_overlimit - assert sessions_to_deprecate[1] in sessions_overlimit - for session in sessions_to_deprecate[2 :]: - assert session not in sessions_overlimit +@mock.patch('awx.main.consumers.emit_channel_notification') +def test_sessions_unlimited(emit, admin): + assert Session.objects.count() == 0 + for i in range(5): + store = import_module(settings.SESSION_ENGINE).SessionStore() + store.create_model_instance({SESSION_KEY: admin.pk}).save() + assert Session.objects.count() == i + 1 + assert emit.call_count == 0 + + +@pytest.mark.django_db +@mock.patch('awx.main.consumers.emit_channel_notification') +def test_session_overlimit(emit, admin, alice): + # If SESSIONS_PER_USER=3, only persist the three most recently created sessions + assert Session.objects.count() == 0 + with override_settings(SESSIONS_PER_USER=3): + created = [] + for i in range(5): + store = import_module(settings.SESSION_ENGINE).SessionStore() + session = store.create_model_instance({SESSION_KEY: admin.pk}) + session.save() + created.append(session.session_key) + assert [s.pk for s in Session.objects.all()] == created[-3:] + assert emit.call_count == 2 # 2 of 5 sessions were evicted + emit.assert_called_with( + 'control-limit_reached_{}'.format(admin.pk), + {'reason': 'limit_reached', 'group_name': 'control'} + ) + + # Allow sessions for a different user to be saved + store = import_module(settings.SESSION_ENGINE).SessionStore() + store.create_model_instance({SESSION_KEY: alice.pk}).save() + assert Session.objects.count() == 4 @pytest.mark.skip(reason="Needs Update - CA") diff --git a/awx/main/tests/functional/test_tasks.py b/awx/main/tests/functional/test_tasks.py index 84d93534c1..cbb3e281d3 100644 --- a/awx/main/tests/functional/test_tasks.py +++ b/awx/main/tests/functional/test_tasks.py @@ -32,16 +32,18 @@ class TestDependentInventoryUpdate: task.revision_path = scm_revision_file proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project) with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck: - task.post_run_hook(proj_update, 'successful') - inv_update_mck.assert_called_once_with(proj_update, mock.ANY) + with mock.patch.object(RunProjectUpdate, 'release_lock'): + task.post_run_hook(proj_update, 'successful') + inv_update_mck.assert_called_once_with(proj_update, mock.ANY) 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) with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck: - task.post_run_hook(proj_update, 'successful') - assert not inv_update_mck.called + with mock.patch.object(RunProjectUpdate, 'release_lock'): + task.post_run_hook(proj_update, 'successful') + assert not inv_update_mck.called def test_dependent_inventory_updates(self, scm_inventory_source): task = RunProjectUpdate() diff --git a/awx/main/tests/unit/api/serializers/test_job_serializers.py b/awx/main/tests/unit/api/serializers/test_job_serializers.py index 3c1529cba1..5688a845ec 100644 --- a/awx/main/tests/unit/api/serializers/test_job_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_serializers.py @@ -1,4 +1,5 @@ # Python +from collections import namedtuple import pytest import mock import json @@ -7,13 +8,17 @@ from six.moves import xrange # AWX from awx.api.serializers import ( + JobDetailSerializer, JobSerializer, JobOptionsSerializer, + ProjectUpdateDetailSerializer, ) from awx.main.models import ( Label, Job, + JobEvent, + ProjectUpdateEvent, ) @@ -53,6 +58,7 @@ def jobs(mocker): @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) class TestJobSerializerGetRelated(): + @pytest.mark.parametrize("related_resource_name", [ 'job_events', 'relaunch', @@ -76,6 +82,7 @@ class TestJobSerializerGetRelated(): @mock.patch('awx.api.serializers.BaseSerializer.to_representation', lambda self,obj: { 'extra_vars': obj.extra_vars}) class TestJobSerializerSubstitution(): + def test_survey_password_hide(self, mocker): job = mocker.MagicMock(**{ 'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', @@ -90,6 +97,7 @@ class TestJobSerializerSubstitution(): @mock.patch('awx.api.serializers.BaseSerializer.get_summary_fields', lambda x,y: {}) class TestJobOptionsSerializerGetSummaryFields(): + def test__summary_field_labels_10_max(self, mocker, job_template, labels): job_template.labels.all = mocker.MagicMock(**{'return_value': labels}) @@ -101,3 +109,87 @@ class TestJobOptionsSerializerGetSummaryFields(): def test_labels_exists(self, test_get_summary_fields, job_template): test_get_summary_fields(JobOptionsSerializer, job_template, 'labels') + + +class TestJobDetailSerializerGetHostStatusCountFields(object): + + def test_hosts_are_counted_once(self, job, mocker): + mock_event = JobEvent(**{ + 'event': 'playbook_on_stats', + 'event_data': { + 'skipped': { + 'localhost': 2, + 'fiz': 1, + }, + 'ok': { + 'localhost': 1, + 'foo': 2, + }, + 'changed': { + 'localhost': 1, + 'bar': 3, + }, + 'dark': { + 'localhost': 2, + 'fiz': 2, + } + } + }) + + mock_qs = namedtuple('mock_qs', ['get'])(mocker.MagicMock(return_value=mock_event)) + job.job_events.only = mocker.MagicMock(return_value=mock_qs) + + serializer = JobDetailSerializer() + host_status_counts = serializer.get_host_status_counts(job) + + assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2} + + def test_host_status_counts_is_empty_dict_without_stats_event(self, job): + job.job_events = JobEvent.objects.none() + + serializer = JobDetailSerializer() + host_status_counts = serializer.get_host_status_counts(job) + + assert host_status_counts == {} + + +class TestProjectUpdateDetailSerializerGetHostStatusCountFields(object): + + def test_hosts_are_counted_once(self, project_update, mocker): + mock_event = ProjectUpdateEvent(**{ + 'event': 'playbook_on_stats', + 'event_data': { + 'skipped': { + 'localhost': 2, + 'fiz': 1, + }, + 'ok': { + 'localhost': 1, + 'foo': 2, + }, + 'changed': { + 'localhost': 1, + 'bar': 3, + }, + 'dark': { + 'localhost': 2, + 'fiz': 2, + } + } + }) + + mock_qs = namedtuple('mock_qs', ['get'])(mocker.MagicMock(return_value=mock_event)) + project_update.project_update_events.only = mocker.MagicMock(return_value=mock_qs) + + serializer = ProjectUpdateDetailSerializer() + host_status_counts = serializer.get_host_status_counts(project_update) + + assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2} + + def test_host_status_counts_is_empty_dict_without_stats_event(self, project_update): + project_update.project_update_events = ProjectUpdateEvent.objects.none() + + serializer = ProjectUpdateDetailSerializer() + host_status_counts = serializer.get_host_status_counts(project_update) + + assert host_status_counts == {} diff --git a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py index 29c0512256..a6f41debb9 100644 --- a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py @@ -97,7 +97,6 @@ class TestJobTemplateSerializerGetSummaryFields(): are put into the serializer user_capabilities""" jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template - jt_obj.id = 5 jt_obj.admin_role = Role(id=9, role_field='admin_role') jt_obj.execute_role = Role(id=8, role_field='execute_role') jt_obj.read_role = Role(id=7, role_field='execute_role') @@ -115,7 +114,7 @@ class TestJobTemplateSerializerGetSummaryFields(): with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'): with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): - with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'): + with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'): with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None): response = serializer.get_summary_fields(jt_obj) diff --git a/awx/main/tests/unit/api/serializers/test_primary_key_related_field.py b/awx/main/tests/unit/api/serializers/test_primary_key_related_field.py new file mode 100644 index 0000000000..0be6d3312e --- /dev/null +++ b/awx/main/tests/unit/api/serializers/test_primary_key_related_field.py @@ -0,0 +1,16 @@ +# Python +import pytest + +# Django Rest Framework +from rest_framework.exceptions import ValidationError + +# AWX +from awx.api.serializers import JobLaunchSerializer + + +def test_primary_key_related_field(): + # We are testing if the PrimaryKeyRelatedField in this serializer can take dictionary. + # PrimaryKeyRelatedField should not be able to take dictionary as input, and should raise a ValidationError. + data = {'credentials' : {'1': '2', '3':'4'}} + with pytest.raises(ValidationError): + JobLaunchSerializer(data=data) diff --git a/awx/main/tests/unit/api/serializers/test_unified_serializers.py b/awx/main/tests/unit/api/serializers/test_unified_serializers.py new file mode 100644 index 0000000000..a03ceb7d7a --- /dev/null +++ b/awx/main/tests/unit/api/serializers/test_unified_serializers.py @@ -0,0 +1,69 @@ +# AWX +from awx.api import serializers +from awx.main.models import UnifiedJob, UnifiedJobTemplate + +# DRF +from rest_framework.generics import ListAPIView + + +def test_unified_template_field_consistency(): + ''' + Example of what is being tested: + The endpoints /projects/N/ and /projects/ should have the same fields as + that same project when it is serialized by the unified job template serializer + in /unified_job_templates/ + ''' + for cls in UnifiedJobTemplate.__subclasses__(): + detail_serializer = getattr(serializers, '{}Serializer'.format(cls.__name__)) + unified_serializer = serializers.UnifiedJobTemplateSerializer().get_sub_serializer(cls()) + assert set(detail_serializer().fields.keys()) == set(unified_serializer().fields.keys()) + + +def test_unified_job_list_field_consistency(): + ''' + Example of what is being tested: + The endpoint /project_updates/ should have the same fields as that + project update when it is serialized by the unified job template serializer + in /unified_jobs/ + ''' + for cls in UnifiedJob.__subclasses__(): + list_serializer = getattr(serializers, '{}ListSerializer'.format(cls.__name__)) + unified_serializer = serializers.UnifiedJobListSerializer().get_sub_serializer(cls()) + assert set(list_serializer().fields.keys()) == set(unified_serializer().fields.keys()), ( + 'Mismatch between {} list serializer & unified list serializer'.format(cls) + ) + + +def test_unified_job_detail_exclusive_fields(): + ''' + For each type, assert that the only fields allowed to be exclusive to + detail view are the allowed types + ''' + allowed_detail_fields = frozenset( + ('result_traceback', 'job_args', 'job_cwd', 'job_env', 'event_processing_finished') + ) + for cls in UnifiedJob.__subclasses__(): + list_serializer = getattr(serializers, '{}ListSerializer'.format(cls.__name__)) + detail_serializer = getattr(serializers, '{}Serializer'.format(cls.__name__)) + list_fields = set(list_serializer().fields.keys()) + detail_fields = set(detail_serializer().fields.keys()) - allowed_detail_fields + assert list_fields == detail_fields, 'List / detail mismatch for serializers of {}'.format(cls) + + +def test_list_views_use_list_serializers(all_views): + ''' + Check that the list serializers are only used for list views, + and vice versa + ''' + list_serializers = tuple( + getattr(serializers, '{}ListSerializer'.format(cls.__name__)) for + cls in (UnifiedJob.__subclasses__() + [UnifiedJob]) + ) + for View in all_views: + if hasattr(View, 'model') and issubclass(getattr(View, 'model'), UnifiedJob): + if issubclass(View, ListAPIView): + assert issubclass(View.serializer_class, list_serializers), ( + 'View {} serializer {} is not a list serializer'.format(View, View.serializer_class) + ) + else: + assert not issubclass(View.model, list_serializers) diff --git a/awx/main/tests/unit/api/test_roles.py b/awx/main/tests/unit/api/test_roles.py deleted file mode 100644 index a51dbb9f58..0000000000 --- a/awx/main/tests/unit/api/test_roles.py +++ /dev/null @@ -1,38 +0,0 @@ -import mock - -from rest_framework.test import APIRequestFactory -from rest_framework.test import force_authenticate - -from django.contrib.contenttypes.models import ContentType - -from awx.api.views import ( - TeamRolesList, -) - -from awx.main.models import ( - User, - Role, -) - - -def test_team_roles_list_post_org_roles(): - with mock.patch('awx.api.views.get_object_or_400') as role_get, \ - mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get: - - role_mock = mock.MagicMock(spec=Role) - content_type_mock = mock.MagicMock(spec=ContentType) - role_mock.content_type = content_type_mock - role_get.return_value = role_mock - ct_get.return_value = content_type_mock - - factory = APIRequestFactory() - view = TeamRolesList.as_view() - - request = factory.post("/team/1/roles", {'id':1}, format="json") - force_authenticate(request, User(username="root", is_superuser=True)) - - response = view(request) - response.render() - - assert response.status_code == 400 - assert 'cannot assign' in response.content diff --git a/awx/main/tests/unit/conftest.py b/awx/main/tests/unit/conftest.py index 2307b3a47d..cb73ee8c55 100644 --- a/awx/main/tests/unit/conftest.py +++ b/awx/main/tests/unit/conftest.py @@ -3,6 +3,11 @@ import logging from mock import PropertyMock +from awx.api.urls import urlpatterns as api_patterns + +# Django +from django.core.urlresolvers import RegexURLResolver, RegexURLPattern + @pytest.fixture(autouse=True) def _disable_database_settings(mocker): @@ -10,6 +15,33 @@ def _disable_database_settings(mocker): m.return_value = [] +@pytest.fixture() +def all_views(): + ''' + returns a set of all views in the app + ''' + patterns = set([]) + url_views = set([]) + # Add recursive URL patterns + unprocessed = set(api_patterns) + while unprocessed: + to_process = unprocessed.copy() + unprocessed = set([]) + for pattern in to_process: + if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'): + continue + patterns.add(pattern) + if isinstance(pattern, RegexURLResolver): + for sub_pattern in pattern.url_patterns: + if sub_pattern not in patterns: + unprocessed.add(sub_pattern) + # Get view classes + for pattern in patterns: + if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'): + url_views.add(pattern.callback.view_class) + return url_views + + @pytest.fixture() def dummy_log_record(): return logging.LogRecord( diff --git a/awx/main/tests/unit/expect/test_expect.py b/awx/main/tests/unit/expect/test_expect.py index a43775ad33..5f7a76b139 100644 --- a/awx/main/tests/unit/expect/test_expect.py +++ b/awx/main/tests/unit/expect/test_expect.py @@ -1,4 +1,6 @@ -import cStringIO +# -*- coding: utf-8 -*- + +import StringIO import mock import os import pytest @@ -16,6 +18,7 @@ from cryptography.hazmat.primitives import serialization from awx.main.expect import run, isolated_manager from django.conf import settings +import six HERE, FILENAME = os.path.split(__file__) @@ -55,7 +58,7 @@ def mock_sleep(request): def test_simple_spawn(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() status, rc = run.run_pexpect( ['ls', '-la'], HERE, @@ -65,11 +68,11 @@ def test_simple_spawn(): ) assert status == 'successful' assert rc == 0 - assert FILENAME in stdout.getvalue() + # assert FILENAME in stdout.getvalue() def test_error_rc(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() status, rc = run.run_pexpect( ['ls', '-nonsense'], HERE, @@ -83,7 +86,7 @@ def test_error_rc(): def test_cancel_callback_error(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() def bad_callback(): raise Exception('unique exception') @@ -102,22 +105,24 @@ def test_cancel_callback_error(): assert extra_fields['job_explanation'] == "System error during job execution, check system logs" -def test_env_vars(): - stdout = cStringIO.StringIO() +@pytest.mark.timeout(3) # https://github.com/ansible/tower/issues/2391#issuecomment-401946895 +@pytest.mark.parametrize('value', ['abc123', six.u('Iñtërnâtiônàlizætiøn')]) +def test_env_vars(value): + stdout = StringIO.StringIO() status, rc = run.run_pexpect( ['python', '-c', 'import os; print os.getenv("X_MY_ENV")'], HERE, - {'X_MY_ENV': 'abc123'}, + {'X_MY_ENV': value}, stdout, cancelled_callback=lambda: False, ) assert status == 'successful' assert rc == 0 - assert 'abc123' in stdout.getvalue() + assert value in stdout.getvalue() def test_password_prompt(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() expect_passwords = OrderedDict() expect_passwords[re.compile(r'Password:\s*?$', re.M)] = 'secret123' status, rc = run.run_pexpect( @@ -134,7 +139,7 @@ def test_password_prompt(): def test_job_timeout(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() extra_update_fields={} status, rc = run.run_pexpect( ['python', '-c', 'import time; time.sleep(5)'], @@ -151,7 +156,7 @@ def test_job_timeout(): def test_manual_cancellation(): - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() status, rc = run.run_pexpect( ['python', '-c', 'print raw_input("Password: ")'], HERE, @@ -167,7 +172,7 @@ def test_manual_cancellation(): def test_build_isolated_job_data(private_data_dir, rsa_key): pem, passphrase = rsa_key mgr = isolated_manager.IsolatedManager( - ['ls', '-la'], HERE, {}, cStringIO.StringIO(), '' + ['ls', '-la'], HERE, {}, StringIO.StringIO(), '' ) mgr.private_data_dir = private_data_dir mgr.build_isolated_job_data() @@ -204,7 +209,7 @@ def test_run_isolated_job(private_data_dir, rsa_key): env = {'JOB_ID': '1'} pem, passphrase = rsa_key mgr = isolated_manager.IsolatedManager( - ['ls', '-la'], HERE, env, cStringIO.StringIO(), '' + ['ls', '-la'], HERE, env, StringIO.StringIO(), '' ) mgr.private_data_dir = private_data_dir secrets = { @@ -215,7 +220,7 @@ def test_run_isolated_job(private_data_dir, rsa_key): 'ssh_key_data': pem } mgr.build_isolated_job_data() - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() # Mock environment variables for callback module with mock.patch('os.getenv') as env_mock: env_mock.return_value = '/path/to/awx/lib' @@ -234,7 +239,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key): env = {'AD_HOC_COMMAND_ID': '1'} pem, passphrase = rsa_key mgr = isolated_manager.IsolatedManager( - ['pwd'], HERE, env, cStringIO.StringIO(), '' + ['pwd'], HERE, env, StringIO.StringIO(), '' ) mgr.private_data_dir = private_data_dir secrets = { @@ -245,7 +250,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key): 'ssh_key_data': pem } mgr.build_isolated_job_data() - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() # Mock environment variables for callback module with mock.patch('os.getenv') as env_mock: env_mock.return_value = '/path/to/awx/lib' @@ -265,7 +270,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key): def test_check_isolated_job(private_data_dir, rsa_key): pem, passphrase = rsa_key - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '') mgr.private_data_dir = private_data_dir mgr.instance = mock.Mock(id=123, pk=123, verbosity=5, spec_set=['id', 'pk', 'verbosity']) @@ -313,9 +318,51 @@ def test_check_isolated_job(private_data_dir, rsa_key): ) +def test_check_isolated_job_with_multibyte_unicode(private_data_dir): + """ + Ensure that multibyte unicode is properly synced when stdout only + contains the first part of the multibyte character + + see: https://github.com/ansible/tower/issues/2315 + """ + def raw_output(): + yield ('failed', '\xe8\xb5\xb7\xe5') # 起 + yield ('successful', '\xe8\xb5\xb7\xe5\x8b\x95') # 起動 + raw_output = raw_output() + stdout = StringIO.StringIO() + mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '') + mgr.private_data_dir = private_data_dir + mgr.instance = mock.Mock(id=123, pk=123, verbosity=5, spec_set=['id', 'pk', 'verbosity']) + mgr.started_at = time.time() + mgr.host = 'isolated-host' + + os.mkdir(os.path.join(private_data_dir, 'artifacts')) + with mock.patch('awx.main.expect.run.run_pexpect') as run_pexpect: + + def _synchronize_job_artifacts(args, cwd, env, buff, **kw): + buff.write('checking job status...') + status, out = next(raw_output) + for filename, data in ( + ['status', status], + ['rc', '0'], + ['stdout', out] + ): + with open(os.path.join(private_data_dir, 'artifacts', filename), 'w') as f: + f.write(data) + f.flush() + return (status, 0) + + run_pexpect.side_effect = _synchronize_job_artifacts + with mock.patch.object(mgr, '_missing_artifacts') as missing_artifacts: + missing_artifacts.return_value = False + status, rc = mgr.check(interval=0) + + assert stdout.getvalue() == '起動' + + def test_check_isolated_job_timeout(private_data_dir, rsa_key): pem, passphrase = rsa_key - stdout = cStringIO.StringIO() + stdout = StringIO.StringIO() extra_update_fields = {} mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '', job_timeout=1, diff --git a/awx/main/tests/unit/models/test_ha.py b/awx/main/tests/unit/models/test_ha.py new file mode 100644 index 0000000000..4ceb83c77a --- /dev/null +++ b/awx/main/tests/unit/models/test_ha.py @@ -0,0 +1,85 @@ +import pytest +import mock +from mock import Mock + +from awx.main.models import ( + Job, + InstanceGroup, +) + + +def T(impact): + j = mock.Mock(Job()) + j.task_impact = impact + return j + + +def Is(param): + ''' + param: + [remaining_capacity1, remaining_capacity2, remaining_capacity3, ...] + [(jobs_running1, capacity1), (jobs_running2, capacity2), (jobs_running3, capacity3), ...] + ''' + + instances = [] + if isinstance(param[0], tuple): + for (jobs_running, capacity) in param: + inst = Mock() + inst.capacity = capacity + inst.jobs_running = jobs_running + instances.append(inst) + else: + for i in param: + inst = Mock() + inst.remaining_capacity = i + instances.append(inst) + return instances + + +class TestInstanceGroup(object): + @pytest.mark.parametrize('task,instances,instance_fit_index,reason', [ + (T(100), Is([100]), 0, "Only one, pick it"), + (T(100), Is([100, 100]), 0, "Two equally good fits, pick the first"), + (T(100), Is([50, 100]), 1, "First instance not as good as second instance"), + (T(100), Is([50, 0, 20, 100, 100, 100, 30, 20]), 3, "Pick Instance [3] as it is the first that the task fits in."), + (T(100), Is([50, 0, 20, 99, 11, 1, 5, 99]), None, "The task don't a fit, you must a quit!"), + ]) + def test_fit_task_to_most_remaining_capacity_instance(self, task, instances, instance_fit_index, reason): + with mock.patch.object(InstanceGroup, + 'instances', + Mock(spec_set=['filter'], + filter=lambda *args, **kargs: Mock(spec_set=['order_by'], + order_by=lambda x: instances))): + ig = InstanceGroup(id=10) + + if instance_fit_index is None: + assert ig.fit_task_to_most_remaining_capacity_instance(task) is None, reason + else: + assert ig.fit_task_to_most_remaining_capacity_instance(task) == \ + instances[instance_fit_index], reason + + + @pytest.mark.parametrize('instances,instance_fit_index,reason', [ + (Is([(0, 100)]), 0, "One idle instance, pick it"), + (Is([(1, 100)]), None, "One un-idle instance, pick nothing"), + (Is([(0, 100), (0, 200), (1, 500), (0, 700)]), 3, "Pick the largest idle instance"), + (Is([(0, 100), (0, 200), (1, 10000), (0, 700), (0, 699)]), 3, "Pick the largest idle instance"), + (Is([(0, 0)]), None, "One idle but down instance, don't pick it"), + ]) + def test_find_largest_idle_instance(self, instances, instance_fit_index, reason): + def filter_offline_instances(*args): + return filter(lambda i: i.capacity > 0, instances) + + with mock.patch.object(InstanceGroup, + 'instances', + Mock(spec_set=['filter'], + filter=lambda *args, **kargs: Mock(spec_set=['order_by'], + order_by=filter_offline_instances))): + ig = InstanceGroup(id=10) + + if instance_fit_index is None: + assert ig.find_largest_idle_instance() is None, reason + else: + assert ig.find_largest_idle_instance() == \ + instances[instance_fit_index], reason + diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index ffdc79a505..91e3c3505d 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -196,7 +196,7 @@ def parse_extra_vars(args): return extra_vars -class TestJobExecution: +class TestJobExecution(object): """ For job runs, test that `ansible-playbook` is invoked with the proper arguments, environment variables, and pexpect passwords for a variety of @@ -440,7 +440,7 @@ class TestGenericRun(TestJobExecution): with pytest.raises(Exception): self.task.run(self.pk) for c in [ - mock.call(self.pk, execution_node=settings.CLUSTER_HOST_ID, status='running', start_args=''), + mock.call(self.pk, status='running', start_args=''), mock.call(self.pk, status='canceled') ]: assert c in self.task.update_model.call_args_list @@ -448,7 +448,7 @@ class TestGenericRun(TestJobExecution): def test_event_count(self): with mock.patch.object(self.task, 'get_stdout_handle') as mock_stdout: handle = OutputEventFilter(lambda event_data: None) - handle._event_ct = 334 + handle._counter = 334 mock_stdout.return_value = handle self.task.run(self.pk) @@ -626,7 +626,14 @@ class TestAdhocRun(TestJobExecution): class TestIsolatedExecution(TestJobExecution): - REMOTE_HOST = 'some-isolated-host' + ISOLATED_HOST = 'some-isolated-host' + ISOLATED_CONTROLLER_HOST = 'some-isolated-controller-host' + + def get_instance(self): + instance = super(TestIsolatedExecution, self).get_instance() + instance.controller_node = self.ISOLATED_CONTROLLER_HOST + instance.execution_node = self.ISOLATED_HOST + return instance def test_with_ssh_credentials(self): ssh = CredentialType.defaults['ssh']() @@ -659,12 +666,12 @@ class TestIsolatedExecution(TestJobExecution): f.write(data) return ('successful', 0) self.run_pexpect.side_effect = _mock_job_artifacts - self.task.run(self.pk, self.REMOTE_HOST) + self.task.run(self.pk) playbook_run = self.run_pexpect.call_args_list[0][0] assert ' '.join(playbook_run[0]).startswith(' '.join([ 'ansible-playbook', 'run_isolated.yml', '-u', settings.AWX_ISOLATED_USERNAME, - '-T', str(settings.AWX_ISOLATED_CONNECTION_TIMEOUT), '-i', self.REMOTE_HOST + ',', + '-T', str(settings.AWX_ISOLATED_CONNECTION_TIMEOUT), '-i', self.ISOLATED_HOST + ',', '-e', ])) extra_vars = playbook_run[0][playbook_run[0].index('-e') + 1] @@ -705,7 +712,7 @@ class TestIsolatedExecution(TestJobExecution): with mock.patch('requests.get') as mock_get: mock_get.return_value = mock.Mock(content=inventory) with pytest.raises(Exception): - self.task.run(self.pk, self.REMOTE_HOST) + self.task.run(self.pk, self.ISOLATED_HOST) class TestJobCredentials(TestJobExecution): @@ -1174,19 +1181,22 @@ class TestJobCredentials(TestJobExecution): self.run_pexpect.side_effect = run_pexpect_side_effect self.task.run(self.pk) - def test_net_credentials(self): + @pytest.mark.parametrize('authorize, expected_authorize', [ + [True, '1'], + [False, '0'], + [None, '0'], + ]) + def test_net_credentials(self, authorize, expected_authorize): net = CredentialType.defaults['net']() - credential = Credential( - pk=1, - credential_type=net, - inputs = { - 'username': 'bob', - 'password': 'secret', - 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY, - 'authorize': True, - 'authorize_password': 'authorizeme' - } - ) + inputs = { + 'username': 'bob', + 'password': 'secret', + 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY, + 'authorize_password': 'authorizeme' + } + if authorize is not None: + inputs['authorize'] = authorize + credential = Credential(pk=1,credential_type=net, inputs = inputs) for field in ('password', 'ssh_key_data', 'authorize_password'): credential.inputs[field] = encrypt_field(credential, field) self.instance.credentials.add(credential) @@ -1195,8 +1205,9 @@ class TestJobCredentials(TestJobExecution): args, cwd, env, stdout = args assert env['ANSIBLE_NET_USERNAME'] == 'bob' assert env['ANSIBLE_NET_PASSWORD'] == 'secret' - assert env['ANSIBLE_NET_AUTHORIZE'] == '1' - assert env['ANSIBLE_NET_AUTH_PASS'] == 'authorizeme' + assert env['ANSIBLE_NET_AUTHORIZE'] == expected_authorize + if authorize: + assert env['ANSIBLE_NET_AUTH_PASS'] == 'authorizeme' assert open(env['ANSIBLE_NET_SSH_KEYFILE'], 'rb').read() == self.EXAMPLE_PRIVATE_KEY return ['successful', 0] @@ -2171,6 +2182,64 @@ class TestInventoryUpdateCredentials(TestJobExecution): self.run_pexpect.side_effect = run_pexpect_side_effect self.task.run(self.pk) + @pytest.mark.parametrize('verify', [True, False]) + def test_tower_source(self, verify): + tower = CredentialType.defaults['tower']() + self.instance.source = 'tower' + self.instance.instance_filters = '12345' + inputs = { + 'host': 'https://tower.example.org', + 'username': 'bob', + 'password': 'secret', + 'verify_ssl': verify + } + + def get_cred(): + cred = Credential(pk=1, credential_type=tower, inputs = inputs) + cred.inputs['password'] = encrypt_field(cred, 'password') + return cred + self.instance.get_cloud_credential = get_cred + + def run_pexpect_side_effect(*args, **kwargs): + args, cwd, env, stdout = args + assert env['TOWER_HOST'] == 'https://tower.example.org' + assert env['TOWER_USERNAME'] == 'bob' + assert env['TOWER_PASSWORD'] == 'secret' + assert env['TOWER_INVENTORY'] == '12345' + if verify: + assert env['TOWER_VERIFY_SSL'] == 'True' + else: + assert env['TOWER_VERIFY_SSL'] == 'False' + return ['successful', 0] + + self.run_pexpect.side_effect = run_pexpect_side_effect + self.task.run(self.pk) + assert self.instance.job_env['TOWER_PASSWORD'] == tasks.HIDDEN_PASSWORD + + def test_tower_source_ssl_verify_empty(self): + tower = CredentialType.defaults['tower']() + self.instance.source = 'tower' + self.instance.instance_filters = '12345' + inputs = { + 'host': 'https://tower.example.org', + 'username': 'bob', + 'password': 'secret', + } + + def get_cred(): + cred = Credential(pk=1, credential_type=tower, inputs = inputs) + cred.inputs['password'] = encrypt_field(cred, 'password') + return cred + self.instance.get_cloud_credential = get_cred + + def run_pexpect_side_effect(*args, **kwargs): + args, cwd, env, stdout = args + assert env['TOWER_VERIFY_SSL'] == 'False' + return ['successful', 0] + + self.run_pexpect.side_effect = run_pexpect_side_effect + self.task.run(self.pk) + def test_awx_task_env(self): gce = CredentialType.defaults['gce']() self.instance.source = 'gce' @@ -2242,6 +2311,7 @@ def test_aquire_lock_acquisition_fail_logged(fcntl_flock, logging_getLogger, os_ instance = mock.Mock() instance.get_lock_file.return_value = 'this_file_does_not_exist' + instance.cancel_flag = False os_open.return_value = 3 @@ -2251,7 +2321,6 @@ def test_aquire_lock_acquisition_fail_logged(fcntl_flock, logging_getLogger, os_ fcntl_flock.side_effect = err ProjectUpdate = tasks.RunProjectUpdate() - with pytest.raises(IOError, message='dummy message'): ProjectUpdate.acquire_lock(instance) os_close.assert_called_with(3) diff --git a/awx/main/tests/unit/test_views.py b/awx/main/tests/unit/test_views.py index f1bad79400..060d979f76 100644 --- a/awx/main/tests/unit/test_views.py +++ b/awx/main/tests/unit/test_views.py @@ -5,9 +5,6 @@ import mock from rest_framework import exceptions from rest_framework.generics import ListAPIView -# Django -from django.core.urlresolvers import RegexURLResolver, RegexURLPattern - # AWX from awx.main.views import ApiErrorView from awx.api.views import JobList, InventorySourceList @@ -58,33 +55,12 @@ def test_disable_post_on_v1_inventory_source_list(version, supports_post): assert ('POST' in inv_source_list.allowed_methods) == supports_post -def test_views_have_search_fields(): - from awx.api.urls import urlpatterns as api_patterns - patterns = set([]) - url_views = set([]) - # Add recursive URL patterns - unprocessed = set(api_patterns) - while unprocessed: - to_process = unprocessed.copy() - unprocessed = set([]) - for pattern in to_process: - if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'): - continue - patterns.add(pattern) - if isinstance(pattern, RegexURLResolver): - for sub_pattern in pattern.url_patterns: - if sub_pattern not in patterns: - unprocessed.add(sub_pattern) - # Get view classes - for pattern in patterns: - if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'): - cls = pattern.callback.view_class - if issubclass(cls, ListAPIView): - url_views.add(pattern.callback.view_class) - +def test_views_have_search_fields(all_views): # Gather any views that don't have search fields defined views_missing_search = [] - for View in url_views: + for View in all_views: + if not issubclass(View, ListAPIView): + continue view = View() if not hasattr(view, 'search_fields') or len(view.search_fields) == 0: views_missing_search.append(view) diff --git a/awx/main/tests/unit/utils/test_common.py b/awx/main/tests/unit/utils/test_common.py index bb98b18bec..d000815cbe 100644 --- a/awx/main/tests/unit/utils/test_common.py +++ b/awx/main/tests/unit/utils/test_common.py @@ -173,11 +173,15 @@ def test_extract_ansible_vars(): def test_get_custom_venv_choices(): - assert common.get_custom_venv_choices() == [] + bundled_venv = os.path.join(settings.BASE_VENV_PATH, 'ansible', '') + assert common.get_custom_venv_choices() == [bundled_venv] - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: + with TemporaryDirectory(dir=settings.BASE_VENV_PATH, prefix='tmp') as temp_dir: os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - assert common.get_custom_venv_choices() == [os.path.join(temp_dir, '')] + assert sorted(common.get_custom_venv_choices()) == [ + bundled_venv, + os.path.join(temp_dir, '') + ] def test_region_sorting(): diff --git a/awx/main/tests/unit/utils/test_filters.py b/awx/main/tests/unit/utils/test_filters.py index a8127fdfb6..f7d25d06f3 100644 --- a/awx/main/tests/unit/utils/test_filters.py +++ b/awx/main/tests/unit/utils/test_filters.py @@ -155,16 +155,16 @@ class TestSmartFilterQueryFromString(): @pytest.mark.parametrize("filter_string,q_expected", [ - ('search=foo', Q(Q(**{u"name__contains": u"foo"}) | Q(**{ u"description__contains": u"foo"}))), - ('group__search=foo', Q(Q(**{u"group__name__contains": u"foo"}) | Q(**{u"group__description__contains": u"foo"}))), + ('search=foo', Q(Q(**{u"name__icontains": u"foo"}) | Q(**{ u"description__icontains": u"foo"}))), + ('group__search=foo', Q(Q(**{u"group__name__icontains": u"foo"}) | Q(**{u"group__description__icontains": u"foo"}))), ('search=foo and group__search=foo', Q( - Q(**{u"name__contains": u"foo"}) | Q(**{ u"description__contains": u"foo"}), - Q(**{u"group__name__contains": u"foo"}) | Q(**{u"group__description__contains": u"foo"}))), + Q(**{u"name__icontains": u"foo"}) | Q(**{ u"description__icontains": u"foo"}), + Q(**{u"group__name__icontains": u"foo"}) | Q(**{u"group__description__icontains": u"foo"}))), ('search=foo or ansible_facts__a=null', - Q(Q(**{u"name__contains": u"foo"}) | Q(**{u"description__contains": u"foo"})) | + Q(Q(**{u"name__icontains": u"foo"}) | Q(**{u"description__icontains": u"foo"})) | Q(**{u"ansible_facts__contains": {u"a": None}})), ('search=foo or ansible_facts__a="null"', - Q(Q(**{u"name__contains": u"foo"}) | Q(**{u"description__contains": u"foo"})) | + Q(Q(**{u"name__icontains": u"foo"}) | Q(**{u"description__icontains": u"foo"})) | Q(**{u"ansible_facts__contains": {u"a": u"\"null\""}})), ]) def test_search_related_fields(self, mock_get_host_model, filter_string, q_expected): diff --git a/awx/main/tests/unit/utils/test_ha.py b/awx/main/tests/unit/utils/test_ha.py index 94cb7d3606..eb5cfcee03 100644 --- a/awx/main/tests/unit/utils/test_ha.py +++ b/awx/main/tests/unit/utils/test_ha.py @@ -6,11 +6,9 @@ # python import pytest import mock -from contextlib import nested # AWX from awx.main.utils.ha import ( - _add_remove_celery_worker_queues, AWXCeleryRouter, ) @@ -18,7 +16,8 @@ from awx.main.utils.ha import ( class TestAddRemoveCeleryWorkerQueues(): @pytest.fixture def instance_generator(self, mocker): - def fn(groups=['east', 'west', 'north', 'south'], hostname='east-1'): + def fn(hostname='east-1'): + groups=['east', 'west', 'north', 'south'] instance = mocker.MagicMock() instance.hostname = hostname instance.rampart_groups = mocker.MagicMock() @@ -40,30 +39,6 @@ class TestAddRemoveCeleryWorkerQueues(): app.control.cancel_consumer = mocker.MagicMock() return app - @pytest.mark.parametrize("broadcast_queues,static_queues,_worker_queues,groups,hostname,added_expected,removed_expected", [ - (['tower_broadcast_all'], ['east', 'west'], ['east', 'west', 'east-1'], [], 'east-1', ['tower_broadcast_all_east-1'], []), - ([], [], ['east', 'west', 'east-1'], ['east', 'west'], 'east-1', [], []), - ([], [], ['east', 'west'], ['east', 'west'], 'east-1', ['east-1'], []), - ([], [], [], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], []), - ([], [], ['china', 'russia'], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], ['china', 'russia']), - ]) - def test__add_remove_celery_worker_queues_noop(self, mock_app, - instance_generator, - worker_queues_generator, - broadcast_queues, - static_queues, _worker_queues, - groups, hostname, - added_expected, removed_expected): - instance = instance_generator(groups=groups, hostname=hostname) - worker_queues = worker_queues_generator(_worker_queues) - with nested( - mock.patch('awx.main.utils.ha.settings.AWX_CELERY_QUEUES_STATIC', static_queues), - mock.patch('awx.main.utils.ha.settings.AWX_CELERY_BCAST_QUEUES_STATIC', broadcast_queues), - mock.patch('awx.main.utils.ha.settings.CLUSTER_HOST_ID', hostname)): - (added_queues, removed_queues) = _add_remove_celery_worker_queues(mock_app, [instance], worker_queues, hostname) - assert set(added_queues) == set(added_expected) - assert set(removed_queues) == set(removed_expected) - class TestUpdateCeleryWorkerRouter(): diff --git a/awx/main/tests/unit/utils/test_handlers.py b/awx/main/tests/unit/utils/test_handlers.py index f57d86158d..9dd06189c0 100644 --- a/awx/main/tests/unit/utils/test_handlers.py +++ b/awx/main/tests/unit/utils/test_handlers.py @@ -22,8 +22,8 @@ from awx.main.utils.formatters import LogstashFormatter @pytest.fixture() -def http_adapter(): - class FakeHTTPAdapter(requests.adapters.HTTPAdapter): +def https_adapter(): + class FakeHTTPSAdapter(requests.adapters.HTTPAdapter): requests = [] status = 200 reason = None @@ -36,7 +36,7 @@ def http_adapter(): resp.request = request return resp - return FakeHTTPAdapter() + return FakeHTTPSAdapter() @pytest.fixture() @@ -127,6 +127,17 @@ def test_invalid_kwarg_to_real_handler(): assert not hasattr(handler, 'verify_cert') +def test_protocol_not_specified(): + settings = LazySettings() + settings.configure(**{ + 'LOG_AGGREGATOR_HOST': 'https://server.invalid', + 'LOG_AGGREGATOR_PORT': 22222, + 'LOG_AGGREGATOR_PROTOCOL': None # awx/settings/defaults.py + }) + handler = AWXProxyHandler().get_handler(custom_settings=settings) + assert isinstance(handler, logging.NullHandler) + + def test_base_logging_handler_emit_system_tracking(dummy_log_record): handler = BaseHandler(host='127.0.0.1', indv_facts=True) handler.setFormatter(LogstashFormatter()) @@ -183,17 +194,22 @@ def test_base_logging_handler_host_format(host, port, normalized, hostname_only) 'status, reason, exc', [(200, '200 OK', None), (404, 'Not Found', LoggingConnectivityException)] ) -def test_https_logging_handler_connectivity_test(http_adapter, status, reason, exc): - http_adapter.status = status - http_adapter.reason = reason +@pytest.mark.parametrize('protocol', ['http', 'https', None]) +def test_https_logging_handler_connectivity_test(https_adapter, status, reason, exc, protocol): + host = 'example.org' + if protocol: + host = '://'.join([protocol, host]) + https_adapter.status = status + https_adapter.reason = reason settings = LazySettings() settings.configure(**{ - 'LOG_AGGREGATOR_HOST': 'example.org', + 'LOG_AGGREGATOR_HOST': host, 'LOG_AGGREGATOR_PORT': 8080, 'LOG_AGGREGATOR_TYPE': 'logstash', 'LOG_AGGREGATOR_USERNAME': 'user', 'LOG_AGGREGATOR_PASSWORD': 'password', 'LOG_AGGREGATOR_LOGGERS': ['awx', 'activity_stream', 'job_events', 'system_tracking'], + 'LOG_AGGREGATOR_PROTOCOL': 'https', 'CLUSTER_HOST_ID': '', 'LOG_AGGREGATOR_TOWER_UUID': str(uuid4()), 'LOG_AGGREGATOR_LEVEL': 'DEBUG', @@ -203,7 +219,7 @@ def test_https_logging_handler_connectivity_test(http_adapter, status, reason, e def __init__(self, *args, **kwargs): super(FakeHTTPSHandler, self).__init__(*args, **kwargs) - self.session.mount('http://', http_adapter) + self.session.mount('{}://'.format(protocol or 'https'), https_adapter) def emit(self, record): return super(FakeHTTPSHandler, self).emit(record) @@ -259,17 +275,17 @@ def test_https_logging_handler_connection_error(connection_error_adapter, @pytest.mark.parametrize('message_type', ['logstash', 'splunk']) -def test_https_logging_handler_emit_without_cred(http_adapter, dummy_log_record, +def test_https_logging_handler_emit_without_cred(https_adapter, dummy_log_record, message_type): handler = HTTPSHandler(host='127.0.0.1', message_type=message_type) handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', http_adapter) + handler.session.mount('https://', https_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(http_adapter.requests) == 1 - request = http_adapter.requests[0] - assert request.url == 'http://127.0.0.1/' + assert len(https_adapter.requests) == 1 + request = https_adapter.requests[0] + assert request.url == 'https://127.0.0.1/' assert request.method == 'POST' if message_type == 'logstash': @@ -280,32 +296,32 @@ def test_https_logging_handler_emit_without_cred(http_adapter, dummy_log_record, assert request.headers['Authorization'] == 'Splunk None' -def test_https_logging_handler_emit_logstash_with_creds(http_adapter, +def test_https_logging_handler_emit_logstash_with_creds(https_adapter, dummy_log_record): handler = HTTPSHandler(host='127.0.0.1', username='user', password='pass', message_type='logstash') handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', http_adapter) + handler.session.mount('https://', https_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(http_adapter.requests) == 1 - request = http_adapter.requests[0] + assert len(https_adapter.requests) == 1 + request = https_adapter.requests[0] assert request.headers['Authorization'] == 'Basic %s' % base64.b64encode("user:pass") -def test_https_logging_handler_emit_splunk_with_creds(http_adapter, +def test_https_logging_handler_emit_splunk_with_creds(https_adapter, dummy_log_record): handler = HTTPSHandler(host='127.0.0.1', password='pass', message_type='splunk') handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', http_adapter) + handler.session.mount('https://', https_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(http_adapter.requests) == 1 - request = http_adapter.requests[0] + assert len(https_adapter.requests) == 1 + request = https_adapter.requests[0] assert request.headers['Authorization'] == 'Splunk pass' diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index c9914cd1c3..a1373894d8 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -47,12 +47,12 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'get_type_for_model', 'get_model_for_type', 'copy_model_by_class', 'region_sorting', 'copy_m2m_relationships', 'prefetch_page_capabilities', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', - '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', + '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'getattr_dne', 'NoDefaultProvided', 'get_current_apps', 'set_current_apps', 'OutputEventFilter', 'OutputVerboseFilter', 'extract_ansible_vars', 'get_search_fields', 'get_system_task_capacity', 'get_cpu_capacity', 'get_mem_capacity', 'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict', 'model_instance_diff', 'timestamp_apiformat', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', - 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices'] + 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account'] def get_object_or_400(klass, *args, **kwargs): @@ -783,7 +783,9 @@ def check_proot_installed(): stderr=subprocess.PIPE) proc.communicate() return bool(proc.returncode == 0) - except (OSError, ValueError): + except (OSError, ValueError) as e: + if isinstance(e, ValueError) or getattr(e, 'errno', 1) != 2: # ENOENT, no such file or directory + logger.exception('bwrap unavailable for unexpected reason.') return False @@ -906,6 +908,13 @@ def getattrd(obj, name, default=NoDefaultProvided): raise +def getattr_dne(obj, name, notfound=ObjectDoesNotExist): + try: + return getattr(obj, name) + except notfound: + return None + + current_apps = apps @@ -926,7 +935,7 @@ def get_custom_venv_choices(): return [ os.path.join(custom_venv_path, x.decode('utf-8'), '') for x in os.listdir(custom_venv_path) - if x not in ('awx', 'ansible') and + 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')) ] @@ -943,8 +952,7 @@ class OutputEventFilter(object): def __init__(self, event_callback): self._event_callback = event_callback - self._event_ct = 0 - self._counter = 1 + self._counter = 0 self._start_line = 0 self._buffer = StringIO() self._last_chunk = '' @@ -989,7 +997,7 @@ class OutputEventFilter(object): if value: self._emit_event(value) self._buffer = StringIO() - self._event_callback(dict(event='EOF')) + self._event_callback(dict(event='EOF', final_counter=self._counter)) def _emit_event(self, buffered_stdout, next_event_data=None): next_event_data = next_event_data or {} @@ -1003,8 +1011,8 @@ class OutputEventFilter(object): stdout_chunks = [] for stdout_chunk in stdout_chunks: - event_data['counter'] = self._counter self._counter += 1 + event_data['counter'] = self._counter event_data['stdout'] = stdout_chunk[:-2] if len(stdout_chunk) > 2 else "" n_lines = stdout_chunk.count('\n') event_data['start_line'] = self._start_line @@ -1012,7 +1020,6 @@ class OutputEventFilter(object): self._start_line += n_lines if self._event_callback: self._event_callback(event_data) - self._event_ct += 1 if next_event_data.get('uuid', None): self._current_event_data = next_event_data @@ -1073,3 +1080,25 @@ def has_model_field_prefetched(model_obj, field_name): # NOTE: Update this function if django internal implementation changes. return getattr(getattr(model_obj, field_name, None), 'prefetch_cache_name', '') in getattr(model_obj, '_prefetched_objects_cache', {}) + + +def get_external_account(user): + from django.conf import settings + from awx.conf.license import feature_enabled + account_type = None + if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'): + try: + if user.pk and user.profile.ldap_dn and not user.has_usable_password(): + account_type = "ldap" + except AttributeError: + pass + if (getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None) or + getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY', None) or + getattr(settings, 'SOCIAL_AUTH_GITHUB_ORG_KEY', None) or + getattr(settings, 'SOCIAL_AUTH_GITHUB_TEAM_KEY', None) or + getattr(settings, 'SOCIAL_AUTH_SAML_ENABLED_IDPS', None)) and user.social_auth.all(): + account_type = "social" + if (getattr(settings, 'RADIUS_SERVER', None) or + getattr(settings, 'TACACSPLUS_HOST', None)) and user.enterprise_auth.all(): + account_type = "enterprise" + return account_type diff --git a/awx/main/utils/filters.py b/awx/main/utils/filters.py index 9563eb6c34..30daf338f2 100644 --- a/awx/main/utils/filters.py +++ b/awx/main/utils/filters.py @@ -144,7 +144,7 @@ class SmartFilter(object): search_kwargs = self._expand_search(k, v) if search_kwargs: kwargs.update(search_kwargs) - q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__contains' % _k:_v}) for _k, _v in kwargs.items()]) + q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k:_v}) for _k, _v in kwargs.items()]) self.result = Host.objects.filter(q) else: kwargs[k] = v diff --git a/awx/main/utils/ha.py b/awx/main/utils/ha.py index 49421ad4cb..538de73f69 100644 --- a/awx/main/utils/ha.py +++ b/awx/main/utils/ha.py @@ -3,56 +3,9 @@ # Copyright (c) 2017 Ansible Tower by Red Hat # All Rights Reserved. -# Django -from django.conf import settings - -# AWX from awx.main.models import Instance -def construct_bcast_queue_name(common_name): - return common_name.encode('utf8') + '_' + settings.CLUSTER_HOST_ID - - -def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, worker_name): - removed_queues = [] - added_queues = [] - ig_names = set() - hostnames = set([instance.hostname for instance in controlled_instances]) - for instance in controlled_instances: - ig_names.update(instance.rampart_groups.values_list('name', flat=True)) - worker_queue_names = set([q['name'] for q in worker_queues]) - - bcast_queue_names = set([construct_bcast_queue_name(n) for n in settings.AWX_CELERY_BCAST_QUEUES_STATIC]) - all_queue_names = ig_names | hostnames | set(settings.AWX_CELERY_QUEUES_STATIC) - desired_queues = bcast_queue_names | (all_queue_names if instance.enabled else set()) - - # Remove queues - for queue_name in worker_queue_names: - if queue_name not in desired_queues: - app.control.cancel_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name]) - removed_queues.append(queue_name.encode("utf8")) - - # Add queues for instance and instance groups - for queue_name in all_queue_names: - if queue_name not in worker_queue_names: - app.control.add_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name]) - added_queues.append(queue_name.encode("utf8")) - - # Add stable-named broadcast queues - for queue_name in settings.AWX_CELERY_BCAST_QUEUES_STATIC: - bcast_queue_name = construct_bcast_queue_name(queue_name) - if bcast_queue_name not in worker_queue_names: - app.control.add_consumer(bcast_queue_name, - exchange=queue_name.encode("utf8"), - exchange_type='fanout', - routing_key=queue_name.encode("utf8"), - reply=True) - added_queues.append(bcast_queue_name) - - return (added_queues, removed_queues) - - class AWXCeleryRouter(object): def route_for_task(self, task, args=None, kwargs=None): (changed, instance) = Instance.objects.get_or_register() @@ -68,22 +21,3 @@ class AWXCeleryRouter(object): if instance.is_controller() and task in isolated_tasks: return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")} - - -def register_celery_worker_queues(app, celery_worker_name): - instance = Instance.objects.me() - controlled_instances = [instance] - if instance.is_controller(): - controlled_instances.extend(Instance.objects.filter( - rampart_groups__controller__instances__hostname=instance.hostname - )) - added_queues = [] - removed_queues = [] - - celery_host_queues = app.control.inspect([celery_worker_name]).active_queues() - - celery_worker_queues = celery_host_queues[celery_worker_name] if celery_host_queues else [] - (added_queues, removed_queues) = _add_remove_celery_worker_queues(app, controlled_instances, - celery_worker_queues, celery_worker_name) - return (controlled_instances, removed_queues, added_queues) - diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index 214c40ff11..3972815dc5 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -203,7 +203,7 @@ class BaseHTTPSHandler(BaseHandler): https://docs.python.org/3/library/concurrent.futures.html#future-objects http://pythonhosted.org/futures/ """ - return self.session.post(self._get_host(scheme='http'), + return self.session.post(self._get_host(scheme='https'), **self._get_post_kwargs(payload)) @@ -257,6 +257,15 @@ class UDPHandler(BaseHandler): return SocketResult(True, reason=self.message) +class AWXNullHandler(logging.NullHandler): + ''' + Only additional this does is accept arbitrary __init__ params because + the proxy handler does not (yet) work with arbitrary handler classes + ''' + def __init__(self, *args, **kwargs): + super(AWXNullHandler, self).__init__() + + HANDLER_MAPPING = { 'https': BaseHTTPSHandler, 'tcp': TCPHandler, @@ -285,7 +294,7 @@ class AWXProxyHandler(logging.Handler): self._old_kwargs = {} def get_handler_class(self, protocol): - return HANDLER_MAPPING[protocol] + return HANDLER_MAPPING.get(protocol, AWXNullHandler) def get_handler(self, custom_settings=None, force_create=False): new_kwargs = {} diff --git a/awx/network_ui/CONTRIBUTING.md b/awx/network_ui/CONTRIBUTING.md deleted file mode 100644 index cd028ef1e1..0000000000 --- a/awx/network_ui/CONTRIBUTING.md +++ /dev/null @@ -1,133 +0,0 @@ -Network UI -========== - -See [awx/ui/client/src/network-ui/CONTRIBUTING.md](../ui/client/src/network-ui/CONTRIBUTING.md) for the introduction -to the Network UI client-side development. - -Server-Side Development ------------------------ - -This document covers the Network UI server-side development. - -The Network UI is a UX driven feature to provide a graphical user -experience that fits well into the network engineer's normal workflow. Their -normal workflow includes a diagram drawn in a graphical drawing program, a -spreadsheet, and the command line interface of their network gear. Network -architects design the network on the graphical diagram and then hand off the -architecture to network operators who implement the architecture on the network -using spreadsheets to manage their data and manually converting the data into -CLI commands using their networking expertise and expertise with their physical -gear. - -The server-side code supports the persistence needed to provide this graphical -user experience of architecting a network and using that information along with -additional information (stored in vars files) to configure the network devices -using the CLI or NETCONF using Ansible playbooks and roles. - -Network UI Data Schema ----------------------- - -For the 3.3 release the persistence needed includes the position information of -the devices on the virtual canvas and the type of the devices as well as -information about the interfaces on the devices and the links connecting those -interfaces. - -These requirements determine the database schema needed for the network UI which -requires these models: Topology, Device, Interface, Link, Client, and TopologyInventory. - -![Models](designs/models.png) - -This diagram shows the relationships between the models in the Network UI schema. - -The models are: - -* Device - a host, switch, router, or other networking device -* Interface - a connection point on a device for a link -* Link - a physical connection between two devices to their respective interfaces -* Topology - a collection of devices and links -* TopologyInventory - a mapping between topologies and Tower inventories -* Client - a UI client session - - -Network UI Websocket Protocol ------------------------------ - -Persistence for the network UI canvas state is implemented using an -asynchronous websocket protocol to send information from the client to the -server and vice-versa. This two-way communication was chosen to support future -features for streaming data to the canvas, broadcast messaging between clients, -and for interaction performance on the UI. - - -Messages --------- - -JSON messages are passed over the `/network_ui/topology` websocket between the -test client and the test server. The protocol that is used for all messages is -in ABNF (RFC5234): - - - message_type = 'DeviceMove' / 'DeviceCreate' / 'DeviceDestroy' / 'DeviceLabelEdit' / 'DeviceSelected' / 'DeviceUnSelected' / 'InterfaceCreate' / 'InterfaceLabelEdit' / 'LinkLabelEdit' / 'LinkCreate' / 'LinkDestroy' / 'LinkSelected' / 'LinkUnSelected' / 'MultipleMessage' / 'Snapshot' - message_data = '{' 'msg_type' ': ' message_type ', ' key-value *( ', ' key-value ) '}' - message = '[ id , ' posint ']' / '[ topology_id , ' posint ']' / '[' message_type ', ' message_data ']' - -See https://github.com/AndyA/abnfgen/blob/master/andy/json.abnf for the rest of -the JSON ABNF. - -See [designs/messages.yml](designs/messages.yml) for the allowable keys and -values for each message type. - - -Initially when the websocket is first opened the server will send four messages -to the client. These are: - -* the client id using the `id` message type. -* the topology id using the `topology` message type. -* a Topology record containing data for the canvas itself. -* a Snapshot message containing all the data of the data on the canvas. - -As the user interacts with the canvas messages will be generated by the client -and the `network_ui.consumers.Persistence` class will update the models that -represent the canvas. - - - -Persistence ------------ - -The class `awx.network_uiconsumers.Persistence` provides persistence for the Network UI canvas. -It does so by providing message handlers that handle storage of the canvas change events -into the database. Each event has a message handle with name `onX` where `X` is the name of the message -type. The handlers use the `filter/values_list`, `filter/values`, `filter/update`, and `filter/delete` -patterns to update the data in the database quickly with a constant O(1) number of queries per event -often with only one query needed. With `filter/update` and `filter/delete` all the work is done -in the database and Python never needs to instaniate and garbage collect the model objects. - -Bulk operations (`filter/values`) in `send_snapshot` are used to produce a constant number of -queries produce a snapshot when the canvas is first loaded. This method avoids creating -the model objects since it only produces dicts that are JSON serializable which are bundled -together for the `Snapshot` message type. - -This method of persistence uses Django as a database query-compiler for transforms from -the event types to the database types. Using Django in this way is very performant since -Python does very little work processing the data and when possible the data never leaves -the database. - - -Client Tracking ---------------- - -Each user session to the network UI canvas is tracked with the `Client` model. Multiple -clients can view and interact with the network UI canvas at a time. They will see each other's -edits to the canvas in real time. This works by broadcasting the canvas change events to -all clients viewing the same topology. - -``` - # Send to all clients editing the topology - Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']}) -``` - -API ---- - -There is no user accessible API for this feature in the 3.3 release. diff --git a/awx/network_ui/__init__.py b/awx/network_ui/__init__.py deleted file mode 100644 index ebed9407c5..0000000000 --- a/awx/network_ui/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc - diff --git a/awx/network_ui/consumers.py b/awx/network_ui/consumers.py deleted file mode 100644 index 9cf8c72982..0000000000 --- a/awx/network_ui/consumers.py +++ /dev/null @@ -1,309 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc -import channels -from channels.auth import channel_session_user, channel_session_user_from_http -from awx.network_ui.models import Topology, Device, Link, Client, Interface -from awx.network_ui.models import TopologyInventory -import urlparse -from django.db.models import Q -from collections import defaultdict -import logging - - -from awx.network_ui.utils import transform_dict - -import json - -logger = logging.getLogger("awx.network_ui.consumers") - - -def parse_inventory_id(data): - inventory_id = data.get('inventory_id', ['null']) - try: - inventory_id = int(inventory_id[0]) - except ValueError: - inventory_id = None - except IndexError: - inventory_id = None - except TypeError: - inventory_id = None - if not inventory_id: - inventory_id = None - return inventory_id - - -class NetworkingEvents(object): - - ''' - Provides handlers for the networking events for the topology canvas. - ''' - - def parse_message_text(self, message_text, client_id): - ''' - See the Messages of CONTRIBUTING.md for the message format. - ''' - data = json.loads(message_text) - if len(data) == 2: - message_type = data.pop(0) - message_value = data.pop(0) - if isinstance(message_value, list): - logger.warning("Message has no sender") - return None, None - if isinstance(message_value, dict) and client_id != message_value.get('sender'): - logger.warning("client_id mismatch expected: %s actual %s", client_id, message_value.get('sender')) - return None, None - return message_type, message_value - else: - logger.error("Invalid message text") - return None, None - - def handle(self, message): - ''' - Dispatches a message based on the message type to a handler function - of name onX where X is the message type. - ''' - topology_id = message.get('topology') - if topology_id is None: - logger.warning("Unsupported message %s: no topology", message) - return - client_id = message.get('client') - if client_id is None: - logger.warning("Unsupported message %s: no client", message) - return - if 'text' not in message: - logger.warning("Unsupported message %s: no data", message) - return - message_type, message_value = self.parse_message_text(message['text'], client_id) - if message_type is None: - logger.warning("Unsupported message %s: no message type", message) - return - handler = self.get_handler(message_type) - if handler is not None: - handler(message_value, topology_id, client_id) - else: - logger.warning("Unsupported message %s: no handler", message_type) - - def get_handler(self, message_type): - return getattr(self, "on{0}".format(message_type), None) - - def onDeviceCreate(self, device, topology_id, client_id): - device = transform_dict(dict(x='x', - y='y', - name='name', - type='device_type', - id='cid', - host_id='host_id'), device) - logger.info("Device created %s", device) - d, _ = Device.objects.get_or_create(topology_id=topology_id, cid=device['cid'], defaults=device) - d.x = device['x'] - d.y = device['y'] - d.device_type = device['device_type'] - d.host_id = device['host_id'] - d.save() - (Topology.objects - .filter(pk=topology_id, device_id_seq__lt=device['cid']) - .update(device_id_seq=device['cid'])) - - def onDeviceDestroy(self, device, topology_id, client_id): - logger.info("Device removed %s", device) - Device.objects.filter(topology_id=topology_id, cid=device['id']).delete() - - def onDeviceMove(self, device, topology_id, client_id): - Device.objects.filter(topology_id=topology_id, cid=device['id']).update(x=device['x'], y=device['y']) - - def onDeviceLabelEdit(self, device, topology_id, client_id): - logger.debug("Device label edited %s", device) - Device.objects.filter(topology_id=topology_id, cid=device['id']).update(name=device['name']) - - def onInterfaceLabelEdit(self, interface, topology_id, client_id): - (Interface.objects - .filter(device__topology_id=topology_id, - cid=interface['id'], - device__cid=interface['device_id']) - .update(name=interface['name'])) - - def onLinkLabelEdit(self, link, topology_id, client_id): - logger.debug("Link label edited %s", link) - Link.objects.filter(from_device__topology_id=topology_id, cid=link['id']).update(name=link['name']) - - def onInterfaceCreate(self, interface, topology_id, client_id): - Interface.objects.get_or_create(device_id=Device.objects.get(cid=interface['device_id'], - topology_id=topology_id).pk, - cid=interface['id'], - defaults=dict(name=interface['name'])) - (Device.objects - .filter(cid=interface['device_id'], - topology_id=topology_id, - interface_id_seq__lt=interface['id']) - .update(interface_id_seq=interface['id'])) - - def onLinkCreate(self, link, topology_id, client_id): - logger.debug("Link created %s", link) - device_map = dict(Device.objects - .filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']]) - .values_list('cid', 'pk')) - if link['from_device_id'] not in device_map: - logger.warning('Device not found') - return - if link['to_device_id'] not in device_map: - logger.warning('Device not found') - return - Link.objects.get_or_create(cid=link['id'], - name=link['name'], - from_device_id=device_map[link['from_device_id']], - to_device_id=device_map[link['to_device_id']], - from_interface_id=Interface.objects.get(device_id=device_map[link['from_device_id']], - cid=link['from_interface_id']).pk, - to_interface_id=Interface.objects.get(device_id=device_map[link['to_device_id']], - cid=link['to_interface_id']).pk) - (Topology.objects - .filter(pk=topology_id, link_id_seq__lt=link['id']) - .update(link_id_seq=link['id'])) - - def onLinkDestroy(self, link, topology_id, client_id): - logger.debug("Link deleted %s", link) - device_map = dict(Device.objects - .filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']]) - .values_list('cid', 'pk')) - if link['from_device_id'] not in device_map: - logger.warning('Device not found') - return - if link['to_device_id'] not in device_map: - logger.warning('Device not found') - return - Link.objects.filter(cid=link['id'], - from_device_id=device_map[link['from_device_id']], - to_device_id=device_map[link['to_device_id']], - from_interface_id=Interface.objects.get(device_id=device_map[link['from_device_id']], - cid=link['from_interface_id']).pk, - to_interface_id=Interface.objects.get(device_id=device_map[link['to_device_id']], - cid=link['to_interface_id']).pk).delete() - - def onDeviceSelected(self, message_value, topology_id, client_id): - 'Ignore DeviceSelected messages' - pass - - def onDeviceUnSelected(self, message_value, topology_id, client_id): - 'Ignore DeviceSelected messages' - pass - - def onLinkSelected(self, message_value, topology_id, client_id): - 'Ignore LinkSelected messages' - pass - - def onLinkUnSelected(self, message_value, topology_id, client_id): - 'Ignore LinkSelected messages' - pass - - def onMultipleMessage(self, message_value, topology_id, client_id): - for message in message_value['messages']: - handler = self.get_handler(message['msg_type']) - if handler is not None: - handler(message, topology_id, client_id) - else: - logger.warning("Unsupported message %s", message['msg_type']) - - -networking_events_dispatcher = NetworkingEvents() - - -@channel_session_user_from_http -def ws_connect(message): - if not message.user.is_authenticated(): - logger.error("Request user is not authenticated to use websocket.") - message.reply_channel.send({"close": True}) - return - else: - message.reply_channel.send({"accept": True}) - - data = urlparse.parse_qs(message.content['query_string']) - inventory_id = parse_inventory_id(data) - topology_ids = list(TopologyInventory.objects.filter(inventory_id=inventory_id).values_list('pk', flat=True)) - topology_id = None - if len(topology_ids) > 0: - topology_id = topology_ids[0] - if topology_id is not None: - topology = Topology.objects.get(pk=topology_id) - else: - topology = Topology(name="topology", scale=1.0, panX=0, panY=0) - topology.save() - TopologyInventory(inventory_id=inventory_id, topology_id=topology.pk).save() - topology_id = topology.pk - message.channel_session['topology_id'] = topology_id - channels.Group("topology-%s" % topology_id).add(message.reply_channel) - client = Client() - client.save() - message.channel_session['client_id'] = client.pk - channels.Group("client-%s" % client.pk).add(message.reply_channel) - message.reply_channel.send({"text": json.dumps(["id", client.pk])}) - message.reply_channel.send({"text": json.dumps(["topology_id", topology_id])}) - topology_data = transform_dict(dict(id='topology_id', - name='name', - panX='panX', - panY='panY', - scale='scale', - link_id_seq='link_id_seq', - device_id_seq='device_id_seq'), topology.__dict__) - - message.reply_channel.send({"text": json.dumps(["Topology", topology_data])}) - send_snapshot(message.reply_channel, topology_id) - - -def send_snapshot(channel, topology_id): - interfaces = defaultdict(list) - - for i in (Interface.objects - .filter(device__topology_id=topology_id) - .values()): - i = transform_dict(dict(cid='id', - device_id='device_id', - id='interface_id', - name='name'), i) - interfaces[i['device_id']].append(i) - devices = list(Device.objects.filter(topology_id=topology_id).values()) - devices = [transform_dict(dict(cid='id', - id='device_id', - device_type='device_type', - host_id='host_id', - name='name', - x='x', - y='y', - interface_id_seq='interface_id_seq'), x) for x in devices] - for device in devices: - device['interfaces'] = interfaces[device['device_id']] - - links = [dict(id=x['cid'], - name=x['name'], - from_device_id=x['from_device__cid'], - to_device_id=x['to_device__cid'], - from_interface_id=x['from_interface__cid'], - to_interface_id=x['to_interface__cid']) - for x in list(Link.objects - .filter(Q(from_device__topology_id=topology_id) | - Q(to_device__topology_id=topology_id)) - .values('cid', - 'name', - 'from_device__cid', - 'to_device__cid', - 'from_interface__cid', - 'to_interface__cid'))] - snapshot = dict(sender=0, - devices=devices, - links=links) - channel.send({"text": json.dumps(["Snapshot", snapshot])}) - - -@channel_session_user -def ws_message(message): - # Send to all clients editing the topology - channels.Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']}) - # Send to networking_events handler - networking_events_dispatcher.handle({"text": message['text'], - "topology": message.channel_session['topology_id'], - "client": message.channel_session['client_id']}) - - -@channel_session_user -def ws_disconnect(message): - if 'topology_id' in message.channel_session: - channels.Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel) - diff --git a/awx/network_ui/docs/README.md b/awx/network_ui/docs/README.md deleted file mode 100644 index 2c8ff94bda..0000000000 --- a/awx/network_ui/docs/README.md +++ /dev/null @@ -1,8 +0,0 @@ - - -The design files in this directory are used in the database schema designer tool. - -* [models.png](models.png) - An image of the database schema design for network UI. -* [models.yml](models.yml) - Provides the main schema design for the network UI project. - -![Models](models.png) diff --git a/awx/network_ui/docs/messages.yml b/awx/network_ui/docs/messages.yml deleted file mode 100644 index 06ee3b9b75..0000000000 --- a/awx/network_ui/docs/messages.yml +++ /dev/null @@ -1,19 +0,0 @@ -messages: - - {msg_type: DeviceMove, fields: [msg_type, sender, id, x, y, previous_x, previous_y]} - - {msg_type: DeviceCreate, fields: [msg_type, sender, id, x, y, name, type, host_id]} - - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type, previous_host_id]} - - {msg_type: DeviceLabelEdit, fields: [msg_type, sender, id, name, previous_name]} - - {msg_type: DeviceSelected, fields: [msg_type, sender, id]} - - {msg_type: DeviceUnSelected, fields: [msg_type, sender, id]} - - {msg_type: InterfaceCreate, fields: [msg_type, sender, device_id, id, name]} - - {msg_type: InterfaceLabelEdit, fields: [msg_type, sender, id, device_id, name, previous_name]} - - {msg_type: LinkLabelEdit, fields: [msg_type, sender, id, name, previous_name]} - - {msg_type: LinkCreate, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]} - - {msg_type: LinkDestroy, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]} - - {msg_type: LinkSelected, fields: [msg_type, sender, id]} - - {msg_type: LinkUnSelected, fields: [msg_type, sender, id]} - - {msg_type: MultipleMessage, fields: [msg_type, sender, messages]} - - {msg_type: Snapshot, fields: [msg_type, sender, devices, links, order, trace_id]} - - {msg_type: id, type: int} - - {msg_type: topology_id, type: int} - - {msg_type: Topology, fields: [topology_id, name, panX, panY, scale, link_id_seq, device_id_seq]} diff --git a/awx/network_ui/docs/models.png b/awx/network_ui/docs/models.png deleted file mode 100644 index c6b22910d8..0000000000 Binary files a/awx/network_ui/docs/models.png and /dev/null differ diff --git a/awx/network_ui/docs/models.yml b/awx/network_ui/docs/models.yml deleted file mode 100644 index 683dde4bfd..0000000000 --- a/awx/network_ui/docs/models.yml +++ /dev/null @@ -1,130 +0,0 @@ -app: awx.network_ui -external_models: [] -models: -- display: name - fields: - - name: device_id - pk: true - type: AutoField - - name: topology - ref: Topology - ref_field: topology_id - type: ForeignKey - - len: 200 - name: name - type: CharField - - name: x - type: IntegerField - - name: y - type: IntegerField - - name: id - type: IntegerField - - len: 200 - name: device_type - type: CharField - - default: 0 - name: interface_id_seq - type: IntegerField - - default: 0 - name: host_id - type: IntegerField - name: Device - x: 348 - y: 124 -- fields: - - name: link_id - pk: true - type: AutoField - - name: from_device - ref: Device - ref_field: device_id - related_name: from_link - type: ForeignKey - - name: to_device - ref: Device - ref_field: device_id - related_name: to_link - type: ForeignKey - - name: from_interface - ref: Interface - ref_field: interface_id - related_name: from_link - type: ForeignKey - - name: to_interface - ref: Interface - ref_field: interface_id - related_name: to_link - type: ForeignKey - - name: id - type: IntegerField - - len: 200 - name: name - type: CharField - name: Link - x: 731 - y: -33 -- display: name - fields: - - name: topology_id - pk: true - type: AutoField - - len: 200 - name: name - type: CharField - - name: scale - type: FloatField - - name: panX - type: FloatField - - name: panY - type: FloatField - - default: 0 - name: device_id_seq - type: IntegerField - - default: 0 - name: link_id_seq - type: IntegerField - name: Topology - x: 111 - y: 127 -- fields: - - name: client_id - pk: true - type: AutoField - name: Client - x: -162 - y: 282 -- display: name - fields: - - name: interface_id - pk: true - type: AutoField - - name: device - ref: Device - ref_field: device_id - type: ForeignKey - - len: 200 - name: name - type: CharField - - name: id - type: IntegerField - name: Interface - x: 977 - y: 312 -- fields: - - name: topology_inventory_id - pk: true - type: AutoField - - name: topology - ref: Topology - ref_field: topology_id - type: ForeignKey - - name: inventory_id - type: IntegerField - name: TopologyInventory - x: -204 - y: 12 -modules: [] -view: - panX: 213.729555519212 - panY: 189.446959094643 - scaleXY: 0.69 diff --git a/awx/network_ui/migrations/0001_initial.py b/awx/network_ui/migrations/0001_initial.py deleted file mode 100644 index 07013104e1..0000000000 --- a/awx/network_ui/migrations/0001_initial.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.11 on 2018-03-23 20:43 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('main', '0027_v330_emitted_events'), - ] - - operations = [ - migrations.CreateModel( - name='Client', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ], - ), - migrations.CreateModel( - name='Device', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(blank=True, max_length=200)), - ('x', models.IntegerField()), - ('y', models.IntegerField()), - ('cid', models.IntegerField()), - ('device_type', models.CharField(blank=True, max_length=200)), - ('interface_id_seq', models.IntegerField(default=0)), - ('host', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.Host')), - ], - ), - migrations.CreateModel( - name='Interface', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(blank=True, max_length=200)), - ('cid', models.IntegerField()), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Device')), - ], - ), - migrations.CreateModel( - name='Link', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('cid', models.IntegerField()), - ('name', models.CharField(blank=True, max_length=200)), - ('from_device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_link', to='network_ui.Device')), - ('from_interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_link', to='network_ui.Interface')), - ('to_device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_link', to='network_ui.Device')), - ('to_interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_link', to='network_ui.Interface')), - ], - ), - migrations.CreateModel( - name='Topology', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(blank=True, max_length=200)), - ('scale', models.FloatField()), - ('panX', models.FloatField()), - ('panY', models.FloatField()), - ('device_id_seq', models.IntegerField(default=0)), - ('link_id_seq', models.IntegerField(default=0)), - ], - ), - migrations.CreateModel( - name='TopologyInventory', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('inventory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Inventory')), - ('topology', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Topology')), - ], - ), - migrations.AddField( - model_name='device', - name='topology', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Topology'), - ), - ] diff --git a/awx/network_ui/migrations/__init__.py b/awx/network_ui/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/awx/network_ui/models.py b/awx/network_ui/models.py deleted file mode 100644 index 07d87e26cc..0000000000 --- a/awx/network_ui/models.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.db import models - - -class Device(models.Model): - - id = models.AutoField(primary_key=True,) - topology = models.ForeignKey('Topology',) - name = models.CharField(max_length=200, blank=True) - x = models.IntegerField() - y = models.IntegerField() - cid = models.IntegerField() - device_type = models.CharField(max_length=200, blank=True) - interface_id_seq = models.IntegerField(default=0,) - host = models.ForeignKey('main.Host', default=None, null=True, on_delete=models.SET_NULL) - - def __unicode__(self): - return self.name - - -class Link(models.Model): - - id = models.AutoField(primary_key=True,) - from_device = models.ForeignKey('Device', related_name='from_link',) - to_device = models.ForeignKey('Device', related_name='to_link',) - from_interface = models.ForeignKey('Interface', related_name='from_link',) - to_interface = models.ForeignKey('Interface', related_name='to_link',) - cid = models.IntegerField() - name = models.CharField(max_length=200, blank=True) - - -class Topology(models.Model): - - id = models.AutoField(primary_key=True,) - name = models.CharField(max_length=200, blank=True) - scale = models.FloatField() - panX = models.FloatField() - panY = models.FloatField() - device_id_seq = models.IntegerField(default=0,) - link_id_seq = models.IntegerField(default=0,) - - def __unicode__(self): - return self.name - - -class Client(models.Model): - - id = models.AutoField(primary_key=True,) - - -class Interface(models.Model): - - id = models.AutoField(primary_key=True,) - device = models.ForeignKey('Device',) - name = models.CharField(max_length=200, blank=True) - cid = models.IntegerField() - - def __unicode__(self): - return self.name - - -class TopologyInventory(models.Model): - - id = models.AutoField(primary_key=True,) - topology = models.ForeignKey('Topology',) - inventory = models.ForeignKey('main.Inventory') diff --git a/awx/network_ui/routing.py b/awx/network_ui/routing.py deleted file mode 100644 index 0a9d07635d..0000000000 --- a/awx/network_ui/routing.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc -from channels.routing import route -from awx.network_ui.consumers import ws_connect, ws_message, ws_disconnect - -channel_routing = [ - route("websocket.connect", ws_connect, path=r"^/network_ui/topology/"), - route("websocket.receive", ws_message, path=r"^/network_ui/topology/"), - route("websocket.disconnect", ws_disconnect, path=r"^/network_ui/topology/"), -] diff --git a/awx/network_ui/tests/__init__.py b/awx/network_ui/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/awx/network_ui/tests/conftest.py b/awx/network_ui/tests/conftest.py deleted file mode 100644 index ca4f2f1cda..0000000000 --- a/awx/network_ui/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from mock import PropertyMock - - -@pytest.fixture(autouse=True) -def _disable_database_settings(mocker): - m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=PropertyMock) - m.return_value = [] - diff --git a/awx/network_ui/tests/unit/__init__.py b/awx/network_ui/tests/unit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/awx/network_ui/tests/unit/test_consumers.py b/awx/network_ui/tests/unit/test_consumers.py deleted file mode 100644 index de5c79e105..0000000000 --- a/awx/network_ui/tests/unit/test_consumers.py +++ /dev/null @@ -1,240 +0,0 @@ - -import mock -import logging -import json -import imp -from mock import patch -patch('channels.auth.channel_session_user', lambda x: x).start() -patch('channels.auth.channel_session_user_from_http', lambda x: x).start() - -from awx.network_ui.consumers import parse_inventory_id, networking_events_dispatcher, send_snapshot # noqa -from awx.network_ui.models import Topology, Device, Link, Interface, TopologyInventory, Client # noqa -import awx # noqa -import awx.network_ui # noqa -import awx.network_ui.consumers # noqa -imp.reload(awx.network_ui.consumers) - - -def test_parse_inventory_id(): - assert parse_inventory_id({}) is None - assert parse_inventory_id({'inventory_id': ['1']}) == 1 - assert parse_inventory_id({'inventory_id': ['0']}) is None - assert parse_inventory_id({'inventory_id': ['X']}) is None - assert parse_inventory_id({'inventory_id': []}) is None - assert parse_inventory_id({'inventory_id': 'x'}) is None - assert parse_inventory_id({'inventory_id': '12345'}) == 1 - assert parse_inventory_id({'inventory_id': 1}) is None - - -def test_network_events_handle_message_incomplete_message1(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle({}) - log_mock.assert_called_once_with( - 'Unsupported message %s: no topology', {}) - - -def test_network_events_handle_message_incomplete_message2(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle({'topology': [0]}) - log_mock.assert_called_once_with( - 'Unsupported message %s: no client', {'topology': [0]}) - - -def test_network_events_handle_message_incomplete_message3(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle({'topology': [1]}) - log_mock.assert_called_once_with( - 'Unsupported message %s: no client', {'topology': [1]}) - - -def test_network_events_handle_message_incomplete_message4(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle({'topology': 1, 'client': 1}) - log_mock.assert_called_once_with('Unsupported message %s: no data', { - 'client': 1, 'topology': 1}) - - -def test_network_events_handle_message_incomplete_message5(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - message = ['DeviceCreate'] - networking_events_dispatcher.handle( - {'topology': 1, 'client': 1, 'text': json.dumps(message)}) - log_mock.assert_called_once_with('Unsupported message %s: no message type', { - 'text': '["DeviceCreate"]', 'client': 1, 'topology': 1}) - - -def test_network_events_handle_message_incomplete_message6(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - message = ['DeviceCreate', []] - networking_events_dispatcher.handle( - {'topology': 1, 'client': 1, 'text': json.dumps(message)}) - log_mock.assert_has_calls([ - mock.call('Message has no sender'), - mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", []]', 'client': 1, 'topology': 1})]) - - -def test_network_events_handle_message_incomplete_message7(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - message = ['DeviceCreate', {}] - networking_events_dispatcher.handle( - {'topology': 1, 'client': 1, 'text': json.dumps(message)}) - log_mock.assert_has_calls([ - mock.call('client_id mismatch expected: %s actual %s', 1, None), - mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", {}]', 'client': 1, 'topology': 1})]) - - -def test_network_events_handle_message_incomplete_message8(): - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock: - message = ['Unsupported', {'sender': 1}] - networking_events_dispatcher.handle( - {'topology': 1, 'client': 1, 'text': json.dumps(message)}) - log_mock.assert_called_once_with( - 'Unsupported message %s: no handler', u'Unsupported') - - -def test_send_snapshot_empty(): - channel = mock.MagicMock() - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects'),\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects'),\ - mock.patch.object(Topology, 'objects'): - send_snapshot(channel, 1) - log_mock.assert_not_called() - channel.send.assert_called_once_with( - {'text': '["Snapshot", {"links": [], "devices": [], "sender": 0}]'}) - - -def test_send_snapshot_single(): - channel = mock.MagicMock() - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects') as interface_objects_mock: - - interface_objects_mock.filter.return_value.values.return_value = [ - dict(cid=1, device_id=1, id=1, name="eth0")] - device_objects_mock.filter.return_value.values.return_value = [ - dict(cid=1, id=1, device_type="host", name="host1", x=0, y=0, - interface_id_seq=1, host_id=1)] - send_snapshot(channel, 1) - device_objects_mock.filter.assert_called_once_with(topology_id=1) - device_objects_mock.filter.return_value.values.assert_called_once_with() - interface_objects_mock.filter.assert_called_once_with( - device__topology_id=1) - interface_objects_mock.filter.return_value.values.assert_called_once_with() - log_mock.assert_not_called() - channel.send.assert_called_once_with( - {'text': '''["Snapshot", {"links": [], "devices": [{"interface_id_seq": 1, \ -"name": "host1", "interfaces": [{"id": 1, "device_id": 1, "name": "eth0", "interface_id": 1}], \ -"device_type": "host", "host_id": 1, "y": 0, "x": 0, "id": 1, "device_id": 1}], "sender": 0}]'''}) - - -def test_ws_disconnect(): - message = mock.MagicMock() - message.channel_session = dict(topology_id=1) - message.reply_channel = 'foo' - with mock.patch('channels.Group') as group_mock: - awx.network_ui.consumers.ws_disconnect(message) - group_mock.assert_called_once_with('topology-1') - group_mock.return_value.discard.assert_called_once_with('foo') - - -def test_ws_disconnect_no_topology(): - message = mock.MagicMock() - with mock.patch('channels.Group') as group_mock: - awx.network_ui.consumers.ws_disconnect(message) - group_mock.assert_not_called() - - -def test_ws_message(): - message = mock.MagicMock() - message.channel_session = dict(topology_id=1, client_id=1) - message.__getitem__.return_value = json.dumps([]) - print (message['text']) - with mock.patch('channels.Group') as group_mock: - awx.network_ui.consumers.ws_message(message) - group_mock.assert_called_once_with('topology-1') - group_mock.return_value.send.assert_called_once_with({'text': '[]'}) - - -def test_ws_connect_unauthenticated(): - message = mock.MagicMock() - message.user.is_authenticated.return_value = False - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch.object(logger, 'error') as log_mock: - awx.network_ui.consumers.ws_connect(message) - log_mock.assert_called_once_with('Request user is not authenticated to use websocket.') - - -def test_ws_connect_new_topology(): - message = mock.MagicMock() - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch('awx.network_ui.consumers.Client') as client_mock,\ - mock.patch('awx.network_ui.consumers.Topology') as topology_mock,\ - mock.patch('channels.Group'),\ - mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\ - mock.patch.object(logger, 'warning'),\ - mock.patch.object(TopologyInventory, 'objects'),\ - mock.patch.object(TopologyInventory, 'save'),\ - mock.patch.object(Topology, 'save'),\ - mock.patch.object(Topology, 'objects'),\ - mock.patch.object(Device, 'objects'),\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects'): - client_mock.return_value.pk = 777 - topology_mock.return_value = Topology( - name="topology", scale=1.0, panX=0, panY=0, pk=999) - awx.network_ui.consumers.ws_connect(message) - message.reply_channel.send.assert_has_calls([ - mock.call({'text': '["id", 777]'}), - mock.call({'text': '["topology_id", 999]'}), - mock.call( - {'text': '["Topology", {"scale": 1.0, "name": "topology", "device_id_seq": 0, "panY": 0, "panX": 0, "topology_id": 999, "link_id_seq": 0}]'}), - ]) - send_snapshot_mock.assert_called_once_with(message.reply_channel, 999) - - -def test_ws_connect_existing_topology(): - message = mock.MagicMock() - logger = logging.getLogger('awx.network_ui.consumers') - with mock.patch('awx.network_ui.consumers.Client') as client_mock,\ - mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\ - mock.patch('channels.Group'),\ - mock.patch.object(logger, 'warning'),\ - mock.patch.object(TopologyInventory, 'objects') as topology_inventory_objects_mock,\ - mock.patch.object(TopologyInventory, 'save'),\ - mock.patch.object(Topology, 'save'),\ - mock.patch.object(Topology, 'objects') as topology_objects_mock,\ - mock.patch.object(Device, 'objects'),\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects'): - topology_inventory_objects_mock.filter.return_value.values_list.return_value = [ - 1] - client_mock.return_value.pk = 888 - topology_objects_mock.get.return_value = Topology(pk=1001, - id=1, - name="topo", - panX=0, - panY=0, - scale=1.0, - link_id_seq=1, - device_id_seq=1) - awx.network_ui.consumers.ws_connect(message) - message.reply_channel.send.assert_has_calls([ - mock.call({'text': '["id", 888]'}), - mock.call({'text': '["topology_id", 1001]'}), - mock.call( - {'text': '["Topology", {"scale": 1.0, "name": "topo", "device_id_seq": 1, "panY": 0, "panX": 0, "topology_id": 1001, "link_id_seq": 1}]'}), - ]) - send_snapshot_mock.assert_called_once_with(message.reply_channel, 1001) diff --git a/awx/network_ui/tests/unit/test_models.py b/awx/network_ui/tests/unit/test_models.py deleted file mode 100644 index e392662a99..0000000000 --- a/awx/network_ui/tests/unit/test_models.py +++ /dev/null @@ -1,15 +0,0 @@ - - -from awx.network_ui.models import Device, Topology, Interface - - -def test_device(): - assert str(Device(name="foo")) == "foo" - - -def test_topology(): - assert str(Topology(name="foo")) == "foo" - - -def test_interface(): - assert str(Interface(name="foo")) == "foo" diff --git a/awx/network_ui/tests/unit/test_network_events.py b/awx/network_ui/tests/unit/test_network_events.py deleted file mode 100644 index d4ce60c7ae..0000000000 --- a/awx/network_ui/tests/unit/test_network_events.py +++ /dev/null @@ -1,451 +0,0 @@ -import mock -import json -import logging - -from awx.network_ui.consumers import networking_events_dispatcher -from awx.network_ui.models import Topology, Device, Link, Interface - - -def message(message): - def wrapper(fn): - fn.tests_message = message - return fn - return wrapper - - -@message('DeviceMove') -def test_network_events_handle_message_DeviceMove(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceMove', dict( - msg_type='DeviceMove', - sender=1, - id=1, - x=100, - y=100, - previous_x=0, - previous_y=0 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock: - networking_events_dispatcher.handle(message) - device_objects_mock.filter.assert_called_once_with( - cid=1, topology_id=1) - device_objects_mock.filter.return_value.update.assert_called_once_with( - x=100, y=100) - log_mock.assert_not_called() - - -@message('DeviceCreate') -def test_network_events_handle_message_DeviceCreate(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceCreate', dict(msg_type='DeviceCreate', - sender=1, - id=1, - x=0, - y=0, - name="test_created", - type='host', - host_id=None)] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Topology.objects, 'filter') as topology_objects_mock,\ - mock.patch.object(Device.objects, 'get_or_create') as device_objects_mock: - device_mock = mock.MagicMock() - filter_mock = mock.MagicMock() - device_objects_mock.return_value = [device_mock, True] - topology_objects_mock.return_value = filter_mock - networking_events_dispatcher.handle(message) - device_objects_mock.assert_called_once_with( - cid=1, - defaults={'name': u'test_created', 'cid': 1, 'device_type': u'host', - 'x': 0, 'y': 0, 'host_id': None}, - topology_id=1) - device_mock.save.assert_called_once_with() - topology_objects_mock.assert_called_once_with( - device_id_seq__lt=1, pk=1) - filter_mock.update.assert_called_once_with(device_id_seq=1) - log_mock.assert_not_called() - - -@message('DeviceLabelEdit') -def test_network_events_handle_message_DeviceLabelEdit(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceLabelEdit', dict( - msg_type='DeviceLabelEdit', - sender=1, - id=1, - name='test_changed', - previous_name='test_created' - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device.objects, 'filter') as device_objects_filter_mock: - networking_events_dispatcher.handle(message) - device_objects_filter_mock.assert_called_once_with( - cid=1, topology_id=1) - log_mock.assert_not_called() - - -@message('DeviceSelected') -def test_network_events_handle_message_DeviceSelected(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceSelected', dict( - msg_type='DeviceSelected', - sender=1, - id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_not_called() - - -@message('DeviceUnSelected') -def test_network_events_handle_message_DeviceUnSelected(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceUnSelected', dict( - msg_type='DeviceUnSelected', - sender=1, - id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_not_called() - - -@message('DeviceDestroy') -def test_network_events_handle_message_DeviceDestory(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['DeviceDestroy', dict( - msg_type='DeviceDestroy', - sender=1, - id=1, - previous_x=0, - previous_y=0, - previous_name="", - previous_type="host", - previous_host_id="1")] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock: - networking_events_dispatcher.handle(message) - device_objects_mock.filter.assert_called_once_with( - cid=1, topology_id=1) - device_objects_mock.filter.return_value.delete.assert_called_once_with() - log_mock.assert_not_called() - - -@message('InterfaceCreate') -def test_network_events_handle_message_InterfaceCreate(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['InterfaceCreate', dict( - msg_type='InterfaceCreate', - sender=1, - device_id=1, - id=1, - name='eth0' - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Interface, 'objects') as interface_objects_mock: - device_objects_mock.get.return_value.pk = 99 - networking_events_dispatcher.handle(message) - device_objects_mock.get.assert_called_once_with(cid=1, topology_id=1) - device_objects_mock.filter.assert_called_once_with( - cid=1, interface_id_seq__lt=1, topology_id=1) - interface_objects_mock.get_or_create.assert_called_once_with( - cid=1, defaults={'name': u'eth0'}, device_id=99) - log_mock.assert_not_called() - - -@message('InterfaceLabelEdit') -def test_network_events_handle_message_InterfaceLabelEdit(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['InterfaceLabelEdit', dict( - msg_type='InterfaceLabelEdit', - sender=1, - id=1, - device_id=1, - name='new name', - previous_name='old name' - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Interface, 'objects') as interface_objects_mock: - networking_events_dispatcher.handle(message) - interface_objects_mock.filter.assert_called_once_with( - cid=1, device__cid=1, device__topology_id=1) - interface_objects_mock.filter.return_value.update.assert_called_once_with( - name=u'new name') - log_mock.assert_not_called() - - -@message('LinkLabelEdit') -def test_network_events_handle_message_LinkLabelEdit(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkLabelEdit', dict( - msg_type='LinkLabelEdit', - sender=1, - id=1, - name='new name', - previous_name='old name' - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Link, 'objects') as link_objects_mock: - networking_events_dispatcher.handle(message) - link_objects_mock.filter.assert_called_once_with( - cid=1, from_device__topology_id=1) - link_objects_mock.filter.return_value.update.assert_called_once_with( - name=u'new name') - log_mock.assert_not_called() - - -@message('LinkCreate') -def test_network_events_handle_message_LinkCreate(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkCreate', dict( - msg_type='LinkCreate', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Link, 'objects') as link_objects_mock,\ - mock.patch.object(Interface, 'objects') as interface_objects_mock,\ - mock.patch.object(Topology, 'objects') as topology_objects_mock: - values_list_mock = mock.MagicMock() - values_list_mock.values_list.return_value = [(1,1), (2,2)] - interface_objects_mock.get.return_value = mock.MagicMock() - interface_objects_mock.get.return_value.pk = 7 - device_objects_mock.filter.return_value = values_list_mock - topology_objects_mock.filter.return_value = mock.MagicMock() - networking_events_dispatcher.handle(message) - device_objects_mock.filter.assert_called_once_with( - cid__in=[1, 2], topology_id=1) - values_list_mock.values_list.assert_called_once_with('cid', 'pk') - link_objects_mock.get_or_create.assert_called_once_with( - cid=1, from_device_id=1, from_interface_id=7, name=u'', - to_device_id=2, to_interface_id=7) - topology_objects_mock.filter.assert_called_once_with( - link_id_seq__lt=1, pk=1) - topology_objects_mock.filter.return_value.update.assert_called_once_with( - link_id_seq=1) - log_mock.assert_not_called() - - -@message('LinkCreate') -def test_network_events_handle_message_LinkCreate_bad_device1(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkCreate', dict( - msg_type='LinkCreate', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects') as interface_objects_mock,\ - mock.patch.object(Topology, 'objects') as topology_objects_mock: - values_list_mock = mock.MagicMock() - values_list_mock.values_list.return_value = [(9,1), (2,2)] - interface_objects_mock.get.return_value = mock.MagicMock() - interface_objects_mock.get.return_value.pk = 7 - device_objects_mock.filter.return_value = values_list_mock - topology_objects_mock.filter.return_value = mock.MagicMock() - networking_events_dispatcher.handle(message) - device_objects_mock.filter.assert_called_once_with( - cid__in=[1, 2], topology_id=1) - values_list_mock.values_list.assert_called_once_with('cid', 'pk') - log_mock.assert_called_once_with('Device not found') - - -@message('LinkCreate') -def test_network_events_handle_message_LinkCreate_bad_device2(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkCreate', dict( - msg_type='LinkCreate', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Link, 'objects'),\ - mock.patch.object(Interface, 'objects') as interface_objects_mock,\ - mock.patch.object(Topology, 'objects') as topology_objects_mock: - values_list_mock = mock.MagicMock() - values_list_mock.values_list.return_value = [(1,1), (9,2)] - interface_objects_mock.get.return_value = mock.MagicMock() - interface_objects_mock.get.return_value.pk = 7 - device_objects_mock.filter.return_value = values_list_mock - topology_objects_mock.filter.return_value = mock.MagicMock() - networking_events_dispatcher.handle(message) - device_objects_mock.filter.assert_called_once_with( - cid__in=[1, 2], topology_id=1) - values_list_mock.values_list.assert_called_once_with('cid', 'pk') - log_mock.assert_called_once_with('Device not found') - - -@message('LinkDestroy') -def test_network_events_handle_message_LinkDestroy(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkDestroy', dict( - msg_type='LinkDestroy', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device.objects, 'filter') as device_filter_mock,\ - mock.patch.object(Link.objects, 'filter') as link_filter_mock,\ - mock.patch.object(Interface.objects, 'get') as interface_get_mock: - values_mock = mock.MagicMock() - interface_get_mock.return_value = mock.MagicMock() - interface_get_mock.return_value.pk = 7 - device_filter_mock.return_value = values_mock - values_mock.values_list.return_value = [(1,1), (2,2)] - networking_events_dispatcher.handle(message) - device_filter_mock.assert_called_once_with( - cid__in=[1, 2], topology_id=1) - values_mock.values_list.assert_called_once_with('cid', 'pk') - link_filter_mock.assert_called_once_with( - cid=1, from_device_id=1, from_interface_id=7, to_device_id=2, to_interface_id=7) - log_mock.assert_not_called() - - -@message('LinkDestroy') -def test_network_events_handle_message_LinkDestroy_bad_device_map1(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkDestroy', dict( - msg_type='LinkDestroy', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device.objects, 'filter') as device_filter_mock,\ - mock.patch.object(Link.objects, 'filter'),\ - mock.patch.object(Interface.objects, 'get') as interface_get_mock: - values_mock = mock.MagicMock() - interface_get_mock.return_value = mock.MagicMock() - interface_get_mock.return_value.pk = 7 - device_filter_mock.return_value = values_mock - values_mock.values_list.return_value = [(9,1), (2,2)] - networking_events_dispatcher.handle(message) - log_mock.assert_called_once_with('Device not found') - - -@message('LinkDestroy') -def test_network_events_handle_message_LinkDestroy_bad_device_map2(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkDestroy', dict( - msg_type='LinkDestroy', - id=1, - sender=1, - name="", - from_device_id=1, - to_device_id=2, - from_interface_id=1, - to_interface_id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock,\ - mock.patch.object(Device.objects, 'filter') as device_filter_mock,\ - mock.patch.object(Link.objects, 'filter'),\ - mock.patch.object(Interface.objects, 'get') as interface_get_mock: - values_mock = mock.MagicMock() - interface_get_mock.return_value = mock.MagicMock() - interface_get_mock.return_value.pk = 7 - device_filter_mock.return_value = values_mock - values_mock.values_list.return_value = [(1,1), (9,2)] - networking_events_dispatcher.handle(message) - log_mock.assert_called_once_with('Device not found') - - -@message('LinkSelected') -def test_network_events_handle_message_LinkSelected(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkSelected', dict( - msg_type='LinkSelected', - sender=1, - id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_not_called() - - -@message('LinkUnSelected') -def test_network_events_handle_message_LinkUnSelected(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['LinkUnSelected', dict( - msg_type='LinkUnSelected', - sender=1, - id=1 - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_not_called() - - -@message('MultipleMessage') -def test_network_events_handle_message_MultipleMessage_unsupported_message(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['MultipleMessage', dict( - msg_type='MultipleMessage', - sender=1, - messages=[dict(msg_type="Unsupported")] - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_called_once_with( - 'Unsupported message %s', u'Unsupported') - - -@message('MultipleMessage') -def test_network_events_handle_message_MultipleMessage(): - logger = logging.getLogger('awx.network_ui.consumers') - message_data = ['MultipleMessage', dict( - msg_type='MultipleMessage', - sender=1, - messages=[dict(msg_type="DeviceSelected")] - )] - message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)} - with mock.patch.object(logger, 'warning') as log_mock: - networking_events_dispatcher.handle(message) - log_mock.assert_not_called() diff --git a/awx/network_ui/tests/unit/test_routing.py b/awx/network_ui/tests/unit/test_routing.py deleted file mode 100644 index d1d7a741dd..0000000000 --- a/awx/network_ui/tests/unit/test_routing.py +++ /dev/null @@ -1,9 +0,0 @@ - -import awx.network_ui.routing - - -def test_routing(): - ''' - Tests that the number of routes in awx.network_ui.routing is 3. - ''' - assert len(awx.network_ui.routing.channel_routing) == 3 diff --git a/awx/network_ui/tests/unit/test_views.py b/awx/network_ui/tests/unit/test_views.py deleted file mode 100644 index 9b55ad72d4..0000000000 --- a/awx/network_ui/tests/unit/test_views.py +++ /dev/null @@ -1,65 +0,0 @@ - -import mock - -from awx.network_ui.views import topology_data, NetworkAnnotatedInterface, json_topology_data, yaml_topology_data -from awx.network_ui.models import Topology, Device, Link, Interface - - - -def test_topology_data(): - with mock.patch.object(Topology, 'objects'),\ - mock.patch.object(Device, 'objects') as device_objects_mock,\ - mock.patch.object(Link, 'objects') as link_objects_mock,\ - mock.patch.object(Interface, 'objects'),\ - mock.patch.object(NetworkAnnotatedInterface, 'filter'): - device_objects_mock.filter.return_value.order_by.return_value = [ - Device(pk=1), Device(pk=2)] - link_objects_mock.filter.return_value = [Link(from_device=Device(name='from', cid=1), - to_device=Device( - name='to', cid=2), - from_interface=Interface( - name="eth0", cid=1), - to_interface=Interface( - name="eth0", cid=1), - name="", - pk=1 - )] - data = topology_data(1) - assert len(data['devices']) == 2 - assert len(data['links']) == 1 - - -def test_json_topology_data(): - request = mock.MagicMock() - request.GET = dict(topology_id=1) - with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock: - topology_data_mock.return_value = dict() - json_topology_data(request) - topology_data_mock.assert_called_once_with(1) - - -def test_yaml_topology_data(): - request = mock.MagicMock() - request.GET = dict(topology_id=1) - with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock: - topology_data_mock.return_value = dict() - yaml_topology_data(request) - topology_data_mock.assert_called_once_with(1) - - -def test_json_topology_data_no_topology_id(): - request = mock.MagicMock() - request.GET = dict() - with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock: - topology_data_mock.return_value = dict() - json_topology_data(request) - topology_data_mock.assert_not_called() - - -def test_yaml_topology_data_no_topology_id(): - request = mock.MagicMock() - request.GET = dict() - with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock: - topology_data_mock.return_value = dict() - yaml_topology_data(request) - topology_data_mock.assert_not_called() diff --git a/awx/network_ui/urls.py b/awx/network_ui/urls.py deleted file mode 100644 index 2101eff59f..0000000000 --- a/awx/network_ui/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc -from django.conf.urls import url - -from awx.network_ui import views - -app_name = 'network_ui' -urlpatterns = [ - url(r'^topology.json/?$', views.json_topology_data, name='json_topology_data'), - url(r'^topology.yaml/?$', views.yaml_topology_data, name='yaml_topology_data'), -] diff --git a/awx/network_ui/utils.py b/awx/network_ui/utils.py deleted file mode 100644 index 9b2eea6c10..0000000000 --- a/awx/network_ui/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc - - -def transform_dict(dict_map, d): - return {to_key: d[from_key] for from_key, to_key in dict_map.iteritems()} - diff --git a/awx/network_ui/views.py b/awx/network_ui/views.py deleted file mode 100644 index b9cd476bcc..0000000000 --- a/awx/network_ui/views.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2017 Red Hat, Inc -from django import forms -from django.http import JsonResponse, HttpResponseBadRequest, HttpResponse -from awx.network_ui.models import Topology, Device, Link, Interface -from django.db.models import Q -import yaml - -NetworkAnnotatedInterface = Interface.objects.values('name', - 'cid', - 'from_link__pk', - 'to_link__pk', - 'from_link__to_device__name', - 'to_link__from_device__name', - 'from_link__to_interface__name', - 'to_link__from_interface__name') - - -def topology_data(topology_id): - - data = dict(devices=[], - links=[]) - - topology = Topology.objects.get(pk=topology_id) - - data['name'] = topology.name - data['topology_id'] = topology_id - - links = list(Link.objects - .filter(Q(from_device__topology_id=topology_id) | - Q(to_device__topology_id=topology_id))) - - interfaces = Interface.objects.filter(device__topology_id=topology_id) - - for device in Device.objects.filter(topology_id=topology_id).order_by('name'): - interfaces = list(NetworkAnnotatedInterface.filter(device_id=device.pk).order_by('name')) - interfaces = [dict(name=x['name'], - network=x['from_link__pk'] or x['to_link__pk'], - remote_device_name=x['from_link__to_device__name'] or x['to_link__from_device__name'], - remote_interface_name=x['from_link__to_interface__name'] or x['to_link__from_interface__name'], - id=x['cid'], - ) for x in interfaces] - data['devices'].append(dict(name=device.name, - type=device.device_type, - x=device.x, - y=device.y, - id=device.cid, - interfaces=interfaces)) - - for link in links: - data['links'].append(dict(from_device=link.from_device.name, - to_device=link.to_device.name, - from_interface=link.from_interface.name, - to_interface=link.to_interface.name, - from_device_id=link.from_device.cid, - to_device_id=link.to_device.cid, - from_interface_id=link.from_interface.cid, - to_interface_id=link.to_interface.cid, - name=link.name, - network=link.pk)) - - return data - - -class TopologyForm(forms.Form): - topology_id = forms.IntegerField() - - -def json_topology_data(request): - form = TopologyForm(request.GET) - if form.is_valid(): - response = JsonResponse(topology_data(form.cleaned_data['topology_id']), - content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.json') - return response - else: - return HttpResponseBadRequest(form.errors) - - -def yaml_topology_data(request): - form = TopologyForm(request.GET) - if form.is_valid(): - response = HttpResponse(yaml.safe_dump(topology_data(form.cleaned_data['topology_id']), - default_flow_style=False), - content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.yaml') - return response - else: - return HttpResponseBadRequest(form.errors) - diff --git a/awx/playbooks/clean_isolated.yml b/awx/playbooks/clean_isolated.yml index f904abe5a8..205dd7199e 100644 --- a/awx/playbooks/clean_isolated.yml +++ b/awx/playbooks/clean_isolated.yml @@ -18,7 +18,7 @@ file: path="{{item}}" state=absent register: result with_items: "{{cleanup_dirs}}" - until: result|succeeded + until: result is succeeded ignore_errors: yes retries: 3 delay: 5 @@ -26,4 +26,4 @@ - name: fail if build artifacts were not cleaned fail: msg: 'Unable to cleanup build artifacts' - when: not result|succeeded + when: not result is succeeded diff --git a/awx/plugins/inventory/openstack.py b/awx/plugins/inventory/openstack.py index 30007e408c..05894c7bf3 100755 --- a/awx/plugins/inventory/openstack.py +++ b/awx/plugins/inventory/openstack.py @@ -81,7 +81,8 @@ def get_groups_from_server(server_vars, namegroup=True): groups.append(cloud) # Create a group on region - groups.append(region) + if region: + groups.append(region) # And one by cloud_region groups.append("%s_%s" % (cloud, region)) diff --git a/awx/plugins/inventory/tower.py b/awx/plugins/inventory/tower.py index ff14f6b731..6f4b7abe9b 100755 --- a/awx/plugins/inventory/tower.py +++ b/awx/plugins/inventory/tower.py @@ -90,7 +90,6 @@ def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, license_ tower_host = "https://{}".format(tower_host) inventory_url = urljoin(tower_host, "/api/v2/inventories/{}/script/?hostvars=1&towervars=1&all=1".format(inventory.replace('/', ''))) config_url = urljoin(tower_host, "/api/v2/config/") - reason = None try: if license_type != "open": config_response = requests.get(config_url, @@ -102,22 +101,25 @@ def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, license_ raise RuntimeError("Tower server licenses must match: source: {} local: {}".format(source_type, license_type)) else: - raise RuntimeError("Failed to validate the license of the remote Tower: {}".format(config_response.data)) + raise RuntimeError("Failed to validate the license of the remote Tower: {}".format(config_response)) response = requests.get(inventory_url, auth=HTTPBasicAuth(tower_user, tower_pass), verify=not ignore_ssl) + if not response.ok: + # If the GET /api/v2/inventories/N/script is not HTTP 200, print the error code + msg = "Connection to remote host failed: {}".format(response) + if response.text: + msg += " with message: {}".format(response.text) + raise RuntimeError(msg) try: - json_response = response.json() + # Attempt to parse JSON + return response.json() except (ValueError, TypeError) as e: - reason = "Failed to parse json from host: {}".format(e) - if response.ok: - return json_response - if not reason: - reason = json_response.get('detail', 'Retrieving Tower Inventory Failed') + # If the JSON parse fails, print the ValueError + raise RuntimeError("Failed to parse json from host: {}".format(e)) except requests.ConnectionError as e: - reason = "Connection to remote host failed: {}".format(e) - raise RuntimeError(reason) + raise RuntimeError("Connection to remote host failed: {}".format(e)) def main(): diff --git a/awx/plugins/library/scan_insights.py b/awx/plugins/library/scan_insights.py index 2e759a28cb..917b81bc86 100755 --- a/awx/plugins/library/scan_insights.py +++ b/awx/plugins/library/scan_insights.py @@ -41,6 +41,8 @@ def get_system_id(filname): pass finally: f.close() + if system_id: + system_id = system_id.strip() return system_id diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 56614bad81..31b6a25293 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -200,13 +200,15 @@ SESSION_COOKIE_SECURE = True # Seconds before sessions expire. # Note: This setting may be overridden by database settings. -SESSION_COOKIE_AGE = 1209600 +SESSION_COOKIE_AGE = 1800 # Maximum number of per-user valid, concurrent sessions. # -1 is unlimited # Note: This setting may be overridden by database settings. SESSIONS_PER_USER = -1 +CSRF_USE_SESSIONS = False + # Disallow sending csrf cookies over insecure connections CSRF_COOKIE_SECURE = True @@ -259,6 +261,7 @@ MIDDLEWARE_CLASSES = ( # NOQA 'awx.sso.middleware.SocialAuthMiddleware', 'crum.CurrentRequestUserMiddleware', 'awx.main.middleware.URLModificationMiddleware', + 'awx.main.middleware.SessionTimeoutMiddleware', ) @@ -286,8 +289,7 @@ INSTALLED_APPS = ( 'awx.api', 'awx.ui', 'awx.sso', - 'solo', - 'awx.network_ui' + 'solo' ) INTERNAL_IPS = ('127.0.0.1',) @@ -348,9 +350,11 @@ AUTHENTICATION_BACKENDS = ( # Django OAuth Toolkit settings OAUTH2_PROVIDER_APPLICATION_MODEL = 'main.OAuth2Application' OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'main.OAuth2AccessToken' +OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken' OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600} +ALLOW_OAUTH2_FOR_EXTERNAL_USERS = False # LDAP server (default to None to skip using LDAP authentication). # Note: This setting may be overridden by database settings. diff --git a/awx/settings/development.py b/awx/settings/development.py index 30cecdaa1d..eadc6ae1f4 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -159,7 +159,9 @@ SERVICE_NAME_DICT = { "nginx": "nginx"} try: - socket.gethostbyname('docker.for.mac.internal') - os.environ['SDB_NOTIFY_HOST'] = 'docker.for.mac.internal' + socket.gethostbyname('docker.for.mac.host.internal') + os.environ['SDB_NOTIFY_HOST'] = 'docker.for.mac.host.internal' except Exception: os.environ['SDB_NOTIFY_HOST'] = os.popen('ip route').read().split(' ')[2] + +WEBSOCKET_ORIGIN_WHITELIST = ['https://localhost:8043', 'https://localhost:3000'] diff --git a/awx/sso/backends.py b/awx/sso/backends.py index d4a2cc2280..cb7d7f0e53 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -18,6 +18,7 @@ from django.core.signals import setting_changed from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend from django_auth_ldap.backend import populate_user +from django.core.exceptions import ImproperlyConfigured # radiusauth from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend @@ -106,7 +107,16 @@ class LDAPBackend(BaseLDAPBackend): return None except User.DoesNotExist: pass + try: + for setting_name, type_ in [ + ('GROUP_SEARCH', 'LDAPSearch'), + ('GROUP_TYPE', 'LDAPGroupType'), + ]: + if getattr(self.settings, setting_name) is None: + raise ImproperlyConfigured( + "{} must be an {} instance.".format(setting_name, type_) + ) return super(LDAPBackend, self).authenticate(username, password) except Exception: logger.exception("Encountered an error authenticating to LDAP") @@ -182,13 +192,13 @@ class RADIUSBackend(BaseRADIUSBackend): Custom Radius backend to verify license status ''' - def authenticate(self, request, username, password): + def authenticate(self, username, password): if not django_settings.RADIUS_SERVER: return None if not feature_enabled('enterprise_auth'): logger.error("Unable to authenticate, license does not support RADIUS authentication") return None - return super(RADIUSBackend, self).authenticate(request, username, password) + return super(RADIUSBackend, self).authenticate(username, password) def get_user(self, user_id): if not django_settings.RADIUS_SERVER: diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 3dbe8d267c..dc9a777782 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -977,7 +977,7 @@ register( 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', field_class=fields.CharField, allow_blank=True, - default='', + required=True, validators=[validate_certificate], label=_('SAML Service Provider Public Certificate'), help_text=_('Create a keypair for Tower to use as a service provider (SP) ' @@ -991,7 +991,7 @@ register( 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', field_class=fields.CharField, allow_blank=True, - default='', + required=True, validators=[validate_private_key], label=_('SAML Service Provider Private Key'), help_text=_('Create a keypair for Tower to use as a service provider (SP) ' diff --git a/awx/sso/fields.py b/awx/sso/fields.py index eec5d396b1..a240e368aa 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -220,6 +220,18 @@ class LDAPDNField(fields.CharField): return None if value == '' else value +class LDAPDNListField(fields.StringListField): + + def __init__(self, **kwargs): + super(LDAPDNListField, self).__init__(**kwargs) + self.validators.append(lambda dn: map(validate_ldap_dn, dn)) + + def run_validation(self, data=empty): + if not isinstance(data, (list, tuple)): + data = [data] + return super(LDAPDNListField, self).run_validation(data) + + class LDAPDNWithUserField(fields.CharField): def __init__(self, **kwargs): @@ -431,7 +443,7 @@ class LDAPUserFlagsField(fields.DictField): 'invalid_flag': _('Invalid user flag: "{invalid_flag}".'), } valid_user_flags = {'is_superuser', 'is_system_auditor'} - child = LDAPDNField() + child = LDAPDNListField() def to_internal_value(self, data): data = super(LDAPUserFlagsField, self).to_internal_value(data) diff --git a/awx/sso/middleware.py b/awx/sso/middleware.py index 1944ff4d0f..015e8fdd09 100644 --- a/awx/sso/middleware.py +++ b/awx/sso/middleware.py @@ -8,12 +8,14 @@ import urllib import six # Django +from django.conf import settings from django.utils.functional import LazyObject from django.shortcuts import redirect # Python Social Auth from social_core.exceptions import SocialAuthBaseException from social_core.utils import social_logger +from social_django import utils from social_django.middleware import SocialAuthExceptionMiddleware @@ -24,6 +26,19 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware): request.session['social_auth_last_backend'] = callback_kwargs['backend'] def process_request(self, request): + if request.path.startswith('/sso'): + # django-social keeps a list of backends in memory that it gathers + # based on the value of settings.AUTHENTICATION_BACKENDS *at import + # time*: + # https://github.com/python-social-auth/social-app-django/blob/c1e2795b00b753d58a81fa6a0261d8dae1d9c73d/social_django/utils.py#L13 + # + # our settings.AUTHENTICATION_BACKENDS can *change* + # dynamically as Tower settings are changed (i.e., if somebody + # configures Github OAuth2 integration), so we need to + # _overwrite_ this in-memory value at the top of every request so + # that we have the latest version + # see: https://github.com/ansible/tower/issues/1979 + utils.BACKENDS = settings.AUTHENTICATION_BACKENDS token_key = request.COOKIES.get('token', '') token_key = urllib.quote(urllib.unquote(token_key).strip('"')) diff --git a/awx/templates/rest_framework/api.html b/awx/templates/rest_framework/api.html index 4c32020f9e..373362a130 100644 --- a/awx/templates/rest_framework/api.html +++ b/awx/templates/rest_framework/api.html @@ -37,7 +37,7 @@
  • Logged in as {{ user }}{% if user.get_full_name %} ({{ user.get_full_name }}){% endif %}
  • Log out
  • {% else %} -
  • Log in
  • +
  • Log in
  • {% endif %}
  • {% trans 'Ansible Tower API Guide' %}
  • {% trans 'Back to Ansible Tower' %}
  • diff --git a/awx/templates/rest_framework/login.html b/awx/templates/rest_framework/login.html index 0f361ecf09..c9c5d64030 100644 --- a/awx/templates/rest_framework/login.html +++ b/awx/templates/rest_framework/login.html @@ -11,7 +11,7 @@ {% csrf_token %} - +
    diff --git a/awx/ui/README.md b/awx/ui/README.md index 9db84cd8a7..8f1d0e7c34 100644 --- a/awx/ui/README.md +++ b/awx/ui/README.md @@ -41,6 +41,28 @@ When using Docker for Mac or native Docker on Linux: $ make ui-docker ``` +If you normally run awx on an external host/server (in this example, `awx.local`), +you'll need to reconfigure the webpack proxy slightly for `make ui-docker` to +work: + +```javascript +/awx/settings/development.py ++ ++CSRF_TRUSTED_ORIGINS = ['awx.local:8043'] + +awx/ui/build/webpack.watch.js +- host: '127.0.0.1', ++ host: '0.0.0.0', ++ disableHostCheck: true, + +/awx/ui/package.json +@@ -7,7 +7,7 @@ + "config": { + ... ++ "django_host": "awx.local" + }, +``` + When using Docker Machine: ``` diff --git a/awx/ui/build/webpack.base.js b/awx/ui/build/webpack.base.js index 633987dde0..e0e0eb383b 100644 --- a/awx/ui/build/webpack.base.js +++ b/awx/ui/build/webpack.base.js @@ -22,7 +22,6 @@ const SRC_PATH = path.join(CLIENT_PATH, 'src'); const STATIC_PATH = path.join(UI_PATH, 'static'); const TEST_PATH = path.join(UI_PATH, 'test'); const THEME_PATH = path.join(LIB_PATH, 'theme'); -const NETWORK_UI_PATH = path.join(SRC_PATH, 'network-ui'); const APP_ENTRY = path.join(SRC_PATH, 'app.js'); const VENDOR_ENTRY = path.join(SRC_PATH, 'vendor.js'); @@ -208,7 +207,6 @@ const base = { '~test': TEST_PATH, '~theme': THEME_PATH, '~ui': UI_PATH, - '~network-ui': NETWORK_UI_PATH, d3$: '~node_modules/d3/d3.min.js', 'codemirror.jsonlint$': '~node_modules/codemirror/addon/lint/json-lint.js', jquery: '~node_modules/jquery/dist/jquery.js', diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index 4450024a30..143058077a 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -51,7 +51,7 @@ const watch = { stats: 'minimal', publicPath: '/static/', host: '127.0.0.1', - https: false, + https: true, port: 3000, clientLogLevel: 'none', proxy: [{ @@ -77,12 +77,6 @@ const watch = { target: TARGET, secure: false, ws: true - }, - { - context: '/network_ui', - target: TARGET, - secure: false, - ws: true }] } }; diff --git a/awx/ui/client/assets/variables.less b/awx/ui/client/assets/variables.less index 40cab8727c..3d8f8af957 100644 --- a/awx/ui/client/assets/variables.less +++ b/awx/ui/client/assets/variables.less @@ -19,6 +19,3 @@ @about-modal-padding-top: initial; @about-modal-margin-top: -40px; @about-modal-margin-left: -20px; - -// Copyright text should be hidden -@copyright-text: 0; diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less index 04be5b31bb..d8685d6639 100644 --- a/awx/ui/client/features/_index.less +++ b/awx/ui/client/features/_index.less @@ -1,2 +1,3 @@ +@import 'portalMode/_index'; @import 'output/_index'; @import 'users/tokens/_index'; diff --git a/awx/ui/client/features/applications/add-applications.controller.js b/awx/ui/client/features/applications/add-applications.controller.js index c848bcd345..4318d29c82 100644 --- a/awx/ui/client/features/applications/add-applications.controller.js +++ b/awx/ui/client/features/applications/add-applications.controller.js @@ -1,4 +1,4 @@ -function AddApplicationsController (models, $state, strings) { +function AddApplicationsController (models, $state, strings, $scope) { const vm = this || {}; const { application, me, organization } = models; @@ -62,12 +62,19 @@ function AddApplicationsController (models, $state, strings) { vm.form.onSaveSuccess = res => { $state.go('applications.edit', { application_id: res.data.id }, { reload: true }); }; + + $scope.$watch('organization', () => { + if ($scope.organization) { + vm.form.organization._idFromModal = $scope.organization; + } + }); } AddApplicationsController.$inject = [ 'resolvedModels', '$state', - 'ApplicationsStrings' + 'ApplicationsStrings', + '$scope' ]; export default AddApplicationsController; diff --git a/awx/ui/client/features/applications/add-edit-applications.view.html b/awx/ui/client/features/applications/add-edit-applications.view.html index a7f2a87580..8593e537dc 100644 --- a/awx/ui/client/features/applications/add-edit-applications.view.html +++ b/awx/ui/client/features/applications/add-edit-applications.view.html @@ -1,7 +1,6 @@ +
    - - {{ vm.panelTitle }} - + {{:: vm.strings.get('tab.DETAILS') }} @@ -9,14 +8,14 @@ - - - - + + + + - - - + + + diff --git a/awx/ui/client/features/applications/applications.strings.js b/awx/ui/client/features/applications/applications.strings.js index 8dfcf521a2..ab4f8ea253 100644 --- a/awx/ui/client/features/applications/applications.strings.js +++ b/awx/ui/client/features/applications/applications.strings.js @@ -25,6 +25,7 @@ function ApplicationsStrings (BaseString) { }; ns.list = { + PANEL_TITLE: t.s('APPLICATIONS'), ROW_ITEM_LABEL_EXPIRED: t.s('EXPIRATION'), ROW_ITEM_LABEL_ORGANIZATION: t.s('ORG'), ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED') diff --git a/awx/ui/client/features/applications/edit-applications.controller.js b/awx/ui/client/features/applications/edit-applications.controller.js index 1bf6b8c91b..cf53a1abc3 100644 --- a/awx/ui/client/features/applications/edit-applications.controller.js +++ b/awx/ui/client/features/applications/edit-applications.controller.js @@ -43,6 +43,12 @@ function EditApplicationsController (models, $state, strings, $scope) { } }); + $scope.$watch('organization', () => { + if ($scope.organization) { + vm.form.organization._idFromModal = $scope.organization; + } + }); + if (isEditable) { vm.form = application.createFormSchema('put', { omit }); } else { diff --git a/awx/ui/client/features/applications/index.js b/awx/ui/client/features/applications/index.js index af4783884f..f0c37e9f7c 100644 --- a/awx/ui/client/features/applications/index.js +++ b/awx/ui/client/features/applications/index.js @@ -12,7 +12,15 @@ const listTemplate = require('~features/applications/list-applications.view.html const indexTemplate = require('~features/applications/index.view.html'); const userListTemplate = require('~features/applications/list-applications-users.view.html'); -function ApplicationsDetailResolve ($q, $stateParams, Me, Application, Organization) { +function ApplicationsDetailResolve ( + $q, + $stateParams, + Me, + Application, + Organization, + ProcessErrors, + strings +) { const id = $stateParams.application_id; const promises = { @@ -42,6 +50,13 @@ function ApplicationsDetailResolve ($q, $stateParams, Me, Application, Organizat return models; }); + }) + .catch(({ data, status, config }) => { + ProcessErrors(null, data, status, null, { + hdr: strings.get('error.HEADER'), + msg: strings.get('error.CALL', { path: `${config.url}`, status }) + }); + return $q.reject(); }); } @@ -50,7 +65,9 @@ ApplicationsDetailResolve.$inject = [ '$stateParams', 'MeModel', 'ApplicationModel', - 'OrganizationModel' + 'OrganizationModel', + 'ProcessErrors', + 'ApplicationsStrings' ]; function ApplicationsRun ($stateExtender, strings) { diff --git a/awx/ui/client/features/applications/list-applications-users.controller.js b/awx/ui/client/features/applications/list-applications-users.controller.js index 1c6856498b..7271fbc5da 100644 --- a/awx/ui/client/features/applications/list-applications-users.controller.js +++ b/awx/ui/client/features/applications/list-applications-users.controller.js @@ -8,7 +8,9 @@ function ListApplicationsUsersController ( $scope, Dataset, resolvedModels, - strings + strings, + $stateParams, + GetBasePath ) { const vm = this || {}; // const application = resolvedModels; @@ -21,9 +23,15 @@ function ListApplicationsUsersController ( $scope.list = { iterator, name, basePath: 'applications' }; $scope.collection = { iterator }; + $scope.tokenBasePath = `${GetBasePath('applications')}${$stateParams.application_id}/tokens`; $scope[key] = Dataset.data; vm.usersCount = Dataset.data.count; $scope[name] = Dataset.data.results; + $scope.$on('updateDataset', (e, dataset) => { + $scope[key] = dataset; + $scope[name] = dataset.results; + vm.usersCount = dataset.count; + }); vm.getLastUsed = user => { const lastUsed = _.get(user, 'last_used'); @@ -49,7 +57,9 @@ ListApplicationsUsersController.$inject = [ '$scope', 'Dataset', 'resolvedModels', - 'ApplicationsStrings' + 'ApplicationsStrings', + '$stateParams', + 'GetBasePath' ]; export default ListApplicationsUsersController; diff --git a/awx/ui/client/features/applications/list-applications-users.view.html b/awx/ui/client/features/applications/list-applications-users.view.html index 3acb66242d..23ed467d8f 100644 --- a/awx/ui/client/features/applications/list-applications-users.view.html +++ b/awx/ui/client/features/applications/list-applications-users.view.html @@ -1,8 +1,8 @@ -
    +
    - - - - + value="{{ user.expires | longDate }}">
    diff --git a/awx/ui/client/features/applications/list-applications.view.html b/awx/ui/client/features/applications/list-applications.view.html index c1ab24bd75..14b9d8c7b2 100644 --- a/awx/ui/client/features/applications/list-applications.view.html +++ b/awx/ui/client/features/applications/list-applications.view.html @@ -1,9 +1,8 @@ - - APPLICATIONS - - {{ vm.applicationsCount }} - + @@ -18,7 +17,7 @@ collection="collection" search-tags="searchTags"> -
    +
    - - - - +
    diff --git a/awx/ui/client/features/credentials/add-credentials.controller.js b/awx/ui/client/features/credentials/add-credentials.controller.js index d0bb961763..27b3eaee4a 100644 --- a/awx/ui/client/features/credentials/add-credentials.controller.js +++ b/awx/ui/client/features/credentials/add-credentials.controller.js @@ -16,6 +16,8 @@ function AddCredentialsController (models, $state, $scope, strings, componentsSt omit: ['user', 'team', 'inputs'] }); + vm.form._formName = 'credential'; + vm.form.disabled = !credential.isCreatable(); vm.form.organization._resource = 'organization'; @@ -62,6 +64,9 @@ function AddCredentialsController (models, $state, $scope, strings, componentsSt delete data.inputs[gceFileInputSchema.id]; } + const filteredInputs = _.omit(data.inputs, (value) => value === ''); + data.inputs = filteredInputs; + return credential.request('post', { data }); }; @@ -112,6 +117,18 @@ function AddCredentialsController (models, $state, $scope, strings, componentsSt return { obj, error }; }; + + $scope.$watch('organization', () => { + if ($scope.organization) { + vm.form.organization._idFromModal = $scope.organization; + } + }); + + $scope.$watch('credential_type', () => { + if ($scope.credential_type) { + vm.form.credential_type._idFromModal = $scope.credential_type; + } + }); } AddCredentialsController.$inject = [ diff --git a/awx/ui/client/features/credentials/add-edit-credentials.view.html b/awx/ui/client/features/credentials/add-edit-credentials.view.html index 182eadf004..dcc9d47e8b 100644 --- a/awx/ui/client/features/credentials/add-edit-credentials.view.html +++ b/awx/ui/client/features/credentials/add-edit-credentials.view.html @@ -1,5 +1,7 @@ +
    +
    - {{ vm.panelTitle }} + {{:: vm.strings.get('tab.DETAILS') }} @@ -7,16 +9,16 @@ - - - - + + + + - + - + {{:: vm.strings.get('inputs.GROUP_TITLE') }} @@ -29,9 +31,9 @@ - {{:: vm.strings.get('permissions.TITLE') }} + - + {{:: vm.strings.get('tab.DETAILS') }} {{:: vm.strings.get('tab.PERMISSIONS') }} diff --git a/awx/ui/client/features/credentials/edit-credentials.controller.js b/awx/ui/client/features/credentials/edit-credentials.controller.js index 3066034f7b..560b2605a6 100644 --- a/awx/ui/client/features/credentials/edit-credentials.controller.js +++ b/awx/ui/client/features/credentials/edit-credentials.controller.js @@ -32,6 +32,18 @@ function EditCredentialsController (models, $state, $scope, strings, componentsS } }); + $scope.$watch('organization', () => { + if ($scope.organization) { + vm.form.organization._idFromModal = $scope.organization; + } + }); + + $scope.$watch('credential_type', () => { + if ($scope.credential_type) { + vm.form.credential_type._idFromModal = $scope.credential_type; + } + }); + // Only exists for permissions compatibility $scope.credential_obj = credential.get(); @@ -113,6 +125,9 @@ function EditCredentialsController (models, $state, $scope, strings, componentsS delete data.inputs[gceFileInputSchema.id]; } + const filteredInputs = _.omit(data.inputs, (value) => value === ''); + data.inputs = filteredInputs; + return credential.request('put', { data }); }; diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index e3a1ea7734..38e291ef8e 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -7,7 +7,16 @@ const MODULE_NAME = 'at.features.credentials'; const addEditTemplate = require('~features/credentials/add-edit-credentials.view.html'); -function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) { +function CredentialsResolve ( + $q, + $stateParams, + Me, + Credential, + CredentialType, + Organization, + ProcessErrors, + strings +) { const id = $stateParams.credential_id; const promises = { @@ -43,6 +52,12 @@ function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, O return models; }); + }).catch(({ data, status, config }) => { + ProcessErrors(null, data, status, null, { + hdr: strings.get('error.HEADER'), + msg: strings.get('error.CALL', { path: `${config.url}`, status }) + }); + return $q.reject(); }); } @@ -52,7 +67,9 @@ CredentialsResolve.$inject = [ 'MeModel', 'CredentialModel', 'CredentialTypeModel', - 'OrganizationModel' + 'OrganizationModel', + 'ProcessErrors', + 'CredentialsStrings' ]; function CredentialsRun ($stateExtender, legacy, strings) { diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index 0a6ec3864c..f206a206bc 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -8,6 +8,7 @@ import atFeaturesOutput from '~features/output'; import atFeaturesTemplates from '~features/templates'; import atFeaturesUsers from '~features/users'; import atFeaturesJobs from '~features/jobs'; +import atFeaturesPortalMode from '~features/portalMode'; const MODULE_NAME = 'at.features'; @@ -21,7 +22,8 @@ angular.module(MODULE_NAME, [ atFeaturesUsers, atFeaturesJobs, atFeaturesOutput, - atFeaturesTemplates + atFeaturesTemplates, + atFeaturesPortalMode, ]); export default MODULE_NAME; diff --git a/awx/ui/client/features/jobs/index.controller.js b/awx/ui/client/features/jobs/index.controller.js new file mode 100644 index 0000000000..f0ab4e2315 --- /dev/null +++ b/awx/ui/client/features/jobs/index.controller.js @@ -0,0 +1,19 @@ +function IndexJobsController ($scope, strings, dataset) { + const vm = this; + vm.strings = strings; + vm.count = dataset.data.count; + + $scope.$on('updateCount', (e, count) => { + if (typeof count === 'number') { + vm.count = count; + } + }); +} + +IndexJobsController.$inject = [ + '$scope', + 'JobsStrings', + 'Dataset' +]; + +export default IndexJobsController; diff --git a/awx/ui/client/features/jobs/index.view.html b/awx/ui/client/features/jobs/index.view.html index 2328e24261..a1fa58df9e 100644 --- a/awx/ui/client/features/jobs/index.view.html +++ b/awx/ui/client/features/jobs/index.view.html @@ -2,18 +2,11 @@
    -
    - - JOBS +
    +
    -
    - - SCHEDULES - -
    -
    diff --git a/awx/ui/client/features/jobs/jobs.strings.js b/awx/ui/client/features/jobs/jobs.strings.js index a902cc4109..d888d293cf 100644 --- a/awx/ui/client/features/jobs/jobs.strings.js +++ b/awx/ui/client/features/jobs/jobs.strings.js @@ -5,6 +5,7 @@ function JobsStrings (BaseString) { const ns = this.jobs; ns.list = { + PANEL_TITLE: t.s('JOBS'), ROW_ITEM_LABEL_STARTED: t.s('Started'), ROW_ITEM_LABEL_FINISHED: t.s('Finished'), ROW_ITEM_LABEL_WORKFLOW_JOB: t.s('Workflow Job'), @@ -13,7 +14,9 @@ function JobsStrings (BaseString) { ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'), ROW_ITEM_LABEL_PROJECT: t.s('Project'), ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'), - NO_RUNNING: t.s('There are no running jobs.') + NO_RUNNING: t.s('There are no running jobs.'), + JOB: t.s('Job'), + STATUS_TOOLTIP: status => t.s('Job {{status}}. Click for details.', { status }) }; } diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js index 77be97e72d..4833fdda9a 100644 --- a/awx/ui/client/features/jobs/jobsList.controller.js +++ b/awx/ui/client/features/jobs/jobsList.controller.js @@ -16,7 +16,8 @@ function ListJobsController ( $filter, ProcessErrors, Wait, - Rest + Rest, + SearchBasePath ) { const vm = this || {}; const [unifiedJob] = resolvedModels; @@ -26,19 +27,21 @@ function ListJobsController ( // smart-search const name = 'jobs'; const iterator = 'job'; - const key = 'job_dataset'; let launchModalOpen = false; let refreshAfterLaunchClose = false; - $scope.list = { iterator, name }; - $scope.collection = { iterator, basePath: 'unified_jobs' }; - $scope[key] = Dataset.data; - $scope[name] = Dataset.data.results; - $scope.$on('updateDataset', (e, dataset) => { - $scope[key] = dataset; - $scope[name] = dataset.results; + vm.searchBasePath = SearchBasePath; + + vm.list = { iterator, name }; + vm.job_dataset = Dataset.data; + vm.jobs = Dataset.data.results; + vm.querySet = $state.params.job_search; + + $scope.$watch('vm.job_dataset.count', () => { + $scope.$emit('updateCount', vm.job_dataset.count, 'jobs'); }); + $scope.$on('ws-jobs', () => { if (!launchModalOpen) { refreshJobs(); @@ -60,8 +63,19 @@ function ListJobsController ( vm.emptyListReason = strings.get('list.NO_RUNNING'); } + vm.isPortalMode = $state.includes('portalMode'); + vm.jobTypes = mapChoices(unifiedJob.options('actions.GET.type.choices')); + vm.buildCredentialTags = (credentials) => + credentials.map(credential => { + const icon = `${credential.kind}`; + const link = `/#/credentials/${credential.id}`; + const value = $filter('sanitize')(credential.name); + + return { icon, link, value }; + }); + vm.getSref = ({ type, id }) => { let sref; @@ -101,13 +115,12 @@ function ListJobsController ( .then(() => { let reloadListStateParams = null; - if ($scope.jobs.length === 1 && $state.params.job_search && - !_.isEmpty($state.params.job_search.page) && + if (vm.jobs.length === 1 && $state.params.job_search && + _.has($state, 'params.job_search.page') && $state.params.job_search.page !== '1') { - const page = `${(parseInt(reloadListStateParams - .job_search.page, 10) - 1)}`; reloadListStateParams = _.cloneDeep($state.params); - reloadListStateParams.job_search.page = page; + reloadListStateParams.job_search.page = + (parseInt(reloadListStateParams.job_search.page, 10) - 1).toString(); } $state.go('.', reloadListStateParams, { reload: true }); @@ -143,7 +156,7 @@ function ListJobsController ( .then(() => { let reloadListStateParams = null; - if ($scope.jobs.length === 1 && $state.params.job_search && + if (vm.jobs.length === 1 && $state.params.job_search && !_.isEmpty($state.params.job_search.page) && $state.params.job_search.page !== '1') { const page = `${(parseInt(reloadListStateParams @@ -178,9 +191,10 @@ function ListJobsController ( }; function refreshJobs () { - qs.search(unifiedJob.path, $state.params.job_search) + qs.search(SearchBasePath, $state.params.job_search) .then(({ data }) => { - $scope.$emit('updateDataset', data); + vm.jobs = data.results; + vm.job_dataset = data; }); } } @@ -196,7 +210,8 @@ ListJobsController.$inject = [ '$filter', 'ProcessErrors', 'Wait', - 'Rest' + 'Rest', + 'SearchBasePath' ]; export default ListJobsController; diff --git a/awx/ui/client/features/jobs/jobsList.view.html b/awx/ui/client/features/jobs/jobsList.view.html index e550cd879f..580a35d8a7 100644 --- a/awx/ui/client/features/jobs/jobsList.view.html +++ b/awx/ui/client/features/jobs/jobsList.view.html @@ -3,24 +3,25 @@ + query-set="vm.querySet" + search-bar-full-width="vm.isPortalMode">
    - + - +
    @@ -54,7 +55,7 @@ + value-link="/#/inventories/{{job.summary_fields.inventory.kind === 'smart' ? 'smart' : 'inventory'}}/{{ job.summary_fields.inventory.id }}"> + @@ -74,13 +76,18 @@ ng-show="job.summary_fields.user_capabilities.start"> + job.status === 'running')) || ($root.user_is_superuser && job.type === 'system_job' && + (job.status === 'pending' || + job.status === 'waiting' || + job.status === 'running'))"> @@ -89,10 +96,10 @@ + base-path="{{vm.searchBasePath}}" + query-set="vm.querySet"> diff --git a/awx/ui/client/features/jobs/routes/hostCompletedJobs.route.js b/awx/ui/client/features/jobs/routes/hostCompletedJobs.route.js new file mode 100644 index 0000000000..20d1a504ba --- /dev/null +++ b/awx/ui/client/features/jobs/routes/hostCompletedJobs.route.js @@ -0,0 +1,62 @@ +import { N_ } from '../../../src/i18n'; +import jobsListController from '../jobsList.controller'; + +const jobsListTemplate = require('~features/jobs/jobsList.view.html'); + +export default { + url: '/completed_jobs', + params: { + job_search: { + value: { + page_size: '20', + job__hosts: '', + order_by: '-id' + }, + dynamic: true, + squash: '' + } + }, + ncyBreadcrumb: { + label: N_('COMPLETED JOBS') + }, + views: { + related: { + templateUrl: jobsListTemplate, + controller: jobsListController, + controllerAs: 'vm' + } + }, + resolve: { + resolvedModels: [ + 'UnifiedJobModel', + (UnifiedJob) => { + const models = [ + new UnifiedJob(['options']), + ]; + return Promise.all(models); + }, + ], + Dataset: [ + '$stateParams', + 'Wait', + 'GetBasePath', + 'QuerySet', + ($stateParams, Wait, GetBasePath, qs) => { + const hostId = $stateParams.host_id; + + const searchParam = _.assign($stateParams + .job_search, { job__hosts: hostId }); + + const searchPath = GetBasePath('unified_jobs'); + + Wait('start'); + return qs.search(searchPath, searchParam) + .finally(() => Wait('stop')); + } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') + ] + } +}; diff --git a/awx/ui/client/features/jobs/routes/instanceGroupJobs.route.js b/awx/ui/client/features/jobs/routes/instanceGroupJobs.route.js index 7bfda756bc..c52ad0cd67 100644 --- a/awx/ui/client/features/jobs/routes/instanceGroupJobs.route.js +++ b/awx/ui/client/features/jobs/routes/instanceGroupJobs.route.js @@ -16,8 +16,7 @@ export default { job_search: { value: { page_size: '10', - order_by: '-id', - status: 'running' + order_by: '-finished' }, dynamic: true } @@ -60,6 +59,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + '$stateParams', + ($stateParams) => `api/v2/instance_groups/${$stateParams.instance_group_id}/jobs` ] } }; diff --git a/awx/ui/client/features/jobs/routes/instanceJobs.route.js b/awx/ui/client/features/jobs/routes/instanceJobs.route.js index 06019bcf22..b88bbb4cd8 100644 --- a/awx/ui/client/features/jobs/routes/instanceJobs.route.js +++ b/awx/ui/client/features/jobs/routes/instanceJobs.route.js @@ -9,7 +9,7 @@ export default { name: 'instanceGroups.instanceJobs', url: '/:instance_group_id/instances/:instance_id/jobs', ncyBreadcrumb: { - parent: 'instanceGroups.edit', + parent: 'instanceGroups.instances', label: N_('JOBS') }, views: { @@ -59,6 +59,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + '$stateParams', + ($stateParams) => `api/v2/instances/${$stateParams.instance_id}/jobs` ] } }; diff --git a/awx/ui/client/features/jobs/routes/inventoryCompletedJobs.route.js b/awx/ui/client/features/jobs/routes/inventoryCompletedJobs.route.js index 333359c6f6..8d4de65623 100644 --- a/awx/ui/client/features/jobs/routes/inventoryCompletedJobs.route.js +++ b/awx/ui/client/features/jobs/routes/inventoryCompletedJobs.route.js @@ -58,6 +58,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') ] } }; diff --git a/awx/ui/client/features/jobs/routes/jobs.route.js b/awx/ui/client/features/jobs/routes/jobs.route.js index 9129fed3d1..52e6456bd7 100644 --- a/awx/ui/client/features/jobs/routes/jobs.route.js +++ b/awx/ui/client/features/jobs/routes/jobs.route.js @@ -1,5 +1,6 @@ import { N_ } from '../../../src/i18n'; import jobsListController from '../jobsList.controller'; +import indexController from '../index.controller'; const indexTemplate = require('~features/jobs/index.view.html'); const jobsListTemplate = require('~features/jobs/jobsList.view.html'); @@ -55,10 +56,16 @@ export default { .finally(() => Wait('stop')); } ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') + ] }, views: { '@': { - templateUrl: indexTemplate + templateUrl: indexTemplate, + controller: indexController, + controllerAs: 'vm' }, 'jobsList@jobs': { templateUrl: jobsListTemplate, diff --git a/awx/ui/client/features/jobs/routes/templateCompletedJobs.route.js b/awx/ui/client/features/jobs/routes/templateCompletedJobs.route.js index 3fc69a5ffe..20a84507be 100644 --- a/awx/ui/client/features/jobs/routes/templateCompletedJobs.route.js +++ b/awx/ui/client/features/jobs/routes/templateCompletedJobs.route.js @@ -55,6 +55,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') ] } }; diff --git a/awx/ui/client/features/jobs/routes/workflowJobTemplateCompletedJobs.route.js b/awx/ui/client/features/jobs/routes/workflowJobTemplateCompletedJobs.route.js index 8970ef8bc0..bd05b2b1eb 100644 --- a/awx/ui/client/features/jobs/routes/workflowJobTemplateCompletedJobs.route.js +++ b/awx/ui/client/features/jobs/routes/workflowJobTemplateCompletedJobs.route.js @@ -54,6 +54,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') ] } }; diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index cb8e2940fd..bd2c18c14d 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -13,21 +13,6 @@ } } - &-menuBottom { - color: @at-gray-848992; - font-size: 10px; - text-transform: uppercase; - font-weight: bold; - position: absolute; - right: 60px; - bottom: 24px; - cursor: pointer; - - &:hover { - color: @at-blue; - } - } - &-menuIconGroup { & > p { margin: 0; @@ -74,23 +59,34 @@ color: @at-blue; } + &-row { + display: flex; + + &:hover div { + background-color: white; + } + } + &-toggle { - color: @at-gray-848992; background-color: @at-gray-eb; + color: @at-gray-848992; + display: flex; + flex: 0 0 30px; font-size: 18px; + justify-content: center; line-height: 12px; & > i { cursor: pointer; } - padding: 0 10px 0 10px; user-select: none; } &-line { color: @at-gray-161b1f; background-color: @at-gray-eb; + flex: 0 0 45px; text-align: right; vertical-align: top; padding-right: 5px; @@ -122,28 +118,36 @@ } } - &-container { - font-family: monospace; + &-wrapper { + display: flex; + flex-flow: column nowrap; height: 100%; - overflow-y: scroll; - font-size: 15px; - border: 1px solid @at-gray-b7; + } + + &-container { background-color: @at-gray-f2; + border-radius: 0 0 4px 4px; + border: 1px solid @at-gray-b7; color: @at-gray-161b1f; - padding: 0; + display: flex; + flex-direction: column; + flex: 1; + font-family: monospace; + font-size: 15px; + height: 100%; margin: 0; - border-radius: 0; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - max-height: ~"calc(100vh - 350px)"; + overflow-y: scroll; + padding: 0; + } - & > table { - table-layout: fixed; + &-borderHeader { + .at-mixin-stdoutBorder(); + height: 10px; + } - tr:hover > td { - background: white; - } - } + &-borderFooter { + .at-mixin-stdoutBorder(); + flex: 1; } &--fullscreen { @@ -153,10 +157,17 @@ } .at-mixin-event() { + flex: 1; padding: 0 10px; - word-wrap: break-word; white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; +} +.at-mixin-stdoutBorder() { + background-color: @at-gray-eb; + border-right: 1px solid @at-gray-b7; + width: 75px; } // Search --------------------------------------------------------------------------------- @@ -178,9 +189,12 @@ padding: 6px @at-padding-input 6px @at-padding-input; } +.jobz-searchClearAllContainer { + .at-mixin-VerticallyCenter(); +} + .jobz-searchClearAll { font-size: 10px; - padding-bottom: @at-space; } .jobz-Button-searchKey { @@ -206,6 +220,7 @@ display: flex; width: 100%; flex-wrap: wrap; + margin-left: -5px; } // Status Bar ----------------------------------------------------------------------------- @@ -284,7 +299,7 @@ background-color: @default-warning; } -.HostStatusBar-tooltipBadge--failed { +.HostStatusBar-tooltipBadge--failures { background-color: @default-err; } @@ -362,6 +377,8 @@ } .JobResults-resultRowText { + display: flex; + flex-flow: row wrap; width: ~"calc(70% - 20px)"; flex: 1 0 auto; text-transform: none; @@ -422,4 +439,4 @@ display: flex; flex-direction: column; } -} \ No newline at end of file +} diff --git a/awx/ui/client/features/output/api.events.service.js b/awx/ui/client/features/output/api.events.service.js index 39499eff46..9da4f34ba8 100644 --- a/awx/ui/client/features/output/api.events.service.js +++ b/awx/ui/client/features/output/api.events.service.js @@ -1,132 +1,137 @@ -const PAGE_LIMIT = 5; -const PAGE_SIZE = 50; +import { + API_MAX_PAGE_SIZE, + OUTPUT_ORDER_BY, + OUTPUT_PAGE_SIZE, +} from './constants'; const BASE_PARAMS = { - order_by: 'start_line', - page_size: PAGE_SIZE, + page_size: OUTPUT_PAGE_SIZE, + order_by: OUTPUT_ORDER_BY, }; const merge = (...objs) => _.merge({}, ...objs); -const getInitialState = params => ({ - results: [], - count: 0, - previous: 1, - page: 1, - next: 1, - last: 1, - params: merge(BASE_PARAMS, params), -}); - function JobEventsApiService ($http, $q) { this.init = (endpoint, params) => { - this.keys = []; - this.cache = {}; - this.pageSizes = {}; this.endpoint = endpoint; - this.state = getInitialState(params); + this.params = merge(BASE_PARAMS, params); + + this.state = { count: 0, maxCounter: 0 }; + this.cache = {}; }; - this.getLastPage = count => Math.ceil(count / this.state.params.page_size); + this.fetch = () => this.getLast() + .then(results => { + this.cache.last = results; - this.fetch = () => { - delete this.cache; - delete this.keys; - delete this.pageSizes; + return this; + }); - this.cache = {}; - this.keys = []; - this.pageSizes = {}; + this.clearCache = () => { + Object.keys(this.cache).forEach(key => { + delete this.cache[key]; + }); + }; - return this.getPage(1).then(() => this); + this.pushMaxCounter = events => { + const maxCounter = Math.max(...events.map(({ counter }) => counter)); + + if (maxCounter > this.state.maxCounter) { + this.state.maxCounter = maxCounter; + } + + return maxCounter; + }; + + this.getFirst = () => { + const params = merge(this.params, { page: 1 }); + + return $http.get(this.endpoint, { params }) + .then(({ data }) => { + const { results, count } = data; + + this.state.count = count; + this.pushMaxCounter(results); + + return results; + }); }; this.getPage = number => { - if (number < 1 || number > this.state.last) { - return $q.resolve(); + if (number < 1 || number > this.getLastPageNumber()) { + return $q.resolve([]); } - if (this.cache[number]) { - if (this.pageSizes[number] === PAGE_SIZE) { - return this.cache[number]; - } + const params = merge(this.params, { page: number }); - delete this.pageSizes[number]; - delete this.cache[number]; - - this.keys.splice(this.keys.indexOf(number)); - } - - const { params } = this.state; - - delete params.page; - - params.page = number; - - const promise = $http.get(this.endpoint, { params }) + return $http.get(this.endpoint, { params }) .then(({ data }) => { const { results, count } = data; - this.state.results = results; this.state.count = count; - this.state.page = number; - this.state.last = this.getLastPage(count); - this.state.previous = Math.max(1, number - 1); - this.state.next = Math.min(this.state.last, number + 1); + this.pushMaxCounter(results); - this.pageSizes[number] = results.length; - - return { results, page: number }; + return results; }); + }; - this.cache[number] = promise; - this.keys.push(number); - - if (this.keys.length > PAGE_LIMIT) { - delete this.cache[this.keys.shift()]; + this.getLast = () => { + if (this.cache.last) { + return $q.resolve(this.cache.last); } - return promise; - }; + const params = merge(this.params, { page: 1, order_by: `-${OUTPUT_ORDER_BY}` }); - this.first = () => this.getPage(1); - this.next = () => this.getPage(this.state.next); - this.previous = () => this.getPage(this.state.previous); - - this.last = () => { - const params = merge({}, this.state.params); - - delete params.page; - delete params.order_by; - - params.page = 1; - params.order_by = '-start_line'; - - const promise = $http.get(this.endpoint, { params }) + return $http.get(this.endpoint, { params }) .then(({ data }) => { const { results, count } = data; - const lastPage = this.getLastPage(count); - results.reverse(); - const shifted = results.splice(count % PAGE_SIZE); + let rotated = results; + + if (count > OUTPUT_PAGE_SIZE) { + rotated = results.splice(count % OUTPUT_PAGE_SIZE); + + if (results.length > 0) { + rotated = results; + } + } - this.state.results = shifted; this.state.count = count; - this.state.page = lastPage; - this.state.next = lastPage; - this.state.last = lastPage; - this.state.previous = Math.max(1, this.state.page - 1); + this.pushMaxCounter(results); - return { results: shifted, page: lastPage }; + return rotated; }); - - return promise; }; + + this.getRange = range => { + if (!range) { + return $q.resolve([]); + } + + const [low, high] = range; + + if (low > high) { + return $q.resolve([]); + } + + const params = merge(this.params, { counter__gte: [low], counter__lte: [high] }); + + params.page_size = API_MAX_PAGE_SIZE; + + return $http.get(this.endpoint, { params }) + .then(({ data }) => { + const { results } = data; + + this.pushMaxCounter(results); + + return results; + }); + }; + + this.getLastPageNumber = () => Math.ceil(this.state.count / OUTPUT_PAGE_SIZE); + this.getMaxCounter = () => this.state.maxCounter; } -JobEventsApiService.$inject = [ - '$http', - '$q' -]; +JobEventsApiService.$inject = ['$http', '$q']; export default JobEventsApiService; diff --git a/awx/ui/client/features/output/constants.js b/awx/ui/client/features/output/constants.js new file mode 100644 index 0000000000..4bab4ca1bb --- /dev/null +++ b/awx/ui/client/features/output/constants.js @@ -0,0 +1,30 @@ +export const API_MAX_PAGE_SIZE = 200; +export const API_ROOT = '/api/v2/'; + +export const EVENT_START_TASK = 'playbook_on_task_start'; +export const EVENT_START_PLAY = 'playbook_on_play_start'; +export const EVENT_START_PLAYBOOK = 'playbook_on_start'; +export const EVENT_STATS_PLAY = 'playbook_on_stats'; + +export const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped']; + +export const JOB_STATUS_COMPLETE = ['successful', 'failed', 'unknown']; +export const JOB_STATUS_INCOMPLETE = ['canceled', 'error']; +export const JOB_STATUS_UNSUCCESSFUL = ['failed'].concat(JOB_STATUS_INCOMPLETE); +export const JOB_STATUS_FINISHED = JOB_STATUS_COMPLETE.concat(JOB_STATUS_INCOMPLETE); + +export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container'; +export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable'; +export const OUTPUT_MAX_LAG = 120; +export const OUTPUT_ORDER_BY = 'counter'; +export const OUTPUT_PAGE_CACHE = true; +export const OUTPUT_PAGE_LIMIT = 5; +export const OUTPUT_PAGE_SIZE = 50; +export const OUTPUT_SCROLL_DELAY = 100; +export const OUTPUT_SCROLL_THRESHOLD = 0.1; +export const OUTPUT_SEARCH_DOCLINK = 'https://docs.ansible.com/ansible-tower/3.3.0/html/userguide/search_sort.html'; +export const OUTPUT_SEARCH_FIELDS = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; +export const OUTPUT_SEARCH_KEY_EXAMPLES = ['host_name:localhost', 'task:set', 'created:>=2000-01-01']; +export const OUTPUT_EVENT_LIMIT = OUTPUT_PAGE_LIMIT * OUTPUT_PAGE_SIZE; + +export const WS_PREFIX = 'ws'; diff --git a/awx/ui/client/features/output/details.component.js b/awx/ui/client/features/output/details.component.js index 84a97ed050..b1cc4734cd 100644 --- a/awx/ui/client/features/output/details.component.js +++ b/awx/ui/client/features/output/details.component.js @@ -26,25 +26,32 @@ function getStatusDetails (jobStatus) { } const choices = mapChoices(resource.model.options('actions.GET.status.choices')); + const label = strings.get('labels.STATUS'); - const label = 'Status'; - const icon = `fa icon-job-${unmapped}`; - const value = choices[unmapped]; + let icon; + let value; + + if (unmapped === 'unknown') { + icon = 'fa icon-job-pending'; + value = strings.get('details.UNKNOWN'); + } else { + icon = `fa icon-job-${unmapped}`; + value = choices[unmapped]; + } return { label, icon, value }; } function getStartDetails (started) { const unfiltered = started || resource.model.get('started'); - - const label = 'Started'; + const label = strings.get('labels.STARTED'); let value; if (unfiltered) { value = $filter('longDate')(unfiltered); } else { - value = 'Not Started'; + value = strings.get('details.NOT_STARTED'); } return { label, value }; @@ -52,15 +59,14 @@ function getStartDetails (started) { function getFinishDetails (finished) { const unfiltered = finished || resource.model.get('finished'); - - const label = 'Finished'; + const label = strings.get('labels.FINISHED'); let value; if (unfiltered) { value = $filter('longDate')(unfiltered); } else { - value = 'Not Finished'; + value = strings.get('details.NOT_FINISHED'); } return { label, value }; @@ -68,7 +74,7 @@ function getFinishDetails (finished) { function getModuleArgDetails () { const value = resource.model.get('module_args'); - const label = 'Module Args'; + const label = strings.get('labels.MODULE_ARGS'); if (!value) { return null; @@ -86,7 +92,7 @@ function getJobTypeDetails () { const choices = mapChoices(resource.model.options('actions.GET.job_type.choices')); - const label = 'Job Type'; + const label = strings.get('labels.JOB_TYPE'); const value = choices[unmapped]; return { label, value }; @@ -101,7 +107,7 @@ function getVerbosityDetails () { const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices')); - const label = 'Verbosity'; + const label = strings.get('labels.VERBOSITY'); const value = choices[verbosity]; return { label, value }; @@ -115,7 +121,7 @@ function getSourceWorkflowJobDetails () { } const link = `/#/workflows/${sourceWorkflowJob.id}`; - const tooltip = strings.get('resourceTooltips.SOURCE_WORKFLOW_JOB'); + const tooltip = strings.get('tooltips.SOURCE_WORKFLOW_JOB'); return { link, tooltip }; } @@ -127,59 +133,14 @@ function getJobTemplateDetails () { return null; } - const label = 'Job Template'; + const label = strings.get('labels.JOB_TEMPLATE'); const link = `/#/templates/job_template/${jobTemplate.id}`; const value = $filter('sanitize')(jobTemplate.name); - const tooltip = strings.get('resourceTooltips.JOB_TEMPLATE'); + const tooltip = strings.get('tooltips.JOB_TEMPLATE'); return { label, link, value, tooltip }; } -function getInventoryJobNameDetails () { - if (resource.model.get('type') !== 'inventory_update') { - return null; - } - - const jobArgs = resource.model.get('job_args'); - - if (!jobArgs) { - return null; - } - - let parsedJobArgs; - - try { - parsedJobArgs = JSON.parse(jobArgs); - } catch (e) { - return null; - } - - if (!Array.isArray(parsedJobArgs)) { - return null; - } - - const jobArgIndex = parsedJobArgs.indexOf('--inventory-id'); - const inventoryId = parsedJobArgs[jobArgIndex + 1]; - - if (jobArgIndex < 0) { - return null; - } - - if (!Number.isInteger(parseInt(inventoryId, 10))) { - return null; - } - - const name = resource.model.get('name'); - const id = resource.model.get('id'); - - const label = 'Name'; - const tooltip = strings.get('resourceTooltips.INVENTORY'); - const value = `${id} - ${$filter('sanitize')(name)}`; - const link = `/#/inventories/inventory/${inventoryId}`; - - return { label, link, tooltip, value }; -} - function getInventorySourceDetails () { if (!resource.model.has('summary_fields.inventory_source.source')) { return null; @@ -188,7 +149,7 @@ function getInventorySourceDetails () { const { source } = resource.model.get('summary_fields.inventory_source'); const choices = mapChoices(resource.model.options('actions.GET.source.choices')); - const label = 'Source'; + const label = strings.get('labels.SOURCE'); const value = choices[source]; return { label, value }; @@ -199,7 +160,7 @@ function getOverwriteDetails () { return null; } - const label = 'Overwrite'; + const label = strings.get('labels.OVERWRITE'); const value = resource.model.get('overwrite'); return { label, value }; @@ -210,7 +171,7 @@ function getOverwriteVarsDetails () { return null; } - const label = 'Overwrite Vars'; + const label = strings.get('labels.OVERWRITE_VARS'); const value = resource.model.get('overwrite_vars'); return { label, value }; @@ -221,7 +182,7 @@ function getLicenseErrorDetails () { return null; } - const label = 'License Error'; + const label = strings.get('labels.LICENSE_ERROR'); const value = resource.model.get('license_error'); return { label, value }; @@ -230,7 +191,6 @@ function getLicenseErrorDetails () { function getLaunchedByDetails () { const createdBy = resource.model.get('summary_fields.created_by'); const jobTemplate = resource.model.get('summary_fields.job_template'); - const relatedSchedule = resource.model.get('related.schedule'); const schedule = resource.model.get('summary_fields.schedule'); @@ -238,18 +198,18 @@ function getLaunchedByDetails () { return null; } - const label = 'Launched By'; + const label = strings.get('labels.LAUNCHED_BY'); let link; let tooltip; let value; if (createdBy) { - tooltip = strings.get('resourceTooltips.USER'); + tooltip = strings.get('tooltips.USER'); link = `/#/users/${createdBy.id}`; value = $filter('sanitize')(createdBy.username); } else if (relatedSchedule && jobTemplate) { - tooltip = strings.get('resourceTooltips.SCHEDULE'); + tooltip = strings.get('tooltips.SCHEDULE'); link = `/#/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`; value = $filter('sanitize')(schedule.name); } else { @@ -268,8 +228,8 @@ function getInventoryDetails () { return null; } - const label = 'Inventory'; - const tooltip = strings.get('resourceTooltips.INVENTORY'); + const label = strings.get('labels.INVENTORY'); + const tooltip = strings.get('tooltips.INVENTORY'); const value = $filter('sanitize')(inventory.name); let link; @@ -290,10 +250,10 @@ function getProjectDetails () { return null; } - const label = 'Project'; + const label = strings.get('labels.PROJECT'); const link = `/#/projects/${project.id}`; - const value = $filter('sanitize')(project.name); - const tooltip = strings.get('resourceTooltips.PROJECT'); + const value = project.name; + const tooltip = strings.get('tooltips.PROJECT'); return { label, link, value, tooltip }; } @@ -318,13 +278,13 @@ function getProjectUpdateDetails (updateId) { } const link = `/#/jobs/project/${jobId}`; - const tooltip = strings.get('resourceTooltips.PROJECT_UPDATE'); + const tooltip = strings.get('tooltips.PROJECT_UPDATE'); return { link, tooltip }; } function getSCMRevisionDetails () { - const label = 'Revision'; + const label = strings.get('labels.SCM_REVISION'); const value = resource.model.get('scm_revision'); if (!value) { @@ -335,7 +295,7 @@ function getSCMRevisionDetails () { } function getPlaybookDetails () { - const label = 'Playbook'; + const label = strings.get('labels.PLAYBOOK'); const value = resource.model.get('playbook'); if (!value) { @@ -353,7 +313,7 @@ function getJobExplanationDetails () { } const limit = 150; - const label = 'Explanation'; + const label = strings.get('labels.JOB_EXPLANATION'); let more = explanation; @@ -380,7 +340,7 @@ function getResultTracebackDetails () { } const limit = 150; - const label = 'Error Details'; + const label = strings.get('labels.RESULT_TRACEBACK'); const more = traceback; const less = $filter('limitTo')(more, limit); @@ -392,31 +352,41 @@ function getResultTracebackDetails () { } function getCredentialDetails () { - const credential = resource.model.get('summary_fields.credential'); + let credentials = []; + let credentialTags = []; - if (!credential) { + if (resource.model.get('type') === 'job') { + credentials = resource.model.get('summary_fields.credentials'); + } else { + const credential = resource.model.get('summary_fields.credential'); + if (credential) { + credentials.push(credential); + } + } + + if (!credentials || credentials.length < 1) { return null; } - let label = 'Credential'; + credentialTags = credentials.map((cred) => buildCredentialDetails(cred)); - if (resource.type === 'playbook') { - label = 'Machine Credential'; - } + const label = strings.get('labels.CREDENTIAL'); + const value = credentialTags; - if (resource.type === 'inventory') { - label = 'Source Credential'; - } + return { label, value }; +} +function buildCredentialDetails (credential) { + const icon = `${credential.kind}`; const link = `/#/credentials/${credential.id}`; - const tooltip = strings.get('resourceTooltips.CREDENTIAL'); + const tooltip = strings.get('tooltips.CREDENTIAL'); const value = $filter('sanitize')(credential.name); - return { label, link, tooltip, value }; + return { icon, link, tooltip, value }; } function getForkDetails () { - const label = 'Forks'; + const label = strings.get('labels.FORKS'); const value = resource.model.get('forks'); if (!value) { @@ -427,7 +397,7 @@ function getForkDetails () { } function getLimitDetails () { - const label = 'Limit'; + const label = strings.get('labels.LIMIT'); const value = resource.model.get('limit'); if (!value) { @@ -444,16 +414,17 @@ function getInstanceGroupDetails () { return null; } - const label = 'Instance Group'; + const label = strings.get('labels.INSTANCE_GROUP'); const value = $filter('sanitize')(instanceGroup.name); + const link = `/#/instance_groups/${instanceGroup.id}`; let isolated = null; if (instanceGroup.controller_id) { - isolated = 'Isolated'; + isolated = strings.get('details.ISOLATED'); } - return { label, value, isolated }; + return { label, value, isolated, link }; } function getJobTagDetails () { @@ -471,7 +442,7 @@ function getJobTagDetails () { return null; } - const label = 'Job Tags'; + const label = strings.get('labels.JOB_TAGS'); const more = false; const value = jobTags.map($filter('sanitize')); @@ -494,8 +465,8 @@ function getSkipTagDetails () { return null; } - const label = 'Skip Tags'; const more = false; + const label = strings.get('labels.SKIP_TAGS'); const value = skipTags.map($filter('sanitize')); return { label, more, value }; @@ -508,8 +479,8 @@ function getExtraVarsDetails () { return null; } - const label = 'Extra Variables'; - const tooltip = 'Read-only view of extra variables added to the job template.'; + const label = strings.get('labels.EXTRA_VARS'); + const tooltip = strings.get('tooltips.EXTRA_VARS'); const value = parse(extraVars); const disabled = true; @@ -517,18 +488,20 @@ function getExtraVarsDetails () { } function getLabelDetails () { - const jobLabels = _.get(resource.model.get('related.labels'), 'results', []); + const jobLabels = _.get(resource.model.get('summary_fields.labels'), 'results', []); if (jobLabels.length < 1) { return null; } - const label = 'Labels'; + const label = strings.get('labels.LABELS'); const more = false; - const value = jobLabels.map(({ name }) => name).map($filter('sanitize')); + const truncate = true; + const truncateLength = 5; + const hasMoreToShow = jobLabels.length > truncateLength; - return { label, more, value }; + return { label, more, hasMoreToShow, value, truncate, truncateLength }; } function createErrorHandler (path, action) { @@ -546,6 +519,32 @@ const ELEMENT_SKIP_TAGS = '#job-results-skip-tags'; const ELEMENT_PROMPT_MODAL = '#prompt-modal'; const TAGS_SLIDE_DISTANCE = 200; +function showLabels () { + this.labels.truncate = !this.labels.truncate; + + const jobLabelsCount = _.get(resource.model.get('summary_fields.labels'), 'count'); + const maxCount = 50; + + if (this.labels.value.length === jobLabelsCount || this.labels.value.length >= maxCount) { + return; + } + + const config = { + params: { + page_size: maxCount + } + }; + + wait('start'); + resource.model.extend('get', 'labels', config) + .then((model) => { + const jobLabels = _.get(model.get('related.labels'), 'results', []); + this.labels.value = jobLabels.map(({ name }) => name).map($filter('sanitize')); + }) + .catch(createErrorHandler('get labels', 'GET')) + .finally(() => wait('stop')); +} + function toggleLabels () { if (!this.labels.more) { $(ELEMENT_LABELS).slideUp(TAGS_SLIDE_DISTANCE); @@ -663,6 +662,7 @@ function JobDetailsController ( vm.$onInit = () => { resource = this.resource; // eslint-disable-line prefer-destructuring + vm.strings = strings; vm.status = getStatusDetails(); vm.started = getStartDetails(); @@ -681,7 +681,7 @@ function JobDetailsController ( vm.launchedBy = getLaunchedByDetails(); vm.jobExplanation = getJobExplanationDetails(); vm.verbosity = getVerbosityDetails(); - vm.credential = getCredentialDetails(); + vm.credentials = getCredentialDetails(); vm.forks = getForkDetails(); vm.limit = getLimitDetails(); vm.instanceGroup = getInstanceGroupDetails(); @@ -689,7 +689,6 @@ function JobDetailsController ( vm.skipTags = getSkipTagDetails(); vm.extraVars = getExtraVarsDetails(); vm.labels = getLabelDetails(); - vm.inventoryJobName = getInventoryJobNameDetails(); vm.inventorySource = getInventorySourceDetails(); vm.overwrite = getOverwriteDetails(); vm.overwriteVars = getOverwriteVarsDetails(); @@ -704,6 +703,7 @@ function JobDetailsController ( vm.toggleJobTags = toggleJobTags; vm.toggleSkipTags = toggleSkipTags; vm.toggleLabels = toggleLabels; + vm.showLabels = showLabels; unsubscribe = subscribe(({ status, started, finished, scm }) => { vm.started = getStartDetails(started); @@ -726,10 +726,10 @@ JobDetailsController.$inject = [ '$state', 'ProcessErrors', 'Prompt', - 'JobStrings', + 'OutputStrings', 'Wait', 'ParseVariableString', - 'JobStatusService', + 'OutputStatusService', ]; export default { diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index 1bc8acc7ee..f681059f90 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -1,6 +1,7 @@ +
    -
    DETAILS
    +
    {{:: vm.strings.get('details.HEADER')}}
    @@ -14,7 +15,7 @@ ng-show="vm.status.value === 'Pending' || vm.status.value === 'Waiting' || vm.status.value === 'Running'" - aw-tool-tip="{{'Cancel' | translate }}" + aw-tool-tip="{{:: vm.strings.get('tooltips.CANCEL') }}" data-original-title="" title=""> @@ -31,7 +32,7 @@ vm.status.value === 'Failed' || vm.status.value === 'Error' || vm.status.value === 'Canceled')" - aw-tool-tip="{{ 'Delete' | translate }}" + aw-tool-tip="{{:: vm.strings.get('tooltips.DELETE') }}" data-original-title="" title=""> @@ -40,24 +41,12 @@
    - -
    - - -
    -
    - - {{ vm.status.value }} + + {{ vm.status.value }}
    @@ -71,7 +60,7 @@ - Show More + {{:: vm.strings.get('details.SHOW_MORE') }}
    - Show Less + {{:: vm.strings.get('details.SHOW_LESS') }}
    @@ -124,7 +113,7 @@ - Show More + {{:: vm.strings.get('details.SHOW_MORE') }}
    - Show Less + {{:: vm.strings.get('details.SHOW_LESS') }}
    @@ -216,15 +205,18 @@
    -
    - +
    +
    - - {{ vm.credential.value }} - + data-tip-watch="credential.tooltip"> +
    @@ -274,7 +266,9 @@
    - {{ vm.instanceGroup.value }} + + {{ vm.instanceGroup.value }} + {{ vm.instanceGroup.isolated }} @@ -298,20 +292,37 @@ ng-show="vm.labels.more" href="" ng-click="vm.toggleLabels()"> - {{ vm.labels.label }} + {{ vm.labels.label }} - {{ vm.labels.label }} + {{ vm.labels.label }}
    -
    -
    -
    {{ label }}
    +
    +
    +
    +
    {{ label }}
    +
    + + {{:: vm.strings.get('details.SHOW_MORE') }} + +
    +
    +
    +
    {{ label }}
    +
    + + {{:: vm.strings.get('details.SHOW_LESS') }} +
    @@ -323,14 +334,14 @@ ng-show="vm.jobTags.more" href="" ng-click="vm.toggleJobTags()"> - {{ vm.jobTags.label }} + {{ vm.jobTags.label }} - {{ vm.jobTags.label }} + {{ vm.jobTags.label }}
    @@ -348,14 +359,14 @@ ng-show="vm.skipTags.more" href="" ng-click="vm.toggleSkipTags()"> - {{ vm.skipTags.label }} + {{ vm.skipTags.label }} - {{ vm.skipTags.label }} + {{ vm.skipTags.label }}
    diff --git a/awx/ui/client/features/output/engine.service.js b/awx/ui/client/features/output/engine.service.js deleted file mode 100644 index 2e6371520f..0000000000 --- a/awx/ui/client/features/output/engine.service.js +++ /dev/null @@ -1,228 +0,0 @@ -const JOB_END = 'playbook_on_stats'; -const MAX_LAG = 120; - -function JobEventEngine ($q) { - this.init = ({ resource, scroll, page, onEventFrame, onStart, onStop }) => { - this.resource = resource; - this.scroll = scroll; - this.page = page; - - this.lag = 0; - this.count = 0; - this.pageCount = 0; - this.chain = $q.resolve(); - this.factors = this.getBatchFactors(this.resource.page.size); - - this.state = { - started: false, - paused: false, - pausing: false, - resuming: false, - ending: false, - ended: false, - counting: false, - }; - - this.hooks = { - onEventFrame, - onStart, - onStop, - }; - - this.lines = { - used: [], - missing: [], - ready: false, - min: 0, - max: 0 - }; - }; - - this.setMinLine = min => { - if (min > this.lines.min) { - this.lines.min = min; - } - }; - - this.getBatchFactors = size => { - const factors = [1]; - - for (let i = 2; i <= size / 2; i++) { - if (size % i === 0) { - factors.push(i); - } - } - - factors.push(size); - - return factors; - }; - - this.getBatchFactorIndex = () => { - const index = Math.floor((this.lag / MAX_LAG) * this.factors.length); - - return index > this.factors.length - 1 ? this.factors.length - 1 : index; - }; - - this.setBatchFrameCount = () => { - const index = this.getBatchFactorIndex(); - - this.framesPerRender = this.factors[index]; - }; - - this.buffer = data => { - const pageAdded = this.page.addToBuffer(data); - - this.pageCount++; - - if (pageAdded) { - this.setBatchFrameCount(); - - if (this.isPausing()) { - this.pause(true); - } else if (this.isResuming()) { - this.resume(true); - } - } - }; - - this.checkLines = data => { - for (let i = data.start_line; i < data.end_line; i++) { - if (i > this.lines.max) { - this.lines.max = i; - } - - this.lines.used.push(i); - } - - const missing = []; - for (let i = this.lines.min; i < this.lines.max; i++) { - if (this.lines.used.indexOf(i) === -1) { - missing.push(i); - } - } - - if (missing.length === 0) { - this.lines.ready = true; - this.lines.min = this.lines.max + 1; - this.lines.used = []; - } else { - this.lines.ready = false; - } - }; - - this.pushJobEvent = data => { - this.lag++; - - this.chain = this.chain - .then(() => { - if (!this.isActive()) { - this.start(); - } else if (data.event === JOB_END) { - if (this.isPaused()) { - this.end(true); - } else { - this.end(); - } - } - - this.checkLines(data); - this.buffer(data); - this.count++; - - if (!this.isReadyToRender()) { - return $q.resolve(); - } - - const events = this.page.emptyBuffer(); - this.count -= events.length; - - return this.renderFrame(events); - }) - .then(() => --this.lag); - - return this.chain; - }; - - this.renderFrame = events => this.hooks.onEventFrame(events) - .then(() => { - if (this.isEnding()) { - const lastEvents = this.page.emptyBuffer(); - - if (lastEvents.length) { - return this.renderFrame(lastEvents); - } - - this.end(true); - } - - return $q.resolve(); - }); - - this.resume = done => { - if (done) { - this.state.resuming = false; - this.state.paused = false; - } else if (!this.isTransitioning()) { - this.scroll.pause(); - this.scroll.lock(); - this.scroll.scrollToBottom(); - this.state.resuming = true; - this.page.removeBookmark(); - } - }; - - this.pause = done => { - if (done) { - this.state.pausing = false; - this.state.paused = true; - this.scroll.resume(); - } else if (!this.isTransitioning()) { - this.scroll.pause(); - this.scroll.unlock(); - this.state.pausing = true; - this.page.setBookmark(); - } - }; - - this.start = () => { - if (!this.state.ending && !this.state.ended) { - this.state.started = true; - this.scroll.pause(); - this.scroll.lock(); - - this.hooks.onStart(); - } - }; - - this.end = done => { - if (done) { - this.state.ending = false; - this.state.ended = true; - this.scroll.unlock(); - this.scroll.resume(); - - this.hooks.onStop(); - - return; - } - - this.state.ending = true; - }; - - this.isReadyToRender = () => this.isDone() || - (!this.isPaused() && this.hasAllLines() && this.isBatchFull()); - this.hasAllLines = () => this.lines.ready; - this.isBatchFull = () => this.count % this.framesPerRender === 0; - this.isPaused = () => this.state.paused; - this.isPausing = () => this.state.pausing; - this.isResuming = () => this.state.resuming; - this.isTransitioning = () => this.isActive() && (this.state.pausing || this.state.resuming); - this.isActive = () => this.state.started && !this.state.ended; - this.isEnding = () => this.state.ending; - this.isDone = () => this.state.ended; -} - -JobEventEngine.$inject = ['$q']; - -export default JobEventEngine; diff --git a/awx/ui/client/features/output/host-event/_index.less b/awx/ui/client/features/output/host-event/_index.less index bec8548cd2..f93f80a56d 100644 --- a/awx/ui/client/features/output/host-event/_index.less +++ b/awx/ui/client/features/output/host-event/_index.less @@ -170,10 +170,13 @@ } .HostEvent-stdoutColumn{ - white-space: pre; - overflow-y: scroll; + overflow-y: hidden; + overflow-x: auto; margin-left: 46px; padding-top: 4px; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; } .HostEvent-noJson{ diff --git a/awx/ui/client/features/output/host-event/host-event-modal.partial.html b/awx/ui/client/features/output/host-event/host-event-modal.partial.html index 47676df842..3dcc12eb6e 100644 --- a/awx/ui/client/features/output/host-event/host-event-modal.partial.html +++ b/awx/ui/client/features/output/host-event/host-event-modal.partial.html @@ -16,23 +16,23 @@
    - CREATED + {{strings.get('host_event_modal.CREATED')}} {{(event.created | longDate) || "No result found"}}
    - ID + {{strings.get('host_event_modal.ID')}} {{event.id || "No result found"}}
    - PLAY + {{strings.get('host_event_modal.PLAY')}} {{event.play || "No result found"}}
    - TASK + {{strings.get('host_event_modal.TASK')}} {{event.task || "No result found"}}
    - MODULE + {{strings.get('host_event_modal.MODULE')}} {{module_name}}
    @@ -48,12 +48,12 @@
    @@ -64,7 +64,7 @@
    - +
    diff --git a/awx/ui/client/features/output/host-event/host-event.controller.js b/awx/ui/client/features/output/host-event/host-event.controller.js index 280bf51818..87f698db9d 100644 --- a/awx/ui/client/features/output/host-event/host-event.controller.js +++ b/awx/ui/client/features/output/host-event/host-event.controller.js @@ -1,14 +1,19 @@ function HostEventsController ( $scope, $state, + $filter, HostEventService, - hostEvent + hostEvent, + OutputStrings ) { $scope.processEventStatus = HostEventService.processEventStatus; $scope.processResults = processResults; $scope.isActiveState = isActiveState; $scope.getActiveHostIndex = getActiveHostIndex; $scope.closeHostEvent = closeHostEvent; + $scope.strings = OutputStrings; + + const sanitize = $filter('sanitize'); function init () { hostEvent.event_name = hostEvent.event; @@ -30,7 +35,7 @@ function HostEventsController ( if (hostEvent.event_data.res.stdout === '') { $scope.stdout = ' '; } else { - $scope.stdout = hostEvent.event_data.res.stdout; + $scope.stdout = sanitize(hostEvent.event_data.res.stdout); } } @@ -38,7 +43,7 @@ function HostEventsController ( if (hostEvent.event_data.res.stderr === '') { $scope.stderr = ' '; } else { - $scope.stderr = hostEvent.event_data.res.stderr; + $scope.stderr = sanitize(hostEvent.event_data.res.stderr); } } @@ -48,13 +53,13 @@ function HostEventsController ( if ($scope.module_name === 'debug' && _.has(hostEvent.event_data, 'res.result.stdout')) { - $scope.stdout = hostEvent.event_data.res.result.stdout; + $scope.stdout = sanitize(hostEvent.event_data.res.result.stdout); } if ($scope.module_name === 'yum' && _.has(hostEvent.event_data, 'res.results') && _.isArray(hostEvent.event_data.res.results)) { const event = hostEvent.event_data.res.results; - $scope.stdout = event[0];// eslint-disable-line prefer-destructuring + $scope.stdout = sanitize(event[0]);// eslint-disable-line prefer-destructuring } // instantiate Codemirror if ($state.current.name === 'output.host-event.json') { @@ -163,8 +168,10 @@ function HostEventsController ( HostEventsController.$inject = [ '$scope', '$state', + '$filter', 'HostEventService', 'hostEvent', + 'OutputStrings' ]; module.exports = HostEventsController; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 935d0a7c79..88d86ca91b 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,363 +1,535 @@ +/* eslint camelcase: 0 */ +import { + EVENT_START_PLAY, + EVENT_START_TASK, + OUTPUT_PAGE_SIZE, +} from './constants'; + let $compile; let $q; let $scope; -let page; -let render; +let $state; + let resource; +let render; let scroll; -let engine; let status; +let slide; +let stream; let vm; -let streaming; -let listeners = []; -function JobsIndexController ( - _resource_, - _page_, - _scroll_, - _render_, - _engine_, - _$scope_, - _$compile_, - _$q_, - _status_, -) { - vm = this || {}; +const bufferState = [0, 0]; // [length, count] +const listeners = []; +const rx = []; - $compile = _$compile_; - $scope = _$scope_; - $q = _$q_; - resource = _resource_; +function bufferInit () { + rx.length = 0; - page = _page_; - scroll = _scroll_; - render = _render_; - engine = _engine_; - status = _status_; - - // Development helper(s) - vm.clear = devClear; - - // Expand/collapse - vm.expanded = false; - vm.toggleExpanded = toggleExpanded; - - // Panel - vm.resource = resource; - vm.title = resource.model.get('name'); - - // Stdout Navigation - vm.scroll = { - showBackToTop: false, - home: scrollHome, - end: scrollEnd, - down: scrollPageDown, - up: scrollPageUp - }; - - render.requestAnimationFrame(() => init()); + bufferState[0] = 0; + bufferState[1] = 0; } -function init () { - status.init({ - resource, - }); +function bufferAdd (event) { + rx.push(event); - page.init({ - resource, - }); + bufferState[0] += 1; + bufferState[1] += 1; - render.init({ - compile: html => $compile(html)($scope), - isStreamActive: engine.isActive, - }); + return bufferState[1]; +} - scroll.init({ - isAtRest: scrollIsAtRest, - previous, - next, - }); +function bufferEmpty (min, max) { + let count = 0; + let removed = []; - engine.init({ - page, - scroll, - resource, - onEventFrame (events) { - return shift().then(() => append(events, true)); - }, - onStart () { - status.setJobStatus('running'); - }, - onStop () { - stopListening(); - status.updateStats(); - status.dispatch(); + for (let i = bufferState[0] - 1; i >= 0; i--) { + if (rx[i].counter <= max) { + removed = removed.concat(rx.splice(i, 1)); + count++; } - }); + } - streaming = false; - return next().then(() => startListening()); + bufferState[0] -= count; + + return removed; } +let lockFrames; +function onFrames (events) { + if (lockFrames) { + events.forEach(bufferAdd); + return $q.resolve(); + } + + events = slide.pushFrames(events); + const popCount = events.length - slide.getCapacity(); + const isAttached = events.length > 0; + + if (!isAttached) { + stopFollowing(); + return $q.resolve(); + } + + if (!vm.isFollowing && canStartFollowing()) { + startFollowing(); + } + + if (!vm.isFollowing && popCount > 0) { + return $q.resolve(); + } + + scroll.pause(); + + if (vm.isFollowing) { + scroll.scrollToBottom(); + } + + return slide.popBack(popCount) + .then(() => { + if (vm.isFollowing) { + scroll.scrollToBottom(); + } + + return slide.pushFront(events); + }) + .then(() => { + if (vm.isFollowing) { + scroll.scrollToBottom(); + } + + scroll.resume(); + + return $q.resolve(); + }); +} + +function first () { + if (scroll.isPaused()) { + return $q.resolve(); + } + + scroll.pause(); + lockFrames = true; + + stopFollowing(); + + return slide.getFirst() + .then(() => { + scroll.resetScrollPosition(); + }) + .finally(() => { + scroll.resume(); + lockFrames = false; + }); +} + +function next () { + if (vm.isFollowing) { + scroll.scrollToBottom(); + + return $q.resolve(); + } + + if (scroll.isPaused()) { + return $q.resolve(); + } + + if (slide.getTailCounter() >= slide.getMaxCounter()) { + return $q.resolve(); + } + + scroll.pause(); + lockFrames = true; + + return slide.getNext() + .finally(() => { + scroll.resume(); + lockFrames = false; + }); +} + +function previous () { + if (scroll.isPaused()) { + return $q.resolve(); + } + + scroll.pause(); + lockFrames = true; + + stopFollowing(); + + const initialPosition = scroll.getScrollPosition(); + + return slide.getPrevious() + .then(popHeight => { + const currentHeight = scroll.getScrollHeight(); + scroll.setScrollPosition(currentHeight - popHeight + initialPosition); + + return $q.resolve(); + }) + .finally(() => { + scroll.resume(); + lockFrames = false; + }); +} + +function last () { + if (scroll.isPaused()) { + return $q.resolve(); + } + + scroll.pause(); + lockFrames = true; + + return slide.getLast() + .then(() => { + stream.setMissingCounterThreshold(slide.getTailCounter() + 1); + scroll.scrollToBottom(); + + return $q.resolve(); + }) + .finally(() => { + scroll.resume(); + lockFrames = false; + }); +} + +let followOnce; +let lockFollow; +function canStartFollowing () { + if (lockFollow) { + return false; + } + + if (slide.isOnLastPage() && scroll.isBeyondLowerThreshold()) { + followOnce = false; + + return true; + } + + if (followOnce && // one-time activation from top of first page + scroll.isBeyondUpperThreshold() && + slide.getHeadCounter() === 1 && + slide.getTailCounter() >= OUTPUT_PAGE_SIZE) { + followOnce = false; + + return true; + } + + return false; +} + +function startFollowing () { + if (vm.isFollowing) { + return; + } + + vm.isFollowing = true; + vm.followTooltip = vm.strings.get('tooltips.MENU_FOLLOWING'); +} + +function stopFollowing () { + if (!vm.isFollowing) { + return; + } + + vm.isFollowing = false; + vm.followTooltip = vm.strings.get('tooltips.MENU_LAST'); +} + +function menuLast () { + if (vm.isFollowing) { + lockFollow = true; + stopFollowing(); + + return $q.resolve(); + } + + lockFollow = false; + + if (slide.isOnLastPage()) { + scroll.scrollToBottom(); + + return $q.resolve(); + } + + return last(); +} + +function down () { + scroll.moveDown(); +} + +function up () { + scroll.moveUp(); +} + +function togglePanelExpand () { + vm.isPanelExpanded = !vm.isPanelExpanded; +} + +function toggleMenuExpand () { + if (scroll.isPaused()) return; + + const recordList = Object.keys(render.record).map(key => render.record[key]); + const playRecords = recordList.filter(({ name }) => name === EVENT_START_PLAY); + const playIds = playRecords.map(({ uuid }) => uuid); + + // get any task record that does not have a parent play record + const orphanTaskRecords = recordList + .filter(({ name }) => name === EVENT_START_TASK) + .filter(({ parents }) => !parents.some(uuid => playIds.indexOf(uuid) >= 0)); + + const toggled = playRecords.concat(orphanTaskRecords) + .map(({ uuid }) => getToggleElements(uuid)) + .filter(({ icon }) => icon.length > 0) + .map(({ icon, lines }) => setExpanded(icon, lines, !vm.isMenuExpanded)); + + if (toggled.length > 0) { + vm.isMenuExpanded = !vm.isMenuExpanded; + } +} + +function toggleLineExpand (uuid) { + if (scroll.isPaused()) return; + + const { icon, lines } = getToggleElements(uuid); + const isExpanded = icon.hasClass('fa-angle-down'); + + setExpanded(icon, lines, !isExpanded); + + vm.isMenuExpanded = !isExpanded; +} + +function getToggleElements (uuid) { + const record = render.record[uuid]; + const lines = $(`.child-of-${uuid}`); + + const iconSelector = '.at-Stdout-toggle > i'; + const additionalSelector = `#${(record.children || []).join(', #')}`; + + let icon = $(`#${uuid} ${iconSelector}`); + if (additionalSelector) { + icon = icon.add($(additionalSelector).find(iconSelector)); + } + + return { icon, lines }; +} + +function setExpanded (icon, lines, expanded) { + if (expanded) { + icon.removeClass('fa-angle-right'); + icon.addClass('fa-angle-down'); + lines.removeClass('hidden'); + } else { + icon.removeClass('fa-angle-down'); + icon.addClass('fa-angle-right'); + lines.addClass('hidden'); + } +} + +function compile (html) { + return $compile(html)($scope); +} + +function showHostDetails (id, uuid) { + $state.go('output.host-event.json', { eventId: id, taskUuid: uuid }); +} + +let streaming; function stopListening () { + streaming = null; + listeners.forEach(deregister => deregister()); - listeners = []; + listeners.length = 0; } function startListening () { stopListening(); + listeners.push($scope.$on(resource.ws.events, (scope, data) => handleJobEvent(data))); listeners.push($scope.$on(resource.ws.status, (scope, data) => handleStatusEvent(data))); + + if (resource.model.get('type') === 'job') return; + if (resource.model.get('type') === 'project_update') return; + + listeners.push($scope.$on(resource.ws.summary, (scope, data) => handleSummaryEvent(data))); +} + +function handleJobEvent (data) { + streaming = streaming || resource.events + .getRange([Math.max(1, data.counter - 50), data.counter + 50]) + .then(results => { + results = results.concat(data); + + const counters = results.map(({ counter }) => counter); + const min = Math.min(...counters); + const max = Math.max(...counters); + + const missing = []; + for (let i = min; i <= max; i++) { + if (counters.indexOf(i) < 0) { + missing.push(i); + } + } + + if (missing.length > 0) { + const maxMissing = Math.max(...missing); + results = results.filter(({ counter }) => counter > maxMissing); + } + + stream.setMissingCounterThreshold(max + 1); + results.forEach(item => { + stream.pushJobEvent(item); + status.pushJobEvent(item); + }); + + return $q.resolve(); + }); + + streaming + .then(() => { + stream.pushJobEvent(data); + status.pushJobEvent(data); + }); } function handleStatusEvent (data) { status.pushStatusEvent(data); } -function handleJobEvent (data) { - streaming = streaming || attachToRunningJob(); - streaming.then(() => { - engine.pushJobEvent(data); - status.pushJobEvent(data); +function handleSummaryEvent (data) { + if (resource.model.get('id') !== data.unified_job_id) return; + if (!data.final_counter) return; + + stream.setFinalCounter(data.final_counter); +} + +function reloadState (params) { + params.isPanelExpanded = vm.isPanelExpanded; + + return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' }); +} + +function OutputIndexController ( + _$compile_, + _$q_, + _$scope_, + _$state_, + _resource_, + _scroll_, + _page_, + _render_, + _status_, + _slide_, + _stream_, + $filter, + strings, + $stateParams, +) { + const { isPanelExpanded } = $stateParams; + + $compile = _$compile_; + $q = _$q_; + $scope = _$scope_; + $state = _$state_; + + resource = _resource_; + scroll = _scroll_; + render = _render_; + status = _status_; + stream = _stream_; + slide = resource.model.get('event_processing_finished') ? _page_ : _slide_; + + vm = this || {}; + + // Panel + vm.title = $filter('sanitize')(resource.model.get('name')); + vm.status = resource.model.get('status'); + vm.strings = strings; + vm.resource = resource; + vm.reloadState = reloadState; + vm.isPanelExpanded = isPanelExpanded; + vm.togglePanelExpand = togglePanelExpand; + + // Stdout Navigation + vm.menu = { last: menuLast, first, down, up }; + vm.isMenuExpanded = true; + vm.isFollowing = false; + vm.toggleMenuExpand = toggleMenuExpand; + vm.toggleLineExpand = toggleLineExpand; + vm.showHostDetails = showHostDetails; + vm.toggleLineEnabled = resource.model.get('type') === 'job'; + vm.followTooltip = vm.strings.get('tooltips.MENU_LAST'); + + render.requestAnimationFrame(() => { + bufferInit(); + + status.init(resource); + slide.init(render, resource.events, scroll); + render.init({ compile, toggles: vm.toggleLineEnabled }); + + scroll.init({ + next, + previous, + onThresholdLeave () { + followOnce = false; + lockFollow = false; + stopFollowing(); + + return $q.resolve(); + }, + }); + + stream.init({ + bufferAdd, + bufferEmpty, + onFrames, + onStop () { + lockFollow = true; + stopFollowing(); + stopListening(); + status.updateStats(); + status.dispatch(); + status.sync(); + scroll.stop(); + } + }); + + if (resource.model.get('event_processing_finished')) { + followOnce = false; + lockFollow = true; + lockFrames = true; + stopListening(); + } else { + followOnce = true; + lockFollow = false; + lockFrames = false; + resource.events.clearCache(); + status.subscribe(data => { vm.status = data.status; }); + startListening(); + } + + return last(); }); } -function attachToRunningJob () { - if (!status.state.running) { - return $q.resolve(); - } - - return page.last() - .then(events => { - if (!events) { - return $q.resolve(); - } - - const minLine = 1 + Math.max(...events.map(event => event.end_line)); - - return render.clear() - .then(() => engine.setMinLine(minLine)); - }); -} - -function next () { - return page.next() - .then(events => { - if (!events) { - return $q.resolve(); - } - - return shift() - .then(() => append(events)) - .then(() => { - if (scroll.isMissing()) { - return next(); - } - - return $q.resolve(); - }); - }); -} - -function previous () { - const initialPosition = scroll.getScrollPosition(); - let postPopHeight; - - return page.previous() - .then(events => { - if (!events) { - return $q.resolve(); - } - - return pop() - .then(() => { - postPopHeight = scroll.getScrollHeight(); - - return prepend(events); - }) - .then(() => { - const currentHeight = scroll.getScrollHeight(); - scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition); - }); - }); -} - -function append (events, eng) { - return render.append(events) - .then(count => { - page.updateLineCount(count, eng); - }); -} - -function prepend (events) { - return render.prepend(events) - .then(count => { - page.updateLineCount(count); - }); -} - -function pop () { - if (!page.isOverCapacity()) { - return $q.resolve(); - } - - const lines = page.trim(); - - return render.pop(lines); -} - -function shift () { - if (!page.isOverCapacity()) { - return $q.resolve(); - } - - const lines = page.trim(true); - - return render.shift(lines); -} - -function scrollHome () { - if (scroll.isPaused()) { - return $q.resolve(); - } - - scroll.pause(); - - return page.first() - .then(events => { - if (!events) { - return $q.resolve(); - } - - return render.clear() - .then(() => prepend(events)) - .then(() => { - scroll.resetScrollPosition(); - scroll.resume(); - }) - .then(() => { - if (scroll.isMissing()) { - return next(); - } - - return $q.resolve(); - }); - }); -} - -function scrollEnd () { - if (engine.isActive()) { - if (engine.isTransitioning()) { - return $q.resolve(); - } - - if (engine.isPaused()) { - engine.resume(); - } else { - engine.pause(); - } - - return $q.resolve(); - } else if (scroll.isPaused()) { - return $q.resolve(); - } - - scroll.pause(); - - return page.last() - .then(events => { - if (!events) { - return $q.resolve(); - } - - return render.clear() - .then(() => append(events)); - }) - .then(() => { - scroll.setScrollPosition(scroll.getScrollHeight()); - scroll.resume(); - }); -} - -function scrollPageUp () { - if (scroll.isPaused()) { - return; - } - - scroll.pageUp(); -} - -function scrollPageDown () { - if (scroll.isPaused()) { - return; - } - - scroll.pageDown(); -} - -function scrollIsAtRest (isAtRest) { - vm.scroll.showBackToTop = !isAtRest; -} - -function toggleExpanded () { - vm.expanded = !vm.expanded; -} - -function devClear () { - render.clear().then(() => init()); -} - -// function showHostDetails (id) { -// jobEvent.request('get', id) -// .then(() => { -// const title = jobEvent.get('host_name'); - -// vm.host = { -// menu: true, -// stdout: jobEvent.get('stdout') -// }; - -// $scope.jobs.modal.show(title); -// }); -// } - -// function toggle (uuid, menu) { -// const lines = $(`.child-of-${uuid}`); -// let icon = $(`#${uuid} .at-Stdout-toggle > i`); - -// if (menu || record[uuid].level === 1) { -// vm.isExpanded = !vm.isExpanded; -// } - -// if (record[uuid].children) { -// icon = icon.add($(`#${record[uuid].children.join(', #')}`) -// .find('.at-Stdout-toggle > i')); -// } - -// if (icon.hasClass('fa-angle-down')) { -// icon.addClass('fa-angle-right'); -// icon.removeClass('fa-angle-down'); - -// lines.addClass('hidden'); -// } else { -// icon.addClass('fa-angle-down'); -// icon.removeClass('fa-angle-right'); - -// lines.removeClass('hidden'); -// } -// } - -JobsIndexController.$inject = [ - 'resource', - 'JobPageService', - 'JobScrollService', - 'JobRenderService', - 'JobEventEngine', - '$scope', +OutputIndexController.$inject = [ '$compile', '$q', - 'JobStatusService', + '$scope', + '$state', + 'resource', + 'OutputScrollService', + 'OutputPageService', + 'OutputRenderService', + 'OutputStatusService', + 'OutputSlideService', + 'OutputStreamService', + '$filter', + 'OutputStrings', + '$stateParams', ]; -module.exports = JobsIndexController; +module.exports = OutputIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index be7bf49b33..ce016c1f19 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -1,15 +1,17 @@ +/* eslint camelcase: 0 */ import atLibModels from '~models'; import atLibComponents from '~components'; -import Strings from '~features/output/jobs.strings'; +import Strings from '~features/output/output.strings'; import Controller from '~features/output/index.controller'; -import PageService from '~features/output/page.service'; import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; -import EngineService from '~features/output/engine.service'; +import StreamService from '~features/output/stream.service'; import StatusService from '~features/output/status.service'; import MessageService from '~features/output/message.service'; import EventsApiService from '~features/output/api.events.service'; +import PageService from '~features/output/page.service'; +import SlideService from '~features/output/slide.service'; import LegacyRedirect from '~features/output/legacy.route'; import DetailsComponent from '~features/output/details.component'; @@ -17,52 +19,55 @@ import SearchComponent from '~features/output/search.component'; import StatsComponent from '~features/output/stats.component'; import HostEvent from './host-event/index'; -const Template = require('~features/output/index.view.html'); +import { + API_ROOT, + OUTPUT_ORDER_BY, + OUTPUT_PAGE_SIZE, + WS_PREFIX, +} from './constants'; const MODULE_NAME = 'at.features.output'; - -const PAGE_CACHE = true; -const PAGE_LIMIT = 5; -const PAGE_SIZE = 50; -const WS_PREFIX = 'ws'; +const Template = require('~features/output/index.view.html'); function resolveResource ( $state, + $stateParams, Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, InventoryUpdate, - $stateParams, qs, Wait, - eventsApi, + Events, ) { - const { id, type, handleErrors } = $stateParams; - const { job_event_search } = $stateParams; // eslint-disable-line camelcase - + const { id, type, handleErrors, job_event_search } = $stateParams; const { name, key } = getWebSocketResource(type); let Resource; - let related = 'events'; + let related; switch (type) { case 'project': Resource = ProjectUpdate; + related = `project_updates/${id}/events/`; break; case 'playbook': Resource = Job; - related = 'job_events'; + related = `jobs/${id}/job_events/`; break; case 'command': Resource = AdHocCommand; + related = `ad_hoc_commands/${id}/events/`; break; case 'system': Resource = SystemJob; + related = `system_jobs/${id}/events/`; break; case 'inventory': Resource = InventoryUpdate; + related = `inventory_updates/${id}/events/`; break; // case 'workflow': // todo: integrate workflow chart components into this view @@ -73,64 +78,40 @@ function resolveResource ( } const params = { - page_size: PAGE_SIZE, - order_by: 'start_line', - }; - - const config = { - params, - pageCache: PAGE_CACHE, - pageLimit: PAGE_LIMIT, + page_size: OUTPUT_PAGE_SIZE, + order_by: OUTPUT_ORDER_BY, }; if (job_event_search) { // eslint-disable-line camelcase const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search)); - Object.assign(config.params, query); + Object.assign(params, query); } - let model; + Events.init(`${API_ROOT}${related}`, params); Wait('start'); - const resourcePromise = new Resource(['get', 'options'], [id, id]) - .then(job => { - const endpoint = `${job.get('url')}${related}/`; - eventsApi.init(endpoint, config.params); - - const promises = [job.getStats(), eventsApi.fetch()]; - - if (job.has('related.labels')) { - promises.push(job.extend('get', 'labels')); - } - - model = job; - return Promise.all(promises); - }) - .then(([stats, events]) => ({ + const promise = Promise.all([new Resource(['get', 'options'], [id, id]), Events.fetch()]) + .then(([model, events]) => ({ id, type, - stats, model, events, - related, ws: { events: `${WS_PREFIX}-${key}-${id}`, status: `${WS_PREFIX}-${name}`, + summary: `${WS_PREFIX}-${name}-summary`, }, - page: { - cache: PAGE_CACHE, - size: PAGE_SIZE, - pageLimit: PAGE_LIMIT - } })); if (!handleErrors) { - return resourcePromise + return promise .finally(() => Wait('stop')); } - return resourcePromise + return promise .catch(({ data, status }) => { qs.error(data, status); + return $state.go($state.current, $state.params, { reload: true }); }) .finally(() => Wait('stop')); @@ -151,7 +132,7 @@ function resolveWebSocketConnection ($stateParams, SocketService) { } }; - return SocketService.addStateResolve(state, id); + SocketService.addStateResolve(state, id); } function getWebSocketResource (type) { @@ -186,9 +167,10 @@ function getWebSocketResource (type) { return { name, key }; } -function JobsRun ($stateRegistry, strings) { +function JobsRun ($stateRegistry, $filter, strings) { const parent = 'jobs'; const ncyBreadcrumb = { parent, label: strings.get('state.BREADCRUMB_DEFAULT') }; + const sanitize = $filter('sanitize'); const state = { url: '/:type/:id?job_event_search', @@ -197,6 +179,7 @@ function JobsRun ($stateRegistry, strings) { ncyBreadcrumb, params: { handleErrors: true, + isPanelExpanded: false, }, data: { activityStream: false, @@ -216,13 +199,13 @@ function JobsRun ($stateRegistry, strings) { ], resource: [ '$state', + '$stateParams', 'JobModel', 'ProjectUpdateModel', 'AdHocCommandModel', 'SystemJobModel', 'WorkflowJobModel', 'InventoryUpdateModel', - '$stateParams', 'QuerySet', 'Wait', 'JobEventsApiService', @@ -231,7 +214,7 @@ function JobsRun ($stateRegistry, strings) { breadcrumbLabel: [ 'resource', ({ model }) => { - ncyBreadcrumb.label = `${model.get('id')} - ${model.get('name')}`; + ncyBreadcrumb.label = `${model.get('id')} - ${sanitize(model.get('name'))}`; } ], }, @@ -240,7 +223,7 @@ function JobsRun ($stateRegistry, strings) { $stateRegistry.register(state); } -JobsRun.$inject = ['$stateRegistry', 'JobStrings']; +JobsRun.$inject = ['$stateRegistry', '$filter', 'OutputStrings']; angular .module(MODULE_NAME, [ @@ -248,14 +231,15 @@ angular atLibComponents, HostEvent ]) - .service('JobStrings', Strings) - .service('JobPageService', PageService) - .service('JobScrollService', ScrollService) - .service('JobRenderService', RenderService) - .service('JobEventEngine', EngineService) - .service('JobStatusService', StatusService) - .service('JobMessageService', MessageService) + .service('OutputStrings', Strings) + .service('OutputScrollService', ScrollService) + .service('OutputRenderService', RenderService) + .service('OutputStreamService', StreamService) + .service('OutputStatusService', StatusService) + .service('OutputMessageService', MessageService) .service('JobEventsApiService', EventsApiService) + .service('OutputPageService', PageService) + .service('OutputSlideService', SlideService) .component('atJobSearch', SearchComponent) .component('atJobStats', StatsComponent) .component('atJobDetails', DetailsComponent) diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index cb932fafd3..08df5f714a 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,64 +1,58 @@
    - - + - -
    - {{ vm.title }} -
    - - - - -
    -
    - + +
    +
    + + {{ vm.title }}
    - -
    - + + + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    -
    - +
    +
    +
    +
    -
    - -
    -
    - -
    - -
    -
    - -
    -            
    -                
    -                    
    -                        
    -                        
    -                        
    -                    
    -                
    -                
    -            
     
    -
    - -
    -
    -

    -

    Back to Top

    -
    - -
    diff --git a/awx/ui/client/features/output/jobs.strings.js b/awx/ui/client/features/output/jobs.strings.js deleted file mode 100644 index c581039172..0000000000 --- a/awx/ui/client/features/output/jobs.strings.js +++ /dev/null @@ -1,35 +0,0 @@ -function JobsStrings (BaseString) { - BaseString.call(this, 'jobs'); - - const { t } = this; - const ns = this.jobs; - - ns.state = { - BREADCRUMB_DEFAULT: t.s('RESULTS'), - }; - - ns.status = { - RUNNING: t.s('The host status bar will update when the job is complete.'), - UNAVAILABLE: t.s('Host status information for this job is unavailable.'), - }; - - ns.resourceTooltips = { - USER: t.s('View the User'), - SCHEDULE: t.s('View the Schedule'), - INVENTORY: t.s('View the Inventory'), - CREDENTIAL: t.s('View the Credential'), - JOB_TEMPLATE: t.s('View the Job Template'), - SOURCE_WORKFLOW_JOB: t.s('View the source Workflow Job'), - PROJECT: t.s('View the Project'), - PROJECT_UPDATE: t.s('View Project checkout results') - }; - - ns.expandCollapse = { - EXPAND: t.s('Expand Output'), - COLLAPSE: t.s('Collapse Output') - }; -} - -JobsStrings.$inject = ['BaseStringService']; - -export default JobsStrings; diff --git a/awx/ui/client/features/output/legacy.route.js b/awx/ui/client/features/output/legacy.route.js index 4abf991dbb..2db75d01d7 100644 --- a/awx/ui/client/features/output/legacy.route.js +++ b/awx/ui/client/features/output/legacy.route.js @@ -45,6 +45,27 @@ function LegacyRedirect ($stateRegistry) { return { state: destination, params: { type: 'project', id } }; } }, + { + name: 'legacySchedulesList', + url: '/jobs/schedules?schedule_search', + redirectTo: (trans) => { + const { + schedule_search // eslint-disable-line camelcase + } = trans.params(); + return { state: 'schedules', params: { schedule_search } }; + } + }, + { + name: 'legacySchedule', + url: '/jobs/schedules/:schedule_id?schedule_search', + redirectTo: (trans) => { + const { + schedule_id, // eslint-disable-line camelcase + schedule_search // eslint-disable-line camelcase + } = trans.params(); + return { state: 'schedules.edit', params: { schedule_id, schedule_search } }; + } + }, ]; routes.forEach(state => $stateRegistry.register(state)); diff --git a/awx/ui/client/features/output/output.strings.js b/awx/ui/client/features/output/output.strings.js new file mode 100644 index 0000000000..538b533cb0 --- /dev/null +++ b/awx/ui/client/features/output/output.strings.js @@ -0,0 +1,119 @@ +function OutputStrings (BaseString) { + BaseString.call(this, 'output'); + + const { t } = this; + const ns = this.output; + + ns.state = { + BREADCRUMB_DEFAULT: t.s('RESULTS'), + }; + + ns.status = { + RUNNING: t.s('The host status bar will update when the job is complete.'), + UNAVAILABLE: t.s('Host status information for this job is unavailable.'), + }; + + ns.tooltips = { + CANCEL: t.s('Cancel'), + COLLAPSE_OUTPUT: t.s('Collapse Output'), + DELETE: t.s('Delete'), + DOWNLOAD_OUTPUT: t.s('Download Output'), + CREDENTIAL: t.s('View the Credential'), + EXPAND_OUTPUT: t.s('Expand Output'), + EXTRA_VARS: t.s('Read-only view of extra variables added to the job template'), + INVENTORY: t.s('View the Inventory'), + JOB_TEMPLATE: t.s('View the Job Template'), + PROJECT: t.s('View the Project'), + PROJECT_UPDATE: t.s('View Project checkout results'), + SCHEDULE: t.s('View the Schedule'), + SOURCE_WORKFLOW_JOB: t.s('View the source Workflow Job'), + USER: t.s('View the User'), + MENU_FIRST: t.s('Go to first page'), + MENU_DOWN: t.s('Get next page'), + MENU_UP: t.s('Get previous page'), + MENU_LAST: t.s('Go to last page of available output'), + MENU_FOLLOWING: t.s('Currently following output as it arrives. Click to unfollow'), + }; + + ns.details = { + HEADER: t.s('Details'), + ISOLATED: t.s('Isolated'), + NOT_FINISHED: t.s('Not Finished'), + NOT_STARTED: t.s('Not Started'), + SHOW_LESS: t.s('Show Less'), + SHOW_MORE: t.s('Show More'), + UNKNOWN: t.s('Finished'), + }; + + ns.labels = { + CREDENTIAL: t.s('Credential'), + EXTRA_VARS: t.s('Extra Variables'), + FINISHED: t.s('Finished'), + FORKS: t.s('Forks'), + INSTANCE_GROUP: t.s('Instance Group'), + INVENTORY: t.s('Inventory'), + JOB_EXPLANATION: t.s('Explanation'), + JOB_TAGS: t.s('Job Tags'), + JOB_TEMPLATE: t.s('Job Template'), + JOB_TYPE: t.s('Job Type'), + LABELS: t.s('Labels'), + LAUNCHED_BY: t.s('Launched By'), + LICENSE_ERROR: t.s('License Error'), + LIMIT: t.s('Limit'), + MACHINE_CREDENTIAL: t.s('Machine Credential'), + MODULE_ARGS: t.s('Module Args'), + NAME: t.s('Name'), + OVERWRITE: t.s('Overwrite'), + OVERWRITE_VARS: t.s('Overwrite Vars'), + PLAYBOOK: t.s('Playbook'), + PROJECT: t.s('Project'), + RESULT_TRACEBACK: t.s('Error Details'), + SCM_REVISION: t.s('Revision'), + SKIP_TAGS: t.s('Skip Tags'), + SOURCE: t.s('Source'), + SOURCE_CREDENTIAL: t.s('Source Credential'), + STARTED: t.s('Started'), + STATUS: t.s('Status'), + VERBOSITY: t.s('Verbosity'), + }; + + ns.search = { + ADDITIONAL_INFORMATION_HEADER: t.s('ADDITIONAL_INFORMATION'), + ADDITIONAL_INFORMATION: t.s('For additional information on advanced search syntax please see the Ansible Tower'), + CLEAR_ALL: t.s('CLEAR ALL'), + DOCUMENTATION: t.s('documentation'), + EXAMPLES: t.s('EXAMPLES'), + FIELDS: t.s('FIELDS'), + KEY: t.s('KEY'), + PLACEHOLDER_DEFAULT: t.s('SEARCH'), + PLACEHOLDER_RUNNING: t.s('JOB IS STILL RUNNING'), + REJECT_DEFAULT: t.s('Failed to update search results.'), + REJECT_INVALID: t.s('Invalid search filter provided.'), + }; + + ns.stats = { + ELAPSED: t.s('Elapsed'), + PLAYS: t.s('Plays'), + TASKS: t.s('Tasks'), + HOSTS: t.s('Hosts') + }; + + ns.stdout = { + BACK_TO_TOP: t.s('Back to Top'), + }; + + ns.host_event_modal = { + CREATED: t.s('CREATED'), + ID: t.s('ID'), + PLAY: t.s('PLAY'), + TASK: t.s('TASK'), + MODULE: t.s('MODULE'), + NO_RESULT_FOUND: t.s('No result found'), + STANDARD_OUT: t.s('Standard Out'), + STANDARD_ERROR: t.s('Standard Error') + }; +} + +OutputStrings.$inject = ['BaseStringService']; + +export default OutputStrings; diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 854bda23b0..786d26ad66 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -1,283 +1,247 @@ -function JobPageService ($q) { - this.init = ({ resource }) => { - this.resource = resource; - this.api = this.resource.events; +/* eslint camelcase: 0 */ +import { OUTPUT_PAGE_LIMIT } from './constants'; - this.page = { - limit: this.resource.page.pageLimit, - size: this.resource.page.size, - cache: [], - state: { - count: 0, - current: 0, - first: 0, - last: 0 - } +function PageService ($q) { + this.init = (storage, api, { getScrollHeight }) => { + const { prepend, append, shift, pop, deleteRecord } = storage; + const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api; + + this.api = { + getPage, + getFirst, + getLast, + getLastPageNumber, + getMaxCounter, }; - this.bookmark = { - pending: false, - set: false, - cache: [], - state: { - count: 0, - first: 0, - last: 0, - current: 0 - } + this.storage = { + prepend, + append, + shift, + pop, + deleteRecord, }; - this.result = { - limit: this.page.limit * this.page.size, - count: 0 + this.hooks = { + getScrollHeight, }; - this.buffer = { - count: 0 + this.records = {}; + this.uuids = {}; + + this.state = { + head: 0, + tail: 0, }; + + this.chain = $q.resolve(); }; - this.addPage = (number, events, push, reference) => { - const page = { number, events, lines: 0 }; - reference = reference || this.getActiveReference(); - - if (push) { - reference.cache.push(page); - reference.state.last = page.number; - reference.state.first = reference.cache[0].number; - } else { - reference.cache.unshift(page); - reference.state.first = page.number; - reference.state.last = reference.cache[reference.cache.length - 1].number; + this.pushFront = (results, key) => { + if (!results) { + return $q.resolve(); } - reference.state.current = page.number; - reference.state.count++; - }; + return this.storage.append(results) + .then(() => { + const tail = key || ++this.state.tail; - this.addToBuffer = event => { - const reference = this.getReference(); - const index = reference.cache.length - 1; - let pageAdded = false; + this.records[tail] = {}; + results.forEach(({ counter, start_line, end_line, uuid }) => { + this.records[tail][counter] = { start_line, end_line }; + this.uuids[counter] = uuid; + }); - if (this.result.count % this.page.size === 0) { - this.addPage(reference.state.current + 1, [event], true, reference); - - if (this.isBookmarkPending()) { - this.setBookmark(); - } - - this.trimBuffer(); - - pageAdded = true; - } else { - reference.cache[index].events.push(event); - } - - this.buffer.count++; - this.result.count++; - - return pageAdded; - }; - - this.trimBuffer = () => { - const reference = this.getReference(); - const diff = reference.cache.length - this.page.limit; - - if (diff <= 0) { - return; - } - - for (let i = 0; i < diff; i++) { - if (reference.cache[i].events) { - this.buffer.count -= reference.cache[i].events.length; - reference.cache[i].events.splice(0, reference.cache[i].events.length); - } - } - }; - - this.isBufferFull = () => { - if (this.buffer.count === 2) { - return true; - } - - return false; - }; - - this.emptyBuffer = () => { - const reference = this.getReference(); - let data = []; - - for (let i = 0; i < reference.cache.length; i++) { - const count = reference.cache[i].events.length; - - if (count > 0) { - this.buffer.count -= count; - data = data.concat(reference.cache[i].events.splice(0, count)); - } - } - - return data; - }; - - this.emptyCache = number => { - const reference = this.getActiveReference(); - - number = number || reference.state.current; - - reference.state.first = number; - reference.state.current = number; - reference.state.last = number; - - reference.cache.splice(0, reference.cache.length); - }; - - this.isOverCapacity = () => { - const reference = this.getActiveReference(); - - return (reference.cache.length - this.page.limit) > 0; - }; - - this.trim = left => { - const reference = this.getActiveReference(); - const excess = reference.cache.length - this.page.limit; - - let ejected; - - if (left) { - ejected = reference.cache.splice(0, excess); - reference.state.first = reference.cache[0].number; - } else { - ejected = reference.cache.splice(-excess); - reference.state.last = reference.cache[reference.cache.length - 1].number; - } - - return ejected.reduce((total, page) => total + page.lines, 0); - }; - - this.isPageBookmarked = number => number >= this.page.bookmark.first && - number <= this.page.bookmark.last; - - this.updateLineCount = (lines, engine) => { - let reference; - - if (engine) { - reference = this.getReference(); - } else { - reference = this.getActiveReference(); - } - - const index = reference.cache.findIndex(item => item.number === reference.state.current); - - reference.cache[index].lines += lines; - }; - - this.isBookmarkPending = () => this.bookmark.pending; - this.isBookmarkSet = () => this.bookmark.set; - - this.setBookmark = () => { - if (this.isBookmarkSet()) { - return; - } - - if (!this.isBookmarkPending()) { - this.bookmark.pending = true; - - return; - } - - this.bookmark.state.first = this.page.state.first; - this.bookmark.state.last = this.page.state.last - 1; - this.bookmark.state.current = this.page.state.current - 1; - this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache)); - this.bookmark.set = true; - this.bookmark.pending = false; - }; - - this.removeBookmark = () => { - this.bookmark.set = false; - this.bookmark.pending = false; - this.bookmark.cache.splice(0, this.bookmark.cache.length); - this.bookmark.state.first = 0; - this.bookmark.state.last = 0; - this.bookmark.state.current = 0; - }; - - this.next = () => { - const reference = this.getActiveReference(); - const number = reference.state.last + 1; - - return this.api.getPage(number) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); - } - - this.addPage(data.page, [], true); - - return data.results; + return $q.resolve(); }); }; - this.previous = () => { - const reference = this.getActiveReference(); + this.pushBack = (results, key) => { + if (!results) { + return $q.resolve(); + } - return this.api.getPage(reference.state.first - 1) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); - } + return this.storage.prepend(results) + .then(() => { + const head = key || --this.state.head; - this.addPage(data.page, [], false); + this.records[head] = {}; + results.forEach(({ counter, start_line, end_line, uuid }) => { + this.records[head][counter] = { start_line, end_line }; + this.uuids[counter] = uuid; + }); - return data.results; + return $q.resolve(); }); }; - this.last = () => this.api.last() - .then(data => { - if (!data || !data.results || !data.results.length > 0) { - return $q.resolve(); - } - - this.emptyCache(data.page); - this.addPage(data.page, [], true); - - return data.results; - }); - - this.first = () => this.api.first() - .then(data => { - if (!data || !data.results) { - return $q.resolve(); - } - - this.emptyCache(data.page); - this.addPage(data.page, [], false); - - return data.results; - }); - - this.getActiveReference = () => (this.isBookmarkSet() ? - this.getReference(true) : this.getReference()); - - this.getReference = (bookmark) => { - if (bookmark) { - return { - bookmark: true, - cache: this.bookmark.cache, - state: this.bookmark.state - }; + this.popBack = () => { + if (this.getRecordCount() === 0) { + return $q.resolve(); } - return { - bookmark: false, - cache: this.page.cache, - state: this.page.state - }; + const pageRecord = this.records[this.state.head] || {}; + + let lines = 0; + const counters = []; + + Object.keys(pageRecord) + .forEach(counter => { + lines += pageRecord[counter].end_line - pageRecord[counter].start_line; + counters.push(counter); + }); + + return this.storage.shift(lines) + .then(() => { + counters.forEach(counter => { + this.storage.deleteRecord(this.uuids[counter]); + delete this.uuids[counter]; + }); + + delete this.records[this.state.head++]; + + return $q.resolve(); + }); }; + + this.popFront = () => { + if (this.getRecordCount() === 0) { + return $q.resolve(); + } + + const pageRecord = this.records[this.state.tail] || {}; + + let lines = 0; + const counters = []; + + Object.keys(pageRecord) + .forEach(counter => { + lines += pageRecord[counter].end_line - pageRecord[counter].start_line; + counters.push(counter); + }); + + return this.storage.pop(lines) + .then(() => { + counters.forEach(counter => { + this.storage.deleteRecord(this.uuids[counter]); + delete this.uuids[counter]; + }); + + delete this.records[this.state.tail--]; + + return $q.resolve(); + }); + }; + + this.getNext = () => { + const lastPageNumber = this.api.getLastPageNumber(); + const number = Math.min(this.state.tail + 1, lastPageNumber); + + const isLoaded = (number >= this.state.head && number <= this.state.tail); + const isValid = (number >= 1 && number <= lastPageNumber); + + let popHeight = this.hooks.getScrollHeight(); + + if (!isValid || isLoaded) { + this.chain = this.chain + .then(() => $q.resolve(popHeight)); + + return this.chain; + } + + const pageCount = this.state.head - this.state.tail; + + if (pageCount >= OUTPUT_PAGE_LIMIT) { + this.chain = this.chain + .then(() => this.popBack()) + .then(() => { + popHeight = this.hooks.getScrollHeight(); + + return $q.resolve(); + }); + } + + this.chain = this.chain + .then(() => this.api.getPage(number)) + .then(events => this.pushFront(events)) + .then(() => $q.resolve(popHeight)); + + return this.chain; + }; + + this.getPrevious = () => { + const number = Math.max(this.state.head - 1, 1); + + const isLoaded = (number >= this.state.head && number <= this.state.tail); + const isValid = (number >= 1 && number <= this.api.getLastPageNumber()); + + let popHeight = this.hooks.getScrollHeight(); + + if (!isValid || isLoaded) { + this.chain = this.chain + .then(() => $q.resolve(popHeight)); + + return this.chain; + } + + const pageCount = this.state.head - this.state.tail; + + if (pageCount >= OUTPUT_PAGE_LIMIT) { + this.chain = this.chain + .then(() => this.popFront()) + .then(() => { + popHeight = this.hooks.getScrollHeight(); + + return $q.resolve(); + }); + } + + this.chain = this.chain + .then(() => this.api.getPage(number)) + .then(events => this.pushBack(events)) + .then(() => $q.resolve(popHeight)); + + return this.chain; + }; + + this.clear = () => { + const count = this.getRecordCount(); + + for (let i = 0; i <= count; ++i) { + this.chain = this.chain.then(() => this.popBack()); + } + + return this.chain; + }; + + this.getLast = () => this.clear() + .then(() => this.api.getLast()) + .then(events => { + const lastPage = this.api.getLastPageNumber(); + + this.state.head = lastPage; + this.state.tail = lastPage; + + return this.pushBack(events, lastPage); + }) + .then(() => this.getPrevious()); + + this.getFirst = () => this.clear() + .then(() => this.api.getFirst()) + .then(events => { + this.state.head = 1; + this.state.tail = 1; + + return this.pushBack(events, 1); + }) + .then(() => this.getNext()); + + this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail; + this.getRecordCount = () => Object.keys(this.records).length; + this.getTailCounter = () => this.state.tail; + this.getMaxCounter = () => this.api.getMaxCounter(); } -JobPageService.$inject = ['$q']; +PageService.$inject = ['$q']; -export default JobPageService; +export default PageService; diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 08a3498fd2..a7f44162a7 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -1,20 +1,22 @@ import Ansi from 'ansi-to-html'; import Entities from 'html-entities'; -const ELEMENT_TBODY = '#atStdoutResultTable'; -const EVENT_START_TASK = 'playbook_on_task_start'; -const EVENT_START_PLAY = 'playbook_on_play_start'; -const EVENT_STATS_PLAY = 'playbook_on_stats'; +import { + EVENT_START_PLAY, + EVENT_STATS_PLAY, + EVENT_START_TASK, + OUTPUT_ELEMENT_TBODY, +} from './constants'; const EVENT_GROUPS = [ EVENT_START_TASK, - EVENT_START_PLAY + EVENT_START_PLAY, ]; const TIME_EVENTS = [ EVENT_START_TASK, EVENT_START_PLAY, - EVENT_STATS_PLAY + EVENT_STATS_PLAY, ]; const ansi = new Ansi(); @@ -30,11 +32,13 @@ const re = new RegExp(pattern); const hasAnsi = input => re.test(input); function JobRenderService ($q, $sce, $window) { - this.init = ({ compile, isStreamActive }) => { + this.init = ({ compile, toggles }) => { this.parent = null; this.record = {}; - this.el = $(ELEMENT_TBODY); - this.hooks = { isStreamActive, compile }; + this.el = $(OUTPUT_ELEMENT_TBODY); + this.hooks = { compile }; + + this.createToggles = toggles; }; this.sortByLineNumber = (a, b) => { @@ -55,17 +59,20 @@ function JobRenderService ($q, $sce, $window) { events.sort(this.sortByLineNumber); - events.forEach(event => { - const line = this.transformEvent(event); - + for (let i = 0; i < events.length; ++i) { + const line = this.transformEvent(events[i]); html += line.html; lines += line.count; - }); + } return { html, lines }; }; this.transformEvent = event => { + if (this.record[event.uuid]) { + return { html: '', count: 0 }; + } + if (!event || !event.stdout) { return { html: '', count: 0 }; } @@ -118,11 +125,13 @@ function JobRenderService ($q, $sce, $window) { const info = { id: event.id, line: ln + 1, + name: event.event, uuid: event.uuid, level: event.event_level, start: event.start_line, end: event.end_line, isTruncated: (event.end_line - event.start_line) > lines.length, + lineCount: lines.length, isHost: this.isHostEvent(event), }; @@ -163,6 +172,24 @@ function JobRenderService ($q, $sce, $window) { return info; }; + this.getRecord = uuid => this.record[uuid]; + + this.deleteRecord = uuid => { + delete this.record[uuid]; + }; + + this.getParentEvents = (uuid, list) => { + list = list || []; + // always push its parent if exists + list.push(uuid); + // if we can get grandparent in current visible lines, we also push it + if (this.record[uuid] && this.record[uuid].parents) { + list = list.concat(this.record[uuid].parents); + } + + return list; + }; + this.createRow = (current, ln, content) => { let id = ''; let timestamp = ''; @@ -177,13 +204,13 @@ function JobRenderService ($q, $sce, $window) { } if (current) { - if (!this.hooks.isStreamActive() && current.isParent && current.line === ln) { + if (this.createToggles && current.isParent && current.line === ln) { id = current.uuid; - tdToggle = ``; + tdToggle = `
    `; } if (current.isHost) { - tdEvent = `${content}`; + tdEvent = `
    ${content}
    `; } if (current.time && current.line === ln) { @@ -196,11 +223,11 @@ function JobRenderService ($q, $sce, $window) { } if (!tdEvent) { - tdEvent = `${content}`; + tdEvent = `
    ${content}
    `; } if (!tdToggle) { - tdToggle = ''; + tdToggle = '
    '; } if (!ln) { @@ -208,12 +235,12 @@ function JobRenderService ($q, $sce, $window) { } return ` - +
    ${tdToggle} - ${ln} +
    ${ln}
    ${tdEvent} - ${timestamp} - `; +
    ${timestamp}
    +
    `; }; this.getTimestamp = created => { @@ -225,32 +252,7 @@ function JobRenderService ($q, $sce, $window) { return `${hour}:${minute}:${second}`; }; - this.getParentEvents = (uuid, list) => { - list = list || []; - - if (this.record[uuid]) { - list.push(uuid); - - if (this.record[uuid].parents) { - list = list.concat(this.record[uuid].parents); - } - } - - return list; - }; - - this.insert = (events, insert) => { - const result = this.transformEventGroup(events); - const html = this.trustHtml(result.html); - - return this.requestAnimationFrame(() => insert(html)) - .then(() => this.compile(html)) - .then(() => result.lines); - }; - - this.remove = elements => this.requestAnimationFrame(() => { - elements.remove(); - }); + this.remove = elements => this.requestAnimationFrame(() => elements.remove()); this.requestAnimationFrame = fn => $q(resolve => { $window.requestAnimationFrame(() => { @@ -262,9 +264,8 @@ function JobRenderService ($q, $sce, $window) { }); }); - this.compile = html => { - html = $(this.el); - this.hooks.compile(html); + this.compile = content => { + this.hooks.compile(content); return this.requestAnimationFrame(); }; @@ -286,9 +287,35 @@ function JobRenderService ($q, $sce, $window) { return this.remove(elements); }; - this.prepend = events => this.insert(events, html => this.el.prepend(html)); + this.prepend = events => { + if (events.length < 1) { + return $q.resolve(); + } - this.append = events => this.insert(events, html => this.el.append(html)); + const result = this.transformEventGroup(events); + const html = this.trustHtml(result.html); + + const newElements = angular.element(html); + + return this.requestAnimationFrame(() => this.el.prepend(newElements)) + .then(() => this.compile(newElements)) + .then(() => result.lines); + }; + + this.append = events => { + if (events.length < 1) { + return $q.resolve(); + } + + const result = this.transformEventGroup(events); + const html = this.trustHtml(result.html); + + const newElements = angular.element(html); + + return this.requestAnimationFrame(() => this.el.append(newElements)) + .then(() => this.compile(newElements)) + .then(() => result.lines); + }; this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html)); diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index a568813ddc..4e6e2eff57 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -1,11 +1,16 @@ -const ELEMENT_CONTAINER = '.at-Stdout-container'; -const ELEMENT_TBODY = '#atStdoutResultTable'; -const DELAY = 100; -const THRESHOLD = 0.1; +import { + OUTPUT_ELEMENT_CONTAINER, + OUTPUT_ELEMENT_TBODY, + OUTPUT_SCROLL_DELAY, + OUTPUT_SCROLL_THRESHOLD, +} from './constants'; + +const MAX_THRASH = 20; function JobScrollService ($q, $timeout) { - this.init = (hooks) => { - this.el = $(ELEMENT_CONTAINER); + this.init = ({ next, previous, onThresholdLeave }) => { + this.el = $(OUTPUT_ELEMENT_CONTAINER); + this.chain = $q.resolve(); this.timer = null; this.position = { @@ -13,19 +18,43 @@ function JobScrollService ($q, $timeout) { current: 0 }; + this.threshold = { + previous: 0, + current: 0, + }; + this.hooks = { - isAtRest: hooks.isAtRest, - next: hooks.next, - previous: hooks.previous + next, + previous, + onThresholdLeave, }; this.state = { - locked: false, paused: false, - top: true + locked: false, + hover: false, + running: true, + thrash: 0, }; this.el.scroll(this.listen); + this.el.mouseenter(this.onMouseEnter); + this.el.mouseleave(this.onMouseLeave); + }; + + this.onMouseEnter = () => { + this.state.hover = true; + + if (this.state.thrash >= MAX_THRASH) { + this.state.thrash = MAX_THRASH - 1; + } + + this.unlock(); + this.unhide(); + }; + + this.onMouseLeave = () => { + this.state.hover = false; }; this.listen = () => { @@ -33,123 +62,149 @@ function JobScrollService ($q, $timeout) { return; } + if (this.state.thrash > 0) { + if (this.isLocked() || this.state.hover) { + this.state.thrash--; + } + } + + if (!this.state.hover) { + this.state.thrash++; + } + + if (this.state.thrash >= MAX_THRASH) { + if (this.isRunning()) { + this.lock(); + this.hide(); + } + } + + if (this.isLocked()) { + return; + } + + if (!this.state.hover) { + return; + } + if (this.timer) { $timeout.cancel(this.timer); } - this.timer = $timeout(this.register, DELAY); + this.timer = $timeout(this.register, OUTPUT_SCROLL_DELAY); }; this.register = () => { - this.pause(); + const position = this.getScrollPosition(); + const viewport = this.getScrollHeight() - this.getViewableHeight(); - const current = this.getScrollPosition(); - const downward = current > this.position.previous; + const threshold = position / viewport; + const downward = position > this.position.previous; - let promise; + const isBeyondUpperThreshold = threshold < OUTPUT_SCROLL_THRESHOLD; + const isBeyondLowerThreshold = (1 - threshold) < OUTPUT_SCROLL_THRESHOLD; - if (downward && this.isBeyondThreshold(downward, current)) { - promise = this.hooks.next; - } else if (!downward && this.isBeyondThreshold(downward, current)) { - promise = this.hooks.previous; + const wasBeyondUpperThreshold = this.threshold.previous < OUTPUT_SCROLL_THRESHOLD; + const wasBeyondLowerThreshold = (1 - this.threshold.previous) < OUTPUT_SCROLL_THRESHOLD; + + const enteredUpperThreshold = isBeyondUpperThreshold && !wasBeyondUpperThreshold; + const enteredLowerThreshold = isBeyondLowerThreshold && !wasBeyondLowerThreshold; + const leftLowerThreshold = !isBeyondLowerThreshold && wasBeyondLowerThreshold; + + const transitions = []; + + if (position <= 0 || enteredUpperThreshold) { + transitions.push(this.hooks.onThresholdLeave); + transitions.push(this.hooks.previous); } - if (!promise) { - this.setScrollPosition(current); - this.isAtRest(); - this.resume(); - - return $q.resolve(); + if (leftLowerThreshold) { + transitions.push(this.hooks.onThresholdLeave); } - return promise() + if (threshold >= 1 || enteredLowerThreshold) { + transitions.push(this.hooks.next); + } + + if (!downward) { + transitions.reverse(); + } + + this.position.current = position; + this.threshold.current = threshold; + + transitions.forEach(promise => { + this.chain = this.chain.then(() => promise()); + }); + + return this.chain .then(() => { this.setScrollPosition(this.getScrollPosition()); - this.isAtRest(); - this.resume(); + + return $q.resolve(); }); }; - this.isBeyondThreshold = (downward, current) => { - const height = this.getScrollHeight(); + /** + * Move scroll position up by one page of visible content. + */ + this.moveUp = () => { + const position = this.getScrollPosition() - this.getViewableHeight(); - if (downward) { - current += this.getViewableHeight(); - - if (current >= height || ((height - current) / height) < THRESHOLD) { - return true; - } - } else if (current <= 0 || (current / height) < THRESHOLD) { - return true; - } - - return false; + this.setScrollPosition(position); }; - this.pageUp = () => { - if (this.isPaused()) { - return; - } + /** + * Move scroll position down by one page of visible content. + */ + this.moveDown = () => { + const position = this.getScrollPosition() + this.getViewableHeight(); - const top = this.getScrollPosition(); - const height = this.getViewableHeight(); - - this.setScrollPosition(top - height); - }; - - this.pageDown = () => { - if (this.isPaused()) { - return; - } - - const top = this.getScrollPosition(); - const height = this.getViewableHeight(); - - this.setScrollPosition(top + height); + this.setScrollPosition(position); }; this.getScrollHeight = () => this.el[0].scrollHeight; this.getViewableHeight = () => this.el[0].offsetHeight; + + /** + * Get the vertical scroll position. + * + * @returns {Number} - The number of pixels that are hidden from view above the scrollable area. + */ this.getScrollPosition = () => this.el[0].scrollTop; this.setScrollPosition = position => { + const viewport = this.getScrollHeight() - this.getViewableHeight(); + this.position.previous = this.position.current; + this.threshold.previous = this.position.previous / viewport; this.position.current = position; + this.el[0].scrollTop = position; - this.isAtRest(); }; this.resetScrollPosition = () => { + this.threshold.previous = 0; this.position.previous = 0; this.position.current = 0; + this.el[0].scrollTop = 0; - this.isAtRest(); }; this.scrollToBottom = () => { this.setScrollPosition(this.getScrollHeight()); }; - this.isAtRest = () => { - if (this.position.current === 0 && !this.state.top) { - this.state.top = true; - this.hooks.isAtRest(true); - } else if (this.position.current > 0 && this.state.top) { - this.state.top = false; - this.hooks.isAtRest(false); - } + this.start = () => { + this.state.running = true; }; - this.resume = () => { - this.state.paused = false; + this.stop = () => { + this.unlock(); + this.unhide(); + this.state.running = false; }; - this.pause = () => { - this.state.paused = true; - }; - - this.isPaused = () => this.state.paused; - this.lock = () => { this.state.locked = true; }; @@ -158,8 +213,52 @@ function JobScrollService ($q, $timeout) { this.state.locked = false; }; + this.pause = () => { + this.state.paused = true; + }; + + this.resume = () => { + this.state.paused = false; + }; + + this.hide = () => { + if (this.state.hidden) { + return; + } + + this.state.hidden = true; + this.el.css('overflow-y', 'hidden'); + }; + + this.unhide = () => { + if (!this.state.hidden) { + return; + } + + this.state.hidden = false; + this.el.css('overflow-y', 'auto'); + }; + + this.isBeyondLowerThreshold = () => { + const position = this.getScrollPosition(); + const viewport = this.getScrollHeight() - this.getViewableHeight(); + const threshold = position / viewport; + + return (1 - threshold) < OUTPUT_SCROLL_THRESHOLD; + }; + + this.isBeyondUpperThreshold = () => { + const position = this.getScrollPosition(); + const viewport = this.getScrollHeight() - this.getViewableHeight(); + const threshold = position / viewport; + + return threshold < OUTPUT_SCROLL_THRESHOLD; + }; + + this.isPaused = () => this.state.paused; + this.isRunning = () => this.state.running; this.isLocked = () => this.state.locked; - this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight(); + this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight(); } JobScrollService.$inject = ['$q', '$timeout']; diff --git a/awx/ui/client/features/output/search.component.js b/awx/ui/client/features/output/search.component.js index b1f40efd3e..09ef3e52f2 100644 --- a/awx/ui/client/features/output/search.component.js +++ b/awx/ui/client/features/output/search.component.js @@ -1,16 +1,14 @@ +import { + OUTPUT_SEARCH_DOCLINK, + OUTPUT_SEARCH_FIELDS, + OUTPUT_SEARCH_KEY_EXAMPLES, +} from './constants'; + const templateUrl = require('~features/output/search.partial.html'); -const searchReloadOptions = { inherit: false, location: 'replace' }; -const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; -const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; - -const PLACEHOLDER_RUNNING = 'CANNOT SEARCH RUNNING JOB'; -const PLACEHOLDER_DEFAULT = 'SEARCH'; -const REJECT_DEFAULT = 'Failed to update search results.'; -const REJECT_INVALID = 'Invalid search filter provided.'; - let $state; let qs; +let strings; let vm; @@ -32,7 +30,7 @@ function getSearchTags (queryset) { .filter(tag => !tag.startsWith('order_by')); } -function reloadQueryset (queryset, rejection = REJECT_DEFAULT) { +function reloadQueryset (queryset, rejection = strings.get('search.REJECT_DEFAULT')) { const params = angular.copy($state.params); const currentTags = vm.tags; @@ -43,7 +41,7 @@ function reloadQueryset (queryset, rejection = REJECT_DEFAULT) { vm.message = ''; vm.tags = getSearchTags(queryset); - return $state.transitionTo($state.current, params, searchReloadOptions) + return vm.reload(params) .catch(() => { vm.tags = currentTags; vm.message = rejection; @@ -52,38 +50,56 @@ function reloadQueryset (queryset, rejection = REJECT_DEFAULT) { }); } +const isFilterable = term => { + const field = term[0].split('.')[0].replace(/^-/, ''); + return (OUTPUT_SEARCH_FIELDS.indexOf(field) > -1); +}; + function removeSearchTag (index) { const searchTerm = vm.tags[index]; const currentQueryset = getCurrentQueryset(); - const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); + const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm, isFilterable); reloadQueryset(modifiedQueryset); } function submitSearch () { - const currentQueryset = getCurrentQueryset(); + // empty input, not submit new search, return. + if (!vm.value) { + return; + } - const searchInputQueryset = qs.getSearchInputQueryset(vm.value); + const currentQueryset = getCurrentQueryset(); + // check duplicate , see if search input already exists in current search tags + if (currentQueryset.search) { + if (currentQueryset.search.includes(vm.value)) { + return; + } + } + + const searchInputQueryset = qs.getSearchInputQueryset(vm.value, isFilterable); const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); - reloadQueryset(modifiedQueryset, REJECT_INVALID); + reloadQueryset(modifiedQueryset, strings.get('search.REJECT_INVALID')); } function clearSearch () { reloadQueryset(); } -function JobSearchController (_$state_, _qs_, { subscribe }) { +function JobSearchController (_$state_, _qs_, _strings_, { subscribe }) { $state = _$state_; qs = _qs_; + strings = _strings_; vm = this || {}; + vm.strings = strings; - vm.examples = searchKeyExamples; - vm.fields = searchKeyFields; + vm.examples = OUTPUT_SEARCH_KEY_EXAMPLES; + vm.fields = OUTPUT_SEARCH_FIELDS; + vm.docLink = OUTPUT_SEARCH_DOCLINK; vm.relatedFields = []; - vm.placeholder = PLACEHOLDER_DEFAULT; vm.clearSearch = clearSearch; vm.toggleSearchKey = toggleSearchKey; @@ -98,11 +114,12 @@ function JobSearchController (_$state_, _qs_, { subscribe }) { vm.key = false; vm.rejected = false; vm.disabled = true; + vm.running = false; vm.tags = getSearchTags(getCurrentQueryset()); unsubscribe = subscribe(({ running }) => { vm.disabled = running; - vm.placeholder = running ? PLACEHOLDER_RUNNING : PLACEHOLDER_DEFAULT; + vm.running = running; }); }; @@ -114,11 +131,15 @@ function JobSearchController (_$state_, _qs_, { subscribe }) { JobSearchController.$inject = [ '$state', 'QuerySet', - 'JobStatusService', + 'OutputStrings', + 'OutputStatusService', ]; export default { templateUrl, controller: JobSearchController, controllerAs: 'vm', + bindings: { + reload: '=', + }, }; diff --git a/awx/ui/client/features/output/search.partial.html b/awx/ui/client/features/output/search.partial.html index c209394815..613f768f91 100644 --- a/awx/ui/client/features/output/search.partial.html +++ b/awx/ui/client/features/output/search.partial.html @@ -1,64 +1,74 @@ - + + -
    - - - - + - + - -
    -

    - {{ vm.message }} -

    + type="button"> {{:: vm.strings.get('search.KEY') }} + + +
    +

    {{ vm.message }}

    -
    -
    -
    EXAMPLES:
    - -
    +
    +
    + {{:: vm.strings.get('search.EXAMPLES') }}: +
    + +
    - FIELDS: + {{:: vm.strings.get('search.FIELDS') }}: {{ field }},
    - ADDITIONAL INFORMATION: - For additional information on advanced search search syntax please see the Ansible Tower - documentation. + {{:: vm.strings.get('search.ADDITIONAL_INFORMATION_HEADER') }}: + {{:: vm.strings.get('search.ADDITIONAL_INFORMATION') }} + + {{:: vm.strings.get('search.DOCUMENTATION') }}. +
    diff --git a/awx/ui/client/features/output/slide.service.js b/awx/ui/client/features/output/slide.service.js new file mode 100644 index 0000000000..8bddc51565 --- /dev/null +++ b/awx/ui/client/features/output/slide.service.js @@ -0,0 +1,416 @@ +/* eslint camelcase: 0 */ +import { + API_MAX_PAGE_SIZE, + OUTPUT_EVENT_LIMIT, + OUTPUT_PAGE_SIZE, +} from './constants'; + +function getContinuous (events, reverse = false) { + const counters = events.map(({ counter }) => counter); + + const min = Math.min(...counters); + const max = Math.max(...counters); + + const missing = []; + for (let i = min; i <= max; i++) { + if (counters.indexOf(i) < 0) { + missing.push(i); + } + } + + if (missing.length === 0) { + return events; + } + + if (reverse) { + const threshold = Math.max(...missing); + + return events.filter(({ counter }) => counter > threshold); + } + + const threshold = Math.min(...missing); + + return events.filter(({ counter }) => counter < threshold); +} + +function SlidingWindowService ($q) { + this.init = (storage, api, { getScrollHeight }) => { + const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage; + const { getRange, getFirst, getLast, getMaxCounter } = api; + + this.api = { + getRange, + getFirst, + getLast, + getMaxCounter, + }; + + this.storage = { + clear, + prepend, + append, + shift, + pop, + getRecord, + deleteRecord, + }; + + this.hooks = { + getScrollHeight, + }; + + this.lines = {}; + this.uuids = {}; + this.chain = $q.resolve(); + + this.state = { head: null, tail: null }; + this.cache = { first: null }; + + this.buffer = { + events: [], + min: 0, + max: 0, + count: 0, + }; + }; + + this.getBoundedRange = range => { + const bounds = [1, this.getMaxCounter()]; + + return [Math.max(range[0], bounds[0]), Math.min(range[1], bounds[1])]; + }; + + this.getNextRange = displacement => { + const tail = this.getTailCounter(); + + return this.getBoundedRange([tail + 1, tail + 1 + displacement]); + }; + + this.getPreviousRange = displacement => { + const head = this.getHeadCounter(); + + return this.getBoundedRange([head - 1 - displacement, head - 1]); + }; + + this.createRecord = ({ counter, uuid, start_line, end_line }) => { + this.lines[counter] = end_line - start_line; + this.uuids[counter] = uuid; + + if (this.state.tail === null) { + this.state.tail = counter; + } + + if (counter > this.state.tail) { + this.state.tail = counter; + } + + if (this.state.head === null) { + this.state.head = counter; + } + + if (counter < this.state.head) { + this.state.head = counter; + } + }; + + this.deleteRecord = counter => { + this.storage.deleteRecord(this.uuids[counter]); + + delete this.uuids[counter]; + delete this.lines[counter]; + }; + + this.getLineCount = counter => { + const record = this.storage.getRecord(counter); + + if (record && record.lineCount) { + return record.lineCount; + } + + if (this.lines[counter]) { + return this.lines[counter]; + } + + return 0; + }; + + this.pushFront = events => { + const tail = this.getTailCounter(); + const newEvents = events.filter(({ counter }) => counter > tail); + + return this.storage.append(newEvents) + .then(() => { + newEvents.forEach(event => this.createRecord(event)); + + return $q.resolve(); + }); + }; + + this.pushBack = events => { + const [head, tail] = this.getRange(); + const newEvents = events + .filter(({ counter }) => counter < head || counter > tail); + + return this.storage.prepend(newEvents) + .then(() => { + newEvents.forEach(event => this.createRecord(event)); + + return $q.resolve(); + }); + }; + + this.popFront = count => { + if (!count || count <= 0) { + return $q.resolve(); + } + + const max = this.getTailCounter(); + const min = max - count; + + let lines = 0; + + for (let i = max; i >= min; --i) { + lines += this.getLineCount(i); + } + + return this.storage.pop(lines) + .then(() => { + for (let i = max; i >= min; --i) { + this.deleteRecord(i); + this.state.tail--; + } + + return $q.resolve(); + }); + }; + + this.popBack = count => { + if (!count || count <= 0) { + return $q.resolve(); + } + + const min = this.getHeadCounter(); + const max = min + count; + + let lines = 0; + + for (let i = min; i <= max; ++i) { + lines += this.getLineCount(i); + } + + return this.storage.shift(lines) + .then(() => { + for (let i = min; i <= max; ++i) { + this.deleteRecord(i); + this.state.head++; + } + + return $q.resolve(); + }); + }; + + this.clear = () => this.storage.clear() + .then(() => { + const [head, tail] = this.getRange(); + + for (let i = head; i <= tail; ++i) { + this.deleteRecord(i); + } + + this.state.head = null; + this.state.tail = null; + + return $q.resolve(); + }); + + this.getNext = (displacement = OUTPUT_PAGE_SIZE) => { + const next = this.getNextRange(displacement); + const [head, tail] = this.getRange(); + + this.chain = this.chain + .then(() => this.api.getRange(next)) + .then(events => { + const results = getContinuous(events); + const min = Math.min(...results.map(({ counter }) => counter)); + + if (min > tail + 1) { + return $q.resolve([]); + } + + return $q.resolve(results); + }) + .then(results => { + const count = (tail - head + results.length); + const excess = count - OUTPUT_EVENT_LIMIT; + + return this.popBack(excess) + .then(() => { + const popHeight = this.hooks.getScrollHeight(); + + return this.pushFront(results).then(() => $q.resolve(popHeight)); + }); + }); + + return this.chain; + }; + + this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => { + const previous = this.getPreviousRange(displacement); + const [head, tail] = this.getRange(); + + this.chain = this.chain + .then(() => this.api.getRange(previous)) + .then(events => { + const results = getContinuous(events, true); + const max = Math.max(...results.map(({ counter }) => counter)); + + if (head > max + 1) { + return $q.resolve([]); + } + + return $q.resolve(results); + }) + .then(results => { + const count = (tail - head + results.length); + const excess = count - OUTPUT_EVENT_LIMIT; + + return this.popFront(excess) + .then(() => { + const popHeight = this.hooks.getScrollHeight(); + + return this.pushBack(results).then(() => $q.resolve(popHeight)); + }); + }); + + return this.chain; + }; + + this.getFirst = () => { + this.chain = this.chain + .then(() => this.clear()) + .then(() => { + if (this.cache.first) { + return $q.resolve(this.cache.first); + } + + return this.api.getFirst(); + }) + .then(events => { + if (events.length === OUTPUT_PAGE_SIZE) { + this.cache.first = events; + } + + return this.pushFront(events); + }); + + return this.chain + .then(() => this.getNext()); + }; + + this.getLast = () => { + this.chain = this.chain + .then(() => this.getFrames()) + .then(frames => { + if (frames.length > 0) { + return $q.resolve(frames); + } + + return this.api.getLast(); + }) + .then(events => { + const min = Math.min(...events.map(({ counter }) => counter)); + + if (min <= this.getTailCounter() + 1) { + return this.pushFront(events); + } + + return this.clear() + .then(() => this.pushBack(events)); + }); + + return this.chain + .then(() => this.getPrevious()); + }; + + this.getTailCounter = () => { + if (this.state.tail === null) { + return 0; + } + + if (this.state.tail < 0) { + return 0; + } + + return this.state.tail; + }; + + this.getHeadCounter = () => { + if (this.state.head === null) { + return 0; + } + + if (this.state.head < 0) { + return 0; + } + + return this.state.head; + }; + + this.pushFrames = events => { + const frames = this.buffer.events.concat(events); + const [head, tail] = this.getRange(); + + let min; + let max; + let count = 0; + + for (let i = frames.length - 1; i >= 0; i--) { + count++; + + if (count > API_MAX_PAGE_SIZE) { + frames.splice(i, 1); + + count--; + continue; + } + + if (!min || frames[i].counter < min) { + min = frames[i].counter; + } + + if (!max || frames[i].counter > max) { + max = frames[i].counter; + } + } + + this.buffer.events = frames; + this.buffer.min = min; + this.buffer.max = max; + this.buffer.count = count; + + if (min >= head && min <= tail + 1) { + return frames.filter(({ counter }) => counter > tail); + } + + return []; + }; + + this.getFrames = () => $q.resolve(this.buffer.events); + + this.getMaxCounter = () => { + if (this.buffer.min) { + return this.buffer.min; + } + + return this.api.getMaxCounter(); + }; + + this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); + this.getRange = () => [this.getHeadCounter(), this.getTailCounter()]; + this.getRecordCount = () => Object.keys(this.lines).length; + this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount(); +} + +SlidingWindowService.$inject = ['$q']; + +export default SlidingWindowService; diff --git a/awx/ui/client/features/output/stats.component.js b/awx/ui/client/features/output/stats.component.js index a7a0ec61f3..31285cfbf1 100644 --- a/awx/ui/client/features/output/stats.component.js +++ b/awx/ui/client/features/output/stats.component.js @@ -11,6 +11,7 @@ function createStatsBarTooltip (key, count) { function JobStatsController (strings, { subscribe }) { vm = this || {}; + vm.strings = strings; let unsubscribe; @@ -21,15 +22,17 @@ function JobStatsController (strings, { subscribe }) { vm.$onInit = () => { vm.download = vm.resource.model.get('related.stdout'); - vm.toggleStdoutFullscreenTooltip = strings.get('expandCollapse.EXPAND'); + vm.tooltips.toggleExpand = vm.expanded ? + strings.get('tooltips.COLLAPSE_OUTPUT') : + strings.get('tooltips.EXPAND_OUTPUT'); - unsubscribe = subscribe(({ running, elapsed, counts, stats, hosts }) => { + unsubscribe = subscribe(({ running, elapsed, counts, hosts }) => { vm.plays = counts.plays; vm.tasks = counts.tasks; vm.hosts = counts.hosts; vm.elapsed = elapsed; vm.running = running; - vm.setHostStatusCounts(stats, hosts); + vm.setHostStatusCounts(hosts); }); }; @@ -37,7 +40,9 @@ function JobStatsController (strings, { subscribe }) { unsubscribe(); }; - vm.setHostStatusCounts = (stats, counts) => { + vm.setHostStatusCounts = counts => { + let statsAreAvailable; + Object.keys(counts).forEach(key => { const count = counts[key]; const statusBarElement = $(`.HostStatusBar-${key}`); @@ -45,22 +50,24 @@ function JobStatsController (strings, { subscribe }) { statusBarElement.css('flex', `${count} 0 auto`); vm.tooltips[key] = createStatsBarTooltip(key, count); + + if (count) statsAreAvailable = true; }); - vm.statsAreAvailable = stats; + vm.statsAreAvailable = statsAreAvailable; }; vm.toggleExpanded = () => { vm.expanded = !vm.expanded; - vm.toggleStdoutFullscreenTooltip = vm.expanded ? - strings.get('expandCollapse.COLLAPSE') : - strings.get('expandCollapse.EXPAND'); + vm.tooltips.toggleExpand = vm.expanded ? + strings.get('tooltips.COLLAPSE_OUTPUT') : + strings.get('tooltips.EXPAND_OUTPUT'); }; } JobStatsController.$inject = [ - 'JobStrings', - 'JobStatusService', + 'OutputStrings', + 'OutputStatusService', ]; export default { diff --git a/awx/ui/client/features/output/stats.partial.html b/awx/ui/client/features/output/stats.partial.html index c55ba4bc8d..fa27beb97b 100644 --- a/awx/ui/client/features/output/stats.partial.html +++ b/awx/ui/client/features/output/stats.partial.html @@ -1,35 +1,34 @@ - +
    plays - ... - {{ vm.plays }} + ... + {{ vm.plays || 0 }} tasks - ... - {{ vm.tasks }} + ... + {{ vm.tasks || 0 }} - hosts - ... - {{ vm.hosts }} + {{:: vm.strings.get('stats.HOSTS')}} + ... + {{ vm.hosts || 1 }} - elapsed - ... - - {{ vm.elapsed * 1000 | duration: "hh:mm:ss" }} + {{:: vm.strings.get('stats.ELAPSED') }} + ... + + {{ (vm.elapsed * 1000 || 0) | duration: "hh:mm:ss"}} + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + diff --git a/awx/ui/client/features/portalMode/portalMode.strings.js b/awx/ui/client/features/portalMode/portalMode.strings.js new file mode 100644 index 0000000000..c1cdd893fd --- /dev/null +++ b/awx/ui/client/features/portalMode/portalMode.strings.js @@ -0,0 +1,15 @@ +function PortalModeStrings (BaseString) { + BaseString.call(this, 'portalMode'); + + const { t } = this; + const ns = this.portalMode; + + ns.list = { + TEMPLATES_PANEL_TITLE: t.s('JOB TEMPLATES'), + JOBS_PANEL_TITLE: t.s('JOBS'), + }; +} + +PortalModeStrings.$inject = ['BaseStringService']; + +export default PortalModeStrings; diff --git a/awx/ui/client/features/jobs/routes/portalModeAllJobs.route.js b/awx/ui/client/features/portalMode/routes/portalModeAllJobs.route.js similarity index 84% rename from awx/ui/client/features/jobs/routes/portalModeAllJobs.route.js rename to awx/ui/client/features/portalMode/routes/portalModeAllJobs.route.js index 9595da3547..fa2396b4df 100644 --- a/awx/ui/client/features/jobs/routes/portalModeAllJobs.route.js +++ b/awx/ui/client/features/portalMode/routes/portalModeAllJobs.route.js @@ -1,10 +1,13 @@ -import jobsListController from '../jobsList.controller'; +import jobsListController from '../../jobs/jobsList.controller'; const jobsListTemplate = require('~features/jobs/jobsList.view.html'); export default { name: 'portalMode.allJobs', url: '/alljobs?{job_search:queryset}', + ncyBreadcrumb: { + skip: true + }, params: { job_search: { value: { @@ -45,6 +48,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') ] } }; diff --git a/awx/ui/client/features/jobs/routes/portalModeMyJobs.route.js b/awx/ui/client/features/portalMode/routes/portalModeMyJobs.route.js similarity index 88% rename from awx/ui/client/features/jobs/routes/portalModeMyJobs.route.js rename to awx/ui/client/features/portalMode/routes/portalModeMyJobs.route.js index 4d0909d66b..61de256d40 100644 --- a/awx/ui/client/features/jobs/routes/portalModeMyJobs.route.js +++ b/awx/ui/client/features/portalMode/routes/portalModeMyJobs.route.js @@ -1,4 +1,4 @@ -import jobsListController from '../jobsList.controller'; +import jobsListController from '../../jobs/jobsList.controller'; const jobsListTemplate = require('~features/jobs/jobsList.view.html'); @@ -51,6 +51,10 @@ export default { return qs.search(searchPath, searchParam) .finally(() => Wait('stop')); } + ], + SearchBasePath: [ + 'GetBasePath', + (GetBasePath) => GetBasePath('unified_jobs') ] } }; diff --git a/awx/ui/client/features/templates/routes/portalModeTemplatesList.route.js b/awx/ui/client/features/portalMode/routes/portalModeTemplatesList.route.js similarity index 70% rename from awx/ui/client/features/templates/routes/portalModeTemplatesList.route.js rename to awx/ui/client/features/portalMode/routes/portalModeTemplatesList.route.js index 55187b5a98..8255cfb169 100644 --- a/awx/ui/client/features/templates/routes/portalModeTemplatesList.route.js +++ b/awx/ui/client/features/portalMode/routes/portalModeTemplatesList.route.js @@ -1,8 +1,9 @@ -import { templateUrl } from '../../../src/shared/template-url/template-url.factory'; import { N_ } from '../../../src/i18n'; -import templatesListController from '../templatesList.controller'; +import templatesListController from '../../templates/templatesList.controller'; +import indexController from '../index.controller'; const templatesListTemplate = require('~features/templates/templatesList.view.html'); +const indexTemplate = require('~features/portalMode/index.view.html'); export default { name: 'portalMode', @@ -13,8 +14,8 @@ export default { }, data: { socket: { - "groups": { - "jobs": ["status_changed"] + groups: { + jobs: ['status_changed'] } } }, @@ -29,19 +30,9 @@ export default { searchPrefix: 'template', views: { '@': { - templateUrl: templateUrl('portal-mode/portal-mode-layout'), - controller: ['$scope', '$state', - function($scope, $state) { - - $scope.filterUser = function() { - $state.go('portalMode.myJobs'); - }; - - $scope.filterAll = function() { - $state.go('portalMode.allJobs'); - }; - } - ] + templateUrl: indexTemplate, + controller: indexController, + controllerAs: 'vm' }, 'templates@portalMode': { templateUrl: templatesListTemplate, diff --git a/awx/ui/client/features/templates/index.controller.js b/awx/ui/client/features/templates/index.controller.js index c859255070..bfbfeb84fd 100644 --- a/awx/ui/client/features/templates/index.controller.js +++ b/awx/ui/client/features/templates/index.controller.js @@ -1,12 +1,12 @@ function IndexTemplatesController ($scope, strings, dataset) { - let vm = this; + const vm = this; vm.strings = strings; vm.count = dataset.data.count; - $scope.$on('updateDataset', (e, { count }) => { - if (count) { - vm.count = count; - } + $scope.$on('updateCount', (e, count) => { + if (typeof count === 'number') { + vm.count = count; + } }); } diff --git a/awx/ui/client/features/templates/index.view.html b/awx/ui/client/features/templates/index.view.html index 346ab0c0f1..7812dcaa29 100644 --- a/awx/ui/client/features/templates/index.view.html +++ b/awx/ui/client/features/templates/index.view.html @@ -1,20 +1,17 @@
    -
    - - {{:: vm.strings.get('list.PANEL_TITLE') }} -
    - {{ vm.count }} -
    +
    +
    -
    - - {{:: vm.strings.get('list.PANEL_TITLE') }} -
    - {{ vm.count }} -
    +
    +
    diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 3bb2d38b66..673895c46a 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -23,6 +23,7 @@ function TemplatesStrings (BaseString) { ns.prompt = { INVENTORY: t.s('Inventory'), CREDENTIAL: t.s('Credential'), + PROMPT: t.s('PROMPT'), OTHER_PROMPTS: t.s('Other Prompts'), SURVEY: t.s('Survey'), PREVIEW: t.s('Preview'), @@ -72,10 +73,6 @@ function TemplatesStrings (BaseString) { UNKNOWN_SCHEDULE: t.s('Unable to determine this template\'s type while scheduling.'), }; - ns.actions = { - COPY_WORKFLOW: t.s('Copy Workflow') - }; - ns.error = { HEADER: this.error.HEADER, CALL: this.error.CALL, @@ -93,8 +90,35 @@ function TemplatesStrings (BaseString) { }; ns.workflows = { - INVALID_JOB_TEMPLATE: t.s('This Job Template is missing a default inventory or project. This must be addressed in the Job Template form before this node can be saved.') + INVALID_JOB_TEMPLATE: t.s('This Job Template is missing a default inventory or project. This must be addressed in the Job Template form before this node can be saved.'), + CREDENTIAL_WITH_PASS: t.s('This Job Template has a credential that requires a password. Credentials requiring passwords on launch are not permitted on workflow nodes.') }; + + ns.workflow_maker = { + DELETE_NODE_PROMPT_TEXT: t.s('Are you sure you want to delete this workflow node?'), + KEY: t.s('KEY'), + ON_SUCCESS: t.s('On Success'), + ON_FAILURE: t.s('On Failure'), + ALWAYS: t.s('Always'), + PROJECT_SYNC: t.s('Project Sync'), + INVENTORY_SYNC: t.s('Inventory Sync'), + WARNING: t.s('Warning'), + TOTAL_TEMPLATES: t.s('TOTAL TEMPLATES'), + ADD_A_TEMPLATE: t.s('ADD A TEMPLATE'), + EDIT_TEMPLATE: t.s('EDIT TEMPLATE'), + JOBS: t.s('JOBS'), + PLEASE_CLICK_THE_START_BUTTON: t.s('Please click the start button to build your workflow.'), + PLEASE_HOVER_OVER_A_TEMPLATE: t.s('Please hover over a template for additional options.'), + RUN: t.s('RUN'), + CHECK: t.s('CHECK'), + SELECT: t.s('SELECT'), + EDGE_CONFLICT: t.s('EDGE CONFLICT'), + DELETED: t.s('DELETED'), + START: t.s('START'), + DETAILS: t.s('DETAILS'), + TITLE: t.s('WORKFLOW VISUALIZER') + } + } TemplatesStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/features/templates/templatesList.controller.js b/awx/ui/client/features/templates/templatesList.controller.js index 5ad7388fd4..26b477b23d 100644 --- a/awx/ui/client/features/templates/templatesList.controller.js +++ b/awx/ui/client/features/templates/templatesList.controller.js @@ -22,7 +22,8 @@ function ListTemplatesController( strings, Wait, qs, - GetBasePath + GetBasePath, + ngToast ) { const vm = this || {}; const [jobTemplate, workflowTemplate] = resolvedModels; @@ -35,7 +36,7 @@ function ListTemplatesController( vm.strings = strings; vm.templateTypes = mapChoices(choices); - vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id); + vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_job_template_id); vm.invalidTooltip = { popover: { text: strings.get('error.INVALID'), @@ -51,21 +52,28 @@ function ListTemplatesController( $scope.canAdd = ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate); // smart-search - $scope.list = { + vm.list = { iterator: 'template', name: 'templates' }; - $scope.collection = { - iterator: 'template', - basePath: 'unified_job_templates' - }; - $scope.template_dataset = Dataset.data; - $scope.templates = Dataset.data.results; - $scope.$on('updateDataset', (e, dataset) => { - $scope.template_dataset = dataset; - $scope.templates = dataset.results; + vm.dataset = Dataset.data; + vm.templates = Dataset.data.results; + + $scope.$watch('vm.dataset.count', () => { + $scope.$emit('updateCount', vm.dataset.count, 'templates'); }); + $scope.$watch('$state.params', function(newValue, oldValue) { + const job_template_id = _.get($state.params, 'job_template_id'); + const workflow_job_template_id = _.get($state.params, 'workflow_job_template_id'); + + if((job_template_id || workflow_job_template_id)) { + vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_job_template_id); + } else { + vm.activeId = ""; + } + }, true); + $scope.$on(`ws-jobs`, () => { if (!launchModalOpen) { refreshTemplates(); @@ -91,20 +99,7 @@ function ListTemplatesController( } }; - vm.scheduleTemplate = template => { - if (!template) { - Alert(strings.get('error.SCHEDULE'), strings.get('alert.MISSING_PARAMETER')); - return; - } - - if (isJobTemplate(template)) { - $state.go('templates.editJobTemplate.schedules', { job_template_id: template.id }); - } else if (isWorkflowTemplate(template)) { - $state.go('templates.editWorkflowJobTemplate.schedules', { workflow_job_template_id: template.id }); - } else { - Alert(strings.get('error.UNKNOWN'), strings.get('alert.UNKNOWN_SCHEDULE')); - } - }; + vm.isPortalMode = $state.includes('portalMode'); vm.deleteTemplate = template => { if (!template) { @@ -155,6 +150,17 @@ function ListTemplatesController( return html; }; + vm.buildCredentialTags = (credentials) => { + return credentials.map(credential => { + const icon = `${credential.kind}`; + const link = `/#/credentials/${credential.id}`; + const tooltip = strings.get('tooltips.VIEW_THE_CREDENTIAL'); + const value = $filter('sanitize')(credential.name); + + return { icon, link, tooltip, value }; + }); + }; + vm.getLastRan = template => { const lastJobRun = _.get(template, 'last_job_run'); @@ -174,13 +180,23 @@ function ListTemplatesController( return html; }; + vm.getType = template => { + if(isJobTemplate(template)) { + return strings.get('list.ADD_DD_JT_LABEL'); + } else { + return strings.get('list.ADD_DD_WF_LABEL');; + } + }; + function refreshTemplates() { + Wait('start'); let path = GetBasePath('unified_job_templates'); qs.search(path, $state.params.template_search) .then(function(searchResponse) { - $scope.template_dataset = searchResponse.data; - $scope.templates = $scope.template_dataset.results; - }); + vm.dataset = searchResponse.data; + vm.templates = vm.dataset.results; + }) + .finally(() => Wait('stop')); } function createErrorHandler(path, action) { @@ -196,9 +212,21 @@ function ListTemplatesController( jobTemplate .create('get', template.id) .then(model => model.copy()) - .then(({ id }) => { - const params = { job_template_id: id }; - $state.go('templates.editJobTemplate', params, { reload: true }); + .then((copiedJT) => { + ngToast.success({ + content: ` +
    +
    + +
    +
    + ${strings.get('SUCCESSFUL_CREATION', copiedJT.name)} +
    +
    `, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); }) .catch(createErrorHandler('copy job template', 'POST')) .finally(() => Wait('stop')); @@ -214,9 +242,21 @@ function ListTemplatesController( $('#prompt-modal').modal('hide'); Wait('start'); model.copy() - .then(({ id }) => { - const params = { workflow_job_template_id: id }; - $state.go('templates.editWorkflowJobTemplate', params, { reload: true }); + .then((copiedWFJT) => { + ngToast.success({ + content: ` +
    +
    + +
    +
    + ${strings.get('SUCCESSFUL_CREATION', copiedWFJT.name)} +
    +
    `, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); }) .catch(createErrorHandler('copy workflow', 'POST')) .finally(() => Wait('stop')); @@ -230,7 +270,7 @@ function ListTemplatesController( actionText: strings.get('COPY'), body: buildWorkflowCopyPromptHTML(model.get('related.copy')), class: 'Modal-primaryButton', - hdr: strings.get('actions.COPY_WORKFLOW'), + hdr: strings.get('listActions.COPY', template.name), }); } else { Alert(strings.get('error.COPY'), strings.get('alert.NO_PERMISSION')); @@ -244,7 +284,7 @@ function ListTemplatesController( const { page } = _.get($state.params, 'template_search'); let reloadListStateParams = null; - if ($scope.templates.length === 1 && !_.isEmpty(page) && page !== '1') { + if (vm.templates.length === 1 && page && page !== '1') { reloadListStateParams = _.cloneDeep($state.params); const pageNumber = (parseInt(reloadListStateParams.template_search.page, 0) - 1); reloadListStateParams.template_search.page = pageNumber.toString(); @@ -355,7 +395,8 @@ ListTemplatesController.$inject = [ 'TemplatesStrings', 'Wait', 'QuerySet', - 'GetBasePath' + 'GetBasePath', + 'ngToast' ]; export default ListTemplatesController; diff --git a/awx/ui/client/features/templates/templatesList.view.html b/awx/ui/client/features/templates/templatesList.view.html index 5a00912663..255efcc3cc 100644 --- a/awx/ui/client/features/templates/templatesList.view.html +++ b/awx/ui/client/features/templates/templatesList.view.html @@ -5,13 +5,13 @@ django-model="templates" base-path="unified_job_templates" iterator="template" - list="list" - dataset="template_dataset" - collection="collection" - search-tags="searchTags" - query-set="querySet"> + list="vm.list" + collection="vm.templates" + dataset="vm.dataset" + search-tags="vm.searchTags" + search-bar-full-width="vm.isPortalMode"> -
    +
    - - + + invalid-tooltip="vm.invalidTooltip" + id="row-{{ template.id }}">
    + value-link="/#/inventories/{{template.summary_fields.inventory.kind === 'smart' ? 'smart' : 'inventory'}}/{{ template.summary_fields.inventory.id }}"> - + - - + ng-show="!vm.isPortalMode && template.summary_fields.user_capabilities.copy" + tooltip="{{:: vm.strings.get('listActions.COPY', vm.getType(template)) }}"> + ng-show="!vm.isPortalMode && template.summary_fields.user_capabilities.delete" + tooltip="{{:: vm.strings.get('listActions.DELETE', vm.getType(template)) }}">
    + base-path="unified_job_templates"> diff --git a/awx/ui/client/features/users/tokens/tokens.strings.js b/awx/ui/client/features/users/tokens/tokens.strings.js index 27b9450992..2f84642eae 100644 --- a/awx/ui/client/features/users/tokens/tokens.strings.js +++ b/awx/ui/client/features/users/tokens/tokens.strings.js @@ -28,7 +28,8 @@ function TokensStrings (BaseString) { DELETE_ACTION_LABEL: t.s('DELETE'), SCOPE_PLACEHOLDER: t.s('Select a scope'), SCOPE_READ_LABEL: t.s('Read'), - SCOPE_WRITE_LABEL: t.s('Write') + SCOPE_WRITE_LABEL: t.s('Write'), + APPLICATION_HELP_TEXT: t.s('Leaving this field blank will result in the creation of a Personal Access Token which is not linked to an Application.') }; ns.list = { @@ -37,6 +38,7 @@ function TokensStrings (BaseString) { ROW_ITEM_LABEL_USED: t.s('LAST USED'), ROW_ITEM_LABEL_SCOPE: t.s('SCOPE'), ROW_ITEM_LABEL_APPLICATION: t.s('APPLICATION'), + PERSONAL_ACCESS_TOKEN: t.s('Personal Access Token'), HEADER: appName => t.s('{{ appName }} Token', { appName }), }; } diff --git a/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js b/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js index 13197ca11d..20173c260a 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js +++ b/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js @@ -13,8 +13,7 @@ export default { } }, data: { - basePath: 'applications', - formChildState: true + basePath: 'applications' }, ncyBreadcrumb: { skip: true @@ -42,14 +41,19 @@ export default { name: { key: true, label: 'Name', - columnClass: 'col-lg-4 col-md-6 col-sm-8 col-xs-8', + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-4', awToolTip: '{{application.description | sanitize}}', dataPlacement: 'top' }, - }, - actions: { - }, - fieldActions: { + organization: { + label: 'Organization', + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-4', + modalColumnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-4', + key: false, + ngBind: 'application.summary_fields.organization.name', + sourceModel: 'organization', + includeModal: true + } } })], Dataset: ['QuerySet', 'GetBasePath', '$stateParams', 'ListDefinition', diff --git a/awx/ui/client/features/users/tokens/users-tokens-add.controller.js b/awx/ui/client/features/users/tokens/users-tokens-add.controller.js index 1421077b1c..4ad1fa2d27 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-add.controller.js +++ b/awx/ui/client/features/users/tokens/users-tokens-add.controller.js @@ -1,59 +1,72 @@ function AddTokensController ( - models, $state, strings, Rest, Alert, Wait, GetBasePath, - $filter, ProcessErrors + models, $state, strings, Alert, Wait, + $filter, ProcessErrors, $scope ) { const vm = this || {}; - const { application } = models; + const { application, token, user } = models; vm.mode = 'add'; vm.strings = strings; vm.panelTitle = strings.get('add.PANEL_TITLE'); - vm.form = {}; - - vm.form.application = { - type: 'field', - label: 'Application', - id: 'application' - }; - vm.form.description = { - type: 'String', - label: 'Description', - id: 'description' - }; - - vm.form.application._resource = 'application'; - vm.form.application._route = 'users.edit.tokens.add.application'; - vm.form.application._model = application; - vm.form.application._placeholder = strings.get('add.APP_PLACEHOLDER'); - vm.form.application.required = true; - - vm.form.description.required = false; - - vm.form.scope = { - choices: [ - '', - 'read', - 'write' - ], - help_text: strings.get('add.SCOPE_HELP_TEXT'), - id: 'scope', - label: 'Scope', - required: true, - _component: 'at-input-select', - _data: [ - strings.get('add.SCOPE_PLACEHOLDER'), - strings.get('add.SCOPE_READ_LABEL'), - strings.get('add.SCOPE_WRITE_LABEL') - ], - _exp: 'choice for (index, choice) in state._data', - _format: 'array' + vm.form = { + application: { + type: 'field', + label: 'Application', + id: 'application', + required: false, + help_text: strings.get('add.APPLICATION_HELP_TEXT'), + _resource: 'application', + _route: 'users.edit.tokens.add.application', + _model: application, + _placeholder: strings.get('add.APP_PLACEHOLDER') + }, + description: { + type: 'String', + label: 'Description', + id: 'description', + required: false + }, + scope: { + choices: [ + [null, ''], + ['read', strings.get('add.SCOPE_READ_LABEL')], + ['write', strings.get('add.SCOPE_WRITE_LABEL')] + ], + help_text: strings.get('add.SCOPE_HELP_TEXT'), + id: 'scope', + label: 'Scope', + required: true, + _component: 'at-input-select', + _data: [ + [null, ''], + ['read', strings.get('add.SCOPE_READ_LABEL')], + ['write', strings.get('add.SCOPE_WRITE_LABEL')] + ], + _exp: 'choice[1] for (index, choice) in state._data', + _format: 'selectFromOptions' + } }; vm.form.save = payload => { - Rest.setUrl(`${GetBasePath('users')}${$state.params.user_id}/authorized_tokens`); - return Rest.post(payload) + const postToken = _.has(payload, 'application') ? + user.postAuthorizedTokens({ + id: $state.params.user_id, + payload + }) : token.request('post', { data: payload }); + + return postToken .then(({ data }) => { + const refreshHTML = data.refresh_token ? + `
    +
    + ${strings.get('add.REFRESH_TOKEN_LABEL')} +
    +
    + ${data.refresh_token} +
    +
    ` : ''; + Alert(strings.get('add.TOKEN_MODAL_HEADER'), `
    @@ -63,14 +76,7 @@ function AddTokensController ( ${data.token}
    -
    -
    - ${strings.get('add.REFRESH_TOKEN_LABEL')} -
    -
    - ${data.refresh_token} -
    -
    + ${refreshHTML}
    ${strings.get('add.TOKEN_EXPIRES_LABEL')} @@ -94,18 +100,23 @@ function AddTokensController ( vm.form.onSaveSuccess = () => { $state.go('^', { user_id: $state.params.user_id }, { reload: true }); }; + + $scope.$watch('application', () => { + if ($scope.application) { + vm.form.application._idFromModal = $scope.application; + } + }); } AddTokensController.$inject = [ 'resolvedModels', '$state', 'TokensStrings', - 'Rest', 'Alert', 'Wait', - 'GetBasePath', '$filter', - 'ProcessErrors' + 'ProcessErrors', + '$scope' ]; export default AddTokensController; diff --git a/awx/ui/client/features/users/tokens/users-tokens-add.partial.html b/awx/ui/client/features/users/tokens/users-tokens-add.partial.html index 21f5a8be04..25bd01a21a 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-add.partial.html +++ b/awx/ui/client/features/users/tokens/users-tokens-add.partial.html @@ -1,7 +1,6 @@ +
    - - {{ vm.panelTitle }} - + @@ -10,7 +9,7 @@ - + diff --git a/awx/ui/client/features/users/tokens/users-tokens-add.route.js b/awx/ui/client/features/users/tokens/users-tokens-add.route.js index 56effe1991..43a663433f 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-add.route.js +++ b/awx/ui/client/features/users/tokens/users-tokens-add.route.js @@ -3,17 +3,36 @@ import AddController from './users-tokens-add.controller'; const addTemplate = require('~features/users/tokens/users-tokens-add.partial.html'); -function TokensDetailResolve ($q, Application) { +function TokensDetailResolve ($q, Application, Token, User) { const promises = {}; promises.application = new Application('options'); + promises.token = new Token('options'); + promises.user = new User('options'); return $q.all(promises); } TokensDetailResolve.$inject = [ '$q', - 'ApplicationModel' + 'ApplicationModel', + 'TokenModel', + 'UserModel' +]; + +function isMeResolve ($rootScope, $stateParams, $state) { + // The user should not be able to add tokens for users other than + // themselves. Adding this redirect so that a user is not able to + // visit the add-token URL directly for a different user. + if (_.has($stateParams, 'user_id') && Number($stateParams.user_id) !== $rootScope.current_user.id) { + $state.go('users'); + } +} + +isMeResolve.$inject = [ + '$rootScope', + '$stateParams', + '$state' ]; export default { @@ -30,13 +49,14 @@ export default { label: N_('CREATE TOKEN') }, views: { - 'preFormView@users.edit': { + 'preFormView@users': { templateUrl: addTemplate, controller: AddController, controllerAs: 'vm' } }, resolve: { - resolvedModels: TokensDetailResolve + resolvedModels: TokensDetailResolve, + isMe: isMeResolve } }; diff --git a/awx/ui/client/features/users/tokens/users-tokens-list.controller.js b/awx/ui/client/features/users/tokens/users-tokens-list.controller.js index 056a77792a..4dde61c441 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-list.controller.js +++ b/awx/ui/client/features/users/tokens/users-tokens-list.controller.js @@ -10,13 +10,15 @@ function ListTokensController ( Dataset, strings, ProcessErrors, - Rest, GetBasePath, Prompt, - Wait + Wait, + models ) { const vm = this || {}; + const { token } = models; + vm.strings = strings; vm.activeId = $state.params.token_id; @@ -48,8 +50,8 @@ function ListTokensController ( return undefined; }; - vm.getLastUsed = token => { - const lastUsed = _.get(token, 'last_used'); + vm.getLastUsed = tokenToCheck => { + const lastUsed = _.get(tokenToCheck, 'last_used'); if (!lastUsed) { return undefined; @@ -57,7 +59,7 @@ function ListTokensController ( let html = $filter('longDate')(lastUsed); - const { username, id } = _.get(token, 'summary_fields.last_used', {}); + const { username, id } = _.get(tokenToCheck, 'summary_fields.last_used', {}); if (username && id) { html += ` ${strings.get('add.LAST_USED_LABEL')} ${$filter('sanitize')(username)}`; @@ -70,8 +72,7 @@ function ListTokensController ( const action = () => { $('#prompt-modal').modal('hide'); Wait('start'); - Rest.setUrl(`${GetBasePath('tokens')}${tok.id}`); - Rest.destroy() + token.request('delete', tok.id) .then(() => { let reloadListStateParams = null; @@ -89,14 +90,12 @@ function ListTokensController ( } else { $state.go('.', reloadListStateParams, { reload: true }); } - }) - .catch(({ data, status }) => { + }).catch(({ data, status }) => { ProcessErrors($scope, data, status, null, { hdr: strings.get('error.HEADER'), msg: strings.get('error.CALL', { path: `${GetBasePath('tokens')}${tok.id}`, status }) }); - }) - .finally(() => { + }).finally(() => { Wait('stop'); }); }; @@ -105,7 +104,9 @@ function ListTokensController ( Prompt({ hdr: strings.get('deleteResource.HEADER'), - resourceName: strings.get('list.HEADER', tok.summary_fields.application.name), + resourceName: _.has(tok, 'summary_fields.application.name') ? + strings.get('list.HEADER', tok.summary_fields.application.name) : + strings.get('list.PERSONAL_ACCESS_TOKEN'), body: deleteModalBody, action, actionText: strings.get('add.DELETE_ACTION_LABEL') @@ -120,10 +121,10 @@ ListTokensController.$inject = [ 'Dataset', 'TokensStrings', 'ProcessErrors', - 'Rest', 'GetBasePath', 'Prompt', - 'Wait' + 'Wait', + 'resolvedModels' ]; export default ListTokensController; diff --git a/awx/ui/client/features/users/tokens/users-tokens-list.partial.html b/awx/ui/client/features/users/tokens/users-tokens-list.partial.html index b92d24811a..9018b6a7b8 100644 --- a/awx/ui/client/features/users/tokens/users-tokens-list.partial.html +++ b/awx/ui/client/features/users/tokens/users-tokens-list.partial.html @@ -25,11 +25,14 @@
    + header-value="{{ token.summary_fields.application.name ? + vm.strings.get('list.HEADER', token.summary_fields.application.name) : + vm.strings.get('list.PERSONAL_ACCESS_TOKEN') + }}"> + value="{{ token.description | sanitize }}"> -
    diff --git a/awx/ui/client/legacy/styles/ansible-ui.less b/awx/ui/client/legacy/styles/ansible-ui.less index 6349dfbd61..dcd4a9bcce 100644 --- a/awx/ui/client/legacy/styles/ansible-ui.less +++ b/awx/ui/client/legacy/styles/ansible-ui.less @@ -1256,6 +1256,10 @@ input[type="checkbox"].checkbox-no-label { color:@red; } + .error-border { + border-color:@red; + } + .connecting-color { color: @warning; } @@ -2345,3 +2349,14 @@ body { margin-top: .3em; margin-bottom: .3em; } + +.Toast-wrapper { + display: flex; + max-width: 250px; +} + +.Toast-icon { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/awx/ui/client/legacy/styles/codemirror.less b/awx/ui/client/legacy/styles/codemirror.less index 68317c844d..8050905d31 100644 --- a/awx/ui/client/legacy/styles/codemirror.less +++ b/awx/ui/client/legacy/styles/codemirror.less @@ -37,15 +37,6 @@ // Disabled textarea[disabled="disabled"] + div[id*="-container"]{ - .CodeMirror { - cursor: not-allowed; - } - - .CodeMirror.cm-s-default, - .CodeMirror-line { - background-color: @ebgrey; - } - .CodeMirror-gutters { border-color: @b7grey; } @@ -55,12 +46,4 @@ textarea[disabled="disabled"] + div[id*="-container"]{ background-color: @default-bg; color: @default-interface-txt; } - - .CodeMirror-lines { - cursor: default; - } - - .CodeMirror-cursors { - display: none; - } } diff --git a/awx/ui/client/legacy/styles/lists.less b/awx/ui/client/legacy/styles/lists.less index f7746f8664..28f9440438 100644 --- a/awx/ui/client/legacy/styles/lists.less +++ b/awx/ui/client/legacy/styles/lists.less @@ -147,6 +147,14 @@ table, tbody { color: @list-actn-icn-hov; } +.List-actionButton + .btn-disabled { + &:hover { + color: @default-icon-hov; + background-color: @list-actn-bg !important; + } + color: @default-icon-hov; +} + .List-actionButton--delete:hover { background-color: @list-actn-del-bg-hov !important; } @@ -214,9 +222,11 @@ table, tbody { } .List-actionHolder--leftAlign { - width: 50%; - margin-left: 50%; + margin-left: 52%; justify-content: flex-start; + button { + height: 34px; + } } .List-actions { @@ -388,7 +398,7 @@ table, tbody { } .List-staticColumn--mediumStatus { - width: 51px; + width: 52px; padding-right: 0px!important; } diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index 72c14fe4b5..7beb200a10 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -8,6 +8,8 @@ @import 'popover/_index'; @import 'relaunchButton/_index'; @import 'tabs/_index'; +@import 'tag/_index'; +@import 'toggle-tag/_index'; @import 'truncate/_index'; @import 'utility/_index'; @import 'code-mirror/_index'; diff --git a/awx/ui/client/lib/components/code-mirror/_index.less b/awx/ui/client/lib/components/code-mirror/_index.less index 770546b151..7e6b6dfe83 100644 --- a/awx/ui/client/lib/components/code-mirror/_index.less +++ b/awx/ui/client/lib/components/code-mirror/_index.less @@ -18,6 +18,11 @@ flex: 1 0 auto; } +.atCodeMirror-labelRightSide{ + display: flex; + align-items: center; +} + .atCodeMirror-labelText{ text-transform: uppercase; color: #707070; @@ -74,3 +79,8 @@ margin-left: 10px; } } + +.atCodeMirror-badge{ + display: initial; + margin-right: 20px; +} \ No newline at end of file diff --git a/awx/ui/client/lib/components/code-mirror/code-mirror.directive.js b/awx/ui/client/lib/components/code-mirror/code-mirror.directive.js index 6d74f2a6aa..0c3e908679 100644 --- a/awx/ui/client/lib/components/code-mirror/code-mirror.directive.js +++ b/awx/ui/client/lib/components/code-mirror/code-mirror.directive.js @@ -13,8 +13,12 @@ function atCodeMirrorController ( ParseVariableString ) { const vm = this; - function init (vars) { + if ($scope.disabled === 'true') { + $scope.disabled = true; + } else if ($scope.disabled === 'false') { + $scope.disabled = false; + } $scope.variables = ParseVariableString(_.cloneDeep(vars)); $scope.parseType = ParseType; const options = { diff --git a/awx/ui/client/lib/components/code-mirror/code-mirror.strings.js b/awx/ui/client/lib/components/code-mirror/code-mirror.strings.js index 0dae4bc6a5..3180e908b2 100644 --- a/awx/ui/client/lib/components/code-mirror/code-mirror.strings.js +++ b/awx/ui/client/lib/components/code-mirror/code-mirror.strings.js @@ -9,8 +9,8 @@ function CodeMirrorStrings (BaseString) { VARIABLES: t.s('VARIABLES'), EXPAND: t.s('EXPAND'), YAML: t.s('YAML'), - JSON: t.s('JSON') - + JSON: t.s('JSON'), + READONLY: t.s('READ ONLY') }; ns.tooltip = { diff --git a/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.directive.js b/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.directive.js index 6a1837272c..317fe2749e 100644 --- a/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.directive.js +++ b/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.directive.js @@ -16,8 +16,12 @@ function atCodeMirrorModalController ( ParseVariableString ) { const vm = this; - function resize () { + if ($scope.disabled === 'true') { + $scope.disabled = true; + } else if ($scope.disabled === 'false') { + $scope.disabled = false; + } const editor = $(`${CodeMirrorModalID} .CodeMirror`)[0].CodeMirror; const height = $(ModalHeight).height() - $(ModalHeader).height() - $(ModalFooter).height() - 100; @@ -30,6 +34,11 @@ function atCodeMirrorModalController ( } function init () { + if ($scope.disabled === 'true') { + $scope.disabled = true; + } else if ($scope.disabled === 'false') { + $scope.disabled = false; + } $(CodeMirrorModalID).modal('show'); $scope.extra_variables = ParseVariableString(_.cloneDeep($scope.variables)); $scope.parseType = ParseType; diff --git a/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.partial.html b/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.partial.html index 056672a2b5..86dd87b5a5 100644 --- a/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.partial.html +++ b/awx/ui/client/lib/components/code-mirror/modal/code-mirror-modal.partial.html @@ -42,7 +42,10 @@
    -
    +
    +
    + {{ vm.strings.get('label.READONLY')}} +
    diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index a53da0676f..4eaa0eea64 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -52,6 +52,15 @@ function ComponentsStrings (BaseString) { COPIED: t.s('Copied to clipboard.') }; + ns.toggle = { + VIEW_MORE: t.s('VIEW MORE'), + VIEW_LESS: t.s('VIEW LESS') + }; + + ns.tooltips = { + VIEW_THE_CREDENTIAL: t.s('View the Credential'), + }; + ns.layout = { CURRENT_USER_LABEL: t.s('Logged in as'), VIEW_DOCS: t.s('View Documentation'), @@ -59,7 +68,7 @@ function ComponentsStrings (BaseString) { DASHBOARD: t.s('Dashboard'), JOBS: t.s('Jobs'), SCHEDULES: t.s('Schedules'), - PORTAL_MODE: t.s('Portal Mode'), + MY_VIEW: t.s('My View'), PROJECTS: t.s('Projects'), CREDENTIALS: t.s('Credentials'), CREDENTIAL_TYPES: t.s('Credential Types'), @@ -75,8 +84,8 @@ function ComponentsStrings (BaseString) { INSTANCE_GROUPS: t.s('Instance Groups'), APPLICATIONS: t.s('Applications'), SETTINGS: t.s('Settings'), - FOOTER_ABOUT: t.s('About'), - FOOTER_COPYRIGHT: t.s('Copyright © 2018 Red Hat, Inc.'), + ABOUT: t.s('About'), + COPYRIGHT: t.s('Copyright © 2018 Red Hat, Inc.'), VIEWS_HEADER: t.s('Views'), RESOURCES_HEADER: t.s('Resources'), ACCESS_HEADER: t.s('Access'), diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js index e0ac85c595..97a30911d0 100644 --- a/awx/ui/client/lib/components/form/form.directive.js +++ b/awx/ui/client/lib/components/form/form.directive.js @@ -114,6 +114,12 @@ function AtFormController (eventService, strings) { if (typeof err.data === 'object') { message = JSON.stringify(err.data); + } if (_.has(err, 'data.__all__')) { + if (typeof err.data.__all__ === 'object' && Array.isArray(err.data.__all__)) { + message = JSON.stringify(err.data.__all__[0]); + } else { + message = JSON.stringify(err.data.__all__); + } } else { message = err.data; } diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 35b0e0193d..aa18751d17 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -32,6 +32,8 @@ import sideNav from '~components/layout/side-nav.directive'; import sideNavItem from '~components/layout/side-nav-item.directive'; import tab from '~components/tabs/tab.directive'; import tabGroup from '~components/tabs/group.directive'; +import tag from '~components/tag/tag.directive'; +import toggleTag from '~components/toggle-tag/toggle-tag.directive'; import topNavItem from '~components/layout/top-nav-item.directive'; import truncate from '~components/truncate/truncate.directive'; import atCodeMirror from '~components/code-mirror'; @@ -78,6 +80,8 @@ angular .directive('atSideNavItem', sideNavItem) .directive('atTab', tab) .directive('atTabGroup', tabGroup) + .directive('atTag', tag) + .directive('atToggleTag', toggleTag) .directive('atTopNavItem', topNavItem) .directive('atTruncate', truncate) .service('BaseInputController', BaseInputController) diff --git a/awx/ui/client/lib/components/input/_index.less b/awx/ui/client/lib/components/input/_index.less index 4867ea2242..1cc1c5f21d 100644 --- a/awx/ui/client/lib/components/input/_index.less +++ b/awx/ui/client/lib/components/input/_index.less @@ -221,6 +221,32 @@ padding: 6px @at-padding-input 0 @at-padding-input; } +.at-InputLookup { + display: flex; + + .at-InputLookup-button { + .at-mixin-InputButton(); + border-radius: @at-border-radius 0 0 @at-border-radius; + border-right: none; + flex: 0 0 35px; + height: auto; + min-height: 30px + } + + .at-InputLookup-tagContainer { + .at-mixin-Border; + display: flex; + flex-flow: row wrap; + padding: 0 10px; + width: 100%; + } + + .at-InputLookup-button + .at-Input, + .at-InputLookup-tagContainer { + border-radius: 0 @at-border-radius @at-border-radius 0; + } +} + .at-InputSlider { display: flex; padding: 5px 0; @@ -252,9 +278,10 @@ height: 1px; width: 100%; } + &::-webkit-slider-thumb { -webkit-appearance: none; - background: @at-color-input-slider-thumb; + background-color: @at-color-input-slider-thumb; border-radius: 50%; border: none; cursor: pointer; @@ -262,6 +289,24 @@ margin-top: -7px; width: 16px; } + + } + + input[type=range]::-moz-range-thumb { + -webkit-appearance: none; + background-color: @at-color-input-slider-thumb; + border-radius: 50%; + border: none; + cursor: pointer; + height: 16px; + width: 16px; + } + + input[type=range]::-moz-range-track { + background: @at-color-input-slider-track; + cursor: pointer; + height: 1px; + width: 100%; } input[type=range][disabled] { diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 32fa30b458..107815e49e 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -41,6 +41,10 @@ function BaseInputController (strings) { form.register(type, scope); + if (scope.form && scope.form.disabled) { + scope.state._enableToggle = false; + } + vm.validate = () => { let isValid = true; let message = ''; @@ -94,6 +98,7 @@ function BaseInputController (strings) { scope.state._value = scope.state._preEditValue; scope.state._activeModel = '_displayValue'; scope.state._placeholder = vm.strings.get('ENCRYPTED'); + vm.check(); } else { scope.state._buttonText = vm.strings.get('REVERT'); scope.state._disabled = false; @@ -101,6 +106,10 @@ function BaseInputController (strings) { scope.state._activeModel = '_value'; scope.state._value = ''; scope.state._placeholder = ''; + vm.check(); + } + if (scope.form && scope.form.disabled) { + scope.state._enableToggle = false; } }; diff --git a/awx/ui/client/lib/components/input/group.directive.js b/awx/ui/client/lib/components/input/group.directive.js index 7e6f1f2426..3820d4ab8b 100644 --- a/awx/ui/client/lib/components/input/group.directive.js +++ b/awx/ui/client/lib/components/input/group.directive.js @@ -16,6 +16,7 @@ function AtInputGroupController ($scope, $compile) { let state; let source; let element; + let formId; vm.init = (_scope_, _form_, _element_) => { form = _form_; @@ -23,6 +24,7 @@ function AtInputGroupController ($scope, $compile) { element = _element_; state = scope.state || {}; source = state._source; + ({ formId } = scope); $scope.$watch('state._source._value', vm.update); }; @@ -145,7 +147,7 @@ function AtInputGroupController ($scope, $compile) { const tabindex = Number(scope.tab) + index; const col = input._expand ? 12 : scope.col; const component = angular.element(`<${input._component} col="${col}" tab="${tabindex}" - state="${state._reference}._group[${index}]"> + state="${state._reference}._group[${index}]" id="${formId}_${input.id}_group"> `); $compile(component)(scope.$parent); @@ -183,7 +185,8 @@ function atInputGroup () { scope: { state: '=', col: '@', - tab: '@' + tab: '@', + formId: '@' } }; } diff --git a/awx/ui/client/lib/components/input/lookup.directive.js b/awx/ui/client/lib/components/input/lookup.directive.js index 0447d6b448..13632d8eb3 100644 --- a/awx/ui/client/lib/components/input/lookup.directive.js +++ b/awx/ui/client/lib/components/input/lookup.directive.js @@ -34,23 +34,20 @@ function AtInputLookupController (baseInputController, $q, $state) { } }; - scope.$watch(scope.state._resource, vm.watchResource); + // This should get triggered when the user selects something in the lookup modal and + // hits save to close the modal. This won't get triggered when the user types in + // a value in the input. + scope.$watch('state._idFromModal', () => { + if (scope.state._idFromModal && + (scope.state._idFromModal !== scope.state._value) + ) { + vm.search({ id: scope.state._idFromModal }); + } + }); vm.check(); }; - vm.watchResource = () => { - if (!scope[scope.state._resource]) { - return; - } - - if (scope[scope.state._resource] !== scope.state._value) { - scope.state._displayValue = scope[`${scope.state._resource}_name`]; - - vm.search(); - } - }; - vm.lookup = () => { const params = {}; @@ -62,6 +59,7 @@ function AtInputLookupController (baseInputController, $q, $state) { }; vm.reset = () => { + scope.state._idFromModal = undefined; scope.state._value = undefined; scope[scope.state._resource] = undefined; }; @@ -80,15 +78,20 @@ function AtInputLookupController (baseInputController, $q, $state) { vm.searchAfterDebounce(); }; - vm.search = () => { + vm.search = (searchParams) => { scope.state._touched = true; - if (scope.state._displayValue === '' && !scope.state._required) { + if (!scope.state._required && + scope.state._displayValue === '' && + !scope.state._idFromModal + ) { scope.state._value = null; return vm.check({ isValid: true }); } - return model.search({ [search.key]: scope.state._displayValue }, search.config) + searchParams = searchParams || { [search.key]: scope.state._displayValue }; + + return model.search(searchParams, search.config) .then(found => { if (!found) { vm.reset(); @@ -99,6 +102,7 @@ function AtInputLookupController (baseInputController, $q, $state) { scope[scope.state._resource] = model.get('id'); scope.state._value = model.get('id'); scope.state._displayValue = model.get('name'); + scope.state._idFromModal = undefined; }) .catch(() => vm.reset()) .finally(() => { diff --git a/awx/ui/client/lib/components/input/lookup.partial.html b/awx/ui/client/lib/components/input/lookup.partial.html index 88cd218e9c..5a951cdb31 100644 --- a/awx/ui/client/lib/components/input/lookup.partial.html +++ b/awx/ui/client/lib/components/input/lookup.partial.html @@ -2,14 +2,12 @@
    -
    - - - +
    + -
    -
    - {{ tag.hostname }} - {{ tag }} -
    -
    - -
    + +
    - -
    -
    \ No newline at end of file +
    diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js index f8f6734c0e..637555d57f 100644 --- a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js @@ -73,12 +73,13 @@ function atLaunchTemplateCtrl ( } else if (vm.template.type === 'workflow_job_template') { const selectedWorkflowJobTemplate = workflowTemplate.create(); const preLaunchPromises = [ + selectedWorkflowJobTemplate.request('get', vm.template.id), selectedWorkflowJobTemplate.getLaunch(vm.template.id), selectedWorkflowJobTemplate.optionsLaunch(vm.template.id), ]; Promise.all(preLaunchPromises) - .then(([launchData, launchOptions]) => { + .then(([wfjtData, launchData, launchOptions]) => { if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) { selectedWorkflowJobTemplate .postLaunch({ id: vm.template.id }) @@ -86,6 +87,9 @@ function atLaunchTemplateCtrl ( $state.go('workflowResults', { id: data.workflow_job }, { reload: true }); }); } else { + launchData.data.defaults = { + extra_vars: wfjtData.data.extra_vars + }; const promptData = { launchConf: launchData.data, launchOptions: launchOptions.data, diff --git a/awx/ui/client/lib/components/layout/_index.less b/awx/ui/client/lib/components/layout/_index.less index ad2dc00958..6d5a7a7229 100644 --- a/awx/ui/client/lib/components/layout/_index.less +++ b/awx/ui/client/lib/components/layout/_index.less @@ -109,26 +109,15 @@ color: @at-color-side-nav-content; display: flex; cursor: pointer; - text-transform: uppercase; - font-size: 12px; + font-size: 13px; i { cursor: pointer; color: @at-color-side-nav-item-icon; font-size: @at-font-size-side-nav-icon; padding: @at-padding-side-nav-item-icon; padding-left: @at-padding-left-side-nav-item-icon; - } - - i.fa-cubes { - margin-left: -4px; - } - - i.fa-user { - margin-left: 1px; - } - - i.fa-users { - margin-left: -1px; + text-align: center; + width: 50px; } &:hover, @@ -140,18 +129,6 @@ color: @at-color-side-nav-content; margin-left: @at-highlight-left-border-margin-makeup; } - - i.fa-cubes { - margin-left: -9px; - } - - i.fa-user { - margin-left: -4px; - } - - i.fa-users { - margin-left: -6px; - } } } @@ -195,6 +172,10 @@ padding-left: @at-width-expanded-side-nav; } + + .at-Layout-main--noLicense { + padding-left: 0; + } + .at-Layout-sideNavSpacer--first { display: inherit; } @@ -221,30 +202,13 @@ width: 100%; flex-direction: column; padding-left: @at-width-collapsed-side-nav; + padding-bottom: @at-space-4x; overflow-x: hidden; } &-content { flex: 1px; } - - &-footer { - background-color: @at-color-footer-background; - color: @at-color-footer; - position: relative; - padding-right: @at-padding-footer-right; - padding-bottom: @at-padding-footer-bottom; - margin-top: @at-margin-footer-top; - flex: 0; - display: flex; - align-items: center; - justify-content: flex-end; - - a { - cursor: pointer; - margin-right: @at-margin-after-footer-link; - } - } } @media screen and (max-width: @at-breakpoint-mobile-layout) { diff --git a/awx/ui/client/lib/components/layout/layout.partial.html b/awx/ui/client/lib/components/layout/layout.partial.html index 4714a23172..4acc9fd108 100644 --- a/awx/ui/client/lib/components/layout/layout.partial.html +++ b/awx/ui/client/lib/components/layout/layout.partial.html @@ -14,6 +14,11 @@ {{ $parent.layoutVm.currentUsername }} + + + + + @@ -40,9 +45,9 @@ - + - +
    @@ -70,7 +75,7 @@ -
    +
    {{:: $parent.layoutVm.getString('ADMINISTRATION_HEADER') }} @@ -88,19 +93,15 @@ ng-show="$parent.layoutVm.isSuperUser || $parent.layoutVm.isOrgAdmin"> + ng-show="$parent.layoutVm.isSuperUser || $parent.layoutVm.isOrgAdmin"> - diff --git a/awx/ui/client/lib/components/layout/side-nav-item.directive.js b/awx/ui/client/lib/components/layout/side-nav-item.directive.js index d4b11bf716..73074e91d1 100644 --- a/awx/ui/client/lib/components/layout/side-nav-item.directive.js +++ b/awx/ui/client/lib/components/layout/side-nav-item.directive.js @@ -11,7 +11,7 @@ function AtSideNavItemController ($scope, strings) { if ($scope.name === 'portal mode') { vm.isRoute = (current && current.indexOf('portalMode') === 0); } else if (current && current.indexOf($scope.route) === 0) { - if (current.indexOf('jobs.schedules') === 0 && $scope.route === 'jobs') { + if (current.indexOf('schedules') === 0 && $scope.route === 'jobs') { vm.isRoute = false; } else { vm.isRoute = true; diff --git a/awx/ui/client/lib/components/layout/side-nav.partial.html b/awx/ui/client/lib/components/layout/side-nav.partial.html index 5786c13537..8996df899f 100644 --- a/awx/ui/client/lib/components/layout/side-nav.partial.html +++ b/awx/ui/client/lib/components/layout/side-nav.partial.html @@ -1,7 +1,7 @@
    + ng-class="{'at-Layout-side--expanded': vm.isExpanded && layoutVm.isLoggedIn}" ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing">
    + ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing">
    diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less index 08b39d8f05..4a804086a4 100644 --- a/awx/ui/client/lib/components/list/_index.less +++ b/awx/ui/client/lib/components/list/_index.less @@ -25,6 +25,10 @@ margin-bottom: @at-margin-bottom-list-toolbar; } +.at-List--toolbar-padAbove{ + margin-top: @at-margin-above-list-toolbar +} + .at-List-search { flex: auto; } @@ -129,6 +133,8 @@ grid-template-columns: 120px 1fr; align-items: center; line-height: @at-height-list-row-item; + word-wrap: break-word; + word-break: break-all; } .at-RowItem-status { @@ -167,19 +173,20 @@ .at-RowItem-tagContainer { display: flex; margin-left: @at-margin-left-list-row-item-tag-container; + flex-wrap: wrap; + line-height: initial; } .at-RowItem-tag { - text-transform: uppercase; font-weight: 100; background-color: @at-color-list-row-item-tag-background; border-radius: @at-border-radius; color: @at-color-list-row-item-tag; font-size: @at-font-size-list-row-item-tag; - margin-left: @at-margin-left-list-row-item-tag; - margin-top: @at-margin-top-list-row-item-tag; + margin: @at-space; padding: @at-padding-list-row-item-tag; line-height: @at-line-height-list-row-item-tag; + word-break: keep-all; } .at-RowItem-tag--primary { @@ -188,7 +195,6 @@ } .at-RowItem-tag--header { - height: @at-height-list-row-item-tag; line-height: inherit; } @@ -263,6 +269,12 @@ } } +@media screen and (max-width: @at-breakpoint-instances-wrap) { + .at-Row-items--instances { + margin-bottom: @at-padding-bottom-instances-wrap; + } +} + @media screen and (max-width: @at-breakpoint-compact-list) { .at-Row-actions { flex-direction: column; diff --git a/awx/ui/client/lib/components/list/row-action.directive.js b/awx/ui/client/lib/components/list/row-action.directive.js index 18b1835499..7cc7f44a04 100644 --- a/awx/ui/client/lib/components/list/row-action.directive.js +++ b/awx/ui/client/lib/components/list/row-action.directive.js @@ -7,7 +7,8 @@ function atRowAction () { transclude: true, templateUrl, scope: { - icon: '@' + icon: '@', + tooltip: '@' } }; } diff --git a/awx/ui/client/lib/components/list/row-action.partial.html b/awx/ui/client/lib/components/list/row-action.partial.html index 58a7479769..b26301f198 100644 --- a/awx/ui/client/lib/components/list/row-action.partial.html +++ b/awx/ui/client/lib/components/list/row-action.partial.html @@ -1,4 +1,7 @@
    + ng-class="{'at-RowAction--danger': (icon === 'fa-trash' || icon === 'fa-times')}" + aw-tool-tip="{{tooltip}}" + data-tip-watch="tooltip" + data-placement="top">
    diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html index 7698ba85a9..23de062d4f 100644 --- a/awx/ui/client/lib/components/list/row-item.partial.html +++ b/awx/ui/client/lib/components/list/row-item.partial.html @@ -1,12 +1,16 @@
    - - +
    + +
    {{ headerValue }} @@ -27,7 +31,7 @@ {{ labelValue }}
    {{ value }} @@ -42,17 +46,6 @@ template-type="smartStatus.type" ng-if="smartStatus">
    -
    - - - - - - - - - {{ tag.name }} -
    +
    diff --git a/awx/ui/client/lib/components/list/row.directive.js b/awx/ui/client/lib/components/list/row.directive.js index 98805a1cf7..e64ed8ff88 100644 --- a/awx/ui/client/lib/components/list/row.directive.js +++ b/awx/ui/client/lib/components/list/row.directive.js @@ -7,7 +7,6 @@ function atRow () { transclude: true, templateUrl, scope: { - templateId: '@', invalid: '=', invalidTooltip: '=' } diff --git a/awx/ui/client/lib/components/list/row.partial.html b/awx/ui/client/lib/components/list/row.partial.html index dc6dfbd9f0..ce1c506e5c 100644 --- a/awx/ui/client/lib/components/list/row.partial.html +++ b/awx/ui/client/lib/components/list/row.partial.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/awx/ui/client/lib/components/panel/_index.less b/awx/ui/client/lib/components/panel/_index.less index b89faeb405..bf3bf2ab61 100644 --- a/awx/ui/client/lib/components/panel/_index.less +++ b/awx/ui/client/lib/components/panel/_index.less @@ -5,12 +5,14 @@ } .at-Panel-heading { - margin: 0; + margin-bottom: 20px; padding: 0; } .at-Panel-headingRow { - margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; } .at-Panel-dismiss { @@ -42,7 +44,13 @@ vertical-align: middle; white-space: nowrap; text-align: center; - margin-left: 5px; + margin-left: 10px; + margin-right: auto; +} + +.at-Panel-headingCustomContent { + display: flex; + flex: 1; } .at-Panel-label { diff --git a/awx/ui/client/lib/components/panel/heading.directive.js b/awx/ui/client/lib/components/panel/heading.directive.js index 8850c10c2f..34296e7f94 100644 --- a/awx/ui/client/lib/components/panel/heading.directive.js +++ b/awx/ui/client/lib/components/panel/heading.directive.js @@ -12,7 +12,11 @@ function atPanelHeading () { replace: true, transclude: true, templateUrl, - link + link, + scope: { + title: '@', + badge: '@?' + } }; } diff --git a/awx/ui/client/lib/components/panel/heading.partial.html b/awx/ui/client/lib/components/panel/heading.partial.html index e57e6a880d..186435588c 100644 --- a/awx/ui/client/lib/components/panel/heading.partial.html +++ b/awx/ui/client/lib/components/panel/heading.partial.html @@ -1,20 +1,27 @@ -
    -
    +

    - + {{ title }}

    -
    -
    + + {{ badge }} + + +
    +
    -

    - + {{ title }}

    + + {{ badge }} +
    diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index 561664185b..4064340d74 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -23,6 +23,13 @@ function atRelaunchCtrl ( const jobObj = new Job(); const jobTemplate = new JobTemplate(); + const transitionOptions = { reload: true }; + + if ($state.includes('output')) { + transitionOptions.inherit = false; + transitionOptions.location = 'replace'; + } + const updateTooltip = () => { if (vm.job.type === 'job' && vm.job.status === 'failed') { vm.tooltip = strings.get('relaunch.HOSTS'); @@ -74,7 +81,7 @@ function atRelaunchCtrl ( }, launchOptions: launchOptions.data, job: vm.job.id, - relaunchHostType: option ? (option.name).toLowerCase() : null, + relaunchHostType: option ? (option.value) : null, prompts: { credentials: { value: populatedJob.summary_fields.credentials ? @@ -118,9 +125,9 @@ function atRelaunchCtrl ( id: vm.job.id, }; - if (_.has(option, 'name')) { + if (_.has(option, 'value')) { launchParams.relaunchData = { - hosts: (option.name).toLowerCase() + hosts: option.value }; } @@ -128,7 +135,7 @@ function atRelaunchCtrl ( .then((launchRes) => { if (!$state.is('jobs')) { const relaunchType = launchRes.data.type === 'job' ? 'playbook' : launchRes.data.type; - $state.go('output', { id: launchRes.data.id, type: relaunchType }, { reload: true }); + $state.go('output', { id: launchRes.data.id, type: relaunchType }, transitionOptions); } }).catch(({ data, status, config }) => { ProcessErrors($scope, data, status, null, { @@ -148,11 +155,13 @@ function atRelaunchCtrl ( vm.dropdownOptions = [ { name: strings.get('relaunch.ALL'), - icon: 'icon-host-all' + icon: 'icon-host-all', + value: 'all' }, { name: strings.get('relaunch.FAILED'), - icon: 'icon-host-failed' + icon: 'icon-host-failed', + value: 'failed' } ]; @@ -173,7 +182,7 @@ function atRelaunchCtrl ( inventorySource.postUpdate(vm.job.inventory_source) .then((postUpdateRes) => { if (!$state.is('jobs')) { - $state.go('output', { id: postUpdateRes.data.id, type: 'inventory' }, { reload: true }); + $state.go('output', { id: postUpdateRes.data.id, type: 'inventory' }, transitionOptions); } }).catch(({ data, status, config }) => { ProcessErrors($scope, data, status, null, { @@ -197,7 +206,7 @@ function atRelaunchCtrl ( project.postUpdate(vm.job.project) .then((postUpdateRes) => { if (!$state.is('jobs')) { - $state.go('output', { id: postUpdateRes.data.id, type: 'project' }, { reload: true }); + $state.go('output', { id: postUpdateRes.data.id, type: 'project' }, transitionOptions); } }).catch(({ data, status, config }) => { ProcessErrors($scope, data, status, null, { @@ -219,7 +228,7 @@ function atRelaunchCtrl ( id: vm.job.id }).then((launchRes) => { if (!$state.is('jobs')) { - $state.go('workflowResults', { id: launchRes.data.id }, { reload: true }); + $state.go('workflowResults', { id: launchRes.data.id }, transitionOptions); } }).catch(({ data, status, config }) => { ProcessErrors($scope, data, status, null, { @@ -243,7 +252,7 @@ function atRelaunchCtrl ( id: vm.job.id }).then((launchRes) => { if (!$state.is('jobs')) { - $state.go('output', { id: launchRes.data.id, type: 'command' }, { reload: true }); + $state.go('output', { id: launchRes.data.id, type: 'command' }, transitionOptions); } }).catch(({ data, status, config }) => { ProcessErrors($scope, data, status, null, { @@ -268,7 +277,7 @@ function atRelaunchCtrl ( relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData) }).then((launchRes) => { if (!$state.is('jobs')) { - $state.go('output', { id: launchRes.data.job, type: 'playbook' }, { reload: true }); + $state.go('output', { id: launchRes.data.job, type: 'playbook' }, transitionOptions); } }).catch(({ data, status }) => { ProcessErrors($scope, data, status, null, { diff --git a/awx/ui/client/lib/components/tabs/_index.less b/awx/ui/client/lib/components/tabs/_index.less index e999a75613..f022a12c43 100644 --- a/awx/ui/client/lib/components/tabs/_index.less +++ b/awx/ui/client/lib/components/tabs/_index.less @@ -27,10 +27,6 @@ } } -.at-TabGroup + .at-Panel-body { - margin-top: 20px; -} - .at-TabGroup--padBelow { margin-bottom: 20px; } diff --git a/awx/ui/client/lib/components/tag/_index.less b/awx/ui/client/lib/components/tag/_index.less new file mode 100644 index 0000000000..5834c5208d --- /dev/null +++ b/awx/ui/client/lib/components/tag/_index.less @@ -0,0 +1,71 @@ +.TagComponent { + color: @at-white; + cursor: default; + background: @at-blue; + border-radius: @at-space; + font-size: 12px; + display: flex; + flex-direction: row; + align-content: center; + min-height: @at-space-4x; + overflow: hidden; + max-width: 200px; + margin: @at-space; +} + +.TagComponent-name { + color: @at-white; + margin: 2px @at-space-2x; + align-self: center; + word-break: break-word; + + &:hover, + &:focus { + color: @at-white; + } +} + +.TagComponent-icon { + .at-mixin-VerticallyCenter(); + line-height: 20px; + margin-left: @at-space-2x; + + &--cloud:before, + &--aws:before, + &--tower:before, + &--azure_rm:before, + { + content: '\f0c2'; + } + + &--insights:before { + content: '\f129'; + } + + &--net:before { + content: '\f0e8'; + } + + &--scm:before { + content: '\f126'; + } + + &--ssh:before { + content: '\f084'; + } + + &--vault:before { + content: '\f187'; + } +} + +.TagComponent-button { + padding: 0 @at-space; + .at-mixin-VerticallyCenter(); +} + +.TagComponent-button:hover { + cursor: pointer; + border-color: @at-color-error; + background-color: @at-color-error; +} diff --git a/awx/ui/client/lib/components/tag/tag.directive.js b/awx/ui/client/lib/components/tag/tag.directive.js new file mode 100644 index 0000000000..f18ae9dce9 --- /dev/null +++ b/awx/ui/client/lib/components/tag/tag.directive.js @@ -0,0 +1,18 @@ +const templateUrl = require('~components/tag/tag.partial.html'); + +function atTag () { + return { + restrict: 'E', + replace: true, + transclude: true, + templateUrl, + scope: { + tag: '=', + icon: '@?', + link: '@?', + removeTag: '&?', + }, + }; +} + +export default atTag; diff --git a/awx/ui/client/lib/components/tag/tag.partial.html b/awx/ui/client/lib/components/tag/tag.partial.html new file mode 100644 index 0000000000..f16da4da42 --- /dev/null +++ b/awx/ui/client/lib/components/tag/tag.partial.html @@ -0,0 +1,8 @@ +
    +
    + {{ tag }} +
    {{ tag }}
    +
    + +
    +
    \ No newline at end of file diff --git a/awx/ui/client/lib/components/toggle-tag/_index.less b/awx/ui/client/lib/components/toggle-tag/_index.less new file mode 100644 index 0000000000..18639c5648 --- /dev/null +++ b/awx/ui/client/lib/components/toggle-tag/_index.less @@ -0,0 +1,19 @@ +.ToggleComponent-wrapper { + line-height: initial; +} + +.ToggleComponent-container { + display: flex; + flex-wrap: wrap; +} + +.ToggleComponent-button { + border: none; + background: transparent; + color: @at-blue; + font-size: @at-font-size; + + &:hover { + color: @at-blue-hover; + } +} \ No newline at end of file diff --git a/awx/ui/client/lib/components/toggle-tag/constants.js b/awx/ui/client/lib/components/toggle-tag/constants.js new file mode 100644 index 0000000000..f3a4951c9c --- /dev/null +++ b/awx/ui/client/lib/components/toggle-tag/constants.js @@ -0,0 +1,2 @@ +export const TRUNCATE_LENGTH = 5; +export const IS_TRUNCATED = true; diff --git a/awx/ui/client/lib/components/toggle-tag/toggle-tag.directive.js b/awx/ui/client/lib/components/toggle-tag/toggle-tag.directive.js new file mode 100644 index 0000000000..4fec4cd794 --- /dev/null +++ b/awx/ui/client/lib/components/toggle-tag/toggle-tag.directive.js @@ -0,0 +1,32 @@ +import { IS_TRUNCATED, TRUNCATE_LENGTH } from './constants'; + +const templateUrl = require('~components/toggle-tag/toggle-tag.partial.html'); + +function controller (strings) { + const vm = this; + vm.truncatedLength = TRUNCATE_LENGTH; + vm.isTruncated = IS_TRUNCATED; + vm.strings = strings; + + vm.toggle = () => { + vm.isTruncated = !vm.isTruncated; + }; +} + +controller.$inject = ['ComponentsStrings']; + +function atToggleTag () { + return { + restrict: 'E', + replace: true, + transclude: true, + controller, + controllerAs: 'vm', + templateUrl, + scope: { + tags: '=', + }, + }; +} + +export default atToggleTag; diff --git a/awx/ui/client/lib/components/toggle-tag/toggle-tag.partial.html b/awx/ui/client/lib/components/toggle-tag/toggle-tag.partial.html new file mode 100644 index 0000000000..fada04154e --- /dev/null +++ b/awx/ui/client/lib/components/toggle-tag/toggle-tag.partial.html @@ -0,0 +1,21 @@ +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    diff --git a/awx/ui/client/lib/models/AdHocCommand.js b/awx/ui/client/lib/models/AdHocCommand.js index 7bea2677ac..9f259a929a 100644 --- a/awx/ui/client/lib/models/AdHocCommand.js +++ b/awx/ui/client/lib/models/AdHocCommand.js @@ -19,17 +19,12 @@ function postRelaunch (params) { return $http(req); } -function getStats () { - return Promise.resolve(null); -} - function AdHocCommandModel (method, resource, config) { BaseModel.call(this, 'ad_hoc_commands'); this.Constructor = AdHocCommandModel; this.postRelaunch = postRelaunch.bind(this); this.getRelaunch = getRelaunch.bind(this); - this.getStats = getStats.bind(this); return this.create(method, resource, config); } diff --git a/awx/ui/client/lib/models/InventoryUpdate.js b/awx/ui/client/lib/models/InventoryUpdate.js index 668a05459d..37951911d7 100644 --- a/awx/ui/client/lib/models/InventoryUpdate.js +++ b/awx/ui/client/lib/models/InventoryUpdate.js @@ -1,14 +1,8 @@ let BaseModel; -function getStats () { - return Promise.resolve(null); -} - function InventoryUpdateModel (method, resource, config) { BaseModel.call(this, 'inventory_updates'); - this.getStats = getStats.bind(this); - this.Constructor = InventoryUpdateModel; return this.create(method, resource, config); diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js index 1e466b0e6d..7e2f017826 100644 --- a/awx/ui/client/lib/models/Job.js +++ b/awx/ui/client/lib/models/Job.js @@ -23,31 +23,6 @@ function postRelaunch (params) { return $http(req); } -function getStats () { - if (!this.has('GET', 'id')) { - return Promise.reject(new Error('No property, id, exists')); - } - - if (!this.has('GET', 'related.job_events')) { - return Promise.reject(new Error('No related property, job_events, exists')); - } - - const req = { - method: 'GET', - url: `${this.path}${this.get('id')}/job_events/`, - params: { event: 'playbook_on_stats' }, - }; - - return $http(req) - .then(({ data }) => { - if (data.results.length > 0) { - return data.results[0]; - } - - return null; - }); -} - function getCredentials (id) { const req = { method: 'GET', @@ -64,7 +39,6 @@ function JobModel (method, resource, config) { this.postRelaunch = postRelaunch.bind(this); this.getRelaunch = getRelaunch.bind(this); - this.getStats = getStats.bind(this); this.getCredentials = getCredentials.bind(this); return this.create(method, resource, config); diff --git a/awx/ui/client/lib/models/ProjectUpdate.js b/awx/ui/client/lib/models/ProjectUpdate.js index df038283cf..a8b1ae1fe9 100644 --- a/awx/ui/client/lib/models/ProjectUpdate.js +++ b/awx/ui/client/lib/models/ProjectUpdate.js @@ -1,50 +1,20 @@ -let $http; let BaseModel; -function getStats () { - if (!this.has('GET', 'id')) { - return Promise.reject(new Error('No property, id, exists')); - } - - if (!this.has('GET', 'related.events')) { - return Promise.reject(new Error('No related property, events, exists')); - } - - const req = { - method: 'GET', - url: `${this.path}${this.get('id')}/events/`, - params: { event: 'playbook_on_stats' }, - }; - - return $http(req) - .then(({ data }) => { - if (data.results.length > 0) { - return data.results[0]; - } - - return null; - }); -} - function ProjectUpdateModel (method, resource, config) { BaseModel.call(this, 'project_updates'); - this.getStats = getStats.bind(this); - this.Constructor = ProjectUpdateModel; return this.create(method, resource, config); } -function ProjectUpdateModelLoader (_$http_, _BaseModel_) { - $http = _$http_; +function ProjectUpdateModelLoader (_BaseModel_) { BaseModel = _BaseModel_; return ProjectUpdateModel; } ProjectUpdateModelLoader.$inject = [ - '$http', 'BaseModel' ]; diff --git a/awx/ui/client/lib/models/SystemJob.js b/awx/ui/client/lib/models/SystemJob.js index 1f1f1c5ee3..cc092ff8f4 100644 --- a/awx/ui/client/lib/models/SystemJob.js +++ b/awx/ui/client/lib/models/SystemJob.js @@ -1,14 +1,8 @@ let BaseModel; -function getStats () { - return Promise.resolve(null); -} - function SystemJobModel (method, resource, config) { BaseModel.call(this, 'system_jobs'); - this.getStats = getStats.bind(this); - this.Constructor = SystemJobModel; return this.create(method, resource, config); diff --git a/awx/ui/client/lib/models/Token.js b/awx/ui/client/lib/models/Token.js new file mode 100644 index 0000000000..c4dfaf1937 --- /dev/null +++ b/awx/ui/client/lib/models/Token.js @@ -0,0 +1,26 @@ +let Base; + +function setDependentResources () { + this.dependentResources = []; +} + +function TokenModel (method, resource, config) { + Base.call(this, 'tokens'); + + this.Constructor = TokenModel; + this.setDependentResources = setDependentResources.bind(this); + + return this.create(method, resource, config); +} + +function TokenModelLoader (BaseModel) { + Base = BaseModel; + + return TokenModel; +} + +TokenModelLoader.$inject = [ + 'BaseModel', +]; + +export default TokenModelLoader; diff --git a/awx/ui/client/lib/models/User.js b/awx/ui/client/lib/models/User.js new file mode 100644 index 0000000000..da521255cb --- /dev/null +++ b/awx/ui/client/lib/models/User.js @@ -0,0 +1,40 @@ +let Base; +let $http; + +function postAuthorizedTokens (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/authorized_tokens/` + }; + + if (params.payload) { + req.data = params.payload; + } + + return $http(req); +} + +function UserModel (method, resource, config) { + Base.call(this, 'users'); + + this.Constructor = UserModel; + this.postAuthorizedTokens = postAuthorizedTokens.bind(this); + + this.model.launch = {}; + + return this.create(method, resource, config); +} + +function UserModelLoader (BaseModel, _$http_) { + Base = BaseModel; + $http = _$http_; + + return UserModel; +} + +UserModelLoader.$inject = [ + 'BaseModel', + '$http' +]; + +export default UserModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index d50c825e22..a851d1f29f 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -22,11 +22,13 @@ import Project from '~models/Project'; import Schedule from '~models/Schedule'; import ProjectUpdate from '~models/ProjectUpdate'; import SystemJob from '~models/SystemJob'; +import Token from '~models/Token'; import UnifiedJobTemplate from '~models/UnifiedJobTemplate'; import WorkflowJob from '~models/WorkflowJob'; import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode'; import UnifiedJob from '~models/UnifiedJob'; +import User from '~models/User'; import ModelsStrings from '~models/models.strings'; @@ -59,10 +61,12 @@ angular .service('UnifiedJobModel', UnifiedJob) .service('ProjectUpdateModel', ProjectUpdate) .service('SystemJobModel', SystemJob) + .service('TokenModel', Token) .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) + .service('UserModel', User) .service('ModelsStrings', ModelsStrings); export default MODULE_NAME; diff --git a/awx/ui/client/lib/services/base-string.service.js b/awx/ui/client/lib/services/base-string.service.js index cab067700e..0956c7c5a1 100644 --- a/awx/ui/client/lib/services/base-string.service.js +++ b/awx/ui/client/lib/services/base-string.service.js @@ -1,6 +1,7 @@ import defaults from '~assets/default.strings.json'; let i18n; +let $filter; function BaseStringService (namespace) { const ERROR_NO_NAMESPACE = 'BaseString cannot be extended without providing a namespace'; @@ -58,6 +59,7 @@ function BaseStringService (namespace) { * the project. */ this.CANCEL = t.s('CANCEL'); + this.CLOSE = t.s('CLOSE'); this.SAVE = t.s('SAVE'); this.OK = t.s('OK'); this.NEXT = t.s('NEXT'); @@ -70,6 +72,8 @@ function BaseStringService (namespace) { this.DELETE = t.s('DELETE'); this.COPY = t.s('COPY'); this.YES = t.s('YES'); + this.CLOSE = t.s('CLOSE'); + this.SUCCESSFUL_CREATION = resource => t.s('{{ resource }} successfully created', { resource: $filter('sanitize')(resource) }); this.deleteResource = { HEADER: t.s('Delete'), @@ -90,6 +94,12 @@ function BaseStringService (namespace) { CALL: ({ path, action, status }) => t.s('Call to {{ path }} failed. {{ action }} returned status: {{ status }}.', { path, action, status }), }; + this.listActions = { + COPY: resourceType => t.s('Copy {{resourceType}}', { resourceType }), + DELETE: resourceType => t.s('Delete the {{resourceType}}', { resourceType }), + CANCEL: resourceType => t.s('Cancel the {{resourceType}}', { resourceType }) + }; + this.ALERT = ({ header, body }) => t.s('{{ header }} {{ body }}', { header, body }); /** @@ -126,12 +136,13 @@ function BaseStringService (namespace) { }; } -function BaseStringServiceLoader (_i18n_) { +function BaseStringServiceLoader (_i18n_, _$filter_) { i18n = _i18n_; + $filter = _$filter_; return BaseStringService; } -BaseStringServiceLoader.$inject = ['i18n']; +BaseStringServiceLoader.$inject = ['i18n', '$filter']; export default BaseStringServiceLoader; diff --git a/awx/ui/client/lib/theme/_mixins.less b/awx/ui/client/lib/theme/_mixins.less index 701613a2ec..3c3f0c66a5 100644 --- a/awx/ui/client/lib/theme/_mixins.less +++ b/awx/ui/client/lib/theme/_mixins.less @@ -20,6 +20,12 @@ padding: 0; } +.at-mixin-Border (@color: @at-border-default-color) { + border-width: @at-border-default-width; + border-style: @at-border-default-style; + border-color: @color +} + .at-mixin-Button () { border-radius: @at-border-radius; height: @at-height-input; @@ -102,4 +108,10 @@ .at-mixin-FontFixedWidth () { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + +.at-mixin-VerticallyCenter () { + display: flex; + flex-direction: column; + justify-content: center; } \ No newline at end of file diff --git a/awx/ui/client/lib/theme/_variables.less b/awx/ui/client/lib/theme/_variables.less index 89261e5dc9..284929253e 100644 --- a/awx/ui/client/lib/theme/_variables.less +++ b/awx/ui/client/lib/theme/_variables.less @@ -177,8 +177,6 @@ @at-color-side-nav-item-icon: @at-white; @at-color-side-nav-item-spacer: @at-gray-d7; @at-color-side-nav-space-collapsed-border: @at-gray-b7; -@at-color-footer-background: @at-gray-fc; -@at-color-footer: @at-gray-70; @at-color-list-empty-border: @at-gray-d7; @at-color-list-empty-background: @at-gray-f6; @@ -241,12 +239,11 @@ @at-padding-left-side-nav-item-icon: 10px; @at-padding-left-side-nav-item-icon-expanded: 15px; @at-padding-between-side-nav-icon-text: @at-space-3x; -@at-padding-footer-right: @at-space-4x; -@at-padding-footer-bottom: @at-space-4x; @at-padding-list-empty: @at-space-2x; -@at-padding-list-row-item-tag: 0 @at-space-2x; +@at-padding-list-row-item-tag: 3px 9px; @at-padding-list-row-action: 7px; @at-padding-list-row: 10px 20px 10px 10px; +@at-padding-bottom-instances-wrap: 30px; @at-margin-input-message: @at-space; @at-margin-item-column: @at-space-3x; @@ -259,13 +256,12 @@ @at-margin-top-nav-item-between-icon-and-name: @at-space-2x; @at-margin-top-nav-item-icon-socket-top-makeup: -3px; @at-margin-side-nav-space-collapsed: 5px 0; -@at-margin-after-footer-link: @at-space; -@at-margin-footer-top: @at-space-4x; @at-margin-top-search-key: @at-space-2x; @at-margin-top-list: @at-space-4x; @at-margin-bottom-list-toolbar: @at-space-4x; +@at-margin-above-list-toolbar: @at-space-4x; @at-margin-left-toolbar-action: @at-space-4x; @at-margin-left-toolbar-carat: @at-space; @at-margin-bottom-list-header: @at-space; @@ -293,14 +289,13 @@ @at-height-list-empty: 200px; @at-height-toolbar-action: 30px; @at-height-list-row-item: 27px; -@at-height-list-row-item-tag: 15px; @at-height-list-row-action: 30px; @at-height-side-nav-toggle-mobile: 40px; @at-width-input-button-sm: 72px; @at-width-input-button-md: 84px; -@at-width-collapsed-side-nav: 50px; -@at-width-expanded-side-nav: 180px; +@at-width-collapsed-side-nav: 55px; +@at-width-expanded-side-nav: 190px; @at-width-list-row-item-label: 120px; @at-width-list-row-action: 30px; @at-width-side-nav-toggle-mobile: 50px; @@ -315,6 +310,10 @@ // 5. Misc ---------------------------------------------------------------------------------------- @at-border-radius: 5px; +@at-border-default-style: solid; +@at-border-default-width: 1px; +@at-border-default-color: @at-gray-b7; +@at-border-style-list-active-indicator: 5px solid @at-color-info; @at-popover-maxwidth: 320px; @at-line-height-short: 0.9; @at-line-height-tall: 2; @@ -323,12 +322,10 @@ @at-highlight-left-border-margin-makeup: -5px; @at-z-index-nav: 1040; @at-z-index-side-nav: 1030; -@at-z-index-footer: 1020; -@at-border-default-width: 1px; -@at-border-style-list-active-indicator: 5px solid @at-color-info; @at-line-height-list-row-item-tag: 22px; // 6. Breakpoints --------------------------------------------------------------------------------- @at-breakpoint-mobile-layout: @at-breakpoint-sm; @at-breakpoint-compact-list: @at-breakpoint-sm; +@at-breakpoint-instances-wrap: 1036px; diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index caf02e2882..29b4c987c1 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -90,7 +90,6 @@ @import '../../src/notifications/notifications.block.less'; @import '../../src/organizations/linkout/addUsers/addUsers.block.less'; @import '../../src/organizations/orgcards.block.less'; -@import '../../src/portal-mode/portal-mode.block.less'; @import '../../src/scheduler/repeatFrequencyOptions.block.less'; @import '../../src/scheduler/schedulerForm.block.less'; @import '../../src/scheduler/schedulerFormDetail.block.less'; @@ -167,9 +166,3 @@ * the transition. */ @import '_resets'; - -/** - * Network Visualization Style - * - */ -@import '../../src/network-ui/style.less'; diff --git a/awx/ui/client/src/about/about.block.less b/awx/ui/client/src/about/about.block.less index d4a1788564..3b9a0ad00a 100644 --- a/awx/ui/client/src/about/about.block.less +++ b/awx/ui/client/src/about/about.block.less @@ -58,7 +58,3 @@ .About-ansibleVersion { color: @default-data-txt; } - -.Copyright-text{ - opacity: @copyright-text; -} diff --git a/awx/ui/client/src/about/about.controller.js b/awx/ui/client/src/about/about.controller.js index 992279aedc..868adf01dc 100644 --- a/awx/ui/client/src/about/about.controller.js +++ b/awx/ui/client/src/about/about.controller.js @@ -1,5 +1,5 @@ -export default ['$rootScope', '$scope', '$state', 'ConfigService', - ($rootScope, $scope, $state, ConfigService) => { +export default ['$rootScope', '$scope', '$location', 'ConfigService', 'lastPath', + ($rootScope, $scope, $location, ConfigService, lastPath) => { ConfigService.getConfig() .then(function(config){ @@ -10,7 +10,7 @@ export default ['$rootScope', '$scope', '$state', 'ConfigService', $('#about-modal').modal('show'); }); - $('#about-modal').on('hidden.bs.modal', () => $state.go('dashboard')); + $('#about-modal').on('hidden.bs.modal', () => $location.url(lastPath)); function createSpeechBubble (brand, version) { let text = `${brand} ${version}`; diff --git a/awx/ui/client/src/about/about.route.js b/awx/ui/client/src/about/about.route.js index e063530c7e..e8fb6b0d10 100644 --- a/awx/ui/client/src/about/about.route.js +++ b/awx/ui/client/src/about/about.route.js @@ -9,6 +9,11 @@ export default { ncyBreadcrumb: { label: N_("ABOUT") }, + resolve: { + lastPath: function($location) { + return $location.url(); + } + }, onExit: function(){ // hacky way to handle user browsing away via URL bar $('.modal-backdrop').remove(); diff --git a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js index 450ab9c997..c0656ebf79 100644 --- a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.controller.js @@ -25,9 +25,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr // the object permissions are being added to scope.object = scope.resourceData.data; // array for all possible roles for the object - scope.roles = _.omit(scope.object.summary_fields.object_roles, (key) => { - return key.name === 'Read'; - }); + scope.roles = scope.object.summary_fields.object_roles; // TODO: get working with api // array w roles and descriptions for key diff --git a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html index a01030455b..46b50b6e06 100644 --- a/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html +++ b/awx/ui/client/src/access/add-rbac-resource/rbac-resource.partial.html @@ -67,7 +67,7 @@
    + ng-repeat="key in roleKey">
    {{ key.name }}
    diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js index fb1f7b20db..7c1f53cc4c 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.controller.js @@ -78,7 +78,7 @@ function(scope, $state, i18n, CreateSelect2, Rest, $q, Wait, ProcessErrors) { // aggregate name/descriptions for each available role, based on resource type // reasoning: function aggregateKey(item, type){ - _.merge(scope.keys[type], _.omit(item.summary_fields.object_roles, 'read_role')); + _.merge(scope.keys[type], item.summary_fields.object_roles); } scope.closeModal = function() { diff --git a/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js b/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js index 39b083f06c..c8333b9434 100644 --- a/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js +++ b/awx/ui/client/src/access/rbac-multiselect/permissionsUsers.list.js @@ -16,6 +16,12 @@ index: false, hover: true, emptyListText : i18n._('No Users exist'), + disableRow: "{{ user.summary_fields.user_capabilities.edit === false }}", + disableRowValue: "user.summary_fields.user_capabilities.edit === false", + disableTooltip: { + placement: 'top', + tipWatch: 'user.tooltip' + }, fields: { first_name: { label: i18n._('First Name'), diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index 8de176322c..175ae7452a 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -7,10 +7,10 @@ /* jshint unused: vars */ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateList', 'ProjectList', 'InventoryList', 'CredentialList', '$compile', 'generateList', - 'OrganizationList', '$window', + 'OrganizationList', '$window', 'i18n', function(addPermissionsTeamsList, addPermissionsUsersList, TemplateList, ProjectList, InventoryList, CredentialList, $compile, generateList, - OrganizationList, $window) { + OrganizationList, $window, i18n) { return { restrict: 'E', scope: { @@ -88,6 +88,7 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11'; break; case 'Users': + list.querySet = { order_by: 'username', page_size: '5' }; list.fields = { username: list.fields.username, first_name: list.fields.first_name, @@ -159,6 +160,22 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL // iterate over the list and add fields like type label, after the // OPTIONS request returns, or the list is sorted/paginated/searched function optionsRequestDataProcessing(){ + if(scope.list.name === 'users'){ + if (scope[list.name] !== undefined) { + scope[`${list.iterator}_queryset`] = list.querySet; + scope[list.name].forEach(function(item, item_idx) { + var itm = scope[list.name][item_idx]; + if(itm.summary_fields.user_capabilities.edit){ + // undefined doesn't render the tooltip, + // which is intended here. + itm.tooltip = undefined; + } + else if(!itm.summary_fields.user_capabilities.edit){ + itm.tooltip = i18n._('You do not have permission to manage this user'); + } + }); + } + } if(scope.list.name === 'projects'){ if (scope[list.name] !== undefined) { scope[list.name].forEach(function(item, item_idx) { diff --git a/awx/ui/client/src/activity-stream/activitystream.controller.js b/awx/ui/client/src/activity-stream/activitystream.controller.js index 73c8f9c732..febc257786 100644 --- a/awx/ui/client/src/activity-stream/activitystream.controller.js +++ b/awx/ui/client/src/activity-stream/activitystream.controller.js @@ -53,8 +53,12 @@ export default ['$scope', '$state', 'subTitle', 'GetTargetTitle', $scope.activities.forEach(function(activity, i) { // build activity.user if ($scope.activities[i].summary_fields.actor) { - $scope.activities[i].user = "" + - $scope.activities[i].summary_fields.actor.username + ""; + if ($scope.activities[i].summary_fields.actor.id) { + $scope.activities[i].user = "" + + $scope.activities[i].summary_fields.actor.username + ""; + } else { + $scope.activities[i].user = $scope.activities[i].summary_fields.actor.username + ' (deleted)'; + } } else { $scope.activities[i].user = 'system'; } diff --git a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js index 97da2ba5e3..0803a957c4 100644 --- a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js +++ b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js @@ -51,6 +51,19 @@ export default function BuildAnchor($log, $filter) { throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'}; } break; + case 'setting': + if (activity.summary_fields.setting[0].category === 'jobs' || + activity.summary_fields.setting[0].category === 'ui') { + url += `configuration/${activity.summary_fields.setting[0].category}`; + } + else if (activity.summary_fields.setting[0].category === 'system' || + activity.summary_fields.setting[0].category === 'logging') { + url += `configuration/system`; + } + else { + url += `configuration/auth`; + } + break; case 'notification_template': url += `notification_templates/${obj.id}`; break; @@ -68,6 +81,10 @@ export default function BuildAnchor($log, $filter) { case 'label': url = null; break; + case 'inventory_source': + const inventoryId = _.get(obj, 'inventory', '').split('-').reverse()[0]; + url += `inventories/inventory/${inventoryId}/inventory_sources/edit/${obj.id}`; + break; default: url += resource + 's/' + obj.id + '/'; } diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 8ed9133251..1c908573cc 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -10,7 +10,6 @@ if ($basePath) { } import start from './app.start'; -import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventoriesHosts from './inventories-hosts/main'; import inventoryScripts from './inventory-scripts/main'; @@ -43,8 +42,6 @@ import atLibComponents from '~components'; import atLibModels from '~models'; import atLibServices from '~services'; -import networkUI from '~network-ui/network.ui.app'; - start.bootstrap(() => { angular.bootstrap(document.body, ['awApp']); }); @@ -86,12 +83,10 @@ angular jobSubmission.name, notifications.name, Templates.name, - portalMode.name, teams.name, users.name, projects.name, scheduler.name, - networkUI.name, 'Utilities', 'templates', @@ -353,6 +348,10 @@ angular } else { $rootScope.$broadcast("RemoveIndicator"); } + + if(_.contains(trans.from().name, 'output') && trans.to().name === 'jobs'){ + $state.reload(); + } }); if (!Authorization.isUserLoggedIn()) { diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 66d2f8818d..e25fa46af8 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -81,7 +81,8 @@ export default [ return authVm.dropdownValue; }; - var activeForm = function() { + + const activeForm = function(revertDropdown) { if(!_.get($scope.$parent, [formTracker.currentFormName(), '$dirty'])) { authVm.activeAuthForm = getActiveAuthForm(); formTracker.setCurrentAuth(authVm.activeAuthForm); @@ -110,6 +111,12 @@ export default [ authVm.activeAuthForm = getActiveAuthForm(); formTracker.setCurrentAuth(authVm.activeAuthForm); $('#FormModal-dialog').dialog('close'); + }).catch(() => { + event.preventDefault(); + $('#FormModal-dialog').dialog('close'); + if (revertDropdown) { + revertDropdown(); + } }); }, "class": "btn btn-primary", @@ -121,6 +128,26 @@ export default [ authVm.ldapSelected = (authVm.activeAuthForm.indexOf('ldap') !== -1); }; + const changeAuthDropdown = (previousVal) => { + activeForm(() => { + authVm.dropdownValue = previousVal; + CreateSelect2({ + element: '#configure-dropdown-nav', + multiple: false, + }); + }); + }; + + const changeLdapDropdown = (previousVal) => { + activeForm(() => { + authVm.ldapDropdownValue = previousVal; + CreateSelect2({ + element: '#configure-ldap-dropdown', + multiple: false, + }); + }); + }; + var dropdownOptions = [ {label: i18n._('Azure AD'), value: 'azure'}, {label: i18n._('GitHub'), value: 'github'}, @@ -409,7 +436,8 @@ export default [ angular.extend(authVm, { - activeForm: activeForm, + changeAuthDropdown: changeAuthDropdown, + changeLdapDropdown: changeLdapDropdown, activeAuthForm: activeAuthForm, authForms: authForms, dropdownOptions: dropdownOptions, diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html index 2bfa19b401..dcbf4e258e 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html @@ -8,7 +8,7 @@ class="form-control" ng-model="authVm.dropdownValue" ng-options="opt.value as opt.label for opt in authVm.dropdownOptions" - ng-change="authVm.activeForm()"> + ng-change="authVm.changeAuthDropdown('{{authVm.dropdownValue}}')">
    @@ -21,7 +21,7 @@ class="form-control" ng-model="authVm.ldapDropdownValue" ng-options="opt.value as opt.label for opt in authVm.ldapDropdownOptions" - ng-change="authVm.activeForm()"> + ng-change="authVm.changeLdapDropdown('{{authVm.ldapDropdownValue}}')">
    diff --git a/awx/ui/client/src/configuration/configuration.block.less b/awx/ui/client/src/configuration/configuration.block.less index 693fd9a9d5..8d85b6bcea 100644 --- a/awx/ui/client/src/configuration/configuration.block.less +++ b/awx/ui/client/src/configuration/configuration.block.less @@ -179,9 +179,15 @@ input#filePickerText { } .LogAggregator-failedNotification{ - max-width: 300px; + max-width: 500px; + word-break: break-word; } hr { height: 1px; } + +.ConfigureTower-errorIcon{ + margin-right:5px; + color:@red; +} diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index 0f65097f71..a7a82495e4 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -8,7 +8,7 @@ import defaultStrings from '~assets/default.strings.json'; export default [ '$scope', '$rootScope', '$state', '$stateParams', '$timeout', '$q', 'Alert', 'ConfigurationService', 'ConfigurationUtils', 'CreateDialog', 'CreateSelect2', 'i18n', 'ParseTypeChange', 'ProcessErrors', 'Store', - 'Wait', 'configDataResolve', 'ToJSON', 'ConfigService', + 'Wait', 'configDataResolve', 'ToJSON', 'ConfigService', 'ngToast', //Form definitions 'configurationAzureForm', 'configurationGithubForm', @@ -32,7 +32,7 @@ export default [ function( $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ConfigurationService, ConfigurationUtils, CreateDialog, CreateSelect2, i18n, ParseTypeChange, ProcessErrors, Store, - Wait, configDataResolve, ToJSON, ConfigService, + Wait, configDataResolve, ToJSON, ConfigService, ngToast, //Form definitions configurationAzureForm, configurationGithubForm, @@ -241,10 +241,15 @@ export default [ }, { label: i18n._("Save changes"), onClick: function() { - vm.formSave(); - $scope[formTracker.currentFormName()].$setPristine(); - $('#FormModal-dialog').dialog('close'); - active(setForm); + vm.formSave().then(() => { + $scope[formTracker.currentFormName()].$setPristine(); + $('#FormModal-dialog').dialog('close'); + active(setForm); + }).catch(()=> { + event.preventDefault(); + $('#FormModal-dialog').dialog('close'); + }); + }, "class": "btn btn-primary", "id": "formmodal-save-button" @@ -312,15 +317,21 @@ export default [ "class": "btn Form-cancelButton", "id": "formmodal-cancel-button", onClick: function() { + clearApiErrors(); + populateFromApi(); + $scope[formTracker.currentFormName()].$setPristine(); $('#FormModal-dialog').dialog('close'); - $state.go('setup'); } }, { label: i18n._("Save changes"), onClick: function() { - $scope.formSave(); - $('#FormModal-dialog').dialog('close'); - $state.go('setup'); + vm.formSave().then(() => { + $scope[formTracker.currentFormName()].$setPristine(); + $('#FormModal-dialog').dialog('close'); + }).catch(()=> { + event.preventDefault(); + $('#FormModal-dialog').dialog('close'); + }); }, "class": "btn btn-primary", "id": "formmodal-save-button" @@ -396,11 +407,12 @@ export default [ } loginUpdate(); }) - .catch(function(error) { - ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + .catch(function(data) { + ProcessErrors($scope, data.error, data.status, formDefs[formTracker.getCurrent()], { - hdr: i18n._('Error!'), - msg: i18n._('There was an error resetting value. Returned status: ') + error.detail + hdr: ` + ${ i18n._('Error!')} `, + msg: i18n._('There was an error resetting value. Returned status: ') + data.error.detail }); }) @@ -473,8 +485,8 @@ export default [ else { // Everything else if (key !== 'LOG_AGGREGATOR_TCP_TIMEOUT' || - ($scope.LOG_AGGREGATOR_PROTOCOL === 'https' || - $scope.LOG_AGGREGATOR_PROTOCOL === 'tcp')) { + ($scope.LOG_AGGREGATOR_PROTOCOL.value === 'https' || + $scope.LOG_AGGREGATOR_PROTOCOL.value === 'tcp')) { payload[key] = $scope[key]; } } @@ -495,14 +507,23 @@ export default [ saveDeferred.resolve(data); $scope[formTracker.currentFormName()].$setPristine(); + ngToast.success({ + timeout: 2000, + dismissButton: false, + dismissOnTimeout: true, + content: `` + + i18n._('Save Complete') + }); }) - .catch(function(error, status) { - ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + .catch(function(data) { + ProcessErrors($scope, data.data, data.status, formDefs[formTracker.getCurrent()], { - hdr: i18n._('Error!'), - msg: i18n._('Failed to save settings. Returned status: ') + status + hdr: ` + ${ i18n._('Error!')} `, + msg: i18n._('Failed to save settings. Returned status: ') + data.status }); - saveDeferred.reject(error); + saveDeferred.reject(data); }) .finally(function() { Wait('stop'); @@ -528,13 +549,14 @@ export default [ .then(function() { //TODO consider updating form values with returned data here }) - .catch(function(error, status) { + .catch(function(data) { //Change back on unsuccessful update $scope[key] = !$scope[key]; - ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + ProcessErrors($scope, data.data, data.status, formDefs[formTracker.getCurrent()], { - hdr: i18n._('Error!'), - msg: i18n._('Failed to save toggle settings. Returned status: ') + error.detail + hdr: ` + ${ i18n._('Error!')} `, + msg: i18n._('Failed to save toggle settings. Returned status: ') + data.status }); }) .finally(function() { @@ -577,11 +599,12 @@ export default [ }); }) - .catch(function(error) { - ProcessErrors($scope, error, status, formDefs[formTracker.getCurrent()], + .catch(function(data) { + ProcessErrors($scope, data.error, data.status, formDefs[formTracker.getCurrent()], { - hdr: i18n._('Error!'), - msg: i18n._('There was an error resetting values. Returned status: ') + error.detail + hdr: ` + ${ i18n._('Error!')} `, + msg: i18n._('There was an error resetting values. Returned status: ') + data.error.detail }); }) .finally(function() { diff --git a/awx/ui/client/src/configuration/configuration.service.js b/awx/ui/client/src/configuration/configuration.service.js index bd25d7179c..1d437dc2cd 100644 --- a/awx/ui/client/src/configuration/configuration.service.js +++ b/awx/ui/client/src/configuration/configuration.service.js @@ -50,7 +50,7 @@ export default ['$rootScope', 'GetBasePath', 'ProcessErrors', '$q', '$http', 'Re .then(({data}) => { deferred.resolve(data); }) - .catch(({error}) => { + .catch((error) => { deferred.reject(error); }); @@ -64,7 +64,7 @@ export default ['$rootScope', 'GetBasePath', 'ProcessErrors', '$q', '$http', 'Re .then(({data}) => { deferred.resolve(data); }) - .catch(({error}) => { + .catch((error) => { deferred.reject(error); }); diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html index 2b92cc1197..32337fc588 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html +++ b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html @@ -27,7 +27,7 @@
    -
    +
    @@ -50,7 +50,7 @@ -
    +
    @@ -74,13 +74,13 @@
    - - -
    diff --git a/awx/ui/client/src/inventories-hosts/hosts/list/host-list.controller.js b/awx/ui/client/src/inventories-hosts/hosts/list/host-list.controller.js index bdb7607389..a895c589ab 100644 --- a/awx/ui/client/src/inventories-hosts/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/hosts/list/host-list.controller.js @@ -17,6 +17,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, $scope.canAdd = canAdd; $scope.enableSmartInventoryButton = false; $scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS'); + $scope.strings = InventoryHostsStrings; // Search init $scope.list = list; diff --git a/awx/ui/client/src/inventories-hosts/hosts/main.js b/awx/ui/client/src/inventories-hosts/hosts/main.js index c2675c2fda..575a2a8e49 100644 --- a/awx/ui/client/src/inventories-hosts/hosts/main.js +++ b/awx/ui/client/src/inventories-hosts/hosts/main.js @@ -14,6 +14,7 @@ import insightsRoute from '../inventories/insights/insights.route'; import hostGroupsRoute from './related/groups/hosts-related-groups.route'; import hostGroupsAssociateRoute from './related/groups/hosts-related-groups-associate.route'; + import hostCompletedJobsRoute from '~features/jobs/routes/hostCompletedJobs.route.js'; import hostGroups from './related/groups/main'; export default @@ -87,6 +88,9 @@ angular.module('host', [ let hostInsights = _.cloneDeep(insightsRoute); hostInsights.name = 'hosts.edit.insights'; + let hostCompletedJobs = _.cloneDeep(hostCompletedJobsRoute); + hostCompletedJobs.name = 'hosts.edit.completed_jobs'; + return Promise.all([ hostTree ]).then((generated) => { @@ -97,7 +101,8 @@ angular.module('host', [ stateExtender.buildDefinition(hostAnsibleFacts), stateExtender.buildDefinition(hostInsights), stateExtender.buildDefinition(hostGroupsRoute), - stateExtender.buildDefinition(hostGroupsAssociateRoute) + stateExtender.buildDefinition(hostGroupsAssociateRoute), + stateExtender.buildDefinition(hostCompletedJobs) ]) }; }); diff --git a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc-credential.route.js b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc-credential.route.js index 82f3321652..c62ba4d547 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc-credential.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc-credential.route.js @@ -31,9 +31,8 @@ export default { } }, resolve: { - ListDefinition: ['CredentialList', 'i18n', function(CredentialList, i18n) { + ListDefinition: ['CredentialList', function(CredentialList) { let list = _.cloneDeep(CredentialList); - list.lookupConfirmText = i18n._('SELECT'); return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventories.route.js b/awx/ui/client/src/inventories-hosts/inventories/inventories.route.js index fc2888a2eb..2fb8425009 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/inventories.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/inventories.route.js @@ -12,7 +12,7 @@ export default { activityStreamTarget: 'inventory', socket: { "groups": { - "inventories": ["status_changed"] + inventories: ["status_changed"] } } }, diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js index abcb526d58..753956f802 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js @@ -19,6 +19,7 @@ export default ['i18n', function(i18n) { basePath: 'inventory', title: false, disableRow: "{{ inventory.pending_deletion }}", + disableRowValue: 'inventory.pending_deletion', fields: { status: { @@ -100,20 +101,14 @@ export default ['i18n', function(i18n) { dataPlacement: 'top', ngShow: '!inventory.pending_deletion && inventory.summary_fields.user_capabilities.edit' }, - network: { - label: i18n._('Network Visualization'), - ngClick: 'goToGraph(inventory)', - awToolTip: i18n._('Network Visualization'), - dataPlacement: 'top', - ngShow: '!inventory.pending_deletion' - }, copy: { label: i18n._('Copy'), ngClick: 'copyInventory(inventory)', - "class": 'btn-danger btn-xs', - awToolTip: i18n._('Copy inventory'), + awToolTip: "{{ inventory.copyTip }}", + dataTipWatch: "inventory.copyTip", dataPlacement: 'top', - ngShow: '!inventory.pending_deletion && inventory.summary_fields.user_capabilities.copy' + ngShow: '!inventory.pending_deletion && inventory.summary_fields.user_capabilities.copy', + ngClass: 'inventory.copyClass' }, view: { label: i18n._('View'), diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js index 9fc16afdd0..7fa200b5f3 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js @@ -13,7 +13,8 @@ function InventoriesList($scope, $filter, Rest, InventoryList, Prompt, ProcessErrors, GetBasePath, Wait, $state, - Dataset, canAdd, i18n, Inventory, InventoryHostsStrings) { + Dataset, canAdd, i18n, Inventory, InventoryHostsStrings, + ngToast) { let inventory = new Inventory(); @@ -40,6 +41,8 @@ function InventoriesList($scope, inventory.host_status_class = "Inventories-hostStatus"; if (inventory.has_inventory_sources) { + inventory.copyTip = i18n._('Inventories with sources cannot be copied'); + inventory.copyClass = "btn-disabled"; if (inventory.inventory_sources_with_failures > 0) { inventory.syncStatus = 'error'; inventory.syncTip = inventory.inventory_sources_with_failures + i18n._(' sources with sync failures. Click for details'); @@ -50,6 +53,8 @@ function InventoriesList($scope, } } else { + inventory.copyTip = i18n._('Copy Inventory'); + inventory.copyClass = ""; inventory.syncStatus = 'na'; inventory.syncTip = i18n._('Not configured for inventory sync.'); inventory.launch_class = "btn-disabled"; @@ -74,19 +79,32 @@ function InventoriesList($scope, } $scope.copyInventory = inventory => { - Wait('start'); - new Inventory('get', inventory.id) - .then(model => model.copy()) - .then(copy => $scope.editInventory(copy, true)) - .catch(({ data, status }) => { - const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; - ProcessErrors($scope, data, status, null, params); - }) - .finally(() => Wait('stop')); - }; - - $scope.goToGraph = function(inventory){ - $state.go('inventories.edit.networking', {inventory_id: inventory.id, inventory_name: inventory.name}); + if (!inventory.has_inventory_sources) { + Wait('start'); + new Inventory('get', inventory.id) + .then(model => model.copy()) + .then(copiedInv => { + ngToast.success({ + content: ` +
    +
    + +
    +
    + ${InventoryHostsStrings.get('SUCCESSFUL_CREATION', copiedInv.name)} +
    +
    `, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); + }) + .catch(({ data, status }) => { + const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; + ProcessErrors($scope, data, status, null, params); + }) + .finally(() => Wait('stop')); + } }; $scope.editInventory = function (inventory, reload) { @@ -153,13 +171,13 @@ function InventoriesList($scope, if (data.status === 'deleted') { let reloadListStateParams = null; - if($scope.inventories.length === 1 && $state.params.inventory_search && !_.isEmpty($state.params.inventory_search.page) && $state.params.inventory_search.page !== '1') { + if($scope.inventories.length === 1 && $state.params.inventory_search && _.has($state, 'params.inventory_search.page') && $state.params.inventory_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.inventory_search.page = (parseInt(reloadListStateParams.inventory_search.page)-1).toString(); } - if (parseInt($state.params.inventory_id) === data.inventory_id) { - $state.go("^", reloadListStateParams, {reload: true}); + if (parseInt($state.params.inventory_id) === data.inventory_id || parseInt($state.params.smartinventory_id) === data.inventory_id) { + $state.go("inventories", reloadListStateParams, {reload: true}); } else { $state.go('.', reloadListStateParams, {reload: true}); } @@ -171,5 +189,5 @@ export default ['$scope', '$filter', 'Rest', 'InventoryList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'Wait', '$state', 'Dataset', 'canAdd', 'i18n', 'InventoryModel', - 'InventoryHostsStrings', InventoriesList + 'InventoryHostsStrings', 'ngToast', InventoriesList ]; diff --git a/awx/ui/client/src/inventories-hosts/inventories/main.js b/awx/ui/client/src/inventories-hosts/inventories/main.js index 66ff152d23..20feecfe10 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/main.js +++ b/awx/ui/client/src/inventories-hosts/inventories/main.js @@ -45,6 +45,7 @@ import hostNestedGroupsAssociateRoute from './related/hosts/related/nested-group import groupNestedGroupsAssociateRoute from './related/groups/related/nested-groups/group-nested-groups-associate.route'; import nestedHostsAssociateRoute from './related/groups/related/nested-hosts/group-nested-hosts-associate.route'; import nestedHostsAddRoute from './related/groups/related/nested-hosts/group-nested-hosts-add.route'; +import hostCompletedJobsRoute from '~features/jobs/routes/hostCompletedJobs.route.js'; export default angular.module('inventory', [ @@ -252,7 +253,6 @@ angular.module('inventory', [ let addSourceCredential = _.cloneDeep(inventorySourcesCredentialRoute); addSourceCredential.name = 'inventories.edit.inventory_sources.add.credential'; - addSourceCredential.url = '/credential'; let addSourceInventoryScript = _.cloneDeep(inventorySourcesInventoryScriptRoute); addSourceInventoryScript.name = 'inventories.edit.inventory_sources.add.inventory_script'; @@ -260,7 +260,6 @@ angular.module('inventory', [ let editSourceCredential = _.cloneDeep(inventorySourcesCredentialRoute); editSourceCredential.name = 'inventories.edit.inventory_sources.edit.credential'; - editSourceCredential.url = '/credential'; let addSourceProject = _.cloneDeep(inventorySourcesProjectRoute); addSourceProject.name = 'inventories.edit.inventory_sources.add.project'; @@ -292,6 +291,9 @@ angular.module('inventory', [ let smartInventoryAdhocCredential = _.cloneDeep(adhocCredentialRoute); smartInventoryAdhocCredential.name = 'inventories.editSmartInventory.adhoc.credential'; + let relatedHostCompletedJobs = _.cloneDeep(hostCompletedJobsRoute); + relatedHostCompletedJobs.name = 'inventories.edit.hosts.edit.completed_jobs'; + return Promise.all([ standardInventoryAdd, standardInventoryEdit, @@ -339,7 +341,8 @@ angular.module('inventory', [ stateExtender.buildDefinition(hostNestedGroupsAssociateRoute), stateExtender.buildDefinition(nestedHostsAssociateRoute), stateExtender.buildDefinition(nestedGroupsAdd), - stateExtender.buildDefinition(nestedHostsAddRoute) + stateExtender.buildDefinition(nestedHostsAddRoute), + stateExtender.buildDefinition(relatedHostCompletedJobs) ]) }; }); diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js index 120d09567b..daf0ab3e81 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js @@ -127,7 +127,7 @@ $scope.confirmDelete = function(){ let reloadListStateParams = null; - if($scope.groups.length === 1 && $state.params.group_search && !_.isEmpty($state.params.group_search.page) && $state.params.group_search.page !== '1') { + if($scope.groups.length === 1 && $state.params.group_search && _.has($state, 'params.group_search.page') && $state.params.group_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.group_search.page = (parseInt(reloadListStateParams.group_search.page)-1).toString(); } diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.route.js index 39dac7cca9..c16e9ba2a1 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.route.js @@ -54,7 +54,7 @@ export default { } else { //reaches here if the user is on the root level group - list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups'; + list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/groups'; } let html = generateList.build({ diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/list/host-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/list/host-list.controller.js index d1f32f366a..e0a49fc059 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/list/host-list.controller.js @@ -112,7 +112,7 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath', let reloadListStateParams = null; - if($scope.hosts.length === 1 && $state.params.host_search && !_.isEmpty($state.params.host_search.page) && $state.params.host_search.page !== '1') { + if($scope.hosts.length === 1 && $state.params.host_search && _.has($state, 'params.host_search.page') && $state.params.host_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.host_search.page = (parseInt(reloadListStateParams.host_search.page)-1).toString(); } diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js index 2ee6de78a2..3b10504b9e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js @@ -122,6 +122,11 @@ function(i18n) { title: i18n._('Insights'), skipGenerator: true, ngIf: "host.insights_system_id!==null && host.summary_fields.inventory.hasOwnProperty('insights_credential_id')" + }, + completed_jobs: { + name: 'completed_jobs', + title: i18n._('Completed Jobs'), + skipGenerator: true } } }; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/schedule/sources-schedule.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/schedule/sources-schedule.route.js index aebe56b624..89cd27dffe 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/schedule/sources-schedule.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/schedule/sources-schedule.route.js @@ -10,10 +10,9 @@ export default { }, views: { 'related': { - templateProvider: function(SchedulesList, generateList){ - SchedulesList.title = false; + templateProvider: function(ScheduleList, generateList){ let html = generateList.build({ - list: SchedulesList, + list: ScheduleList, mode: 'edit' }); return html; @@ -47,6 +46,7 @@ export default { (SchedulesList, inventorySource) => { let list = _.cloneDeep(SchedulesList); list.basePath = `${inventorySource.get().related.schedules}`; + list.title = false; return list; } ] diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js index 6b2d43ee55..1a9f96bbe3 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.controller.js @@ -108,7 +108,6 @@ {status_tooltip: inventory_source_status.tooltip}, {launch_tooltip: inventory_source_status.launch_tip}, {launch_class: inventory_source_status.launch_class}, - {group_schedule_tooltip: inventory_source_status.schedule_tip}, {source: inventory_source ? inventory_source.source : null}, {status: inventory_source ? inventory_source.status : null}); } @@ -213,17 +212,13 @@ $scope.cancelUpdate = function (id) { CancelSourceUpdate({ scope: $scope, id: id }); }; + $scope.viewUpdateStatus = function (id) { ViewUpdateStatus({ scope: $scope, inventory_source_id: id }); }; - $scope.scheduleSource = function(id) { - // Add this inv source's id to the array of inv source id's so that it gets - // added to the breadcrumb trail - $state.go('inventories.edit.inventory_sources.edit.schedules',{inventory_source_id: id}); - }; $scope.syncAllSources = function() { InventoryUpdate({ diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.route.js index c9d9882581..0bc50c9e75 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/list/sources-list.route.js @@ -17,7 +17,8 @@ export default { data: { socket: { groups: { - jobs: ["status_changed"] + jobs: ["status_changed"], + inventories: ["status_changed"] } } }, diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js index b80e65c1c9..90879be5f2 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js @@ -36,8 +36,11 @@ export default { ListDefinition: ['CredentialList', function(list) { return list; }], - Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { + Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$', + (list, qs, $stateParams, GetBasePath, $transition$) => { + const toState = $transition$.to(); + toState.params.credential_search.value.kind = _.get($stateParams, 'credential_search.kind', null); + toState.params.credential_search.value.credential_type__kind = _.get($stateParams, 'credential_search.credential_type__kind', null); return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]); } ] 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 48ad68b6bc..ec554a5225 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 @@ -15,7 +15,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ var notifications_object = { name: 'notifications', index: false, - basePath: "notifications", + basePath: "notification_templates", include: "NotificationsList", title: i18n._('Notifications'), iterator: 'notification', diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.list.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.list.js index 8fdf58bec2..21df43f7df 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.list.js @@ -104,14 +104,6 @@ dataPlacement: "top", iconClass: "fa fa-minus-circle" }, - schedule: { - mode: 'all', - ngClick: "scheduleSource(inventory_source.id)", - awToolTip: "{{ inventory_source.group_schedule_tooltip }}", - ngClass: "inventory_source.scm_type_class", - dataPlacement: 'top', - ngShow: "!(inventory_source.summary_fields.inventory_source.source === '') && inventory_source.summary_fields.user_capabilities.schedule" - }, view: { mode: 'all', ngClick: "editSource(inventory_source.id)", diff --git a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js index 1c9f1dd13d..98f6e62998 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js @@ -176,7 +176,7 @@ function(i18n) { relatedButtons: { remediate_inventory: { ngClick: 'remediateInventory(id, insights_credential)', - ngShow: 'is_insights && mode !== "add" && canRemediate', + ngShow: "is_insights && mode !== 'add' && canRemediate && ($state.is('inventories.edit') || $state.is('inventories.edit.hosts'))", label: i18n._('Remediate Inventory'), class: 'Form-primaryButton' } diff --git a/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js b/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js index 6c46f7c9d5..a5b35168ad 100644 --- a/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js +++ b/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js @@ -21,6 +21,19 @@ function InventoryHostsStrings (BaseString) { DELETE_HOST: count => t.p(count, 'Delete host', 'Delete hosts'), }; + ns.inventory = { + EDIT_HOST: t.s('Edit host'), + VIEW_HOST: t.s('View host'), + VIEW_INSIGHTS: t.s('View Insights Data') + }; + + ns.hostList = { + DISABLED_TOGGLE_TOOLTIP: () => t.s('{{ str1 }}

    {{ str2 }}

    ', { + str1: t.s('Indicates if a host is available and should be included in running jobs.'), + str2: t.s('For hosts that are part of an external inventory, this flag cannot be changed. It will be set by the inventory sync process.') + }) + }; + ns.smartinventories = { hostfilter: { MISSING_ORG: t.s('Please select an organization before editing the host filter.'), @@ -30,8 +43,8 @@ function InventoryHostsStrings (BaseString) { }; ns.smartinventorybutton = { - DISABLED_INSTRUCTIONS: "Please enter at least one search term to create a new Smart Inventory.", - ENABLED_INSTRUCTIONS: "Create a new Smart Inventory from search results." + DISABLED_INSTRUCTIONS: t.s("Please enter at least one search term to create a new Smart Inventory."), + ENABLED_INSTRUCTIONS: t.s("Create a new Smart Inventory from search results.

    Note: changing the organization of the Smart Inventory could change the hosts included in the Smart Inventory.") }; } diff --git a/awx/ui/client/src/inventories-hosts/shared/inventories.service.js b/awx/ui/client/src/inventories-hosts/shared/inventories.service.js index 3cd069c4c2..b6dab8728a 100644 --- a/awx/ui/client/src/inventories-hosts/shared/inventories.service.js +++ b/awx/ui/client/src/inventories-hosts/shared/inventories.service.js @@ -52,8 +52,8 @@ var url = GetBasePath('groups') + id + '/children'; return url; }, - rootGroupsUrl: function(id){ - var url = GetBasePath('inventory') + id+ '/root_groups'; + groupsUrl: function(id){ + var url = GetBasePath('inventory') + id+ '/groups'; return url; }, inventorySourcesOptions: function(inventoryId) { diff --git a/awx/ui/client/src/inventory-scripts/list/list.controller.js b/awx/ui/client/src/inventory-scripts/list/list.controller.js index b4307b54c8..4202a07f5e 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.controller.js +++ b/awx/ui/client/src/inventory-scripts/list/list.controller.js @@ -7,12 +7,12 @@ export default ['$rootScope', '$scope', 'Wait', 'InventoryScriptsList', 'GetBasePath', 'Rest', 'ProcessErrors', 'Prompt', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'InventoryScriptModel', 'InventoryScriptsStrings', - 'i18n', + 'i18n', 'ngToast', function( $rootScope, $scope, Wait, InventoryScriptsList, GetBasePath, Rest, ProcessErrors, Prompt, $state, $filter, Dataset, rbacUiControlService, InventoryScript, InventoryScriptsStrings, - i18n + i18n, ngToast ) { let inventoryScript = new InventoryScript(); var defaultUrl = GetBasePath('inventory_scripts'), @@ -51,9 +51,21 @@ export default ['$rootScope', '$scope', 'Wait', 'InventoryScriptsList', Wait('start'); new InventoryScript('get', inventoryScript.id) .then(model => model.copy()) - .then(({ id }) => { - const params = { inventory_script_id: id }; - $state.go('inventoryScripts.edit', params, { reload: true }); + .then((copiedInvScript) => { + ngToast.success({ + content: ` +
    +
    + +
    +
    + ${InventoryScriptsStrings.get('SUCCESSFUL_CREATION', copiedInvScript.name)} +
    +
    `, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); }) .catch(({ data, status }) => { const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; @@ -73,7 +85,7 @@ export default ['$rootScope', '$scope', 'Wait', 'InventoryScriptsList', let reloadListStateParams = null; - if($scope.inventory_scripts.length === 1 && $state.params.inventory_script_search && !_.isEmpty($state.params.inventory_script_search.page) && $state.params.inventory_script_search.page !== '1') { + if($scope.inventory_scripts.length === 1 && $state.params.inventory_script_search && _.has($state, 'params.inventory_script_search.page') && $state.params.inventory_script_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.inventory_script_search.page = (parseInt(reloadListStateParams.inventory_script_search.page)-1).toString(); } diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js index 35323e4458..39e211c4f5 100644 --- a/awx/ui/client/src/license/license.controller.js +++ b/awx/ui/client/src/license/license.controller.js @@ -9,9 +9,9 @@ import {N_} from "../i18n"; export default ['Wait', '$state', '$scope', '$rootScope', 'ProcessErrors', 'CheckLicense', 'moment','$window', - 'ConfigService', 'FeaturesService', 'pendoService', 'i18n', 'config', 'Rest', 'GetBasePath', + 'ConfigService', 'FeaturesService', 'pendoService', 'i18n', 'config', function(Wait, $state, $scope, $rootScope, ProcessErrors, CheckLicense, moment, - $window, ConfigService, FeaturesService, pendoService, i18n, config, Rest, GetBasePath) { + $window, ConfigService, FeaturesService, pendoService, i18n, config) { const calcDaysRemaining = function(seconds) { // calculate the number of days remaining on the license @@ -53,21 +53,9 @@ export default $scope.time.expiresOn = calcExpiresOn($scope.license.license_info.license_date); $scope.valid = CheckLicense.valid($scope.license.license_info); $scope.compliant = $scope.license.license_info.compliant; - $scope.newLicense = {}; - - Rest.setUrl(`${GetBasePath('settings')}ui`); - Rest.get() - .then(({data}) => { - if (data.PENDO_TRACKING_STATE === 'off' && !$rootScope.licenseMissing) { - $scope.newLicense.pendo = false; - } else { - $scope.newLicense.pendo = true; - } - }) - .catch(() => { - // default pendo tracking to true when settings is not accessible - $scope.newLicense.pendo = true; - }); + $scope.newLicense = { + pendo: true + }; }; init(config); @@ -110,37 +98,36 @@ export default $scope.submit = function() { Wait('start'); CheckLicense.post($scope.newLicense.file, $scope.newLicense.eula) - .then(() => { + .then((licenseInfo) => { reset(); - ConfigService.delete(); - ConfigService.getConfig() - .then(function(config) { - delete($rootScope.features); - FeaturesService.get(); + ConfigService.delete(); + ConfigService.getConfig(licenseInfo) + .then(function(config) { + delete($rootScope.features); + FeaturesService.get(); - if ($scope.newLicense.pendo) { - pendoService.updatePendoTrackingState('detailed'); - pendoService.issuePendoIdentity(); - } else { - pendoService.updatePendoTrackingState('off'); - } - - if ($rootScope.licenseMissing === true) { - $state.go('dashboard', { - licenseMissing: false + if ($rootScope.licenseMissing === true) { + if ($scope.newLicense.pendo) { + pendoService.updatePendoTrackingState('detailed'); + pendoService.issuePendoIdentity(); + } else { + pendoService.updatePendoTrackingState('off'); + } + $state.go('dashboard', { + licenseMissing: false + }); + } else { + init(config); + $scope.success = true; + $rootScope.licenseMissing = false; + // for animation purposes + const successTimeout = setTimeout(function() { + $scope.success = false; + clearTimeout(successTimeout); + }, 4000); + } }); - } else { - init(config); - $scope.success = true; - $rootScope.licenseMissing = false; - // for animation purposes - const successTimeout = setTimeout(function() { - $scope.success = false; - clearTimeout(successTimeout); - }, 4000); - } }); - }); }; }]; diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html index 5c2acf0e5d..17efee2c08 100644 --- a/awx/ui/client/src/license/license.partial.html +++ b/awx/ui/client/src/license/license.partial.html @@ -122,7 +122,7 @@
    -
    +
    -
    - + collection="organizations">
    diff --git a/awx/ui/client/src/organizations/organizations.form.js b/awx/ui/client/src/organizations/organizations.form.js index 79d8098afe..9fbe157783 100644 --- a/awx/ui/client/src/organizations/organizations.form.js +++ b/awx/ui/client/src/organizations/organizations.form.js @@ -41,11 +41,11 @@ export default ['NotificationsList', 'i18n', dataTitle: i18n._('Instance Groups'), dataContainer: 'body', dataPlacement: 'right', - control: '', + control: '', }, custom_virtualenv: { label: i18n._('Ansible Environment'), - defaultText: i18n._('Default Environment'), + defaultText: i18n._('Use Default Environment'), type: 'select', ngOptions: 'venv for venv in custom_virtualenvs_options track by venv', awPopOver: "

    " + i18n._("Select the custom Python virtual environment for this organization to run on.") + "

    ", @@ -53,7 +53,7 @@ export default ['NotificationsList', 'i18n', dataContainer: 'body', dataPlacement: 'right', ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)', - ngShow: 'custom_virtualenvs_options.length > 0' + ngShow: 'custom_virtualenvs_visible' } }, diff --git a/awx/ui/client/src/partials/survey-maker-modal.html b/awx/ui/client/src/partials/survey-maker-modal.html index b6acbe2ab3..476af0bc30 100644 --- a/awx/ui/client/src/partials/survey-maker-modal.html +++ b/awx/ui/client/src/partials/survey-maker-modal.html @@ -57,17 +57,17 @@ {{question.question_description}}
    - +  
    - -
    diff --git a/awx/ui/client/src/portal-mode/main.js b/awx/ui/client/src/portal-mode/main.js deleted file mode 100644 index d74c97ddd9..0000000000 --- a/awx/ui/client/src/portal-mode/main.js +++ /dev/null @@ -1,17 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -// import route from './portal-mode.route'; -import templatesRoute from '~features/templates/routes/portalModeTemplatesList.route.js'; -import myJobsRoute from '~features/jobs/routes/portalModeMyJobs.route.js'; -import allJobsRoute from '~features/jobs/routes/portalModeAllJobs.route.js'; -export default - angular.module('portalMode', []) - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(templatesRoute); - $stateExtender.addState(myJobsRoute); - $stateExtender.addState(allJobsRoute); - }]); diff --git a/awx/ui/client/src/portal-mode/portal-mode-layout.partial.html b/awx/ui/client/src/portal-mode/portal-mode-layout.partial.html deleted file mode 100644 index 417e97e1bc..0000000000 --- a/awx/ui/client/src/portal-mode/portal-mode-layout.partial.html +++ /dev/null @@ -1,52 +0,0 @@ -
    -
    -
    -
    -
    -
    -
    -
    JOB TEMPLATES
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    JOBS
    -
    -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index a5ccdbdbf2..d790510402 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -21,7 +21,8 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', function init() { $scope.canEditOrg = true; - $scope.custom_virtualenvs_options = ConfigData.custom_virtualenvs; + const virtualEnvs = ConfigData.custom_virtualenvs || []; + $scope.custom_virtualenvs_options = virtualEnvs; Rest.setUrl(GetBasePath('projects')); Rest.options() diff --git a/awx/ui/client/src/projects/edit/projects-edit.controller.js b/awx/ui/client/src/projects/edit/projects-edit.controller.js index 8f28ac1b79..d614a40d8b 100644 --- a/awx/ui/client/src/projects/edit/projects-edit.controller.js +++ b/awx/ui/client/src/projects/edit/projects-edit.controller.js @@ -25,7 +25,8 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest', function init() { $scope.project_local_paths = []; $scope.base_dir = ''; - $scope.custom_virtualenvs_options = ConfigData.custom_virtualenvs; + const virtualEnvs = ConfigData.custom_virtualenvs || []; + $scope.custom_virtualenvs_options = virtualEnvs; } $scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) { diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js index c4aa5db6c8..11be31c54c 100644 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ b/awx/ui/client/src/projects/list/projects-list.controller.js @@ -8,11 +8,11 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', 'ProjectList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'ProjectUpdate', 'Wait', 'Empty', 'Find', 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService', 'Dataset', 'i18n', 'QuerySet', 'ProjectModel', - 'ProjectsStrings', + 'ProjectsStrings', 'ngToast', function($scope, $rootScope, $log, Rest, Alert, ProjectList, Prompt, ProcessErrors, GetBasePath, ProjectUpdate, Wait, Empty, Find, GetProjectIcon, GetProjectToolTip, $filter, $state, rbacUiControlService, - Dataset, i18n, qs, Project, ProjectsStrings) { + Dataset, i18n, qs, Project, ProjectsStrings, ngToast) { let project = new Project(); @@ -74,7 +74,6 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', project.statusIcon = GetProjectIcon(project.status); project.statusTip = GetProjectToolTip(project.status); project.scm_update_tooltip = i18n._("Get latest SCM revision"); - project.scm_schedule_tooltip = i18n._("Schedule SCM revision updates"); project.scm_type_class = ""; if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { @@ -88,7 +87,6 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', } if (project.scm_type === 'manual') { project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - project.scm_schedule_tooltip = i18n._('Manual projects do not require a schedule'); project.scm_type_class = 'btn-disabled'; project.statusTip = i18n._('Not configured for SCM'); project.statusIcon = 'none'; @@ -117,7 +115,7 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { $scope.reloadList(); } else { - project.scm_update_tooltip = "SCM update currently running"; + project.scm_update_tooltip = i18n._("SCM update currently running"); project.scm_type_class = "btn-disabled"; } project.status = data.status; @@ -158,9 +156,21 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', Wait('start'); new Project('get', project.id) .then(model => model.copy()) - .then(({ id }) => { - const params = { project_id: id }; - $state.go('projects.edit', params, { reload: true }); + .then((copiedProj) => { + ngToast.success({ + content: ` +
    +
    + +
    +
    + ${ProjectsStrings.get('SUCCESSFUL_CREATION', copiedProj.name)} +
    +
    `, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); }) .catch(({ data, status }) => { const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; @@ -198,7 +208,7 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', let reloadListStateParams = null; - if($scope.projects.length === 1 && $state.params.project_search && !_.isEmpty($state.params.project_search.page) && $state.params.project_search.page !== '1') { + if($scope.projects.length === 1 && $state.params.project_search && _.has($state, 'params.project_search.page') && $state.params.project_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.project_search.page = (parseInt(reloadListStateParams.project_search.page)-1).toString(); } @@ -328,12 +338,5 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', } }); }; - - $scope.editSchedules = function(id) { - var project = Find({ list: $scope.projects, key: 'id', val: id }); - if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) { - $state.go('projects.edit.schedules', { project_id: id }); - } - }; } ]; diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index bd82dabbc2..c6315ab954 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -104,7 +104,7 @@ export default ['i18n', 'NotificationsList', 'TemplateList', ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)' }, scm_url: { - label: 'SCM URL', + label: i18n._('SCM URL'), type: 'text', ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' ", awRequiredWhen: { @@ -206,14 +206,14 @@ export default ['i18n', 'NotificationsList', 'TemplateList', custom_virtualenv: { label: i18n._('Ansible Environment'), type: 'select', - defaultText: i18n._('Default Environment'), + defaultText: i18n._('Use Default Environment'), ngOptions: 'venv for venv in custom_virtualenvs_options track by venv', awPopOver: "

    " + i18n._("Select the custom Python virtual environment for this project to run on.") + "

    ", dataTitle: i18n._('Ansible Environment'), dataContainer: 'body', dataPlacement: 'right', ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', - ngShow: 'custom_virtualenvs_options.length > 0' + ngShow: 'custom_virtualenvs_options.length > 1' }, }, @@ -261,8 +261,9 @@ export default ['i18n', 'NotificationsList', 'TemplateList', fields: { username: { + key: true, label: i18n._('User'), - uiSref: 'users({user_id: field.id})', + linkBase: 'users', class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4' }, role: { diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js index 86547c8b66..64b0ddb4ab 100644 --- a/awx/ui/client/src/projects/projects.list.js +++ b/awx/ui/client/src/projects/projects.list.js @@ -96,14 +96,6 @@ export default ['i18n', function(i18n) { dataPlacement: 'top', ngShow: "project.summary_fields.user_capabilities.start" }, - schedule: { - mode: 'all', - ngClick: "editSchedules(project.id)", - awToolTip: "{{ project.scm_schedule_tooltip }}", - ngClass: "project.scm_type_class", - dataPlacement: 'top', - ngShow: "project.summary_fields.user_capabilities.schedule" - }, copy: { label: i18n._('Copy'), ngClick: 'copyProject(project)', diff --git a/awx/ui/client/src/rest/interceptors.service.js b/awx/ui/client/src/rest/interceptors.service.js index e30b1d5b99..4fdb1f0279 100644 --- a/awx/ui/client/src/rest/interceptors.service.js +++ b/awx/ui/client/src/rest/interceptors.service.js @@ -11,13 +11,20 @@ *************************************************/ export default - [ '$rootScope', '$q', '$injector', - function ($rootScope, $q, $injector) { + [ '$rootScope', '$q', '$injector', '$cookies', + function ($rootScope, $q, $injector, $cookies) { return { + request: function (config) { + config.headers['X-Requested-With'] = 'XMLHttpRequest'; + if (['GET', 'HEAD', 'OPTIONS'].indexOf(config.method)===-1) { + config.headers['X-CSRFToken'] = $cookies.get('csrftoken'); + } + return config; + }, response: function(config) { - if(config.headers('auth-token-timeout') !== null){ + if(config.headers('Session-Timeout') !== null){ $rootScope.loginConfig.promise.then(function () { - $AnsibleConfig.session_timeout = Number(config.headers('auth-token-timeout')); + $AnsibleConfig.session_timeout = Number(config.headers('Session-Timeout')); }); } return config; diff --git a/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js index 2321028055..d4526cab8b 100644 --- a/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js +++ b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js @@ -28,7 +28,7 @@ export default let reloadListStateParams = null; - if(scope.schedules.length === 1 && $state.params.schedule_search && !_.isEmpty($state.params.schedule_search.page) && $state.params.schedule_search.page !== '1') { + if(scope.schedules.length === 1 && $state.params.schedule_search && _.has($state, 'params.schedule_search.page') && $state.params.schedule_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.schedule_search.page = (parseInt(reloadListStateParams.schedule_search.page)-1).toString(); } diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 038053e83a..80592b2e59 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -14,6 +14,7 @@ import SchedulePost from './factories/schedule-post.factory'; import ToggleSchedule from './factories/toggle-schedule.factory'; import SchedulesList from './schedules.list'; import ScheduledJobsList from './scheduled-jobs.list'; +import SchedulerStrings from './scheduler.strings'; export default angular.module('scheduler', []) @@ -26,4 +27,5 @@ export default .factory('ToggleSchedule', ToggleSchedule) .factory('SchedulesList', SchedulesList) .factory('ScheduledJobsList', ScheduledJobsList) - .directive('schedulerDatePicker', schedulerDatePicker); + .directive('schedulerDatePicker', schedulerDatePicker) + .service('SchedulerStrings', SchedulerStrings); diff --git a/awx/ui/client/src/scheduler/scheduled-jobs.list.js b/awx/ui/client/src/scheduler/scheduled-jobs.list.js index 709a98bd3c..8a15b3cb92 100644 --- a/awx/ui/client/src/scheduler/scheduled-jobs.list.js +++ b/awx/ui/client/src/scheduler/scheduled-jobs.list.js @@ -11,8 +11,8 @@ export default ['i18n', function(i18n) { name: 'schedules', iterator: 'schedule', editTitle: i18n._('SCHEDULED JOBS'), + listTitle: i18n._('SCHEDULED JOBS'), hover: true, - well: false, emptyListText: i18n._('No schedules exist'), fields: { diff --git a/awx/ui/client/src/scheduler/scheduler.strings.js b/awx/ui/client/src/scheduler/scheduler.strings.js new file mode 100644 index 0000000000..95b66edfdf --- /dev/null +++ b/awx/ui/client/src/scheduler/scheduler.strings.js @@ -0,0 +1,69 @@ +function SchedulerStrings (BaseString) { + BaseString.call(this, 'scheduler'); + + const { t } = this; + const ns = this.scheduler; + + ns.state = { + CREATE_SCHEDULE: t.s('CREATE SCHEDULE'), + EDIT_SCHEDULE: t.s('EDIT SCHEDULE') + }; + + ns.list = { + CLICK_TO_EDIT: t.s('Click to edit schedule.'), + SCHEDULE_IS_ACTIVE: t.s('Schedule is active.'), + SCHEDULE_IS_ACTIVE_CLICK_TO_STOP: t.s('Schedule is active. Click to stop.'), + SCHEDULE_IS_STOPPED: t.s('Schedule is stopped.'), + SCHEDULE_IS_STOPPED_CLICK_TO_STOP: t.s('Schedule is stopped. Click to activate.') + }; + + ns.form = { + NAME: t.s('Name'), + NAME_REQUIRED_MESSAGE: t.s('A schedule name is required.'), + START_DATE: t.s('Start Date'), + START_TIME: t.s('Start Time'), + START_TIME_ERROR_MESSAGE: t.s('The time must be in HH24:MM:SS format.'), + LOCAL_TIME_ZONE: t.s('Local Time Zone'), + REPEAT_FREQUENCY: t.s('Repeat frequency'), + FREQUENCY_DETAILS: t.s('Frequency Details'), + EVERY: t.s('Every'), + REPEAT_FREQUENCY_ERROR_MESSAGE: t.s('Please provide a value between 1 and 999.'), + ON_DAY: t.s('on day'), + MONTH_DAY_ERROR_MESSAGE: t.s('The day must be between 1 and 31.'), + ON_THE: t.s('on the'), + ON: t.s('on'), + ON_DAYS: t.s('on days'), + SUN: t.s('Sun'), + MON: t.s('Mon'), + TUE: t.s('Tue'), + WED: t.s('Wed'), + THU: t.s('Thu'), + FRI: t.s('Fri'), + SAT: t.s('Sat'), + WEEK_DAY_ERROR_MESSAGE: t.s('Please select one or more days.'), + END: t.s('End'), + OCCURENCES: t.s('Occurrences'), + END_DATE: t.s('End Date'), + PROVIDE_VALID_DATE: t.s('Please provide a valid date.'), + END_TIME: t.s('End Time'), + SCHEDULER_OPTIONS_ARE_INVALID: t.s('The scheduler options are invalid, incomplete, or a date is in the past.'), + SCHEDULE_DESCRIPTION: t.s('Schedule Description'), + LIMITED_TO_FIRST_TEN: t.s('Limited to first 10'), + DATE_FORMAT: t.s('Date format'), + EXTRA_VARIABLES: t.s('Extra Variables'), + PROMPT: t.s('Prompt'), + CLOSE: t.s('Close'), + CANCEL: t.s('Cancel'), + SAVE: t.s('Save'), + WARNING: t.s('Warning'), + CREDENTIAL_REQUIRES_PASSWORD_WARNING: t.s('This Job Template has a default credential that requires a password before launch. Adding or editing schedules is prohibited while this credential is selected. To add or edit a schedule, credentials that require a password must be removed from the Job Template.') + }; + + ns.prompt = { + CONFIRM: t.s('CONFIRM') + }; +} + +SchedulerStrings.$inject = ['BaseStringService']; + +export default SchedulerStrings; diff --git a/awx/ui/client/src/scheduler/schedulerAdd.controller.js b/awx/ui/client/src/scheduler/schedulerAdd.controller.js index 5c87f3c484..e3fdda845f 100644 --- a/awx/ui/client/src/scheduler/schedulerAdd.controller.js +++ b/awx/ui/client/src/scheduler/schedulerAdd.controller.js @@ -8,12 +8,12 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'GetBasePath', 'Rest', 'ParentObject', 'JobTemplateModel', '$q', 'Empty', 'SchedulePost', 'ProcessErrors', 'SchedulerInit', '$location', 'PromptService', 'RRuleToAPI', 'moment', - 'WorkflowJobTemplateModel', 'TemplatesStrings', 'rbacUiControlService', + 'WorkflowJobTemplateModel', 'SchedulerStrings', 'rbacUiControlService', 'Alert', function($filter, $state, $stateParams, $http, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange, GetBasePath, Rest, ParentObject, JobTemplate, $q, Empty, SchedulePost, ProcessErrors, SchedulerInit, $location, PromptService, RRuleToAPI, moment, - WorkflowJobTemplate, TemplatesStrings, rbacUiControlService + WorkflowJobTemplate, SchedulerStrings, rbacUiControlService, Alert ) { var base = $scope.base || $location.path().replace(/^\//, '').split('/')[0], @@ -30,7 +30,12 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', $scope.canAdd = params.canAdd; }); } - let processSchedulerEndDt = function(){ + + /* + * Keep processSchedulerEndDt method on the $scope + * because angular-scheduler references it + */ + $scope.processSchedulerEndDt = function(){ // set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight var dt = new Date($scope.schedulerUTCTime); // increment date by 1 day @@ -41,7 +46,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', }; $scope.preventCredsWithPasswords = true; - $scope.strings = TemplatesStrings; + $scope.strings = SchedulerStrings; /* * This is a workaround for the angular-scheduler library inserting `ll` into fields after an @@ -107,6 +112,14 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', .then((responses) => { let launchConf = responses[1].data; + if (launchConf.passwords_needed_to_start && + launchConf.passwords_needed_to_start.length > 0 && + !launchConf.ask_credential_on_launch + ) { + Alert(SchedulerStrings.get('form.WARNING'), SchedulerStrings.get('form.CREDENTIAL_REQUIRES_PASSWORD_WARNING'), 'alert-info'); + $state.go('^', { reload: true }); + } + let watchForPromptChanges = () => { let promptValuesToWatch = [ 'promptData.prompts.inventory.value', @@ -151,7 +164,6 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', !launchConf.survey_enabled && !launchConf.credential_needed_to_start && !launchConf.inventory_needed_to_start && - launchConf.passwords_needed_to_start.length === 0 && launchConf.variables_needed_to_start.length === 0) { $scope.showPromptButton = false; } else { @@ -329,7 +341,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', }); if ($scope.schedulerUTCTime) { // The UTC time is already set - processSchedulerEndDt(); + $scope.processSchedulerEndDt(); } else { // We need to wait for it to be set by angular-scheduler because the following function depends // on it @@ -337,7 +349,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', if (newVal) { // Remove the watcher schedulerUTCTimeWatcher(); - processSchedulerEndDt(); + $scope.processSchedulerEndDt(); } }); } @@ -370,9 +382,6 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', scheduler.clear(); $scope.$on("htmlDetailReady", function() { $scope.hideForm = false; - $scope.$on("formUpdated", function() { - $rootScope.$broadcast("loadSchedulerDetailPane"); - }); $scope.$watchGroup(["schedulerName", "schedulerStartDt", "schedulerStartHour", @@ -398,11 +407,11 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', "schedulerEndMinute", "schedularEndSecond" ], function() { - $scope.$emit("formUpdated"); + $rootScope.$broadcast("loadSchedulerDetailPane"); }, true); $scope.$watch("weekDays", function() { - $scope.$emit("formUpdated"); + $rootScope.$broadcast("loadSchedulerDetailPane"); }, true); Wait('stop'); diff --git a/awx/ui/client/src/scheduler/schedulerEdit.controller.js b/awx/ui/client/src/scheduler/schedulerEdit.controller.js index 3a2d3cd187..4aa3044adf 100644 --- a/awx/ui/client/src/scheduler/schedulerEdit.controller.js +++ b/awx/ui/client/src/scheduler/schedulerEdit.controller.js @@ -1,11 +1,11 @@ export default ['$filter', '$state', '$stateParams', 'Wait', '$scope', 'moment', '$rootScope', '$http', 'CreateSelect2', 'ParseTypeChange', 'ParentObject', 'ProcessErrors', 'Rest', 'GetBasePath', 'SchedulerInit', 'SchedulePost', 'JobTemplateModel', '$q', 'Empty', 'PromptService', 'RRuleToAPI', -'WorkflowJobTemplateModel', 'TemplatesStrings', 'scheduleResolve', 'timezonesResolve', +'WorkflowJobTemplateModel', 'SchedulerStrings', 'scheduleResolve', 'timezonesResolve', 'Alert', function($filter, $state, $stateParams, Wait, $scope, moment, $rootScope, $http, CreateSelect2, ParseTypeChange, ParentObject, ProcessErrors, Rest, GetBasePath, SchedulerInit, SchedulePost, JobTemplate, $q, Empty, PromptService, RRuleToAPI, - WorkflowJobTemplate, TemplatesStrings, scheduleResolve, timezonesResolve + WorkflowJobTemplate, SchedulerStrings, scheduleResolve, timezonesResolve, Alert ) { let schedule, scheduler, scheduleCredentials = []; @@ -21,8 +21,12 @@ function($filter, $state, $stateParams, Wait, $scope, moment, $scope.hideForm = true; $scope.parseType = 'yaml'; - $scope.strings = TemplatesStrings; + $scope.strings = SchedulerStrings; + /* + * Keep processSchedulerEndDt method on the $scope + * because angular-scheduler references it + */ $scope.processSchedulerEndDt = function(){ // set the schedulerEndDt to be equal to schedulerStartDt + 1 day @ midnight var dt = new Date($scope.schedulerUTCTime); @@ -245,8 +249,15 @@ function($filter, $state, $stateParams, Wait, $scope, moment, .then((responses) => { let launchOptions = responses[0].data, launchConf = responses[1].data; + scheduleCredentials = responses[2].data.results; - scheduleCredentials = responses[2].data.results; + if (launchConf.passwords_needed_to_start && + launchConf.passwords_needed_to_start.length > 0 && + !launchConf.ask_credential_on_launch + ) { + Alert(SchedulerStrings.get('form.WARNING'), SchedulerStrings.get('form.CREDENTIAL_REQUIRES_PASSWORD_WARNING'), 'alert-info'); + $scope.credentialRequiresPassword = true; + } let watchForPromptChanges = () => { let promptValuesToWatch = [ diff --git a/awx/ui/client/src/scheduler/schedulerForm.block.less b/awx/ui/client/src/scheduler/schedulerForm.block.less index d40eb0e15e..ea1244e120 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.block.less +++ b/awx/ui/client/src/scheduler/schedulerForm.block.less @@ -16,6 +16,10 @@ padding-top: 4px; } +input.DatePicker-input[disabled] { + background: @ebgrey; +} + @media (min-width: 901px) { .SchedulerForm-formGroup { flex: 0 0 auto; diff --git a/awx/ui/client/src/scheduler/schedulerForm.partial.html b/awx/ui/client/src/scheduler/schedulerForm.partial.html index 8afed91a0c..db98a8d4ef 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/scheduler/schedulerForm.partial.html @@ -1,7 +1,7 @@
    -
    {{ schedulerName || "ADD SCHEDULE"}}
    -
    {{ schedulerName || "EDIT SCHEDULE"}}
    +
    {{ schedulerName || strings.get('state.CREATE_SCHEDULE') }}
    +
    {{ schedulerName || strings.get('state.EDIT_SCHEDULE') }}
    @@ -286,7 +286,7 @@
    - The day must be between 1 and 31. + {{ strings.get('form.MONTH_DAY_ERROR_MESSAGE') }}
    * - on the + {{ strings.get('form.ON_THE') }}
    @@ -361,7 +361,7 @@ ng-if="schedulerFrequency && schedulerFrequency.value == 'weekly'">
    @@ -370,73 +370,73 @@ data-toggle="buttons-checkbox" id="weekdaySelect">
    - Please select one or more days. + {{ strings.get('form.WEEK_DAY_ERROR_MESSAGE') }}
    - Please provide a value between 1 and 999. + {{ strings.get('form.REPEAT_FREQUENCY_ERROR_MESSAGE') }}
    + disabled="!(schedule_obj.summary_fields.user_capabilities.edit || canAdd) || credentialRequiresPassword">
    - Please provide a valid date. + {{ strings.get('form.PROVIDE_VALID_DATE') }}
    @@ -574,13 +574,13 @@ SchedulerFormDetail-container--error" ng-show="(preview_list.isEmpty && scheduler_form.$dirty) || (!schedulerIsValid && scheduler_form.$dirty)">

    - The scheduler options are invalid, incomplete, or a date is in the past. + {{ strings.get('form.SCHEDULER_OPTIONS_ARE_INVALID') }}

    {{ rrule_nlp_description }} @@ -588,17 +588,17 @@
    diff --git a/awx/ui/client/src/scheduler/schedulerList.controller.js b/awx/ui/client/src/scheduler/schedulerList.controller.js index e09566a797..5a6ea5e0e1 100644 --- a/awx/ui/client/src/scheduler/schedulerList.controller.js +++ b/awx/ui/client/src/scheduler/schedulerList.controller.js @@ -13,13 +13,12 @@ export default [ '$filter', '$scope', '$location', '$stateParams', 'ScheduleList', 'Rest', - 'rbacUiControlService', - 'ToggleSchedule', 'DeleteSchedule', '$q', '$state', 'Dataset', 'ParentObject', 'UnifiedJobsOptions', - function($filter, $scope, $location, $stateParams, - ScheduleList, Rest, - rbacUiControlService, - ToggleSchedule, DeleteSchedule, - $q, $state, Dataset, ParentObject, UnifiedJobsOptions) { + 'rbacUiControlService', 'JobTemplateModel', 'ToggleSchedule', 'DeleteSchedule', + '$q', '$state', 'Dataset', 'ParentObject', 'UnifiedJobsOptions', 'i18n', 'SchedulerStrings', + function($filter, $scope, $location, $stateParams, ScheduleList, Rest, + rbacUiControlService, JobTemplate, ToggleSchedule, DeleteSchedule, + $q, $state, Dataset, ParentObject, UnifiedJobsOptions, i18n, strings + ) { var base, scheduleEndpoint, list = ScheduleList; @@ -35,6 +34,19 @@ export default [ .then(function(params) { $scope.canAdd = params.canAdd; }); + if (_.has(ParentObject, 'type') && ParentObject.type === 'job_template') { + const jobTemplate = new JobTemplate(); + jobTemplate.getLaunch(ParentObject.id) + .then(({data}) => { + if (data.passwords_needed_to_start && + data.passwords_needed_to_start.length > 0 && + !ParentObject.ask_credential_on_launch + ) { + $scope.credentialRequiresPassword = true; + $scope.addTooltip = i18n._("Using a credential that requires a password on launch is prohibited when creating a Job Template schedule"); + } + }); + } } // search init @@ -89,7 +101,7 @@ export default [ } buildTooltips(itm); - if (!$state.is('jobs.schedules')){ + if (!$state.is('schedules')){ if($state.current.name.endsWith('.add')) { itm.linkToDetails = `^.edit({schedule_id:schedule.id})`; } @@ -107,13 +119,15 @@ export default [ function buildTooltips(schedule) { var job = schedule.summary_fields.unified_job_template; if (schedule.enabled) { - schedule.play_tip = 'Schedule is active. Click to stop.'; + const tip = (schedule.summary_fields.user_capabilities.edit || $scope.credentialRequiresPassword) ? strings.get('list.SCHEDULE_IS_ACTIVE') : strings.get('list.SCHEDULE_IS_ACTIVE_CLICK_TO_STOP'); + schedule.play_tip = tip; schedule.status = 'active'; - schedule.status_tip = 'Schedule is active. Click to stop.'; + schedule.status_tip = tip; } else { - schedule.play_tip = 'Schedule is stopped. Click to activate.'; + const tip = (schedule.summary_fields.user_capabilities.edit || $scope.credentialRequiresPassword) ? strings.get('list.SCHEDULE_IS_STOPPED') : strings.get('list.SCHEDULE_IS_STOPPED_CLICK_TO_STOP');//i18n._('Schedule is stopped.') : i18n._('Schedule is stopped. Click to activate.'); + schedule.play_tip = tip; schedule.status = 'stopped'; - schedule.status_tip = 'Schedule is stopped. Click to activate.'; + schedule.status_tip = tip; } schedule.nameTip = $filter('sanitize')(schedule.name); @@ -126,7 +140,7 @@ export default [ schedule.nameTip += "job "; } schedule.nameTip += $filter('sanitize')(job.name); - schedule.nameTip += ". Click to edit schedule."; + schedule.nameTip += `. ${strings.get('list.CLICK_TO_EDIT')}`; } $scope.refreshSchedules = function() { @@ -143,8 +157,8 @@ export default [ }; $scope.editSchedule = function(schedule) { - if ($state.is('jobs.schedules')){ - $state.go('jobs.schedules.edit', {schedule_id: schedule.id}); + if ($state.is('schedules')){ + $state.go('schedules.edit', {schedule_id: schedule.id}); } else { if($state.current.name.endsWith('.add')) { diff --git a/awx/ui/client/src/scheduler/schedules.list.js b/awx/ui/client/src/scheduler/schedules.list.js index c1e3e54185..a3a32b174d 100644 --- a/awx/ui/client/src/scheduler/schedules.list.js +++ b/awx/ui/client/src/scheduler/schedules.list.js @@ -26,7 +26,7 @@ export default ['i18n', function(i18n) { ngShow: '!isValid(schedule)' }, toggleSchedule: { - ngDisabled: "!schedule.summary_fields.user_capabilities.edit", + ngDisabled: "!schedule.summary_fields.user_capabilities.edit || credentialRequiresPassword", label: '', columnClass: 'List-staticColumn--toggle', type: "toggle", @@ -70,11 +70,13 @@ export default ['i18n', function(i18n) { }, add: { mode: 'all', - ngClick: 'addSchedule()', + ngClick: 'credentialRequiresPassword || addSchedule()', awToolTip: i18n._('Add a new schedule'), + dataTipWatch: 'addTooltip', actionClass: 'at-Button--add', actionId: 'button-add', - ngShow: 'canAdd' + ngShow: 'canAdd', + ngClass: "{ 'Form-tab--disabled': credentialRequiresPassword }" } }, @@ -85,14 +87,14 @@ export default ['i18n', function(i18n) { icon: 'icon-edit', awToolTip: i18n._('Edit schedule'), dataPlacement: 'top', - ngShow: 'schedule.summary_fields.user_capabilities.edit' + ngShow: 'schedule.summary_fields.user_capabilities.edit && !credentialRequiresPassword' }, view: { label: i18n._('View'), ngClick: "editSchedule(schedule)", awToolTip: i18n._('View schedule'), dataPlacement: 'top', - ngShow: '!schedule.summary_fields.user_capabilities.edit' + ngShow: '!schedule.summary_fields.user_capabilities.edit || credentialRequiresPassword' }, "delete": { label: i18n._('Delete'), diff --git a/awx/ui/client/src/scheduler/schedules.route.js b/awx/ui/client/src/scheduler/schedules.route.js index 965b42f97d..fc0a39c3a2 100644 --- a/awx/ui/client/src/scheduler/schedules.route.js +++ b/awx/ui/client/src/scheduler/schedules.route.js @@ -9,7 +9,6 @@ const jobTemplatesSchedulesListRoute = { data: { activityStream: true, activityStreamTarget: 'job_template', - activityStreamId: 'id' }, ncyBreadcrumb: { label: N_('SCHEDULES') @@ -97,8 +96,7 @@ const workflowSchedulesRoute = { route: '/schedules', data: { activityStream: true, - activityStreamTarget: 'job_template', - activityStreamId: 'id' + activityStreamTarget: 'workflow_job_template', }, ncyBreadcrumb: { label: N_('SCHEDULES') @@ -186,7 +184,6 @@ const projectsSchedulesListRoute = { data: { activityStream: true, activityStreamTarget: 'project', - activityStreamId: 'id' }, ncyBreadcrumb: { label: N_('SCHEDULES') @@ -269,7 +266,7 @@ const projectsSchedulesEditRoute = { const jobsSchedulesRoute = { searchPrefix: 'schedule', - name: 'jobs.schedules', + name: 'schedules', route: '/schedules', params: { schedule_search: { @@ -284,7 +281,6 @@ const jobsSchedulesRoute = { activityStream: false, }, ncyBreadcrumb: { - parent: 'jobs', label: N_('SCHEDULES') }, resolve: { @@ -312,13 +308,16 @@ const jobsSchedulesRoute = { }] }, views: { - 'schedulesList@jobs': { + '@': { templateProvider: function(ScheduleList, generateList){ + let html = generateList.build({ list: ScheduleList, - mode: 'edit', - title: false + mode: 'edit' }); + html = generateList.wrapPanel(html); + let formPlaceholder = generateList.insertFormView(); + html = formPlaceholder + html; return html; }, controller: 'schedulerListController' @@ -339,16 +338,32 @@ const parentResolve = { }; const jobsSchedulesEditRoute = { - name: 'jobs.schedules.edit', + name: 'schedules.edit', route: '/:schedule_id', ncyBreadcrumb: { - parent: 'jobs.schedules', + parent: 'schedules', label: "{{breadcrumb.schedule_name}}" }, views: { - 'scheduler@jobs': { - controller: 'schedulerEditController', - templateUrl: templateUrl("scheduler/schedulerForm"), + 'form':{ + templateProvider: function(ParentObject, $http){ + let path; + if(ParentObject.type === 'system_job_template'){ + path = templateUrl('management-jobs/scheduler/schedulerForm'); + } + else { + path = templateUrl('scheduler/schedulerForm'); + } + return $http.get(path).then(response => response.data); + }, + controllerProvider: function(ParentObject){ + if (ParentObject.type === 'system_job_template') { + return 'managementJobEditController'; + } + else { + return 'schedulerEditController'; + } + } } }, resolve: _.merge(editScheduleResolve(), parentResolve) diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 301a7eae9d..fdfb0a118e 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -165,8 +165,7 @@ angular.module('Utilities', ['RestServices', 'Utilities']) Alert('Conflict', data.conflict || "Resource currently in use."); } else if (status === 410) { Alert('Deleted Object', 'The requested object was previously deleted and can no longer be accessed.'); - } else if ((status === 'Session is expired') || (status === 401 && data.detail && data.detail === 'Token is expired') || - (status === 401 && data && data.detail && data.detail === 'Invalid token')) { + } else if ((status === 'Session is expired') || (status === 401)) { if ($rootScope.sessionTimer) { $rootScope.sessionTimer.expireSession('idle'); } @@ -209,14 +208,17 @@ angular.module('Utilities', ['RestServices', 'Utilities']) } else { if (data[field]) { scope[field + '_api_error'] = data[field][0]; - //scope[form.name + '_form'][field].$setValidity('apiError', false); $('[name="' + field + '"]').addClass('ng-invalid'); + $('label[for="' + field + '"] span').addClass('error-color'); $('html, body').animate({scrollTop: $('[name="' + field + '"]').offset().top}, 0); fieldErrors = true; + if(form.fields[field].codeMirror){ + $(`#cm-${field}-container .CodeMirror`).addClass('error-border'); + } } } } - if ((!fieldErrors) && defaultMsg) { + if (defaultMsg) { Alert(defaultMsg.hdr, defaultMsg.msg); } } else if (typeof data === 'object' && data !== null) { diff --git a/awx/ui/client/src/shared/config/config.service.js b/awx/ui/client/src/shared/config/config.service.js index edf476cc35..910e89c9e4 100644 --- a/awx/ui/client/src/shared/config/config.service.js +++ b/awx/ui/client/src/shared/config/config.service.js @@ -21,7 +21,7 @@ export default delete(this.config); }, - getConfig: function () { + getConfig: function (licenseInfo) { var config = this.get(), that = this, deferred = $q.defer(); @@ -31,6 +31,11 @@ export default Wait('start'); var promise = Rest.get(); promise.then(function (response) { + // if applicable, use the license POSTs response if the config GET request is not returned due to a + // cluster cache update race condition + if (_.isEmpty(response.data.license_info) && !_.isEmpty(licenseInfo)) { + response.data.license_info = licenseInfo; + } var config = response.data; $rootScope.configReady = true; Wait('stop'); diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 574e8e8342..f9e1b9e426 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -167,7 +167,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat wrapPanel(html, ignorePanel){ if(ignorePanel) { return ` -
    ${html}
    @@ -176,7 +175,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } else { return ` -
    ${MessageBar(this.form)}
    ${html} @@ -742,7 +740,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock' && field.type !== 'workflow-chart') { - html += "
    ${field.onError.text}
    `; + } html += "
    \n"; + // Add help panel(s) html += (field.helpCollapse) ? this.buildHelpCollapse(field.helpCollapse) : ''; @@ -1728,8 +1739,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat button.label = i18n._('View Survey'); button['class'] = 'Form-surveyButton'; } - if (btn === 'workflow_editor') { - button.label = i18n._('Workflow Editor'); + if (btn === 'workflow_visualizer') { + button.label = i18n._('Workflow Visualizer'); button['class'] = 'Form-primaryButton'; } @@ -2023,5 +2034,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat ${options.text} `; } + } ]); diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index de973cff2f..da9ede3e06 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -139,7 +139,7 @@ angular.module('GeneratorHelpers', [systemStatus.name]) icon = 'fa-refresh'; break; case 'scm_update': - icon = 'fa-cloud-download'; + icon = 'fa-refresh'; break; case 'run': case 'rerun': diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html index df82cf5d85..ac82763c5a 100644 --- a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html @@ -8,13 +8,7 @@
    -
    - {{ tag.name }} -
    -
    - -
    +
    diff --git a/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js b/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js index 2471a1cf29..a2ee0a4a9d 100644 --- a/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js +++ b/awx/ui/client/src/shared/limit-panels/limit-panels.directive.js @@ -10,9 +10,10 @@ export default [function() { const maxPanels = parseInt(scope.maxPanels); scope.$watch( - () => angular.element('#' + scope.panelContainer).find('.Panel').length, + () => angular.element('#' + scope.panelContainer).find('.Panel, .at-Panel').length, () => { - const panels = angular.element('#' + scope.panelContainer).find('.Panel'); + const panels = angular.element('#' + scope.panelContainer).find('.Panel, .at-Panel'); + if(panels.length > maxPanels) { // hide the excess panels $(panels).each(function( index ) { diff --git a/awx/ui/client/src/shared/list-generator/list-actions.partial.html b/awx/ui/client/src/shared/list-generator/list-actions.partial.html index 7581e3195d..8144650a95 100644 --- a/awx/ui/client/src/shared/list-generator/list-actions.partial.html +++ b/awx/ui/client/src/shared/list-generator/list-actions.partial.html @@ -50,6 +50,7 @@ data-placement="{{options.dataPlacement}}" data-container="{{options.dataContainer}}" class="{{options.actionClass}}" + ng-class="{{options.ngClass}}" id="{{options.actionId}}" data-title="{{options.dataTitle}}" ng-disabled="{{options.ngDisabled}}" diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index a1f7088769..92fb28cf71 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -290,6 +290,7 @@ export default ['$compile', 'Attr', 'Icon', // gotcha: transcluded elements require custom scope linking - binding to $parent models assumes a very rigid DOM hierarchy // see: lookup-modal.directive.js for example innerTable += options.mode === 'lookup' ? `` : `"\n"`; + innerTable += "\n"; - if (list.index) { innerTable += "{{ $index + ((" + list.iterator + "_page - 1) * " + list.iterator + "_page_size) + 1 }}.\n"; } if (list.multiSelect) { - innerTable += ''; + innerTable += ''; } // Change layout if a lookup list, place radio buttons before labels if (options.mode === 'lookup') { if (options.input_type === "radio") { //added by JT so that lookup forms can be either radio inputs or check box inputs - innerTable += ` `; + innerTable += ` `; } else { // its assumed that options.input_type = checkbox innerTable += " - `; + for (fld in list.fields) { + if(fld === 'name' || _.has(list.fields[fld], 'includeModal')){ + let customClass = list.fields.name.modalColumnClass || ''; + html += ` + `; + } + + } if(list.fields.info) { - customClass = list.fields.name.modalColumnClass || ''; + let customClass = list.fields.name.modalColumnClass || ''; const infoHeaderClass = _.get(list.fields.info, 'infoHeaderClass', 'List-tableHeader--info'); html += ` diff --git a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js index aa8397e904..20b9c4e0a0 100644 --- a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js +++ b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js @@ -27,10 +27,11 @@ export default return { restrict: 'E', scope: { - item: '=item' + item: '=item', + disabled: '=' }, require: '^multiSelectList', - template: '', + template: '', link: function(scope, element, attrs, multiSelectList) { scope.decoratedItem = multiSelectList.registerItem(scope.item); diff --git a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html index 48eb41b1b5..891026dd8a 100644 --- a/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html +++ b/awx/ui/client/src/shared/multi-select-preview/multi-select-preview.partial.html @@ -6,13 +6,8 @@
    -
    - -
    -
    - {{selectedRow.name}} - {{selectedRow.hostname}} -
    + +
    diff --git a/awx/ui/client/src/shared/paginate/paginate.controller.js b/awx/ui/client/src/shared/paginate/paginate.controller.js index 5c4a9f1090..28be44eeae 100644 --- a/awx/ui/client/src/shared/paginate/paginate.controller.js +++ b/awx/ui/client/src/shared/paginate/paginate.controller.js @@ -56,6 +56,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q } $scope.dataset = res.data; $scope.collection = res.data.results; + $scope.$emit('updateDataset', res.data); }); }; diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index cb0b6087fc..edfa33dcdf 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -44,49 +44,50 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc replaceEncodedTokens(value) { return decodeURIComponent(value).replace(/"|'/g, ""); }, - encodeTerms (values, key) { + encodeTerms(value, key){ key = this.replaceDefaultFlags(key); - - if (!Array.isArray(values)) { - values = [values]; - } - - return values - .map(value => { - value = this.replaceDefaultFlags(value); - value = this.replaceEncodedTokens(value); - return [key, value]; + value = this.replaceDefaultFlags(value); + var that = this; + if (Array.isArray(value)){ + value = _.uniq(_.flattenDeep(value)); + let concated = ''; + angular.forEach(value, function(item){ + if(item && typeof item === 'string') { + item = that.replaceEncodedTokens(item); + } + concated += `${key}=${item}&`; }); + return concated; + } + else { + if(value && typeof value === 'string') { + value = this.replaceEncodedTokens(value); + } + + return `${key}=${value}&`; + } }, // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL encodeQueryset(params) { - if (typeof params !== 'object') { - return ''; - } + let queryset; + queryset = _.reduce(params, (result, value, key) => { + return result + this.encodeTerms(value, key); + }, ''); + queryset = queryset.substring(0, queryset.length - 1); + return angular.isObject(params) ? `?${queryset}` : ''; - return _.reduce(params, (result, value, key) => { - if (result !== '?') { - result += '&'; - } - - const encodedTermString = this.encodeTerms(value, key) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - - return result += encodedTermString; - }, '?'); }, // like encodeQueryset, but return an actual unstringified API-consumable http param object encodeQuerysetObject(params) { return _.reduce(params, (obj, value, key) => { - const encodedTerms = this.encodeTerms(value, key); + const encodedKey = this.replaceDefaultFlags(key); + const values = Array.isArray(value) ? value : [value]; - for (let encodedIndex in encodedTerms) { - const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; - obj[encodedKey] = obj[encodedKey] || []; - obj[encodedKey].push(encodedValue); - } + obj[encodedKey] = values + .map(value => this.replaceDefaultFlags(value)) + .map(value => this.replaceEncodedTokens(value)) + .join(','); return obj; }, {}); @@ -441,7 +442,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); }); - const paramPartIndex = searchParamParts.indexOf(value); + const paramPartIndex = searchParamParts.indexOf(decodeURIComponent(value)); if (paramPartIndex !== -1) { searchParamParts.splice(paramPartIndex, 1); diff --git a/awx/ui/client/src/shared/smart-search/smart-search.block.less b/awx/ui/client/src/shared/smart-search/smart-search.block.less index cd97490440..41f35eb37b 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.block.less +++ b/awx/ui/client/src/shared/smart-search/smart-search.block.less @@ -76,10 +76,15 @@ z-index: 1; } -.SmartSearch-searchButton:hover { +.SmartSearch-searchButton:not(.SmartSearch-searchButton--disabled):hover { background-color: @default-tertiary-bg; } +.SmartSearch-searchButton--disabled { + cursor: not-allowed; + opacity: 0.65; +} + .SmartSearch-flexContainer { display: flex; width: 100%; @@ -154,6 +159,9 @@ .SmartSearch-deleteContainer:hover > .SmartSearch-tagDelete { color: @default-bg; } +.SmartSearch-clearAll-container{ + .at-mixin-VerticallyCenter(); +} .SmartSearch-clearAll{ font-size: 10px; padding-top: 14px; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index ad14dd5344..90eef5e23b 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -16,71 +16,26 @@ function SmartSearchController ( let queryset; let transitionSuccessListener; - configService.getConfig() - .then(config => init(config)); - - function init (config) { - let version; - - try { - [version] = config.version.split('-'); - } catch (err) { - version = 'latest'; - } - - $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; - $scope.searchPlaceholder = i18n._('Search'); - - if ($scope.defaultParams) { - defaults = $scope.defaultParams; - } else { - // steps through the current tree of $state configurations, grabs default search params - const stateConfig = _.find($state.$current.path, step => _.has(step, `params.${searchKey}`)); - defaults = stateConfig.params[searchKey].config.value; - } - - if ($scope.querySet) { - queryset = _.cloneDeep($scope.querySet); - } else { - queryset = $state.params[searchKey]; - } - - path = GetBasePath($scope.basePath) || $scope.basePath; - generateSearchTags(); - - qs.initFieldset(path, $scope.djangoModel) - .then((data) => { - $scope.models = data.models; - $scope.options = data.options.data; - $scope.keyFields = _.reduce(data.models[$scope.djangoModel].base, function(result, value, key) { - if (value.filterable) { - result.push(key); - } - return result; - }, []); - if ($scope.list) { - $scope.$emit(optionsKey, data.options); - } - }); - - function compareParams (a, b) { - for (let key in a) { - if (!(key in b) || a[key].toString() !== b[key].toString()) { - return false; - } + const compareParams = (a, b) => { + for (let key in a) { + if (!(key in b) || a[key].toString() !== b[key].toString()) { + return false; } - for (let key in b) { - if (!(key in a)) { - return false; - } + } + for (let key in b) { + if (!(key in a)) { + return false; } - return true; } + return true; + }; - if (transitionSuccessListener) { - transitionSuccessListener(); - } + const generateSearchTags = () => { + const { singleSearchParam } = $scope; + $scope.searchTags = qs.createSearchTagsFromQueryset(queryset, defaults, singleSearchParam); + }; + const listenForTransitionSuccess = () => { transitionSuccessListener = $transitions.onSuccess({}, trans => { // State has changed - check to see if this is a param change if (trans.from().name === trans.to().name) { @@ -99,29 +54,22 @@ function SmartSearchController ( } } }); + }; - $scope.$on('$destroy', transitionSuccessListener); - $scope.$watch('disableSearch', disableSearch => { - if (disableSearch) { - $scope.searchPlaceholder = i18n._('Cannot search running job'); - } else { - $scope.searchPlaceholder = i18n._('Search'); - } - }); - } + const isAnsibleFactField = (termParts) => { + const rootField = termParts[0].split('.')[0].replace(/^-/, ''); + return rootField === 'ansible_facts'; + }; - function generateSearchTags () { - const { singleSearchParam } = $scope; - $scope.searchTags = qs.createSearchTagsFromQueryset(queryset, defaults, singleSearchParam); - } - - function revertSearch (queryToBeRestored) { + const revertSearch = (queryToBeRestored) => { queryset = queryToBeRestored; // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic // This transition will not reload controllers/resolves/views // but will register new $stateParams[$scope.iterator + '_search'] terms if (!$scope.querySet) { - $state.go('.', { [searchKey]: queryset }); + transitionSuccessListener(); + $state.go('.', { [searchKey]: queryset }) + .then(() => listenForTransitionSuccess()); } qs.search(path, queryset).then((res) => { if ($scope.querySet) { @@ -129,23 +77,15 @@ function SmartSearchController ( } $scope.dataset = res.data; $scope.collection = res.data.results; + $scope.$emit('updateDataset', res.data); }); $scope.searchTerm = null; generateSearchTags(); - } - - $scope.toggleKeyPane = () => { - $scope.showKeyPane = !$scope.showKeyPane; }; - function isAnsibleFactField (termParts) { - const rootField = termParts[0].split('.')[0].replace(/^-/, ''); - return rootField === 'ansible_facts'; - } - - function isFilterableBaseField (termParts) { + const isFilterableBaseField = (termParts) => { const rootField = termParts[0].split('.')[0].replace(/^-/, ''); const listName = $scope.list.name; const baseFieldPath = `models.${listName}.base.${rootField}`; @@ -155,9 +95,9 @@ function SmartSearchController ( const isBaseModelRelatedSearchTermField = (_.get($scope, `${baseFieldPath}.type`) === 'field'); return isBaseField && !isBaseModelRelatedSearchTermField && isFilterable; - } + }; - function isRelatedField (termParts) { + const isRelatedField = (termParts) => { const rootField = termParts[0].split('.')[0].replace(/^-/, ''); const listName = $scope.list.name; const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`; @@ -166,43 +106,113 @@ function SmartSearchController ( const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field'); return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField); - } + }; + + configService.getConfig() + .then(config => { + let version; + + try { + [version] = config.version.split('-'); + } catch (err) { + version = 'latest'; + } + + $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; + $scope.searchPlaceholder = i18n._('Search'); + + if ($scope.defaultParams) { + defaults = $scope.defaultParams; + } else { + // steps through the current tree of $state configurations, grabs default search params + const stateConfig = _.find($state.$current.path, step => _.has(step, `params.${searchKey}`)); + defaults = stateConfig.params[searchKey].config.value; + } + + if ($scope.querySet) { + queryset = $scope.querySet; + } else { + queryset = $state.params[searchKey]; + } + + path = GetBasePath($scope.basePath) || $scope.basePath; + generateSearchTags(); + + qs.initFieldset(path, $scope.djangoModel) + .then((data) => { + $scope.models = data.models; + $scope.options = data.options.data; + $scope.keyFields = _.reduce(data.models[$scope.djangoModel].base, (result, value, key) => { + if (value.filterable) { + result.push(key); + } + return result; + }, []); + if ($scope.list) { + $scope.$emit(optionsKey, data.options); + } + }); + $scope.$on('$destroy', () => { + if (transitionSuccessListener) { + transitionSuccessListener(); + } + }); + $scope.$watch('disableSearch', disableSearch => { + if (disableSearch) { + $scope.searchPlaceholder = i18n._('Cannot search running job'); + } else { + $scope.searchPlaceholder = i18n._('Search'); + } + }); + + listenForTransitionSuccess(); + }); + + + $scope.toggleKeyPane = () => { + $scope.showKeyPane = !$scope.showKeyPane; + }; $scope.addTerms = terms => { - const { singleSearchParam } = $scope; - const unmodifiedQueryset = _.clone(queryset); + if (terms && terms !== "") { + const { singleSearchParam } = $scope; + const unmodifiedQueryset = _.clone(queryset); - const searchInputQueryset = qs.getSearchInputQueryset(terms, isFilterableBaseField, isRelatedField, isAnsibleFactField, singleSearchParam); - queryset = qs.mergeQueryset(queryset, searchInputQueryset, singleSearchParam); + const searchInputQueryset = qs.getSearchInputQueryset(terms, isFilterableBaseField, isRelatedField, isAnsibleFactField, singleSearchParam); + queryset = qs.mergeQueryset(queryset, searchInputQueryset, singleSearchParam); - // Go back to the first page after a new search - delete queryset.page; + // Go back to the first page after a new search + delete queryset.page; - // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic - // This transition will not reload controllers/resolves/views but will register new - // $stateParams[searchKey] terms. - if (!$scope.querySet) { - $state.go('.', { [searchKey]: queryset }) - .then(() => { - // same as above in $scope.remove. For some reason deleting the page - // from the queryset works for all lists except lists in modals. - delete $stateParams[searchKey].page; - }); + // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic + // This transition will not reload controllers/resolves/views but will register new + // $stateParams[searchKey] terms. + if (!$scope.querySet) { + transitionSuccessListener(); + $state.go('.', { [searchKey]: queryset }) + .then(() => { + // same as above in $scope.remove. For some reason deleting the page + // from the queryset works for all lists except lists in modals. + delete $stateParams[searchKey].page; + listenForTransitionSuccess(); + }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + $scope.$emit('updateDataset', data); + }) + .catch(() => revertSearch(unmodifiedQueryset)); + + $scope.searchTerm = null; + + generateSearchTags(); } - - qs.search(path, queryset) - .then(({ data }) => { - if ($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = data; - $scope.collection = data.results; - }) - .catch(() => revertSearch(unmodifiedQueryset)); - - $scope.searchTerm = null; - - generateSearchTags(); }; // remove tag, merge new queryset, $state.go $scope.removeTerm = index => { @@ -212,6 +222,7 @@ function SmartSearchController ( queryset = qs.removeTermsFromQueryset(queryset, term, isFilterableBaseField, isRelatedField, isAnsibleFactField, singleSearchParam); if (!$scope.querySet) { + transitionSuccessListener(); $state.go('.', { [searchKey]: queryset }) .then(() => { // for some reason deleting a tag from a list in a modal does not @@ -219,6 +230,7 @@ function SmartSearchController ( // that that happened and remove it if it didn't. const clearedParams = qs.removeTermsFromQueryset($stateParams[searchKey], term, isFilterableBaseField, isRelatedField, isAnsibleFactField, singleSearchParam); $stateParams[searchKey] = clearedParams; + listenForTransitionSuccess(); }); } @@ -229,20 +241,27 @@ function SmartSearchController ( } $scope.dataset = data; $scope.collection = data.results; + $scope.$emit('updateDataset', data); }); generateSearchTags(); }; $scope.clearAllTerms = () => { + _.forOwn(defaults, (value, key) => { + // preserve the `credential_type` queryset param if it exists + if (key === 'credential_type') { + defaults[key] = queryset[key]; + } + }); const cleared = _(defaults).omit(_.isNull).value(); - delete cleared.page; - queryset = cleared; if (!$scope.querySet) { - $state.go('.', { [searchKey]: queryset }); + transitionSuccessListener(); + $state.go('.', { [searchKey]: queryset }) + .then(() => listenForTransitionSuccess()); } qs.search(path, queryset) @@ -252,6 +271,7 @@ function SmartSearchController ( } $scope.dataset = data; $scope.collection = data.results; + $scope.$emit('updateDataset', data); }); $scope.searchTags = qs.stripDefaultParams(queryset, defaults); diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html index a44a90f649..cc85dd8f30 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -7,7 +7,7 @@ -
    +
    @@ -20,14 +20,11 @@
    @@ -49,7 +46,7 @@
    {{ 'ADDITIONAL INFORMATION' | translate }}: - {{ 'For additional information on advanced search search syntax please see the Ansible Tower' | translate }} + {{ 'For additional information on advanced search syntax please see the Ansible Tower' | translate }} {{ 'documentation' | translate }}.
    diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index f58c2d6928..5ccc4ab460 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -5,8 +5,8 @@ *************************************************/ import ReconnectingWebSocket from 'reconnectingwebsocket'; export default -['$rootScope', '$location', '$log','$state', '$q', 'i18n', - function ($rootScope, $location, $log, $state, $q, i18n) { +['$rootScope', '$location', '$log','$state', '$q', 'i18n', 'GetBasePath', 'Rest', '$cookies', + function ($rootScope, $location, $log, $state, $q, i18n, GetBasePath, Rest, $cookies) { var needsResubscribing = false, socketPromise = $q.defer(), needsRefreshAfterBlur; @@ -130,13 +130,20 @@ export default else if(data.group_name==="inventory_update_events"){ str = `ws-${data.group_name}-${data.inventory_update}`; } - else if(data.group_name==="control"){ - // As of v. 3.1.0, there is only 1 "control" - // message, which is for expiring the session if the - // session limit is breached. + else if(data.group_name === "control" && data.reason === "limit_reached"){ + // If we got a `limit_reached_` message, determine + // if the current session is still valid (it may have been + // invalidated) + // If so, log the user out and show a meaningful error $log.debug(data.reason); - $rootScope.sessionTimer.expireSession('session_limit'); - $state.go('signOut'); + let url = GetBasePath('me'); + Rest.get(url) + .catch(function(resp) { + if (resp.status === 401) { + $rootScope.sessionTimer.expireSession('session_limit'); + $state.go('signOut'); + } + }); } else { // The naming scheme is "ws" then a @@ -150,7 +157,7 @@ export default if(this.socket){ this.socket.close(); delete this.socket; - console.log("Socket deleted: "+this.socket); + $log.debug("Socket deleted: "+this.socket); } }, subscribe: function(state){ @@ -158,6 +165,8 @@ export default // listen for specific messages. A subscription object could // look like {"groups":{"jobs": ["status_changed", "summary"]}. // This is used by all socket-enabled $states + state.data.socket.groups.control = ['limit_reached_' + $rootScope.current_user.id]; + state.data.socket.xrftoken = $cookies.get('csrftoken'); this.emit(JSON.stringify(state.data.socket)); this.setLast(state); }, @@ -166,6 +175,7 @@ export default // on a socket-enabled page, and sends an empty groups object // to the API: {"groups": {}}. // This is used for all pages that are socket-disabled + state.data.socket.xrftoken = $cookies.get('csrftoken'); if(this.requiresNewSubscribe(state)){ this.emit(JSON.stringify(state.data.socket) || JSON.stringify({"groups": {}})); } diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 99dc4f6f99..8972b9c19a 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -209,9 +209,10 @@ function($injector, $stateExtender, $log, i18n) { FormDefinition: [params.form, function(definition) { return definition; }], - resourceData: ['FormDefinition', 'Rest', '$stateParams', 'GetBasePath', - function(FormDefinition, Rest, $stateParams, GetBasePath) { + resourceData: ['FormDefinition', 'Rest', '$stateParams', 'GetBasePath', '$q', 'ProcessErrors', + function(FormDefinition, Rest, $stateParams, GetBasePath, $q, ProcessErrors) { let form, path; + let deferred = $q.defer(); form = typeof(FormDefinition) === 'function' ? FormDefinition() : FormDefinition; if (GetBasePath(form.basePath) === undefined && GetBasePath(form.stateTree) === undefined ){ @@ -221,7 +222,18 @@ function($injector, $stateExtender, $log, i18n) { path = (GetBasePath(form.basePath) || GetBasePath(form.stateTree) || form.basePath) + $stateParams[`${form.name}_id`]; } Rest.setUrl(path); - return Rest.get(); + Rest.get() + .then((response) => deferred.resolve(response)) + .catch(({ data, status }) => { + ProcessErrors(null, data, status, null, + { + hdr: i18n._('Error!'), + msg: i18n._('Unable to get resource: ') + status + } + ); + deferred.reject(); + }); + return deferred.promise; } ] }, @@ -868,7 +880,11 @@ function($injector, $stateExtender, $log, i18n) { $stateParams[`${list.iterator}_search`].role_level = "admin_role"; $stateParams[`${list.iterator}_search`].credential_type = InsightsCredTypePK.toString() ; } - + if(list.iterator === 'credential') { + if($state.current.name.includes('projects.edit') || $state.current.name.includes('projects.add')) { + state.params[`${list.iterator}_search`].value = _.merge(state.params[`${list.iterator}_search`].value, $stateParams[`${list.iterator}_search`]); + } + } return qs.search(path, $stateParams[`${list.iterator}_search`]); } diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index 1caf92e5ba..7945cfe085 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default ['$scope', '$filter', - function ($scope, $filter) { +export default ['$scope', '$filter', 'i18n', + function ($scope, $filter, i18n) { function isFailureState(status) { return status === 'failed' || status === 'error' || status === 'canceled'; @@ -40,7 +40,7 @@ export default ['$scope', '$filter', jobId: job.id, sortDate: job.finished || "running" + job.id, finished: finished, - status_tip: "JOB ID: " + job.id + "
    STATUS: " + job.status.toUpperCase() + "
    FINISHED: " + finished, + status_tip: `${i18n._('JOB ID')}: ${job.id}
    ${i18n._('STATUS')}: ${job.status.toUpperCase()}
    ${i18n._('FINISHED')}: ${finished}`, detailsUrl: detailsBaseUrl + job.id }; diff --git a/awx/ui/client/src/teams/edit/teams-edit.controller.js b/awx/ui/client/src/teams/edit/teams-edit.controller.js index 41a37b19cb..dc9beea14d 100644 --- a/awx/ui/client/src/teams/edit/teams-edit.controller.js +++ b/awx/ui/client/src/teams/edit/teams-edit.controller.js @@ -5,14 +5,15 @@ *************************************************/ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', - 'ProcessErrors', 'GetBasePath', 'Wait', '$state', 'OrgAdminLookup', 'resolvedModels', + 'ProcessErrors', 'GetBasePath', 'Wait', '$state', 'OrgAdminLookup', 'resolvedModels', 'resourceData', function($scope, $rootScope, $stateParams, TeamForm, Rest, ProcessErrors, - GetBasePath, Wait, $state, OrgAdminLookup, models) { + GetBasePath, Wait, $state, OrgAdminLookup, models, Dataset) { const { me } = models; - var form = TeamForm, - id = $stateParams.team_id, - defaultUrl = GetBasePath('teams') + id; + const { data } = Dataset; + const id = $stateParams.team_id; + const defaultUrl = GetBasePath('teams') + id; + let form = TeamForm; init(); @@ -20,26 +21,19 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', $scope.canEdit = me.get('summary_fields.user_capabilities.edit'); $scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0; $scope.team_id = id; - Rest.setUrl(defaultUrl); - Wait('start'); - Rest.get(defaultUrl).then(({data}) => { - setScopeFields(data); - $scope.organization_name = data.summary_fields.organization.name; + setScopeFields(data); + $scope.organization_name = data.summary_fields.organization.name; - OrgAdminLookup.checkForAdminAccess({organization: data.organization}) + OrgAdminLookup.checkForAdminAccess({organization: data.organization}) .then(function(canEditOrg){ $scope.canEditOrg = canEditOrg; }); - $scope.team_obj = data; - Wait('stop'); - }); + $scope.team_obj = data; $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { $scope.canAdd = (val === false) ? false : true; }); - - } // @issue I think all this really want to do is _.forEach(form.fields, (field) =>{ $scope[field] = data[field]}) diff --git a/awx/ui/client/src/teams/list/teams-list.controller.js b/awx/ui/client/src/teams/list/teams-list.controller.js index 024f74361b..88df298f55 100644 --- a/awx/ui/client/src/teams/list/teams-list.controller.js +++ b/awx/ui/client/src/teams/list/teams-list.controller.js @@ -53,7 +53,7 @@ export default ['$scope', 'Rest', 'TeamList', 'Prompt', let reloadListStateParams = null; - if($scope.teams.length === 1 && $state.params.team_search && !_.isEmpty($state.params.team_search.page) && $state.params.team_search.page !== '1') { + if($scope.teams.length === 1 && $state.params.team_search && _.has($state, 'params.team_search.page') && $state.params.team_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.team_search.page = (parseInt(reloadListStateParams.team_search.page)-1).toString(); } diff --git a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js index be9ebd5ed3..1ee7f244ea 100644 --- a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js +++ b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js @@ -89,7 +89,8 @@ } } $scope.job_type = $scope.job_type_options[form.fields.job_type.default]; - $scope.custom_virtualenvs_options = ConfigData.custom_virtualenvs; + const virtualEnvs = ConfigData.custom_virtualenvs || []; + $scope.custom_virtualenvs_options = virtualEnvs; CreateSelect2({ element:'#job_template_job_type', @@ -200,18 +201,17 @@ var msg; switch (data.status) { case 'failed': - msg = "
    The Project selected has a status of \"failed\". You must run a successful update before you can select a playbook. You will not be able to save this Job Template without a valid playbook."; + msg = `
    ${i18n._('The Project selected has a status of')} \"${i18n._('failed')}\". ${i18n._('You must run a successful update before you can select a playbook. You will not be able to save this Job Template without a valid playbook.')}
    `; break; case 'never updated': - msg = "
    The Project selected has a status of \"never updated\". You must run a successful update before you can select a playbook. You will not be able to save this Job Template without a valid playbook."; + msg = `
    ${i18n._('The Project selected has a status of')} \"${i18n._('never updated')}\". ${i18n._('You must run a successful update before you can select a playbook. You will not be able to save this Job Template without a valid playbook.')}
    `; break; case 'missing': - msg = '
    The selected project has a status of \"missing\". Please check the server and make sure ' + - ' the directory exists and file permissions are set correctly.
    '; + msg = `
    ${i18n._('The selected project has a status of')} \"${i18n._('missing')}\". ${i18n._('Please check the server and make sure the directory exists and file permissions are set correctly.')}
    `; break; } if (msg) { - Alert('Warning', msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); + Alert(i18n._('Warning'), msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); } }) .catch(({data, status}) => { @@ -495,5 +495,50 @@ $scope.formCancel = function () { $state.transitionTo('templates'); }; + + let handleLabelCount = () => { + /** + * This block of code specifically handles the client-side validation of the `labels` field. + * Due to it's detached nature in relation to the other job template fields, we must + * validate this field client-side in order to avoid the edge case where a user can make a + * successful POST to the `job_templates` endpoint but however encounter a 200 error from + * the `labels` endpoint due to a character limit. + * + * We leverage two of select2's available events, `select` and `unselect`, to detect when the user + * has either added or removed a label. From there, we set a flag and do simple string length + * checks to make sure a label's chacacter count remains under 512. Otherwise, we disable the "Save" button + * by invalidating the field and inform the user of the error. + */ + + + $scope.job_template_labels_isValid = true; + const maxCount = 512; + const jt_label_id = 'job_template_labels'; + + // Detect when a new label is added + $(`#${jt_label_id}`).on('select2:select', (e) => { + const { text } = e.params.data; + + // If the character count of an added label is greater than 512, we set `labels` field as invalid + if (text.length > maxCount) { + $scope.job_template_form.labels.$setValidity(`${jt_label_id}`, false); + $scope.job_template_labels_isValid = false; + } + }); + + // Detect when a label is removed + $(`#${jt_label_id}`).on('select2:unselect', (e) => { + const { text } = e.params.data; + + /* If the character count of a removed label is greater than 512 AND the field is currently marked + as invalid, we set it back to valid */ + if (text.length > maxCount && $scope.job_template_form.labels.$error) { + $scope.job_template_form.labels.$setValidity(`${jt_label_id}`, true); + $scope.job_template_labels_isValid = true; + } + }); + }; + + handleLabelCount(); } ]; diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 85bc0882a2..1b6b4a1c99 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -62,7 +62,8 @@ export default $scope.surveyTooltip = i18n._('Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.'); $scope.job_tag_options = []; $scope.skip_tag_options = []; - $scope.custom_virtualenvs_options = ConfigData.custom_virtualenvs; + const virtualEnvs = ConfigData.custom_virtualenvs || []; + $scope.custom_virtualenvs_options = virtualEnvs; SurveyControllerInit({ scope: $scope, @@ -499,7 +500,7 @@ export default null, true); } - MultiCredentialService + var credDefer = MultiCredentialService .saveRelated(jobTemplateData, $scope.multiCredential.selectedCredentials); InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups) @@ -579,7 +580,7 @@ export default Rest.setUrl(data.related.labels); - var defers = []; + var defers = [credDefer]; for (var i = 0; i < toPost.length; i++) { defers.push(Rest.post(toPost[i])); } @@ -643,22 +644,7 @@ export default data.ask_credential_on_launch = $scope.ask_credential_on_launch ? $scope.ask_credential_on_launch : false; data.job_tags = (Array.isArray($scope.job_tags)) ? $scope.job_tags.join() : ""; data.skip_tags = (Array.isArray($scope.skip_tags)) ? $scope.skip_tags.join() : ""; - if ($scope.selectedCredentials && $scope.selectedCredentials - .machine && $scope.selectedCredentials - .machine.id) { - data.credential = $scope.selectedCredentials - .machine.id; - } else { - data.credential = null; - } - if ($scope.selectedCredentials && $scope.selectedCredentials - .vault && $scope.selectedCredentials - .vault.id) { - data.vault_credential = $scope.selectedCredentials - .vault.id; - } else { - data.vault_credential = null; - } + data.extra_vars = ToJSON($scope.parseType, $scope.extra_vars, true); @@ -699,13 +685,8 @@ export default data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : ""; data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : ""; - // drop legacy 'credential' and 'vault_credential' keys from the update request as they will - // be provided to the related credentials endpoint by the template save success handler. - delete data.credential; - delete data.vault_credential; - Rest.setUrl(defaultUrl + $state.params.job_template_id); - Rest.put(data) + Rest.patch(data) .then(({data}) => { $scope.$emit('templateSaveSuccess', data); }) @@ -723,5 +704,49 @@ export default $scope.formCancel = function () { $state.go('templates'); }; + + let handleLabelCount = () => { + /** + * This block of code specifically handles the client-side validation of the `labels` field. + * Due to it's detached nature in relation to the other job template fields, we must + * validate this field client-side in order to avoid the edge case where a user can make a + * successful POST to the `job_templates` endpoint but however encounter a 200 error from + * the `labels` endpoint due to a character limit. + * + * We leverage two of select2's available events, `select` and `unselect`, to detect when the user + * has either added or removed a label. From there, we set a flag and do simple string length + * checks to make sure a label's chacacter count remains under 512. Otherwise, we disable the "Save" button + * by invalidating the field and inform the user of the error. + */ + + $scope.job_template_labels_isValid = true; + const maxCount = 512; + const jt_label_id = 'job_template_labels'; + + // Detect when a new label is added + $(`#${jt_label_id}`).on('select2:select', (e) => { + const { text } = e.params.data; + + // If the character count of an added label is greater than 512, we set `labels` field as invalid + if (text.length > maxCount) { + $scope.job_template_form.labels.$setValidity(`${jt_label_id}`, false); + $scope.job_template_labels_isValid = false; + } + }); + + // Detect when a label is removed + $(`#${jt_label_id}`).on('select2:unselect', (e) => { + const { text } = e.params.data; + + /* If the character count of a removed label is greater than 512 AND the field is currently marked + as invalid, we set it back to valid */ + if (text.length > maxCount && $scope.job_template_form.labels.$error) { + $scope.job_template_form.labels.$setValidity(`${jt_label_id}`, true); + $scope.job_template_labels_isValid = true; + } + }); + }; + + handleLabelCount(); } ]; diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index bfc1073c7f..7b75e16415 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -229,19 +229,24 @@ function(NotificationsList, i18n) { dataPlacement: 'right', awPopOver: "

    " + i18n._("Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.") + "

    ", dataContainer: 'body', + onError: { + ngShow: 'job_template_labels_isValid !== true', + text: i18n._('Max 512 characters per label.'), + }, ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' }, custom_virtualenv: { label: i18n._('Ansible Environment'), type: 'select', - defaultText: i18n._('Default Environment'), + defaultText: i18n._('Use Default Environment'), ngOptions: 'venv for venv in custom_virtualenvs_options track by venv', + awPopOver: "

    " + i18n._("Select the custom Python virtual environment for this job template to run on.") + "

    ", dataTitle: i18n._('Ansible Environment'), dataContainer: 'body', dataPlacement: 'right', ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)', - ngShow: 'custom_virtualenvs_options.length > 0' + ngShow: 'custom_virtualenvs_options.length > 1' }, instance_groups: { label: i18n._('Instance Groups'), diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js index c7ca29e3ee..3c6bacd515 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js @@ -78,7 +78,7 @@ function MultiCredentialModal( function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) { const vm = this; - const { createTag, isReadOnly } = MultiCredentialService; + const { createTag } = MultiCredentialService; const types = {}; const unwatch = []; @@ -100,7 +100,14 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) scope.credentials = scope.credential_dataset.results; scope.credentialType = getInitialCredentialType(); - scope.displayedCredentialTypes = scope.credentialTypes; + scope.displayedCredentialTypes = []; + + scope.credentialTypes.forEach((credentialType => { + if(credentialType.kind + .match(/^(machine|cloud|net|ssh|vault)$/)) { + scope.displayedCredentialTypes.push(credentialType); + } + })); const watchType = scope.$watch('credentialType', (newValue, oldValue) => { if (newValue !== oldValue) { @@ -109,7 +116,6 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) }); scope.$watchCollection('modalSelectedCredentials', updateListView); scope.$watchCollection('modalSelectedCredentials', updateTagView); - scope.$watchCollection('modalSelectedCredentials', updateDisplayedCredentialTypes); scope.$watchCollection('credentials', updateListView); unwatch.push(watchType); @@ -137,30 +143,11 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) }); } - function updateDisplayedCredentialTypes() { - const displayedCredentialTypes = _.cloneDeep(scope.credentialTypes); - - scope.modalSelectedCredentials.forEach(credential => { - const credentialTypeId = credential.credential_type || credential.credential_type_id; - - if(isReadOnly(credential) && credentialTypeId !== types.Vault) { - const index = displayedCredentialTypes - .map(t => t.id).indexOf(credentialTypeId); - - if (index > -1) { - displayedCredentialTypes.splice(index, 1); - } - } - }); - - scope.displayedCredentialTypes = displayedCredentialTypes; - } - function getInitialCredentialType () { const selectedMachineCredential = scope.modalSelectedCredentials .find(c => c.id === types.Machine); - if (selectedMachineCredential && isReadOnly(selectedMachineCredential)) { + if (selectedMachineCredential) { return `${types.Vault}`; } @@ -176,12 +163,6 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) return qs.search(endpoint, scope.credential_default_params) .then(({ data }) => { - const results = data.results.filter(c => !isReadOnly(c)); - const readOnlyCount = data.results.length - results.length; - - data.results = results; - data.count = data.count - readOnlyCount; - scope.credential_dataset = data; scope.credentials = data.results; @@ -211,7 +192,7 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) vm.toggle_credential = credential => { // This is called only when a checkbox input is clicked directly. Clicks anywhere else - // on the row or direct radio button clicks invoke the toggle_row handler instead. We + // on the row or direct radio button clicks invoke the toggle_row handler instead. We // pass this through to the other function so that the behavior is consistent. vm.toggle_row(credential); }; diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html index ebdb08fc98..bd74206060 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html @@ -21,8 +21,8 @@
    -
    -
    +
    +
    @@ -30,16 +30,7 @@
    -
    - - - - - - -
    -
    +
    {{ tag.name }} @@ -48,8 +39,7 @@
    + ng-click="vm.removeCredential(tag)">
    diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html index 25692aa177..e2a9ba38c7 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html @@ -18,7 +18,7 @@
    -
    +
    @@ -26,7 +26,7 @@
    -
    +
    @@ -35,7 +35,7 @@
    + ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled, 'MultiCredential-tag--disabled': fieldIsDisabled}"> {{ tag.name }} @@ -45,7 +45,7 @@
    + ng-hide="fieldIsDisabled">
    diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js index ecb2512247..e687834645 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js @@ -38,14 +38,11 @@ function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) { .filter(id => selected.indexOf(id) < 0) .map(id => disassociate({ related }, id)); - const associationPromises = selected - .filter(id => currentlyAssociated.indexOf(id) < 0) - .map(id => associate({ related }, id)); - - const promises = disassociationPromises - .concat(associationPromises); - - return $q.all(promises); + return $q.all(disassociationPromises).then(() => { + _.each(selected.filter(id => currentlyAssociated.indexOf(id) < 0), (id) => { + return associate({related}, id); + }); + }); }); }; @@ -70,13 +67,6 @@ function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) { .catch(handleError('GET', 'credential types')); }; - this.isReadOnly = credential => { - const canEdit = _.get(credential, 'summary_fields.user_capabilities.edit'); - const canDelete = _.get(credential, 'summary_fields.user_capabilities.delete'); - - return !(canEdit || canDelete); - }; - this.createTag = (credential, credential_types) => { const credentialTypeId = credential.credential_type || credential.credential_type_id; const credentialType = credential_types.find(t => t.id === credentialTypeId); @@ -86,8 +76,7 @@ function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) { name: credential.name, kind: _.get(credentialType, 'kind'), typeName: _.get(credentialType, 'name'), - info: _.get(credential, 'inputs.vault_id'), - readOnly: this.isReadOnly(credential), + info: _.get(credential, 'inputs.vault_id') }; }; } diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 4bb8a15b49..a7890c288b 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -482,19 +482,19 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p } $scope.toggle_row = function(selectedRow) { + if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { + $scope.job_templates.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.job_templates[i].checked = 1; + $scope.selection[list.iterator] = { + id: row.id, + name: row.name + }; - $scope.job_templates.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.job_templates[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - + $scope.templateManuallySelected(row); + } + }); + } }; $scope.$watch('selectedTemplate', () => { @@ -559,19 +559,19 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p } $scope.toggle_row = function(selectedRow) { + if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { + $scope.workflow_inventory_sources.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.workflow_inventory_sources[i].checked = 1; + $scope.selection[list.iterator] = { + id: row.id, + name: row.name + }; - $scope.workflow_inventory_sources.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.workflow_inventory_sources[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - + $scope.templateManuallySelected(row); + } + }); + } }; $scope.$watch('selectedTemplate', () => { @@ -636,19 +636,19 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p } $scope.toggle_row = function(selectedRow) { + if ($scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) { + $scope.projects.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.projects[i].checked = 1; + $scope.selection[list.iterator] = { + id: row.id, + name: row.name + }; - $scope.projects.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.projects[i].checked = 1; - $scope.selection[list.iterator] = { - id: row.id, - name: row.name - }; - - $scope.templateManuallySelected(row); - } - }); - + $scope.templateManuallySelected(row); + } + }); + } }; $scope.$watch('selectedTemplate', () => { @@ -708,6 +708,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p delete list.fields.labels; delete list.fieldActions; list.fields.name.columnClass = "col-md-8"; + list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; + list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; list.iterator = 'job_template'; list.name = 'job_templates'; list.basePath = "job_templates"; @@ -733,6 +735,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p list.fields.name.columnClass = "col-md-11"; list.maxVisiblePages = 5; list.searchBarFullWidth = true; + list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; + list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; return list; } @@ -742,6 +746,8 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p let list = _.cloneDeep(InventorySourcesList); list.maxVisiblePages = 5; list.searchBarFullWidth = true; + list.disableRow = "{{ !workflowJobTemplateObj.summary_fields.user_capabilities.edit }}"; + list.disableRowValue = '!workflowJobTemplateObj.summary_fields.user_capabilities.edit'; return list; } diff --git a/awx/ui/client/src/templates/prompt/prompt.block.less b/awx/ui/client/src/templates/prompt/prompt.block.less index 4b2814d5f5..b074a31b40 100644 --- a/awx/ui/client/src/templates/prompt/prompt.block.less +++ b/awx/ui/client/src/templates/prompt/prompt.block.less @@ -21,6 +21,7 @@ padding-left:15px; padding-right: 15px; min-width: 85px; + margin-left: 20px; } .Prompt-actionButton:disabled { background-color: @d7grey; @@ -42,7 +43,6 @@ padding-right: 15px; height: 30px; min-width: 85px; - margin-right: 20px; } .Prompt-defaultButton:hover{ background-color: @btn-bg-hov; @@ -65,8 +65,6 @@ border: 1px solid @default-border; padding: 10px; border-radius: 5px; - max-height: 120px; - overflow-y: auto; } .Prompt-selectedItemRevert { display: flex; @@ -108,8 +106,9 @@ line-height: 29px; } .Prompt-previewTags--outer { + display: flex; flex: 1 0 auto; - max-width: ~"calc(100% - 140px)"; + width: ~"calc(100% - 140px)"; } .Prompt-previewTags--inner { display: flex; @@ -123,11 +122,13 @@ color: @default-list-header-bg; } .Prompt-previewTagRevert { - flex: 0 0 60px; - line-height: 29px; + display: flex; + align-items: center; + justify-content: center; } .Prompt-previewTagContainer { display: flex; + flex-flow: row wrap; } .Prompt-previewRow--flex { display: flex; @@ -142,8 +143,10 @@ text-transform: uppercase; } .Prompt-previewRowValue { - flex: 1 0 auto; max-width: 508px; + display: flex; + flex-wrap: wrap; + align-items: flex-start; } .Prompt-noSelectedItem { height: 30px; diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js index 362e1223fe..1c5c33a6c5 100644 --- a/awx/ui/client/src/templates/prompt/prompt.controller.js +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -20,6 +20,7 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', vm.actionButtonClicked = false; if(vm.promptData && vm.promptData.triggerModalOpen) { + scope.$emit('launchModalOpen', true); vm.promptDataClone = _.cloneDeep(vm.promptData); vm.steps = { @@ -73,61 +74,65 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', vm.promptDataClone.prompts.credentials.passwords = {}; - if(vm.promptDataClone.launchConf.passwords_needed_to_start) { - let machineCredPassObj = null; - vm.promptDataClone.launchConf.passwords_needed_to_start.forEach((passwordNeeded) => { - if (passwordNeeded === "ssh_password" || - passwordNeeded === "become_password" || - passwordNeeded === "ssh_key_unlock" - ) { - if (!machineCredPassObj) { - vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - if (defaultCredential.kind && defaultCredential.kind === "ssh") { - machineCredPassObj = { - id: defaultCredential.id, - name: defaultCredential.name - }; - } else if (defaultCredential.passwords_needed) { - defaultCredential.passwords_needed.forEach((neededPassword) => { - if (neededPassword === passwordNeeded) { - machineCredPassObj = { - id: defaultCredential.id, - name: defaultCredential.name - }; - } - }); - } - }); - } - - vm.promptDataClone.prompts.credentials.passwords[passwordNeeded] = angular.copy(machineCredPassObj); - } else if (passwordNeeded.startsWith("vault_password")) { - let vault_id = null; - if (passwordNeeded.includes('.')) { - vault_id = passwordNeeded.split(/\.(.+)/)[1]; - } - - if (!vm.promptDataClone.prompts.credentials.passwords.vault) { + vm.promptDataClone.prompts.credentials.value.forEach((credential) => { + if(credential.inputs) { + if(credential.inputs.password && credential.inputs.password === "ASK") { + vm.promptDataClone.prompts.credentials.passwords.ssh_password = { + id: credential.id, + name: credential.name + }; + } + if(credential.inputs.become_password && credential.inputs.become_password === "ASK") { + vm.promptDataClone.prompts.credentials.passwords.become_password = { + id: credential.id, + name: credential.name + }; + } + if(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") { + vm.promptDataClone.prompts.credentials.passwords.ssh_key_unlock = { + id: credential.id, + name: credential.name + }; + } + if(credential.inputs.vault_password && credential.inputs.vault_password === "ASK") { + if(!vm.promptDataClone.prompts.credentials.passwords.vault) { vm.promptDataClone.prompts.credentials.passwords.vault = []; } - - // Loop across the default credentials to find the name of the - // credential that requires a password - vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => { - if (vm.promptDataClone.prompts.credentials.credentialTypes[defaultCredential.credential_type] === "vault") { - let defaultCredVaultId = defaultCredential.vault_id || _.get(defaultCredential, 'inputs.vault_id') || null; - if (defaultCredVaultId === vault_id) { - vm.promptDataClone.prompts.credentials.passwords.vault.push({ - id: defaultCredential.id, - name: defaultCredential.name, - vault_id: defaultCredVaultId - }); - } - } + vm.promptDataClone.prompts.credentials.passwords.vault.push({ + id: credential.id, + name: credential.name, + vault_id: credential.inputs.vault_id }); } - }); - } + } else if(credential.passwords_needed && credential.passwords_needed.length > 0) { + credential.passwords_needed.forEach((passwordNeeded) => { + if (passwordNeeded === "ssh_password" || + passwordNeeded === "become_password" || + passwordNeeded === "ssh_key_unlock" + ) { + vm.promptDataClone.prompts.credentials.passwords[passwordNeeded] = { + id: credential.id, + name: credential.name + }; + } else if (passwordNeeded.startsWith("vault_password")) { + let vault_id = null; + if (passwordNeeded.includes('.')) { + vault_id = passwordNeeded.split(/\.(.+)/)[1]; + } + + if (!vm.promptDataClone.prompts.credentials.passwords.vault) { + vm.promptDataClone.prompts.credentials.passwords.vault = []; + } + + vm.promptDataClone.prompts.credentials.passwords.vault.push({ + id: credential.id, + name: credential.name, + vault_id: vault_id + }); + } + }); + } + }); vm.promptDataClone.credentialTypeMissing = []; @@ -141,11 +146,17 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', }; order++; } - if(vm.promptDataClone.launchConf.ask_credential_on_launch || (vm.promptDataClone.launchConf.passwords_needed_to_start && vm.promptDataClone.launchConf.passwords_needed_to_start.length > 0)) { + if (vm.promptDataClone.launchConf.ask_credential_on_launch || + (_.has(vm, 'promptDataClone.prompts.credentials.passwords.vault') && + vm.promptDataClone.prompts.credentials.passwords.vault.length > 0) || + _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_key_unlock') || + _.has(vm, 'promptDataClone.prompts.credentials.passwords.become_password') || + _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_password') + ) { vm.steps.credential.includeStep = true; vm.steps.credential.tab = { _active: order === 1 ? true : false, - _disabled: order === 1 ? false : true, + _disabled: (order === 1 || vm.readOnlyPrompts) ? false : true, order: order }; order++; @@ -154,29 +165,35 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', vm.steps.other_prompts.includeStep = true; vm.steps.other_prompts.tab = { _active: order === 1 ? true : false, - _disabled: order === 1 ? false : true, + _disabled: (order === 1 || vm.readOnlyPrompts) ? false : true, order: order }; order++; + + let codemirror = () => { + return { + validate:{} + }; + }; + vm.codeMirror = new codemirror(); } if(vm.promptDataClone.launchConf.survey_enabled) { vm.steps.survey.includeStep = true; vm.steps.survey.tab = { _active: order === 1 ? true : false, - _disabled: order === 1 ? false : true, + _disabled: (order === 1 || vm.readOnlyPrompts) ? false : true, order: order }; order++; } vm.steps.preview.tab.order = order; + vm.steps.preview.tab._disabled = vm.readOnlyPrompts ? false : true; modal.show('PROMPT'); vm.promptData.triggerModalOpen = false; modal.onClose = () => { scope.$emit('launchModalOpen', false); }; - - scope.$emit('launchModalOpen', true); }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { @@ -189,6 +206,16 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', }; vm.next = (currentTab) => { + if(_.has(vm, 'steps.other_prompts.tab._active') && vm.steps.other_prompts.tab._active === true){ + try { + if (vm.codeMirror.validate) { + vm.codeMirror.validate(); + } + } catch (err) { + event.preventDefault(); + return; + } + } Object.keys(vm.steps).forEach(step => { if(vm.steps[step].tab) { if(vm.steps[step].tab.order === currentTab.order) { diff --git a/awx/ui/client/src/templates/prompt/prompt.directive.js b/awx/ui/client/src/templates/prompt/prompt.directive.js index e151760bb7..dcc25fb784 100644 --- a/awx/ui/client/src/templates/prompt/prompt.directive.js +++ b/awx/ui/client/src/templates/prompt/prompt.directive.js @@ -6,7 +6,8 @@ export default [ 'templateUrl', promptData: '=', onFinish: '&', actionText: '@', - preventCredsWithPasswords: '<' + preventCredsWithPasswords: '<', + readOnlyPrompts: '=' }, templateUrl: templateUrl('templates/prompt/prompt'), replace: true, diff --git a/awx/ui/client/src/templates/prompt/prompt.partial.html b/awx/ui/client/src/templates/prompt/prompt.partial.html index 820ea8553d..4f3d17847e 100644 --- a/awx/ui/client/src/templates/prompt/prompt.partial.html +++ b/awx/ui/client/src/templates/prompt/prompt.partial.html @@ -1,45 +1,61 @@ -
    +
    - {{:: vm.strings.get('prompt.INVENTORY') }} - {{:: vm.strings.get('prompt.CREDENTIAL') }} - {{:: vm.strings.get('prompt.OTHER_PROMPTS') }} - {{:: vm.strings.get('prompt.SURVEY') }} - {{:: vm.strings.get('prompt.PREVIEW') }} + {{:: vm.strings.get('prompt.INVENTORY') }} + {{:: vm.strings.get('prompt.CREDENTIAL') }} + {{:: vm.strings.get('prompt.OTHER_PROMPTS') }} + {{:: vm.strings.get('prompt.SURVEY') }} + {{:: vm.strings.get('prompt.PREVIEW') }}
    -
    - +
    + +
    -
    +
    + prevent-creds-with-passwords="vm.preventCredsWithPasswords" + read-only-prompts="vm.readOnlyPrompts">
    -
    - +
    + +
    -
    - +
    + +
    -
    +
    diff --git a/awx/ui/client/src/templates/prompt/prompt.service.js b/awx/ui/client/src/templates/prompt/prompt.service.js index f29e3ca856..12fe5d4470 100644 --- a/awx/ui/client/src/templates/prompt/prompt.service.js +++ b/awx/ui/client/src/templates/prompt/prompt.service.js @@ -38,8 +38,8 @@ function PromptService (Empty, $filter) { prompts.jobType.choices = _.get(params, 'launchOptions.actions.POST.job_type.choices', []).map(c => ({label: c[1], value: c[0]})); prompts.jobType.value = _.has(params, 'currentValues.job_type') && params.currentValues.job_type ? _.find(prompts.jobType.choices, item => item.value === params.currentValues.job_type) : _.find(prompts.jobType.choices, item => item.value === params.launchConf.defaults.job_type); prompts.limit.value = _.has(params, 'currentValues.limit') && params.currentValues.limit ? params.currentValues.limit : (_.has(params, 'launchConf.defaults.limit') ? params.launchConf.defaults.limit : ""); - prompts.tags.options = prompts.tags.value = (jobTags && jobTags !== "") ? jobTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; - prompts.skipTags.options = prompts.skipTags.value = (skipTags && skipTags !== "") ? skipTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; + prompts.tags.value = (jobTags && jobTags !== "") ? jobTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; + prompts.skipTags.value = (skipTags && skipTags !== "") ? skipTags.split(',').map((i) => ({name: i, label: i, value: i})) : []; prompts.diffMode.value = _.has(params, 'currentValues.diff_mode') && typeof params.currentValues.diff_mode === 'boolean' ? params.currentValues.diff_mode : (_.has(params, 'launchConf.defaults.diff_mode') ? params.launchConf.defaults.diff_mode : null); return prompts; @@ -70,7 +70,7 @@ function PromptService (Empty, $filter) { } else { question.model = question.default.split(/\n/); } - question.choices = question.choices.split(/\n/); + question.choices = typeof question.choices.split === 'function' ? question.choices.split(/\n/) : question.choices; } else if(question.type === "multiplechoice") { if(params.extra_data && params.extra_data[question.variable]) { @@ -80,7 +80,7 @@ function PromptService (Empty, $filter) { question.model = question.default ? angular.copy(question.default) : ""; } - question.choices = question.choices.split(/\n/); + question.choices = typeof question.choices.split === 'function' ? question.choices.split(/\n/) : question.choices; // Add a default empty string option to the choices array. If this choice is // selected then the extra var will not be sent when we POST to the launch diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js index f42792a822..9258ecdbec 100644 --- a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.controller.js @@ -18,9 +18,16 @@ export default if(scope.credentials && scope.credentials.length > 0) { scope.credentials.forEach((credential, i) => { scope.credentials[i].checked = 0; + }); scope.promptData.prompts.credentials.value.forEach((selectedCredential) => { - if(selectedCredential.credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { + if (_.get(selectedCredential, 'inputs.vault_id') || _.get(selectedCredential, 'vault_id')) { + const vaultId = selectedCredential.vault_id ? selectedCredential.vault_id : _.get(selectedCredential, 'inputs.vault_id'); + selectedCredential.tag = `${selectedCredential.name } | ${vaultId}`; + } else { + selectedCredential.tag = selectedCredential.name; + } + if (selectedCredential.credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { scope.credentials.forEach((credential, i) => { if(scope.credentials[i].id === selectedCredential.id) { scope.credentials[i].checked = 1; @@ -135,78 +142,82 @@ export default launch = _launch_; scope.toggle_row = (selectedRow) => { - let selectedCred = _.cloneDeep(selectedRow); + if (!scope.readOnlyPrompts) { + let selectedCred = _.cloneDeep(selectedRow); - for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { - if(scope.promptData.prompts.credentials.value[i].credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { - wipePasswords(scope.promptData.prompts.credentials.value[i]); - scope.promptData.prompts.credentials.value.splice(i, 1); + for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { + if(scope.promptData.prompts.credentials.value[i].credential_type === parseInt(scope.promptData.prompts.credentials.credentialKind)) { + wipePasswords(scope.promptData.prompts.credentials.value[i]); + scope.promptData.prompts.credentials.value.splice(i, 1); + } } - } - scope.promptData.prompts.credentials.value.push(selectedCred); - updateNeededPasswords(selectedRow); + scope.promptData.prompts.credentials.value.push(selectedCred); + updateNeededPasswords(selectedRow); - for (let i = scope.promptData.credentialTypeMissing.length - 1; i >= 0; i--) { - if(scope.promptData.credentialTypeMissing[i].credential_type === selectedRow.credential_type) { - scope.promptData.credentialTypeMissing.splice(i,1); - i = -1; + for (let i = scope.promptData.credentialTypeMissing.length - 1; i >= 0; i--) { + if(scope.promptData.credentialTypeMissing[i].credential_type === selectedRow.credential_type) { + scope.promptData.credentialTypeMissing.splice(i,1); + i = -1; + } } } }; scope.toggle_credential = (cred) => { - // This is a checkbox click. At the time of writing this the only - // multi-select credentials on launch are vault credentials so this - // logic should only get executed when a vault credential checkbox - // is clicked. + if (!scope.readOnlyPrompts) { + // This is a checkbox click. At the time of writing this the only + // multi-select credentials on launch are vault credentials so this + // logic should only get executed when a vault credential checkbox + // is clicked. - let uncheck = false; + let uncheck = false; - let removeCredential = (credentialToRemove, index) => { - wipePasswords(credentialToRemove); - scope.promptData.prompts.credentials.value.splice(index, 1); - }; + let removeCredential = (credentialToRemove, index) => { + wipePasswords(credentialToRemove); + scope.promptData.prompts.credentials.value.splice(index, 1); + }; - // Only one vault credential per vault_id is allowed so we need to check - // to see if one has already been selected and if so replace it. - for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { - if(cred.credential_type === scope.promptData.prompts.credentials.value[i].credential_type) { - if(scope.promptData.prompts.credentials.value[i].id === cred.id) { - removeCredential(scope.promptData.prompts.credentials.value[i], i); - i = -1; - uncheck = true; - } - else if(scope.promptData.prompts.credentials.value[i].inputs) { - if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].inputs.vault_id) { + // Only one vault credential per vault_id is allowed so we need to check + // to see if one has already been selected and if so replace it. + for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { + if(cred.credential_type === scope.promptData.prompts.credentials.value[i].credential_type) { + if(scope.promptData.prompts.credentials.value[i].id === cred.id) { removeCredential(scope.promptData.prompts.credentials.value[i], i); + i = -1; + uncheck = true; } - } else if(scope.promptData.prompts.credentials.value[i].vault_id) { - if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].vault_id) { - removeCredential(scope.promptData.prompts.credentials.value[i], i); - } - } else { - // The currently selected vault credential does not have a vault_id - if(!cred.inputs.vault_id || cred.inputs.vault_id === "") { - removeCredential(scope.promptData.prompts.credentials.value[i], i); + else if(scope.promptData.prompts.credentials.value[i].inputs) { + if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].inputs.vault_id) { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } + } else if(scope.promptData.prompts.credentials.value[i].vault_id) { + if(cred.inputs.vault_id === scope.promptData.prompts.credentials.value[i].vault_id) { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } + } else { + // The currently selected vault credential does not have a vault_id + if(!cred.inputs.vault_id || cred.inputs.vault_id === "") { + removeCredential(scope.promptData.prompts.credentials.value[i], i); + } } } } - } - if(!uncheck) { - scope.promptData.prompts.credentials.value.push(cred); - updateNeededPasswords(cred); + if(!uncheck) { + scope.promptData.prompts.credentials.value.push(cred); + updateNeededPasswords(cred); - _.remove(scope.promptData.credentialTypeMissing, (missingCredType) => { - return ( - missingCredType.credential_type === cred.credential_type && - _.get(cred, 'inputs.vault_id') === _.get(missingCredType, 'vault_id') - ); - }); - } else { - if(scope.promptData.launchConf.defaults.credentials && scope.promptData.launchConf.defaults.credentials.length > 0) { - checkMissingCredType(cred); + _.remove(scope.promptData.credentialTypeMissing, (missingCredType) => { + return ( + missingCredType.credential_type === cred.credential_type && + _.get(cred, 'inputs.vault_id') === _.get(missingCredType, 'vault_id') + ); + }); + } else { + if(scope.promptData.launchConf.defaults.credentials && scope.promptData.launchConf.defaults.credentials.length > 0) { + checkMissingCredType(cred); + } } } }; @@ -259,7 +270,8 @@ export default }); }; - vm.deleteSelectedCredential = (credentialToDelete) => { + vm.deleteSelectedCredential = (index) => { + const credentialToDelete = scope.promptData.prompts.credentials.value[index]; for (let i = scope.promptData.prompts.credentials.value.length - 1; i >= 0; i--) { if(scope.promptData.prompts.credentials.value[i].id === credentialToDelete.id) { if(scope.promptData.launchConf.defaults.credentials && scope.promptData.launchConf.defaults.credentials.length > 0) { @@ -312,7 +324,7 @@ export default }; vm.showRevertCredentials = () => { - if(scope.promptData.launchConf.ask_credential_on_launch) { + if(!scope.readOnlyPrompts && scope.promptData.launchConf.ask_credential_on_launch) { if(scope.promptData.prompts.credentials.value && _.has(scope, 'promptData.launchConf.defaults.credentials') && (scope.promptData.prompts.credentials.value.length === scope.promptData.launchConf.defaults.credentials.length)) { let selectedIds = scope.promptData.prompts.credentials.value.map((x) => { return x.id; }).sort(); let defaultIds = _.has(scope, 'promptData.launchConf.defaults.credentials') ? scope.promptData.launchConf.defaults.credentials.map((x) => { return x.id; }).sort() : []; diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js index 80d277db7a..8d971dfff8 100644 --- a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.directive.js @@ -12,7 +12,8 @@ export default [ 'templateUrl', '$compile', 'generateList', scope: { promptData: '=', credentialPasswordsForm: '=', - preventCredsWithPasswords: '<' + preventCredsWithPasswords: '<', + readOnlyPrompts: '<' }, templateUrl: templateUrl('templates/prompt/steps/credential/prompt-credential'), controller: promptCredentialController, @@ -43,6 +44,9 @@ export default [ 'templateUrl', '$compile', 'generateList', }; } + list.disableRow = "{{ readOnlyPrompts }}"; + list.disableRowValue = "readOnlyPrompts"; + let html = GenerateList.build({ list: list, input_type: inputType, diff --git a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html index 7b1ca2b093..ed1e7204c8 100644 --- a/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/credential/prompt-credential.partial.html @@ -7,31 +7,19 @@
    {{:: vm.strings.get('prompt.NO_CREDENTIALS_SELECTED') }}
    -
    -
    - - - - - - -
    -
    - - {{ credential.name }} - - - {{ credential.name }} | {{ credential.vault_id ? credential.vault_id : credential.inputs.vault_id }} - -
    -
    - -
    -
    + + + + + +
    @@ -118,7 +106,7 @@
    - +
    diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js index 2529558349..658626906f 100644 --- a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.controller.js @@ -18,8 +18,23 @@ export default launch = _launch_; scope.toggle_row = (row) => { - scope.promptData.prompts.inventory.value = row; + if (!scope.readOnlyPrompts) { + scope.promptData.prompts.inventory.value = row; + } }; + + scope.$watchCollection('inventories', () => { + if(scope.inventories && scope.inventories.length > 0) { + scope.inventories.forEach((credential, i) => { + if (_.has(scope, 'promptData.prompts.inventory.value.id') && scope.promptData.prompts.inventory.value.id === scope.inventories[i].id) { + scope.inventories[i].checked = 1; + } else { + scope.inventories[i].checked = 0; + } + + }); + } + }); }; vm.deleteSelectedInventory = () => { diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js index e3b1d24823..4f6c4eed8d 100644 --- a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.directive.js @@ -10,7 +10,8 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com (templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => { return { scope: { - promptData: '=' + promptData: '=', + readOnlyPrompts: '<' }, templateUrl: templateUrl('templates/prompt/steps/inventory/prompt-inventory'), controller: promptInventoryController, @@ -43,6 +44,8 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com scope.inventories = scope.inventory_dataset.results; let invList = _.cloneDeep(InventoryList); + invList.disableRow = "{{ readOnlyPrompts }}"; + invList.disableRowValue = "readOnlyPrompts"; let html = GenerateList.build({ list: invList, input_type: 'radio', diff --git a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html index 3292da4a98..17dc1ce918 100644 --- a/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/inventory/prompt-inventory.partial.html @@ -6,19 +6,11 @@
    {{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}
    -
    -
    -
    - {{promptData.prompts.inventory.value.name}} -
    -
    - -
    -
    -
    + +
    diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js index 96a087218d..1003ff0ec1 100644 --- a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.controller.js @@ -5,7 +5,7 @@ *************************************************/ export default - ['ParseTypeChange', 'CreateSelect2', 'TemplatesStrings', '$timeout', function(ParseTypeChange, CreateSelect2, strings, $timeout) { + ['ParseTypeChange', 'CreateSelect2', 'TemplatesStrings', '$timeout', 'ToJSON', function(ParseTypeChange, CreateSelect2, strings, $timeout, ToJSON) { const vm = this; vm.strings = strings; @@ -55,6 +55,16 @@ export default } if(scope.promptData.launchConf.ask_tags_on_launch) { + // Ensure that the options match the currently selected tags. These two things + // might get out of sync if the user re-opens the prompts before saving the + // schedule/wf node + scope.promptData.prompts.tags.options = _.map(scope.promptData.prompts.tags.value, function(tag){ + return { + value: tag.value, + name: tag.name, + label: tag.label + }; + }); CreateSelect2({ element: '#job_launch_job_tags', multiple: true, @@ -63,6 +73,16 @@ export default } if(scope.promptData.launchConf.ask_skip_tags_on_launch) { + // Ensure that the options match the currently selected tags. These two things + // might get out of sync if the user re-opens the prompts before saving the + // schedule/wf node + scope.promptData.prompts.skipTags.options = _.map(scope.promptData.prompts.skipTags.value, function(tag){ + return { + value: tag.value, + name: tag.name, + label: tag.label + }; + }); CreateSelect2({ element: '#job_launch_skip_tags', multiple: true, @@ -79,8 +99,15 @@ export default codemirrorExtraVars(); } }); + + function validate () { + return ToJSON(scope.parseType, scope.extraVariables, true); + } + scope.validate = validate; }; + + vm.toggleDiff = () => { scope.promptData.prompts.diffMode.value = !scope.promptData.prompts.diffMode.value; }; diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js index c551fc8bd7..361f60e145 100644 --- a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.directive.js @@ -12,7 +12,9 @@ export default [ 'templateUrl', scope: { promptData: '=', otherPromptsForm: '=', - isActiveStep: '=' + isActiveStep: '=', + validate: '=', + readOnlyPrompts: '<' }, templateUrl: templateUrl('templates/prompt/steps/other-prompts/prompt-other-prompts'), controller: promptOtherPrompts, diff --git a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html index 9d731d185f..9ab3aa09d5 100644 --- a/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/other-prompts/prompt-other-prompts.partial.html @@ -13,6 +13,7 @@ name="job_type" tabindex="-1" aria-hidden="true" + ng-disabled="readOnlyPrompts" required> @@ -23,7 +24,12 @@ {{:: vm.strings.get('prompt.LIMIT') }}
    - +
    @@ -40,6 +46,7 @@ name="verbosity" tabindex="-1" aria-hidden="true" + ng-disabled="readOnlyPrompts" required> @@ -58,6 +65,7 @@ name="job_tags" tabindex="-1" aria-hidden="true" + ng-disabled="readOnlyPrompts" multiple>
    @@ -75,6 +83,7 @@ name="skip_tags" tabindex="-1" aria-hidden="true" + ng-disabled="readOnlyPrompts" multiple>
    @@ -85,8 +94,8 @@
    - - + +
    @@ -104,7 +113,7 @@
    - +
    diff --git a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html index ba83655f9b..5b1cb42fd4 100644 --- a/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/preview/prompt-preview.partial.html @@ -9,19 +9,11 @@
    {{:: vm.strings.get('prompt.CREDENTIAL') }}
    -
    -
    - - - - - - - - - -
    -
    + +
    @@ -44,8 +36,8 @@
    -
    -
    +
    +
    {{tag.name}}
    @@ -60,8 +52,8 @@
    -
    -
    +
    +
    {{tag.name}}
    diff --git a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js index 80e07fd404..eb1ae7169f 100644 --- a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js +++ b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.directive.js @@ -11,7 +11,8 @@ export default [ 'templateUrl', return { scope: { promptData: '=', - surveyForm: '=' + surveyForm: '=', + readOnlyPrompts: '<' }, templateUrl: templateUrl('templates/prompt/steps/survey/prompt-survey'), controller: promptSurvey, diff --git a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html index 73d23c6f81..98c32ff217 100644 --- a/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html +++ b/awx/ui/client/src/templates/prompt/steps/survey/prompt-survey.partial.html @@ -9,12 +9,12 @@
    - +
    {{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
    Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
    - +
    {{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
    Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
    @@ -23,20 +23,20 @@ - - + +
    {{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
    Please enter an answer between {{question.minlength}} to {{question.maxlength}} characters long.
    - +
    {{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
    {{:: vm.strings.get('prompt.VALID_INTEGER') }}
    Please enter an answer between {{question.minValue}} and {{question.maxValue}}.
    - +
    {{:: vm.strings.get('prompt.PLEASE_ENTER_ANSWER') }}
    {{:: vm.strings.get('prompt.VALID_DECIMAL') }}
    Please enter an answer between {{question.minValue}} and {{question.maxValue}}.
    @@ -49,6 +49,7 @@ choices="question.choices" ng-required="question.required" ng-model="question.model" + ng-disabled="readOnlyPrompts" form-element-name="survey_question_{{$index}}">
    @@ -61,6 +62,7 @@ choices="question.choices" ng-required="question.required" ng-model="question.model" + ng-disabled="readOnlyPrompts" form-element-name="survey_question_{{$index}}">
    {{:: vm.strings.get('prompt.PLEASE_SELECT_VALUE') }}
    diff --git a/awx/ui/client/src/templates/survey-maker/render/survey-question.directive.js b/awx/ui/client/src/templates/survey-maker/render/survey-question.directive.js index ed613e745b..97df8d1872 100644 --- a/awx/ui/client/src/templates/survey-maker/render/survey-question.directive.js +++ b/awx/ui/client/src/templates/survey-maker/render/survey-question.directive.js @@ -99,7 +99,7 @@ function link($sce, $filter, Empty, scope, element, attrs) { // Split out choices to be consumed by the multiple-choice directive if (!_.isUndefined(scope.question.choices)) { - scope.choices = scope.question.choices.split('\n'); + scope.choices = typeof scope.question.choices.split === 'function' ? scope.question.choices.split('\n') : scope.question.choices; } sanitizeDefault(); diff --git a/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js b/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js index 30613f21d5..20af705eca 100644 --- a/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js +++ b/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js @@ -273,7 +273,7 @@ export default ['i18n', function(i18n){ '
    '+ '
    '+ ''+ - ''+ + ''+ ''+ ''+ '
    '+ diff --git a/awx/ui/client/src/templates/survey-maker/surveys/init.factory.js b/awx/ui/client/src/templates/survey-maker/surveys/init.factory.js index f3f7d09e84..61f0d2226c 100644 --- a/awx/ui/client/src/templates/survey-maker/surveys/init.factory.js +++ b/awx/ui/client/src/templates/survey-maker/surveys/init.factory.js @@ -1,6 +1,6 @@ export default function Init(DeleteSurvey, EditSurvey, AddSurvey, GenerateForm, SurveyQuestionForm, Wait, Alert, - GetBasePath, Rest, ProcessErrors, EditQuestion, CreateSelect2) { + GetBasePath, Rest, ProcessErrors, EditQuestion, CreateSelect2, i18n) { return function(params) { var scope = params.scope, id = params.id, @@ -10,14 +10,18 @@ export default scope.sce = sce; scope.survey_questions = []; scope.answer_types=[ - {name: 'Text' , type: 'text'}, - {name: 'Textarea', type: 'textarea'}, - {name: 'Password', type: 'password'}, - {name: 'Multiple Choice (single select)', type: 'multiplechoice'}, - {name: 'Multiple Choice (multiple select)', type: 'multiselect'}, - {name: 'Integer', type: 'integer'}, - {name: 'Float', type: 'float'} + {name: i18n._('Text'), type: 'text'}, + {name: i18n._('Textarea'), type: 'textarea'}, + {name: i18n._('Password'), type: 'password'}, + {name: i18n._('Multiple Choice (single select)'), type: 'multiplechoice'}, + {name: i18n._('Multiple Choice (multiple select)'), type: 'multiselect'}, + {name: i18n._('Integer'), type: 'integer'}, + {name: i18n._('Float'), type: 'float'} ]; + scope.disableSurveyTooltip = i18n._('Disble Survey'); + scope.editQuestionTooltip = i18n._('Edit Question'); + scope.deleteQuestionTooltip = i18n._('Delete Question'); + scope.dragQuestionTooltip = i18n._('Drag to reorder question'); /* SURVEY RELATED FUNCTIONS */ @@ -476,10 +480,10 @@ export default inputId = id, buttonInnerHTML = $(buttonId).html(); if (buttonInnerHTML.indexOf("SHOW") > -1) { - $(buttonId).html("HIDE"); + $(buttonId).html(i18n._("HIDE")); $(inputId).attr("type", "text"); } else { - $(buttonId).html("SHOW"); + $(buttonId).html(i18n._("SHOW")); $(inputId).attr("type", "password"); } }; @@ -511,7 +515,7 @@ export default // Watcher that updates the survey enabled/disabled tooltip based on scope.survey_enabled scope.$watch('survey_enabled', function(newVal) { - scope.surveyEnabledTooltip = (newVal) ? "Disable survey" : "Enable survey"; + scope.surveyEnabledTooltip = (newVal) ? i18n._("Disable survey") : i18n._("Enable survey"); }); }; @@ -529,5 +533,6 @@ Init.$inject = 'Rest', 'ProcessErrors', 'editQuestion', - 'CreateSelect2' + 'CreateSelect2', + 'i18n' ]; diff --git a/awx/ui/client/src/templates/templates.list.js b/awx/ui/client/src/templates/templates.list.js index e45a1fa5ba..b2c82634c2 100644 --- a/awx/ui/client/src/templates/templates.list.js +++ b/awx/ui/client/src/templates/templates.list.js @@ -89,13 +89,6 @@ export default ['i18n', function(i18n) { // The submit key lets the list generator know that we want to use the // at-launch-template directive }, - schedule: { - label: i18n._('Schedule'), - mode: 'all', - ngClick: 'scheduleJob(template)', - awToolTip: i18n._('Schedule job template runs'), - dataPlacement: 'top', - }, copy: { label: i18n._('Copy'), ngClick: 'copyTemplate(template)', diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index b484d33c7e..50a3ee867c 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -77,6 +77,10 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { dataPlacement: 'right', awPopOver: "

    " + i18n._("Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.") + "

    ", dataContainer: 'body', + onError: { + ngShow: 'workflow_job_template_labels_isValid !== true', + text: i18n._('Max 512 characters per label.'), + }, ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)' }, variables: { @@ -214,12 +218,12 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { awToolTip: '{{surveyTooltip}}', dataPlacement: 'top' }, - workflow_editor: { + workflow_visualizer: { ngClick: 'openWorkflowMaker()', ngShow: '$state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\')', - awToolTip: '{{workflowEditorTooltip}}', + awToolTip: '{{workflowVisualizerTooltip}}', dataPlacement: 'top', - label: i18n._('Workflow Editor'), + label: i18n._('Workflow Visualizer'), class: 'Form-primaryButton' } } diff --git a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js index 8321e839f9..84bfa0186d 100644 --- a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js +++ b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js @@ -178,5 +178,45 @@ export default [ $scope.formCancel = function () { $state.transitionTo('templates'); }; + + let handleLabelCount = () => { + /** + * This block of code specifically handles the client-side validation of the `labels` field. + * Due to it's detached nature in relation to the other job template fields, we must + * validate this field client-side in order to avoid the edge case where a user can make a + * successful POST to the `workflow_job_templates` endpoint but however encounter a 200 error from + * the `labels` endpoint due to a character limit. + * + * We leverage two of select2's available events, `select` and `unselect`, to detect when the user + * has either added or removed a label. From there, we set a flag and do simple string length + * checks to make sure a label's chacacter count remains under 512. Otherwise, we disable the "Save" button + * by invalidating the field and inform the user of the error. + */ + + $scope.workflow_job_template_labels_isValid = true; + const maxCount = 512; + const wfjt_label_id = 'workflow_job_template_labels'; + // Detect when a new label is added + $(`#${wfjt_label_id}`).on('select2:select', (e) => { + const { text } = e.params.data; + // If the character count of an added label is greater than 512, we set `labels` field as invalid + if (text.length > maxCount) { + $scope.workflow_job_template_form.labels.$setValidity(`${wfjt_label_id}`, false); + $scope.workflow_job_template_labels_isValid = false; + } + }); + // Detect when a label is removed + $(`#${wfjt_label_id}`).on('select2:unselect', (e) => { + const { text } = e.params.data; + /* If the character count of a removed label is greater than 512 AND the field is currently marked + as invalid, we set it back to valid */ + if (text.length > maxCount && $scope.workflow_job_template_form.labels.$error) { + $scope.workflow_job_template_form.labels.$setValidity(`${wfjt_label_id}`, true); + $scope.workflow_job_template_labels_isValid = true; + } + }); + }; + + handleLabelCount(); } ]; diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index 0ef57520d9..dc6a35d402 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -258,7 +258,7 @@ export default [ opts: opts }); - $scope.workflowEditorTooltip = i18n._("Click here to open the workflow graph editor."); + $scope.workflowVisualizerTooltip = i18n._("Click here to open the workflow visualizer."); $scope.surveyTooltip = i18n._('Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.'); $scope.workflow_job_template_obj = workflowJobTemplateData; @@ -295,6 +295,11 @@ export default [ $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; } + if (form.fields[fld].type === 'checkbox_group') { + for(var j=0; j { + /** + * This block of code specifically handles the client-side validation of the `labels` field. + * Due to it's detached nature in relation to the other job template fields, we must + * validate this field client-side in order to avoid the edge case where a user can make a + * successful POST to the `workflow_job_templates` endpoint but however encounter a 200 error from + * the `labels` endpoint due to a character limit. + * + * We leverage two of select2's available events, `select` and `unselect`, to detect when the user + * has either added or removed a label. From there, we set a flag and do simple string length + * checks to make sure a label's chacacter count remains under 512. Otherwise, we disable the "Save" button + * by invalidating the field and inform the user of the error. + */ + + $scope.workflow_job_template_labels_isValid = true; + const maxCount = 512; + const wfjt_label_id = 'workflow_job_template_labels'; + // Detect when a new label is added + $(`#${wfjt_label_id}`).on('select2:select', (e) => { + const { text } = e.params.data; + // If the character count of an added label is greater than 512, we set `labels` field as invalid + if (text.length > maxCount) { + $scope.workflow_job_template_form.labels.$setValidity(`${wfjt_label_id}`, false); + $scope.workflow_job_template_labels_isValid = false; + } + }); + // Detect when a label is removed + $(`#${wfjt_label_id}`).on('select2:unselect', (e) => { + const { text } = e.params.data; + /* If the character count of a removed label is greater than 512 AND the field is currently marked + as invalid, we set it back to valid */ + if (text.length > maxCount && $scope.workflow_job_template_form.labels.$error) { + $scope.workflow_job_template_form.labels.$setValidity(`${wfjt_label_id}`, true); + $scope.workflow_job_template_labels_isValid = true; + } + }); + }; + + handleLabelCount(); } ]; diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 68f380a82a..2a1d16487a 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -86,7 +86,7 @@ .workflowChart-nodeStatus--success { fill: @default-succ; } -.workflowChart-nodeStatus--failed { +.workflowChart-nodeStatus--failed, .workflowChart-nodeStatus--canceled { fill: @default-err; } .WorkflowChart-detailsLink { diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index dc1db1718c..ce25538807 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'GetBasePath', 'ProcessErrors', - function($state, moment, $timeout, $window, $filter, Rest, GetBasePath, ProcessErrors) { +export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'GetBasePath', 'ProcessErrors', 'TemplatesStrings', + function($state, moment, $timeout, $window, $filter, Rest, GetBasePath, ProcessErrors, TemplatesStrings) { return { scope: { @@ -280,7 +280,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("y", 30) .attr("dy", ".35em") .attr("class", "WorkflowChart-startText") - .text(function () { return "START"; }) + .text(function () { return TemplatesStrings.get('workflow_maker.START'); }) .call(add_node); } else { @@ -333,7 +333,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .style("font-size","0.7em") .attr("class", "WorkflowChart-conflictText") .html(function () { - return "\uf06a EDGE CONFLICT"; + return `\uf06a ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}`; }) .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; }); @@ -344,7 +344,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("text-anchor", "middle") .attr("class", "WorkflowChart-defaultText WorkflowChart-deletedText") .html(function () { - return "DELETED"; + return `${TemplatesStrings.get('workflow_maker.DELETED')}`; }) .style("display", function(d) { return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); @@ -423,7 +423,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("class", "WorkflowChart-detailsLink") .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) .text(function () { - return "DETAILS"; + return TemplatesStrings.get('workflow_maker.DETAILS'); }) .call(details); thisNode.append("circle") @@ -535,6 +535,9 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge case "error": statusClass += "workflowChart-nodeStatus--failed"; break; + case "canceled": + statusClass += "workflowChart-nodeStatus--canceled"; + break; } } @@ -785,6 +788,9 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge case "error": statusClass += "workflowChart-nodeStatus--failed"; break; + case "canceled": + statusClass += "workflowChart-nodeStatus--canceled"; + break; } } 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 efe7bf6642..0d0c1a817e 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 @@ -6,12 +6,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', '$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel', - 'Empty', 'PromptService', 'Rest', 'TemplatesStrings', + 'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout', function($scope, WorkflowService, GetBasePath, TemplatesService, $state, ProcessErrors, CreateSelect2, $q, JobTemplate, - Empty, PromptService, Rest, TemplatesStrings) { + Empty, PromptService, Rest, TemplatesStrings, $timeout) { - let promptWatcher, surveyQuestionWatcher; + let promptWatcher, surveyQuestionWatcher, credentialsWatcher; $scope.strings = TemplatesStrings; $scope.preventCredsWithPasswords = true; @@ -23,10 +23,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }; $scope.job_type_options = [{ - label: "Run", + label: $scope.strings.get('workflow_maker.RUN'), value: "run" }, { - label: "Check", + label: $scope.strings.get('workflow_maker.CHECK'), value: "check" }]; @@ -36,15 +36,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.edgeTypeOptions = [ { - label: 'Always', + label: $scope.strings.get('workflow_maker.ALWAYS'), value: 'always' }, { - label: 'On Success', + label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: 'success' }, { - label: 'On Failure', + label: $scope.strings.get('workflow_maker.ON_FAILURE'), value: 'failure' } ]; @@ -216,7 +216,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); let credentialsToAdd = credentialsNotInPriorCredentials.filter(function(credNotInPrior) { - return !params.node.promptData.prompts.credentials.previousOverrides.some(function(priorCred) { + let previousOverrides = params.node.promptData.prompts.credentials.previousOverrides ? params.node.promptData.prompts.credentials.previousOverrides : []; + return !previousOverrides.some(function(priorCred) { return credNotInPrior.id === priorCred.id; }); }); @@ -252,8 +253,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } } - if ((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep - + if (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType)) { let parentIsDeleted = false; _.forEach($scope.treeData.data.deletedNodes, function(deletedNode) { @@ -319,17 +319,17 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', optionsToInclude.forEach((optionToInclude) => { if (optionToInclude === "always") { $scope.edgeTypeOptions.push({ - label: 'Always', + label: $scope.strings.get('workflow_maker.ALWAYS'), value: 'always' }); } else if (optionToInclude === "success") { $scope.edgeTypeOptions.push({ - label: 'On Success', + label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: 'success' }); } else if (optionToInclude === "failure") { $scope.edgeTypeOptions.push({ - label: 'On Failure', + label: $scope.strings.get('workflow_maker.ON_FAILURE'), value: 'failure' }); } @@ -342,6 +342,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); }; + let checkCredentialsForRequiredPasswords = () => { + let credentialRequiresPassword = false; + $scope.promptData.prompts.credentials.value.forEach((credential) => { + if ((credential.passwords_needed && + credential.passwords_needed.length > 0) || + (_.has(credential, 'inputs.vault_password') && + credential.inputs.vault_password === "ASK") + ) { + credentialRequiresPassword = true; + } + }); + $scope.credentialRequiresPassword = credentialRequiresPassword; + }; + let watchForPromptChanges = () => { let promptDataToWatch = [ 'promptData.prompts.inventory.value', @@ -358,6 +372,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } $scope.promptModalMissingReqFields = missingPromptValue; }); + + if ($scope.promptData.launchConf.ask_credential_on_launch && $scope.credentialRequiresPassword) { + credentialsWatcher = $scope.$watch('promptData.prompts.credentials', () => { + checkCredentialsForRequiredPasswords(); + }); + } }; $scope.closeWorkflowMaker = function() { @@ -414,10 +434,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', .then(function() { $scope.closeDialog(); }).catch(({data, status}) => { - ProcessErrors($scope, data, status, null); + ProcessErrors($scope, data, status, null, {}); }); }).catch(({data, status}) => { - ProcessErrors($scope, data, status, null); + ProcessErrors($scope, data, status, null, {}); }); }; @@ -467,20 +487,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); // Set the default to success - let edgeType = {label: "On Success", value: "success"}; + let edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"}; if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) { // We don't want to give the user the option to select // a type as this node will always be executed updateEdgeDropdownOptions(["always"]); - edgeType = {label: "Always", value: "always"}; + edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"}; } else { if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { updateEdgeDropdownOptions(["success", "failure"]); - edgeType = {label: "On Success", value: "success"}; + edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"}; } else if (_.includes(siblingConnectionTypes, "always")) { updateEdgeDropdownOptions(["always"]); - edgeType = {label: "Always", value: "always"}; + edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"}; } else { updateEdgeDropdownOptions(); } @@ -538,6 +558,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', surveyQuestionWatcher(); } + if (credentialsWatcher) { + credentialsWatcher(); + } + $scope.promptData = null; // Reset the edgeConflict flag @@ -565,6 +589,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', surveyQuestionWatcher(); } + if (credentialsWatcher) { + credentialsWatcher(); + } + $scope.promptData = null; $scope.selectedTemplateInvalid = false; $scope.showPromptButton = false; @@ -613,7 +641,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', if (!_.isEmpty($scope.nodeBeingEdited.promptData)) { $scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData); - } else if ($scope.nodeBeingEdited.unifiedJobTemplate){ + } else if ( + _.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job_template' || + _.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template' + ) { let promises = [jobTemplate.optionsLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id)]; if (_.has($scope, 'nodeBeingEdited.originalNodeObj.related.credentials')) { @@ -669,6 +700,24 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.selectedTemplateInvalid = false; } + let credentialRequiresPassword = false; + + prompts.credentials.value.forEach((credential) => { + if(credential.inputs) { + if ((credential.inputs.password && credential.inputs.password === "ASK") || + (credential.inputs.become_password && credential.inputs.become_password === "ASK") || + (credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") || + (credential.inputs.vault_password && credential.inputs.vault_password === "ASK") + ) { + credentialRequiresPassword = true; + } + } else if (credential.passwords_needed && credential.passwords_needed.length > 0) { + credentialRequiresPassword = true; + } + }); + + $scope.credentialRequiresPassword = credentialRequiresPassword; + if (!launchConf.survey_enabled && !launchConf.ask_inventory_on_launch && !launchConf.ask_credential_on_launch && @@ -680,7 +729,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', !launchConf.ask_diff_mode_on_launch && !launchConf.survey_enabled && !launchConf.credential_needed_to_start && - launchConf.passwords_needed_to_start.length === 0 && launchConf.variables_needed_to_start.length === 0) { $scope.showPromptButton = false; $scope.promptModalMissingReqFields = false; @@ -725,6 +773,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.missingSurveyValue = missingSurveyValue; }, true); + checkCredentialsForRequiredPasswords(); + watchForPromptChanges(); }); } else { @@ -734,6 +784,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', prompts: prompts, template: $scope.nodeBeingEdited.unifiedJobTemplate.id }; + + checkCredentialsForRequiredPasswords(); + watchForPromptChanges(); } } @@ -805,7 +858,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', break; } - updateEdgeDropdownOptions(edgeDropdownOptions); + $timeout(updateEdgeDropdownOptions(edgeDropdownOptions)); $scope.$broadcast("refreshWorkflowChart"); }; @@ -974,6 +1027,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.templateManuallySelected = function(selectedTemplate) { + if (promptWatcher) { + promptWatcher(); + } + + if (surveyQuestionWatcher) { + surveyQuestionWatcher(); + } + + if (credentialsWatcher) { + credentialsWatcher(); + } + + $scope.promptData = null; + if (selectedTemplate.type === "job_template") { let jobTemplate = new JobTemplate(); @@ -987,6 +1054,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.selectedTemplateInvalid = false; } + if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) { + $scope.credentialRequiresPassword = true; + } else { + $scope.credentialRequiresPassword = false; + } + $scope.selectedTemplate = angular.copy(selectedTemplate); if (!launchConf.survey_enabled && @@ -1000,7 +1073,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', !launchConf.ask_diff_mode_on_launch && !launchConf.survey_enabled && !launchConf.credential_needed_to_start && - launchConf.passwords_needed_to_start.length === 0 && launchConf.variables_needed_to_start.length === 0) { $scope.showPromptButton = false; $scope.promptModalMissingReqFields = false; @@ -1063,7 +1135,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } }); } else { - // TODO - clear out prompt data? $scope.selectedTemplate = angular.copy(selectedTemplate); $scope.selectedTemplateInvalid = false; $scope.showPromptButton = false; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index aca4d30f13..264a5ab55c 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -4,7 +4,7 @@
    -
    {{ workflowJobTemplateObj.name }}
    +
    {{strings.get('workflow_maker.TITLE')}} | {{ workflowJobTemplateObj.name }}
    - TOTAL TEMPLATES + {{strings.get('workflow_maker.TOTAL_TEMPLATES')}}
    @@ -80,54 +80,66 @@
    -
    {{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : "EDIT TEMPLATE") : "ADD A TEMPLATE"}}
    -
    +
    {{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : strings.get('workflow_maker.EDIT_TEMPLATE')) : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
    +
    -
    JOBS
    -
    PROJECT SYNC
    -
    INVENTORY SYNC
    +
    {{strings.get('workflow_maker.JOBS')}}
    +
    {{strings.get('workflow_maker.PROJECT_SYNC')}}
    +
    {{strings.get('workflow_maker.INVENTORY_SYNC')}}
    -
    -
    - - {{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }} + +
    +
    + + {{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }} +
    -
    -
    - -
    - +
    +
    + + {{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }} +
    -
    -
    - - - - -
    +
    + +
    + +
    +
    +
    + + + + +
    +
    - - + +
    - +
    diff --git a/awx/ui/client/src/users/add/users-add.controller.js b/awx/ui/client/src/users/add/users-add.controller.js index c822b7bf9d..7cc98629ed 100644 --- a/awx/ui/client/src/users/add/users-add.controller.js +++ b/awx/ui/client/src/users/add/users-add.controller.js @@ -14,10 +14,10 @@ const user_type_options = [ export default ['$scope', '$rootScope', 'UserForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'GetBasePath', - 'Wait', 'CreateSelect2', '$state', '$location', 'i18n', + 'Wait', 'CreateSelect2', '$state', '$location', 'i18n', 'canAdd', function($scope, $rootScope, UserForm, GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, GetBasePath, Wait, CreateSelect2, - $state, $location, i18n) { + $state, $location, i18n, canAdd) { var defaultUrl = GetBasePath('organizations'), form = UserForm; @@ -28,6 +28,7 @@ export default ['$scope', '$rootScope', 'UserForm', 'GenerateForm', 'Rest', // apply form definition's default field values GenerateForm.applyDefaults(form, $scope); + $scope.canAdd = canAdd; $scope.isAddForm = true; $scope.ldap_user = false; $scope.not_ldap_user = !$scope.ldap_user; diff --git a/awx/ui/client/src/users/edit/users-edit.controller.js b/awx/ui/client/src/users/edit/users-edit.controller.js index f2294a1703..f44c34b2a8 100644 --- a/awx/ui/client/src/users/edit/users-edit.controller.js +++ b/awx/ui/client/src/users/edit/users-edit.controller.js @@ -14,10 +14,10 @@ const user_type_options = [ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest', 'ProcessErrors', 'GetBasePath', 'Wait', 'CreateSelect2', - '$state', 'i18n', 'resolvedModels', + '$state', 'i18n', 'resolvedModels', 'resourceData', function($scope, $rootScope, $stateParams, UserForm, Rest, ProcessErrors, - GetBasePath, Wait, CreateSelect2, $state, i18n, models) { - + GetBasePath, Wait, CreateSelect2, $state, i18n, models, resourceData) { + for (var i = 0; i < user_type_options.length; i++) { user_type_options[i].label = i18n._(user_type_options[i].label); } @@ -26,7 +26,10 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest', var form = UserForm, master = {}, id = $stateParams.user_id, - defaultUrl = GetBasePath('users') + id; + defaultUrl = GetBasePath('users') + id, + user_obj = resourceData.data; + + $scope.breadcrumb.user_name = user_obj.username; init(); @@ -40,49 +43,39 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest', $scope.user_type = user_type_options[0]; $scope.$watch('user_type', user_type_sync($scope)); $scope.$watch('is_superuser', hidePermissionsTabSmartSearchAndPaginationIfSuperUser($scope)); - Rest.setUrl(defaultUrl); - Wait('start'); - Rest.get(defaultUrl).then(({data}) => { - $scope.user_id = id; - $scope.ldap_user = (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') ? true : false; - $scope.not_ldap_user = !$scope.ldap_user; - master.ldap_user = $scope.ldap_user; - $scope.socialAuthUser = (data.auth.length > 0) ? true : false; - $scope.external_account = data.external_account; + $scope.user_id = id; + $scope.ldap_user = (user_obj.ldap_dn !== null && user_obj.ldap_dn !== undefined && user_obj.ldap_dn !== '') ? true : false; + $scope.not_ldap_user = !$scope.ldap_user; + master.ldap_user = $scope.ldap_user; + $scope.socialAuthUser = (user_obj.auth.length > 0) ? true : false; + $scope.external_account = user_obj.external_account; - $scope.user_type = $scope.user_type_options[0]; - $scope.is_system_auditor = false; - $scope.is_superuser = false; - if (data.is_system_auditor) { - $scope.user_type = $scope.user_type_options[1]; - $scope.is_system_auditor = true; - } - if (data.is_superuser) { - $scope.user_type = $scope.user_type_options[2]; - $scope.is_superuser = true; - } + $scope.user_type = $scope.user_type_options[0]; + $scope.is_system_auditor = false; + $scope.is_superuser = false; + if (user_obj.is_system_auditor) { + $scope.user_type = $scope.user_type_options[1]; + $scope.is_system_auditor = true; + } + if (user_obj.is_superuser) { + $scope.user_type = $scope.user_type_options[2]; + $scope.is_superuser = true; + } - $scope.user_obj = data; - $scope.name = data.username; + $scope.user_obj = user_obj; + $scope.name = user_obj.username; - CreateSelect2({ - element: '#user_user_type', - multiple: false - }); + CreateSelect2({ + element: '#user_user_type', + multiple: false + }); - $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { - $scope.canAdd = (val === false) ? false : true; - }); + $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { + $scope.canAdd = (val === false) ? false : true; + }); - setScopeFields(data); - Wait('stop'); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status - }); - }); + setScopeFields(user_obj); + } function user_type_sync($scope) { diff --git a/awx/ui/client/src/users/list/users-list.controller.js b/awx/ui/client/src/users/list/users-list.controller.js index b684a5bbdb..96d01ebf04 100644 --- a/awx/ui/client/src/users/list/users-list.controller.js +++ b/awx/ui/client/src/users/list/users-list.controller.js @@ -68,7 +68,7 @@ export default ['$scope', '$rootScope', 'Rest', 'UserList', 'Prompt', let reloadListStateParams = null; - if($scope.users.length === 1 && $state.params.user_search && !_.isEmpty($state.params.user_search.page) && $state.params.user_search.page !== '1') { + if($scope.users.length === 1 && $state.params.user_search && _.has($state, 'params.user_search.page') && $state.params.user_search.page !== '1') { reloadListStateParams = _.cloneDeep($state.params); reloadListStateParams.user_search.page = (parseInt(reloadListStateParams.user_search.page)-1).toString(); } diff --git a/awx/ui/client/src/users/main.js b/awx/ui/client/src/users/main.js index 3701d4f626..64222e3977 100644 --- a/awx/ui/client/src/users/main.js +++ b/awx/ui/client/src/users/main.js @@ -10,12 +10,11 @@ import UsersEdit from './edit/users-edit.controller'; import UserForm from './users.form'; import UserList from './users.list'; +import userListRoute from './users.route'; import UserTokensListRoute from '../../features/users/tokens/users-tokens-list.route'; import UserTokensAddRoute from '../../features/users/tokens/users-tokens-add.route'; import UserTokensAddApplicationRoute from '../../features/users/tokens/users-tokens-add-application.route'; -import { N_ } from '../i18n'; - export default angular.module('Users', []) .controller('UsersList', UsersList) @@ -29,20 +28,52 @@ angular.module('Users', []) let stateExtender = $stateExtenderProvider.$get(); function generateStateTree() { - let userTree = stateDefinitions.generateTree({ - parent: 'users', - modes: ['add', 'edit'], - list: 'UserList', + let userAdd = stateDefinitions.generateTree({ + name: 'users.add', + url: '/add', + modes: ['add'], form: 'UserForm', controllers: { - list: UsersList, - add: UsersAdd, - edit: UsersEdit + add: 'UsersAdd' + }, + resolve: { + add: { + canAdd: ['rbacUiControlService', '$state', function(rbacUiControlService, $state) { + return rbacUiControlService.canAdd('users') + .then(function(res) { + return res.canAdd; + }) + .catch(function() { + $state.go('users'); + }); + }], + resolvedModels: ['MeModel', '$q', function(Me, $q) { + const promises= { + me: new Me('get').then((me) => me.extend('get', 'admin_of_organizations')) + }; + + return $q.all(promises); + }] + } + } + }); + + let userEdit = stateDefinitions.generateTree({ + name: 'users.edit', + url: '/:user_id', + modes: ['edit'], + form: 'UserForm', + parent: 'users', + controllers: { + edit: 'UsersEdit' }, data: { activityStream: true, activityStreamTarget: 'user' }, + breadcrumbs: { + edit: "{{breadcrumb.user_name}}" + }, resolve: { edit: { resolvedModels: ['MeModel', '$q', function(Me, $q) { @@ -50,31 +81,21 @@ angular.module('Users', []) me: new Me('get').then((me) => me.extend('get', 'admin_of_organizations')) }; - return $q.all(promises); - }] - }, - list: { - resolvedModels: ['MeModel', '$q', function(Me, $q) { - const promises= { - me: new Me('get') - }; - return $q.all(promises); }] } }, - ncyBreadcrumb: { - label: N_('USERS') - } }); - + return Promise.all([ - userTree + userAdd, + userEdit ]).then((generated) => { return { states: _.reduce(generated, (result, definition) => { return result.concat(definition.states); }, [ + stateExtender.buildDefinition(userListRoute), stateExtender.buildDefinition(UserTokensListRoute), stateExtender.buildDefinition(UserTokensAddRoute), stateExtender.buildDefinition(UserTokensAddApplicationRoute) diff --git a/awx/ui/client/src/users/users.form.js b/awx/ui/client/src/users/users.form.js index 0479eb91a2..60f9e422d7 100644 --- a/awx/ui/client/src/users/users.form.js +++ b/awx/ui/client/src/users/users.form.js @@ -29,14 +29,14 @@ export default ['i18n', function(i18n) { label: i18n._('First Name'), type: 'text', ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)', - required: true, + required: false, capitalize: true }, last_name: { label: i18n._('Last Name'), type: 'text', ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)', - required: true + required: false, }, organization: { label: i18n._('Organization'), diff --git a/awx/ui/client/src/users/users.partial.html b/awx/ui/client/src/users/users.partial.html new file mode 100644 index 0000000000..41b78528d4 --- /dev/null +++ b/awx/ui/client/src/users/users.partial.html @@ -0,0 +1,6 @@ +
    + +
    +
    +
    +
    diff --git a/awx/ui/client/src/users/users.route.js b/awx/ui/client/src/users/users.route.js new file mode 100644 index 0000000000..80f581a93b --- /dev/null +++ b/awx/ui/client/src/users/users.route.js @@ -0,0 +1,55 @@ +import {templateUrl} from '../shared/template-url/template-url.factory'; +import { N_ } from '../i18n'; + +export default { + name: 'users', + route: '/users', + ncyBreadcrumb: { + label: N_('USERS') + }, + data: { + activityStream: true, + activityStreamTarget: 'user' + }, + params: { + user_search: { + value: { + page_size: 20, + order_by: 'username' + } + } + }, + views: { + '@': { + templateUrl: templateUrl('users/users') + }, + 'list@users': { + templateProvider: function(UserList, generateList) { + let html = generateList.build({ + list: UserList, + mode: 'edit' + }); + html = generateList.wrapPanel(html); + return html; + }, + controller: 'UsersList' + } + }, + searchPrefix: 'user', + resolve: { + Dataset: ['UserList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + resolvedModels: ['MeModel', '$q', function(Me, $q) { + const promises= { + me: new Me('get') + }; + + return $q.all(promises); + }] + + } +}; diff --git a/awx/ui/client/src/vendor.js b/awx/ui/client/src/vendor.js index c4263249f1..de496ac02d 100644 --- a/awx/ui/client/src/vendor.js +++ b/awx/ui/client/src/vendor.js @@ -63,11 +63,3 @@ require('ng-toast'); require('lr-infinite-scroll'); require('codemirror/mode/yaml/yaml'); require('codemirror/mode/javascript/javascript'); - -// Network Visualization -require('angular-mousewheel'); -require('angular-xeditable'); -require('hamsterjs'); -require('titlecase'); -require('inherits'); -require('mathjs'); diff --git a/awx/ui/client/src/workflow-results/standard-out.block.less b/awx/ui/client/src/workflow-results/standard-out.block.less index c5fad72826..aab04ed73b 100644 --- a/awx/ui/client/src/workflow-results/standard-out.block.less +++ b/awx/ui/client/src/workflow-results/standard-out.block.less @@ -20,11 +20,12 @@ .StandardOut-actionButton { font-size: 16px; - height: 20px; + height: 30px; min-width: 30px; color: @list-action-icon; background-color: inherit; border: none; + border-radius: 5px; } .StandardOut-actionButton:hover { diff --git a/awx/ui/client/src/workflow-results/workflow-results.block.less b/awx/ui/client/src/workflow-results/workflow-results.block.less index 53a98f4db5..d2af14d1f0 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.block.less +++ b/awx/ui/client/src/workflow-results/workflow-results.block.less @@ -17,7 +17,8 @@ min-height: 350px; .Panel { - overflow: scroll; + overflow-x: auto; + overflow-y: auto; } } diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index 23e2fe7269..31ad3191f8 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -31,9 +31,47 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', $scope.scheduled_by_link = getLink('schedule'); $scope.cloud_credential_link = getLink('cloud_credential'); $scope.network_credential_link = getLink('network_credential'); + + $scope.strings = { + tooltips: { + RELAUNCH: i18n._('Relaunch using the same parameters'), + CANCEL: i18n._('Cancel'), + DELETE: i18n._('Delete'), + EDIT_USER: i18n._('Edit the user'), + EDIT_WORKFLOW: i18n._('Edit the workflow job template'), + EDIT_SCHEDULE: i18n._('Edit the schedule'), + TOGGLE_STDOUT_FULLSCREEN: i18n._('Expand Output'), + STATUS: '' // re-assigned elsewhere + }, + labels: { + TEMPLATE: i18n._('Template'), + LAUNCHED_BY: i18n._('Launched By'), + STARTED: i18n._('Started'), + FINISHED: i18n._('Finished'), + LABELS: i18n._('Labels'), + STATUS: i18n._('Status') + }, + details: { + HEADER: i18n._('DETAILS'), + NOT_FINISHED: i18n._('Not Finished'), + NOT_STARTED: i18n._('Not Started'), + }, + results: { + TOTAL_JOBS: i18n._('Total Jobs'), + ELAPSED: i18n._('Elapsed'), + }, + legend: { + ON_SUCCESS: i18n._('On Success'), + ON_FAIL: i18n._('On Fail'), + ALWAYS: i18n._('Always'), + PROJECT_SYNC: i18n._('Project Sync'), + INVENTORY_SYNC: i18n._('Inventory Sync'), + KEY: i18n._('KEY'), + } + }; }; - var getLabels = function() { + var getLabelsAndTooltips = function() { var getLabel = function(key) { if ($scope.workflowOptions && $scope.workflowOptions[key]) { return $scope.workflowOptions[key].choices @@ -44,9 +82,8 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', } }; - $scope.status_label = getLabel('status'); - $scope.type_label = getLabel('job_type'); - $scope.verbosity_label = getLabel('verbosity'); + $scope.workflow.statusLabel = i18n._(getLabel('status')); + $scope.strings.tooltips.STATUS = `${i18n._('Job')} ${$scope.workflow.statusLabel}`; }; var updateWorkflowJobElapsedTimer = function(time) { @@ -72,14 +109,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', $scope.workflow_job_template_link = `/#/templates/workflow_job_template/${$scope.workflow.summary_fields.workflow_job_template.id}`; } - // stdout full screen toggle tooltip text - $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); - // turn related api browser routes into front end routes getLinks(); // use options labels to manipulate display of details - getLabels(); + getLabelsAndTooltips(); // set up a read only code mirror for extra vars $scope.variables = ParseVariableString($scope.workflow.extra_vars); @@ -187,7 +221,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateWorkflowJobElapsedTimer); } - if(data.status === "successful" || data.status === "failed" || data.status === "error"){ + if(data.status === "successful" || data.status === "failed" || data.status === "canceled" || data.status === "error"){ $state.go('.', null, { reload: true }); } } @@ -200,7 +234,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', // can happen if the GET request on the workflow job returns "waiting" and // the sockets aren't established yet so we miss the event that indicates // the workflow job has moved into a running state. - if (!_.includes(['running', 'successful', 'failed', 'error'], $scope.workflow.status)){ + if (!_.includes(['running', 'successful', 'failed', 'error', 'canceled'], $scope.workflow.status)){ $scope.workflow.status = 'running'; runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateWorkflowJobElapsedTimer); } @@ -224,6 +258,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', .getCounts($scope.workflow_nodes); $scope.$broadcast("refreshWorkflowChart"); } + getLabelsAndTooltips(); }); $scope.$on('$destroy', function() { diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index dd8def8283..97ef30a914 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -15,7 +15,7 @@
    - DETAILS + {{ strings.details.HEADER }}
    @@ -26,7 +26,7 @@ data-placement="top" mode="all" ng-click="relaunchJob()" - aw-tool-tip="{{'Relaunch using the same parameters'|translate}}" + aw-tool-tip="{{ strings.tooltips.RELAUNCH }}" data-original-title="" title=""> @@ -39,7 +39,7 @@ ng-click="cancelJob()" ng-show="workflow.status == 'running' || workflow.status=='pending' " - aw-tool-tip="{{'Cancel'|translate}}" + aw-tool-tip="{{ strings.tooltips.CANCEL }}" data-original-title="" title=""> @@ -51,7 +51,7 @@ ng-click="deleteJob()" ng-hide="workflow.status == 'running' || workflow.status == 'pending' " - aw-tool-tip="{{'Delete'|translate}}" + aw-tool-tip="{{ strings.tooltips.DELETE }}" data-original-title="" title=""> @@ -65,13 +65,13 @@
    - {{ status_label }} + {{ workflow.statusLabel }}
    @@ -79,10 +79,10 @@
    - {{ workflow.started | longDate }} + {{ (workflow.started | longDate) || strings.details.NOT_STARTED }}
    @@ -90,11 +90,11 @@
    {{ (workflow.finished | - longDate) || "Not Finished" }} + longDate) || strings.details.NOT_FINISHED }}
    @@ -102,11 +102,11 @@
    {{ workflow.summary_fields.workflow_job_template.name }} @@ -117,11 +117,11 @@
    {{ workflow.summary_fields.created_by.username }} @@ -133,11 +133,11 @@ ng-show="workflow.summary_fields.schedule.name">
    {{ workflow.summary_fields.schedule.name }} @@ -163,7 +163,7 @@ ng-show="lessLabels" href="" ng-click="toggleLessLabels()"> - Labels + {{ strings.labels.LABELS }} @@ -172,7 +172,7 @@ ng-show="!lessLabels" href="" ng-click="toggleLessLabels()"> - Labels + {{ strings.labels.LABELS }} @@ -207,7 +207,7 @@ @@ -218,7 +218,7 @@
    - Total Jobs + {{ strings.results.TOTAL_JOBS }}
    {{ workflow_nodes.length || 0}} @@ -226,7 +226,7 @@
    - Elapsed + {{ strings.results.ELAPSED }}
    {{ workflow.elapsed * 1000 | duration: "hh:mm:ss"}} @@ -238,8 +238,8 @@