mirror of
https://github.com/ansible/awx.git
synced 2026-01-28 00:51:27 -03:30
Merge remote-tracking branch 'downstream/release_3.3.0' into devel
# Conflicts: # awx/main/notifications/slack_backend.py
This commit is contained in:
commit
580004b395
1
Makefile
1
Makefile
@ -382,6 +382,7 @@ test:
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
py.test -n auto $(TEST_DIRS)
|
||||
awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file'
|
||||
|
||||
test_combined: test_ansible test
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ class DeprecatedCredentialField(serializers.IntegerField):
|
||||
kwargs['allow_null'] = True
|
||||
kwargs['default'] = None
|
||||
kwargs['min_value'] = 1
|
||||
kwargs['help_text'] = 'This resource has been deprecated and will be removed in a future release'
|
||||
kwargs.setdefault('help_text', 'This resource has been deprecated and will be removed in a future release')
|
||||
super(DeprecatedCredentialField, self).__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, pk):
|
||||
|
||||
@ -390,7 +390,6 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
]:
|
||||
d[key] = self.metadata_class().get_serializer_info(serializer, method=method)
|
||||
d['settings'] = settings
|
||||
d['has_named_url'] = self.model in settings.NAMED_URL_GRAPH
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@ -1903,7 +1903,9 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
credential = DeprecatedCredentialField()
|
||||
credential = DeprecatedCredentialField(
|
||||
help_text=_('Cloud credential to use for inventory updates.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential',
|
||||
|
||||
@ -54,8 +54,6 @@ within all designated text fields of a model.
|
||||
|
||||
?search=findme
|
||||
|
||||
_Added in AWX 1.4_
|
||||
|
||||
(_Added in Ansible Tower 3.1.0_) Search across related fields:
|
||||
|
||||
?related__search=findme
|
||||
@ -84,7 +82,7 @@ To exclude results matching certain criteria, prefix the field parameter with
|
||||
|
||||
?not__field=value
|
||||
|
||||
(_Added in AWX 1.4_) By default, all query string filters are AND'ed together, so
|
||||
By default, all query string filters are AND'ed together, so
|
||||
only the results matching *all* filters will be returned. To combine results
|
||||
matching *any* one of multiple criteria, prefix each query string parameter
|
||||
with `or__`:
|
||||
|
||||
@ -10,7 +10,7 @@ object containing groups, including the hosts, children and variables for each
|
||||
group. The response data is equivalent to that returned by passing the
|
||||
`--list` argument to an inventory script.
|
||||
|
||||
_(Added in AWX 1.3)_ Specify a query string of `?hostvars=1` to retrieve the JSON
|
||||
Specify a query string of `?hostvars=1` to retrieve the JSON
|
||||
object above including all host variables. The `['_meta']['hostvars']` object
|
||||
in the response contains an entry for each host with its variables. This
|
||||
response format can be used with Ansible 1.3 and later to avoid making a
|
||||
@ -18,11 +18,16 @@ separate API request for each host. Refer to
|
||||
[Tuning the External Inventory Script](http://docs.ansible.com/developing_inventory.html#tuning-the-external-inventory-script)
|
||||
for more information on this feature.
|
||||
|
||||
_(Added in AWX 1.4)_ By default, the inventory script will only return hosts that
|
||||
By default, the inventory script will only return hosts that
|
||||
are enabled in the inventory. This feature allows disabled hosts to be skipped
|
||||
when running jobs without removing them from the inventory. Specify a query
|
||||
string of `?all=1` to return all hosts, including disabled ones.
|
||||
|
||||
Specify a query string of `?towervars=1` to add variables
|
||||
to the hostvars of each host that specifies its enabled state and database ID.
|
||||
|
||||
To apply multiple query strings, join them with the `&` character, like `?hostvars=1&all=1`.
|
||||
|
||||
## Host Response
|
||||
|
||||
Make a GET request to this resource with a query string similar to
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
{% if has_named_url %}
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
{% if has_named_url %}
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
{% if has_named_url %}
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
{% if has_named_url %}
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
|
||||
@ -115,9 +115,10 @@ class ActivityStreamEnforcementMixin(object):
|
||||
Mixin to check that license supports activity streams.
|
||||
'''
|
||||
def check_permissions(self, request):
|
||||
ret = super(ActivityStreamEnforcementMixin, self).check_permissions(request)
|
||||
if not feature_enabled('activity_streams'):
|
||||
raise LicenseForbids(_('Your license does not allow use of the activity stream.'))
|
||||
return super(ActivityStreamEnforcementMixin, self).check_permissions(request)
|
||||
return ret
|
||||
|
||||
|
||||
class SystemTrackingEnforcementMixin(object):
|
||||
@ -125,9 +126,10 @@ class SystemTrackingEnforcementMixin(object):
|
||||
Mixin to check that license supports system tracking.
|
||||
'''
|
||||
def check_permissions(self, request):
|
||||
ret = super(SystemTrackingEnforcementMixin, self).check_permissions(request)
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids(_('Your license does not permit use of system tracking.'))
|
||||
return super(SystemTrackingEnforcementMixin, self).check_permissions(request)
|
||||
return ret
|
||||
|
||||
|
||||
class WorkflowsEnforcementMixin(object):
|
||||
@ -135,9 +137,10 @@ class WorkflowsEnforcementMixin(object):
|
||||
Mixin to check that license supports workflows.
|
||||
'''
|
||||
def check_permissions(self, request):
|
||||
ret = super(WorkflowsEnforcementMixin, self).check_permissions(request)
|
||||
if not feature_enabled('workflows') and request.method not in ('GET', 'OPTIONS', 'DELETE'):
|
||||
raise LicenseForbids(_('Your license does not allow use of workflows.'))
|
||||
return super(WorkflowsEnforcementMixin, self).check_permissions(request)
|
||||
return ret
|
||||
|
||||
|
||||
class UnifiedJobDeletionMixin(object):
|
||||
@ -442,9 +445,9 @@ class ApiV1ConfigView(APIView):
|
||||
data.update(dict(
|
||||
project_base_dir = settings.PROJECTS_ROOT,
|
||||
project_local_paths = Project.get_local_path_choices(),
|
||||
custom_virtualenvs = get_custom_venv_choices()
|
||||
))
|
||||
|
||||
if JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||
elif JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||
|
||||
return Response(data)
|
||||
@ -2883,17 +2886,14 @@ class InventorySourceCredentialsList(SubListAttachDetachAPIView):
|
||||
relationship = 'credentials'
|
||||
|
||||
def is_valid_relation(self, parent, sub, created=False):
|
||||
# Inventory source credentials are exclusive with all other credentials
|
||||
# subject to change for https://github.com/ansible/awx/issues/277
|
||||
# or https://github.com/ansible/awx/issues/223
|
||||
if parent.credentials.exists():
|
||||
return {'msg': _("Source already has credential assigned.")}
|
||||
error = InventorySource.cloud_credential_validation(parent.source, sub)
|
||||
if error:
|
||||
return {'msg': error}
|
||||
if sub.credential_type == 'vault':
|
||||
# TODO: support this
|
||||
return {"msg": _("Vault credentials are not yet supported for inventory sources.")}
|
||||
else:
|
||||
# Cloud credentials are exclusive with all other cloud credentials
|
||||
cloud_cred_qs = parent.credentials.exclude(credential_type__kind='vault')
|
||||
if cloud_cred_qs.exists():
|
||||
return {'msg': _("Source already has cloud credential assigned.")}
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@ -24,7 +24,12 @@ import os
|
||||
import pwd
|
||||
|
||||
# PSUtil
|
||||
import psutil
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
raise ImportError('psutil is missing; {}bin/pip install psutil'.format(
|
||||
os.environ['VIRTUAL_ENV']
|
||||
))
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
@ -27,7 +27,13 @@ import os
|
||||
import stat
|
||||
import threading
|
||||
import uuid
|
||||
import memcache
|
||||
|
||||
try:
|
||||
import memcache
|
||||
except ImportError:
|
||||
raise ImportError('python-memcached is missing; {}bin/pip install python-memcached'.format(
|
||||
os.environ['VIRTUAL_ENV']
|
||||
))
|
||||
|
||||
from six.moves import xrange
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-03 19:04+0000\n"
|
||||
"POT-Creation-Date: 2018-08-14 13:52+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -125,15 +125,15 @@ msgid ""
|
||||
"our REST API, the Content-Type must be application/json"
|
||||
msgstr ""
|
||||
|
||||
#: awx/api/generics.py:629 awx/api/generics.py:691
|
||||
#: awx/api/generics.py:635 awx/api/generics.py:697
|
||||
msgid "\"id\" field must be an integer."
|
||||
msgstr ""
|
||||
|
||||
#: awx/api/generics.py:688
|
||||
#: awx/api/generics.py:694
|
||||
msgid "\"id\" is required to disassociate"
|
||||
msgstr ""
|
||||
|
||||
#: awx/api/generics.py:739
|
||||
#: awx/api/generics.py:745
|
||||
msgid "{} 'id' field is missing."
|
||||
msgstr ""
|
||||
|
||||
@ -1642,86 +1642,86 @@ msgstr ""
|
||||
msgid "Bad data found in related field %s."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:304
|
||||
#: awx/main/access.py:302
|
||||
msgid "License is missing."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:306
|
||||
#: awx/main/access.py:304
|
||||
msgid "License has expired."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:314
|
||||
#: awx/main/access.py:312
|
||||
#, python-format
|
||||
msgid "License count of %s instances has been reached."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:316
|
||||
#: awx/main/access.py:314
|
||||
#, python-format
|
||||
msgid "License count of %s instances has been exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:318
|
||||
#: awx/main/access.py:316
|
||||
msgid "Host count exceeds available instances."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:322
|
||||
#: awx/main/access.py:320
|
||||
#, python-format
|
||||
msgid "Feature %s is not enabled in the active license."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:324
|
||||
#: awx/main/access.py:322
|
||||
msgid "Features not found in active license."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:837
|
||||
#: awx/main/access.py:835
|
||||
msgid "Unable to change inventory on a host."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:854 awx/main/access.py:899
|
||||
#: awx/main/access.py:852 awx/main/access.py:897
|
||||
msgid "Cannot associate two items from different inventories."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:887
|
||||
#: awx/main/access.py:885
|
||||
msgid "Unable to change inventory on a group."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1148
|
||||
#: awx/main/access.py:1146
|
||||
msgid "Unable to change organization on a team."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1165
|
||||
#: awx/main/access.py:1163
|
||||
msgid "The {} role cannot be assigned to a team"
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1167
|
||||
#: awx/main/access.py:1165
|
||||
msgid "The admin_role for a User cannot be assigned to a team"
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1533 awx/main/access.py:1967
|
||||
#: awx/main/access.py:1531 awx/main/access.py:1965
|
||||
msgid "Job was launched with prompts provided by another user."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1553
|
||||
#: awx/main/access.py:1551
|
||||
msgid "Job has been orphaned from its job template."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1555
|
||||
#: awx/main/access.py:1553
|
||||
msgid "Job was launched with unknown prompted fields."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1557
|
||||
#: awx/main/access.py:1555
|
||||
msgid "Job was launched with prompted fields."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1559
|
||||
#: awx/main/access.py:1557
|
||||
msgid " Organization level permissions required."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1561
|
||||
#: awx/main/access.py:1559
|
||||
msgid " You do not have permission to related resources."
|
||||
msgstr ""
|
||||
|
||||
#: awx/main/access.py:1981
|
||||
#: awx/main/access.py:1979
|
||||
msgid ""
|
||||
"You do not have permission to the workflow job resources required for "
|
||||
"relaunch."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -611,7 +611,8 @@ class OAuth2ApplicationAccess(BaseAccess):
|
||||
select_related = ('user',)
|
||||
|
||||
def filtered_queryset(self):
|
||||
return self.model.objects.filter(organization__in=self.user.organizations)
|
||||
org_access_qs = Organization.accessible_objects(self.user, 'member_role')
|
||||
return self.model.objects.filter(organization__in=org_access_qs)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser or self.check_related('organization', Organization, data, obj=obj,
|
||||
|
||||
@ -117,10 +117,10 @@ class IsolatedManager(object):
|
||||
|
||||
@classmethod
|
||||
def awx_playbook_path(cls):
|
||||
return os.path.join(
|
||||
return os.path.abspath(os.path.join(
|
||||
os.path.dirname(awx.__file__),
|
||||
'playbooks'
|
||||
)
|
||||
))
|
||||
|
||||
def path_to(self, *args):
|
||||
return os.path.join(self.private_data_dir, *args)
|
||||
|
||||
@ -208,6 +208,12 @@ def run_isolated_job(private_data_dir, secrets, logfile=sys.stdout):
|
||||
env['AWX_ISOLATED_DATA_DIR'] = private_data_dir
|
||||
env['PYTHONPATH'] = env.get('PYTHONPATH', '') + callback_dir + ':'
|
||||
|
||||
venv_path = env.get('VIRTUAL_ENV')
|
||||
if venv_path and not os.path.exists(venv_path):
|
||||
raise RuntimeError(
|
||||
'a valid Python virtualenv does not exist at {}'.format(venv_path)
|
||||
)
|
||||
|
||||
return run_pexpect(args, cwd, env, logfile,
|
||||
expect_passwords=expect_passwords,
|
||||
idle_timeout=idle_timeout,
|
||||
|
||||
12
awx/main/management/commands/check_migrations.py
Normal file
12
awx/main/management/commands/check_migrations.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.db import connections
|
||||
from django.db.backends.sqlite3.base import DatabaseWrapper
|
||||
from django.core.management.commands.makemigrations import Command as MakeMigrations
|
||||
|
||||
|
||||
class Command(MakeMigrations):
|
||||
|
||||
def execute(self, *args, **options):
|
||||
settings = connections['default'].settings_dict.copy()
|
||||
settings['ENGINE'] = 'sqlite3'
|
||||
connections['default'] = DatabaseWrapper(settings)
|
||||
return MakeMigrations().execute(*args, **options)
|
||||
@ -491,7 +491,7 @@ class Command(BaseCommand):
|
||||
for host in hosts_qs.filter(pk__in=del_pks):
|
||||
host_name = host.name
|
||||
host.delete()
|
||||
logger.info('Deleted host "%s"', host_name)
|
||||
logger.debug('Deleted host "%s"', host_name)
|
||||
if settings.SQL_DEBUG:
|
||||
logger.warning('host deletions took %d queries for %d hosts',
|
||||
len(connection.queries) - queries_before,
|
||||
@ -528,7 +528,7 @@ class Command(BaseCommand):
|
||||
group_name = group.name
|
||||
with ignore_inventory_computed_fields():
|
||||
group.delete()
|
||||
logger.info('Group "%s" deleted', group_name)
|
||||
logger.debug('Group "%s" deleted', group_name)
|
||||
if settings.SQL_DEBUG:
|
||||
logger.warning('group deletions took %d queries for %d groups',
|
||||
len(connection.queries) - queries_before,
|
||||
@ -549,7 +549,7 @@ class Command(BaseCommand):
|
||||
db_groups = self.inventory_source.groups
|
||||
for db_group in db_groups.all():
|
||||
if self.inventory_source.deprecated_group_id == db_group.id: # TODO: remove in 3.3
|
||||
logger.info(
|
||||
logger.debug(
|
||||
'Group "%s" from v1 API child group/host connections preserved',
|
||||
db_group.name
|
||||
)
|
||||
@ -566,8 +566,8 @@ class Command(BaseCommand):
|
||||
for db_child in db_children.filter(pk__in=child_group_pks):
|
||||
group_group_count += 1
|
||||
db_group.children.remove(db_child)
|
||||
logger.info('Group "%s" removed from group "%s"',
|
||||
db_child.name, db_group.name)
|
||||
logger.debug('Group "%s" removed from group "%s"',
|
||||
db_child.name, db_group.name)
|
||||
# FIXME: Inventory source group relationships
|
||||
# Delete group/host relationships not present in imported data.
|
||||
db_hosts = db_group.hosts
|
||||
@ -594,8 +594,8 @@ class Command(BaseCommand):
|
||||
if db_host not in db_group.hosts.all():
|
||||
continue
|
||||
db_group.hosts.remove(db_host)
|
||||
logger.info('Host "%s" removed from group "%s"',
|
||||
db_host.name, db_group.name)
|
||||
logger.debug('Host "%s" removed from group "%s"',
|
||||
db_host.name, db_group.name)
|
||||
if settings.SQL_DEBUG:
|
||||
logger.warning('group-group and group-host deletions took %d queries for %d relationships',
|
||||
len(connection.queries) - queries_before,
|
||||
@ -614,9 +614,9 @@ class Command(BaseCommand):
|
||||
if db_variables != all_obj.variables_dict:
|
||||
all_obj.variables = json.dumps(db_variables)
|
||||
all_obj.save(update_fields=['variables'])
|
||||
logger.info('Inventory variables updated from "all" group')
|
||||
logger.debug('Inventory variables updated from "all" group')
|
||||
else:
|
||||
logger.info('Inventory variables unmodified')
|
||||
logger.debug('Inventory variables unmodified')
|
||||
|
||||
def _create_update_groups(self):
|
||||
'''
|
||||
@ -648,11 +648,11 @@ class Command(BaseCommand):
|
||||
group.variables = json.dumps(db_variables)
|
||||
group.save(update_fields=['variables'])
|
||||
if self.overwrite_vars:
|
||||
logger.info('Group "%s" variables replaced', group.name)
|
||||
logger.debug('Group "%s" variables replaced', group.name)
|
||||
else:
|
||||
logger.info('Group "%s" variables updated', group.name)
|
||||
logger.debug('Group "%s" variables updated', group.name)
|
||||
else:
|
||||
logger.info('Group "%s" variables unmodified', group.name)
|
||||
logger.debug('Group "%s" variables unmodified', group.name)
|
||||
existing_group_names.add(group.name)
|
||||
self._batch_add_m2m(self.inventory_source.groups, group)
|
||||
for group_name in all_group_names:
|
||||
@ -666,7 +666,7 @@ class Command(BaseCommand):
|
||||
'description':'imported'
|
||||
}
|
||||
)[0]
|
||||
logger.info('Group "%s" added', group.name)
|
||||
logger.debug('Group "%s" added', group.name)
|
||||
self._batch_add_m2m(self.inventory_source.groups, group)
|
||||
self._batch_add_m2m(self.inventory_source.groups, flush=True)
|
||||
if settings.SQL_DEBUG:
|
||||
@ -705,24 +705,24 @@ class Command(BaseCommand):
|
||||
if update_fields:
|
||||
db_host.save(update_fields=update_fields)
|
||||
if 'name' in update_fields:
|
||||
logger.info('Host renamed from "%s" to "%s"', old_name, mem_host.name)
|
||||
logger.debug('Host renamed from "%s" to "%s"', old_name, mem_host.name)
|
||||
if 'instance_id' in update_fields:
|
||||
if old_instance_id:
|
||||
logger.info('Host "%s" instance_id updated', mem_host.name)
|
||||
logger.debug('Host "%s" instance_id updated', mem_host.name)
|
||||
else:
|
||||
logger.info('Host "%s" instance_id added', mem_host.name)
|
||||
logger.debug('Host "%s" instance_id added', mem_host.name)
|
||||
if 'variables' in update_fields:
|
||||
if self.overwrite_vars:
|
||||
logger.info('Host "%s" variables replaced', mem_host.name)
|
||||
logger.debug('Host "%s" variables replaced', mem_host.name)
|
||||
else:
|
||||
logger.info('Host "%s" variables updated', mem_host.name)
|
||||
logger.debug('Host "%s" variables updated', mem_host.name)
|
||||
else:
|
||||
logger.info('Host "%s" variables unmodified', mem_host.name)
|
||||
logger.debug('Host "%s" variables unmodified', mem_host.name)
|
||||
if 'enabled' in update_fields:
|
||||
if enabled:
|
||||
logger.info('Host "%s" is now enabled', mem_host.name)
|
||||
logger.debug('Host "%s" is now enabled', mem_host.name)
|
||||
else:
|
||||
logger.info('Host "%s" is now disabled', mem_host.name)
|
||||
logger.debug('Host "%s" is now disabled', mem_host.name)
|
||||
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
||||
|
||||
def _create_update_hosts(self):
|
||||
@ -796,9 +796,9 @@ class Command(BaseCommand):
|
||||
host_attrs['instance_id'] = instance_id
|
||||
db_host = self.inventory.hosts.update_or_create(name=mem_host_name, defaults=host_attrs)[0]
|
||||
if enabled is False:
|
||||
logger.info('Host "%s" added (disabled)', mem_host_name)
|
||||
logger.debug('Host "%s" added (disabled)', mem_host_name)
|
||||
else:
|
||||
logger.info('Host "%s" added', mem_host_name)
|
||||
logger.debug('Host "%s" added', mem_host_name)
|
||||
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
||||
|
||||
self._batch_add_m2m(self.inventory_source.hosts, flush=True)
|
||||
@ -827,10 +827,10 @@ class Command(BaseCommand):
|
||||
child_names = all_child_names[offset2:(offset2 + self._batch_size)]
|
||||
db_children_qs = self.inventory.groups.filter(name__in=child_names)
|
||||
for db_child in db_children_qs.filter(children__id=db_group.id):
|
||||
logger.info('Group "%s" already child of group "%s"', db_child.name, db_group.name)
|
||||
logger.debug('Group "%s" already child of group "%s"', db_child.name, db_group.name)
|
||||
for db_child in db_children_qs.exclude(children__id=db_group.id):
|
||||
self._batch_add_m2m(db_group.children, db_child)
|
||||
logger.info('Group "%s" added as child of "%s"', db_child.name, db_group.name)
|
||||
logger.debug('Group "%s" added as child of "%s"', db_child.name, db_group.name)
|
||||
self._batch_add_m2m(db_group.children, flush=True)
|
||||
if settings.SQL_DEBUG:
|
||||
logger.warning('Group-group updates took %d queries for %d group-group relationships',
|
||||
@ -854,19 +854,19 @@ class Command(BaseCommand):
|
||||
host_names = all_host_names[offset2:(offset2 + self._batch_size)]
|
||||
db_hosts_qs = self.inventory.hosts.filter(name__in=host_names)
|
||||
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
||||
logger.info('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||
logger.debug('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
||||
self._batch_add_m2m(db_group.hosts, db_host)
|
||||
logger.info('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||
logger.debug('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||
all_instance_ids = sorted([h.instance_id for h in mem_group.hosts if h.instance_id])
|
||||
for offset2 in xrange(0, len(all_instance_ids), self._batch_size):
|
||||
instance_ids = all_instance_ids[offset2:(offset2 + self._batch_size)]
|
||||
db_hosts_qs = self.inventory.hosts.filter(instance_id__in=instance_ids)
|
||||
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
||||
logger.info('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||
logger.debug('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
||||
self._batch_add_m2m(db_group.hosts, db_host)
|
||||
logger.info('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||
logger.debug('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||
self._batch_add_m2m(db_group.hosts, flush=True)
|
||||
if settings.SQL_DEBUG:
|
||||
logger.warning('Group-host updates took %d queries for %d group-host relationships',
|
||||
|
||||
@ -6,6 +6,22 @@ from django.core.management.base import BaseCommand
|
||||
import six
|
||||
|
||||
|
||||
class Ungrouped(object):
|
||||
|
||||
name = 'ungrouped'
|
||||
policy_instance_percentage = None
|
||||
policy_instance_minimum = None
|
||||
controller = None
|
||||
|
||||
@property
|
||||
def instances(self):
|
||||
return Instance.objects.filter(rampart_groups__isnull=True)
|
||||
|
||||
@property
|
||||
def capacity(self):
|
||||
return sum([x.capacity for x in self.instances])
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""List instances from the Tower database
|
||||
"""
|
||||
@ -13,12 +29,28 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
super(Command, self).__init__()
|
||||
|
||||
for instance in Instance.objects.all():
|
||||
print(six.text_type(
|
||||
"hostname: {0.hostname}; created: {0.created}; "
|
||||
"heartbeat: {0.modified}; capacity: {0.capacity}").format(instance))
|
||||
for instance_group in InstanceGroup.objects.all():
|
||||
print(six.text_type(
|
||||
"Instance Group: {0.name}; created: {0.created}; "
|
||||
"capacity: {0.capacity}; members: {1}").format(instance_group,
|
||||
[x.hostname for x in instance_group.instances.all()]))
|
||||
groups = list(InstanceGroup.objects.all())
|
||||
ungrouped = Ungrouped()
|
||||
if len(ungrouped.instances):
|
||||
groups.append(ungrouped)
|
||||
|
||||
for instance_group in groups:
|
||||
fmt = '[{0.name} capacity={0.capacity}'
|
||||
if instance_group.policy_instance_percentage:
|
||||
fmt += ' policy={0.policy_instance_percentage}%'
|
||||
if instance_group.policy_instance_minimum:
|
||||
fmt += ' policy>={0.policy_instance_minimum}'
|
||||
if instance_group.controller:
|
||||
fmt += ' controller={0.controller.name}'
|
||||
print(six.text_type(fmt + ']').format(instance_group))
|
||||
for x in instance_group.instances.all():
|
||||
color = '\033[92m'
|
||||
if x.capacity == 0 or x.enabled is False:
|
||||
color = '\033[91m'
|
||||
fmt = '\t' + color + '{0.hostname} capacity={0.capacity} version={1}'
|
||||
if x.last_isolated_check:
|
||||
fmt += ' last_isolated_check="{0.last_isolated_check:%Y-%m-%d %H:%M:%S}"'
|
||||
if x.capacity:
|
||||
fmt += ' heartbeat="{0.modified:%Y-%m-%d %H:%M:%S}"'
|
||||
print(six.text_type(fmt + '\033[0m').format(x, x.version or '?'))
|
||||
print('')
|
||||
|
||||
@ -95,7 +95,7 @@ class ReplayJobEvents():
|
||||
raise RuntimeError("Job is of type {} and replay is not yet supported.".format(type(job)))
|
||||
sys.exit(1)
|
||||
|
||||
def run(self, job_id, speed=1.0, verbosity=0, skip=0):
|
||||
def run(self, job_id, speed=1.0, verbosity=0, skip_range=[]):
|
||||
stats = {
|
||||
'events_ontime': {
|
||||
'total': 0,
|
||||
@ -127,7 +127,7 @@ class ReplayJobEvents():
|
||||
|
||||
je_previous = None
|
||||
for n, je_current in enumerate(job_events):
|
||||
if n < skip:
|
||||
if je_current.counter in skip_range:
|
||||
continue
|
||||
|
||||
if not je_previous:
|
||||
@ -193,19 +193,29 @@ class Command(BaseCommand):
|
||||
|
||||
help = 'Replay job events over websockets ordered by created on date.'
|
||||
|
||||
def _parse_slice_range(self, slice_arg):
|
||||
slice_arg = tuple([int(n) for n in slice_arg.split(':')])
|
||||
slice_obj = slice(*slice_arg)
|
||||
|
||||
start = slice_obj.start or 0
|
||||
stop = slice_obj.stop or -1
|
||||
step = slice_obj.step or 1
|
||||
|
||||
return range(start, stop, step)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--job_id', dest='job_id', type=int, metavar='j',
|
||||
help='Id of the job to replay (job or adhoc)')
|
||||
parser.add_argument('--speed', dest='speed', type=int, metavar='s',
|
||||
help='Speedup factor.')
|
||||
parser.add_argument('--skip', dest='skip', type=int, metavar='k',
|
||||
help='Number of events to skip.')
|
||||
parser.add_argument('--skip-range', dest='skip_range', type=str, metavar='k',
|
||||
default='0:-1:1', help='Range of events to skip')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
job_id = options.get('job_id')
|
||||
speed = options.get('speed') or 1
|
||||
verbosity = options.get('verbosity') or 0
|
||||
skip = options.get('skip') or 0
|
||||
skip = self._parse_slice_range(options.get('skip_range'))
|
||||
|
||||
replayer = ReplayJobEvents()
|
||||
replayer.run(job_id, speed, verbosity, skip)
|
||||
|
||||
@ -64,15 +64,22 @@ class CallbackBrokerWorker(ConsumerMixin):
|
||||
return _handler
|
||||
|
||||
if use_workers:
|
||||
django_connection.close()
|
||||
django_cache.close()
|
||||
for idx in range(settings.JOB_EVENT_WORKERS):
|
||||
queue_actual = MPQueue(settings.JOB_EVENT_MAX_QUEUE_SIZE)
|
||||
w = Process(target=self.callback_worker, args=(queue_actual, idx,))
|
||||
w.start()
|
||||
if settings.DEBUG:
|
||||
logger.info('Started worker %s' % str(idx))
|
||||
logger.info('Starting worker %s' % str(idx))
|
||||
self.worker_queues.append([0, queue_actual, w])
|
||||
|
||||
# It's important to close these _right before_ we fork; we
|
||||
# don't want the forked processes to inherit the open sockets
|
||||
# for the DB and memcached connections (that way lies race
|
||||
# conditions)
|
||||
django_connection.close()
|
||||
django_cache.close()
|
||||
for _, _, w in self.worker_queues:
|
||||
w.start()
|
||||
|
||||
elif settings.DEBUG:
|
||||
logger.warn('Started callback receiver (no workers)')
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
@ -9,12 +11,15 @@ import time
|
||||
import cProfile
|
||||
import pstats
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db import IntegrityError, connection
|
||||
from django.http import HttpResponse
|
||||
from django.utils.functional import curry
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.apps import apps
|
||||
@ -128,8 +133,9 @@ class SessionTimeoutMiddleware(object):
|
||||
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)
|
||||
expiry = int(settings.SESSION_COOKIE_AGE)
|
||||
request.session.set_expiry(expiry)
|
||||
response['Session-Timeout'] = expiry
|
||||
return response
|
||||
|
||||
|
||||
@ -203,6 +209,56 @@ class URLModificationMiddleware(object):
|
||||
request.path_info = new_path
|
||||
|
||||
|
||||
class DeprecatedAuthTokenMiddleware(object):
|
||||
"""
|
||||
Used to emulate support for the old Auth Token endpoint to ease the
|
||||
transition to OAuth2.0. Specifically, this middleware:
|
||||
|
||||
1. Intercepts POST requests to `/api/v2/authtoken/` (which now no longer
|
||||
_actually_ exists in our urls.py)
|
||||
2. Rewrites `request.path` to `/api/v2/users/N/personal_tokens/`
|
||||
3. Detects the username and password in the request body (either in JSON,
|
||||
or form-encoded variables) and builds an appropriate HTTP_AUTHORIZATION
|
||||
Basic header
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
if re.match('^/api/v[12]/authtoken/?$', request.path):
|
||||
if request.method != 'POST':
|
||||
return HttpResponse('HTTP {} is not allowed.'.format(request.method), status=405)
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
except (ValueError, TypeError):
|
||||
payload = request.POST
|
||||
if 'username' not in payload or 'password' not in payload:
|
||||
return HttpResponse('Unable to login with provided credentials.', status=401)
|
||||
username = payload['username']
|
||||
password = payload['password']
|
||||
try:
|
||||
pk = User.objects.get(username=username).pk
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponse('Unable to login with provided credentials.', status=401)
|
||||
new_path = reverse('api:user_personal_token_list', kwargs={
|
||||
'pk': pk,
|
||||
'version': 'v2'
|
||||
})
|
||||
request._body = ''
|
||||
request.META['CONTENT_TYPE'] = 'application/json'
|
||||
request.path = request.path_info = new_path
|
||||
auth = ' '.join([
|
||||
'Basic',
|
||||
base64.b64encode(
|
||||
six.text_type('{}:{}').format(username, password)
|
||||
)
|
||||
])
|
||||
request.environ['HTTP_AUTHORIZATION'] = auth
|
||||
logger.warn(
|
||||
'The Auth Token API (/api/v2/authtoken/) is deprecated and will '
|
||||
'be replaced with OAuth2.0 in the next version of Ansible Tower '
|
||||
'(see /api/o/ for more details).'
|
||||
)
|
||||
|
||||
|
||||
class MigrationRanCheckMiddleware(object):
|
||||
|
||||
def process_request(self, request):
|
||||
|
||||
@ -157,7 +157,7 @@ class Migration(migrations.Migration):
|
||||
('status', models.CharField(default=b'pending', max_length=20, editable=False, choices=[(b'pending', 'Pending'), (b'successful', 'Successful'), (b'failed', 'Failed')])),
|
||||
('error', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('notifications_sent', models.IntegerField(default=0, editable=False)),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'rocketchat', 'Rocket.Chat'), (b'irc', 'IRC')])),
|
||||
('recipients', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('subject', models.TextField(default=b'', editable=False, blank=True)),
|
||||
('body', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
@ -174,7 +174,7 @@ class Migration(migrations.Migration):
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('name', models.CharField(unique=True, max_length=512)),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])),
|
||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'rocketchat', 'Rocket.Chat'), (b'irc', 'IRC')])),
|
||||
('notification_configuration', jsonfield.fields.JSONField(default=dict)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
|
||||
@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('inventory_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.InventoryUpdate')),
|
||||
('inventory_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_update_events', to='main.InventoryUpdate')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-pk',),
|
||||
@ -53,7 +53,7 @@ class Migration(migrations.Migration):
|
||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('project_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.ProjectUpdate')),
|
||||
('project_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='project_update_events', to='main.ProjectUpdate')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('pk',),
|
||||
@ -72,12 +72,24 @@ class Migration(migrations.Migration):
|
||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('system_job', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.SystemJob')),
|
||||
('system_job', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='system_job_events', to='main.SystemJob')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-pk',),
|
||||
},
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='inventoryupdateevent',
|
||||
index_together=set([('inventory_update', 'start_line'), ('inventory_update', 'uuid'), ('inventory_update', 'end_line')]),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='projectupdateevent',
|
||||
index_together=set([('project_update', 'event'), ('project_update', 'end_line'), ('project_update', 'start_line'), ('project_update', 'uuid')]),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='systemjobevent',
|
||||
index_together=set([('system_job', 'end_line'), ('system_job', 'uuid'), ('system_job', 'start_line')]),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='unifiedjob',
|
||||
name='result_stdout_file',
|
||||
|
||||
@ -64,12 +64,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='o_auth2_access_token',
|
||||
field=models.ManyToManyField(to='main.OAuth2AccessToken', blank=True, related_name='main_o_auth2_accesstoken'),
|
||||
field=models.ManyToManyField(to='main.OAuth2AccessToken', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
name='o_auth2_application',
|
||||
field=models.ManyToManyField(to='main.OAuth2Application', blank=True, related_name='main_o_auth2_application'),
|
||||
field=models.ManyToManyField(to='main.OAuth2Application', blank=True),
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
@ -16,6 +16,6 @@ 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. Must be a simple space-separated string with allowed scopes ['read', 'write']."),
|
||||
field=models.TextField(blank=True, default=b'write', help_text="Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']."),
|
||||
),
|
||||
]
|
||||
|
||||
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='oauth2accesstoken',
|
||||
name='modified',
|
||||
field=models.DateTimeField(editable=False),
|
||||
field=models.DateTimeField(editable=False, auto_now=True),
|
||||
),
|
||||
]
|
||||
|
||||
@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-16 16:46
|
||||
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 = [
|
||||
('main', '0047_v330_activitystream_instance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credential', u'model_name': 'credential'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credential', u'model_name': 'credential'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credentialtype',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credentialtype', u'model_name': 'credentialtype'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credentialtype',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credentialtype', u'model_name': 'credentialtype'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custominventoryscript',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'custominventoryscript', u'model_name': 'custominventoryscript'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='custominventoryscript',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'custominventoryscript', u'model_name': 'custominventoryscript'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'group', u'model_name': 'group'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='group',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'group', u'model_name': 'group'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='host',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'host', u'model_name': 'host'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='host',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'host', u'model_name': 'host'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'inventory', u'model_name': 'inventory'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'inventory', u'model_name': 'inventory'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='label',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'label', u'model_name': 'label'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='label',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'label', u'model_name': 'label'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationtemplate',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'notificationtemplate', u'model_name': 'notificationtemplate'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationtemplate',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'notificationtemplate', u'model_name': 'notificationtemplate'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'organization', u'model_name': 'organization'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'organization', u'model_name': 'organization'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'schedule', u'model_name': 'schedule'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'schedule', u'model_name': 'schedule'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'team', u'model_name': 'team'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'team', u'model_name': 'team'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unifiedjob',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjob', u'model_name': 'unifiedjob'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unifiedjob',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjob', u'model_name': 'unifiedjob'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjobtemplate', u'model_name': 'unifiedjobtemplate'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjobtemplate', u'model_name': 'unifiedjobtemplate'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-08-17 16:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0048_v330_django_created_modified_by_model_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='instance',
|
||||
name='capacity_adjustment',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('1'), max_digits=3, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
||||
@ -35,9 +35,9 @@ def sanitize_event_keys(kwargs, valid_keys):
|
||||
for key in [
|
||||
'play', 'role', 'task', 'playbook'
|
||||
]:
|
||||
if isinstance(kwargs.get(key), six.string_types):
|
||||
if len(kwargs[key]) > 1024:
|
||||
kwargs[key] = Truncator(kwargs[key]).chars(1024)
|
||||
if isinstance(kwargs.get('event_data', {}).get(key), six.string_types):
|
||||
if len(kwargs['event_data'][key]) > 1024:
|
||||
kwargs['event_data'][key] = Truncator(kwargs['event_data'][key]).chars(1024)
|
||||
|
||||
|
||||
def create_host_status_counts(event_data):
|
||||
|
||||
@ -6,6 +6,7 @@ import random
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models, connection
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
@ -81,6 +82,7 @@ class Instance(HasPolicyEditsMixin, BaseModel):
|
||||
default=Decimal(1.0),
|
||||
max_digits=3,
|
||||
decimal_places=2,
|
||||
validators=[MinValueValidator(0)]
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
|
||||
@ -1262,6 +1262,11 @@ class InventorySourceOptions(BaseModel):
|
||||
'Credentials of type machine, source control, insights and vault are '
|
||||
'disallowed for custom inventory sources.'
|
||||
)
|
||||
elif source == 'scm' and cred and cred.credential_type.kind in ('insights', 'vault'):
|
||||
return _(
|
||||
'Credentials of type insights and vault are '
|
||||
'disallowed for scm inventory sources.'
|
||||
)
|
||||
return None
|
||||
|
||||
def get_inventory_plugin_name(self):
|
||||
|
||||
@ -238,11 +238,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
|
||||
host_config_key = models.CharField(
|
||||
host_config_key = prevent_search(models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
default='',
|
||||
)
|
||||
))
|
||||
ask_diff_mode_on_launch = AskForField(
|
||||
blank=True,
|
||||
default=False,
|
||||
|
||||
@ -37,6 +37,7 @@ class SlackBackend(AWXBaseEmailBackend):
|
||||
if self.color:
|
||||
ret = connection.api_call("chat.postMessage",
|
||||
channel=r,
|
||||
as_user=True,
|
||||
attachments=[{
|
||||
"color": self.color,
|
||||
"text": m.subject
|
||||
|
||||
@ -76,7 +76,8 @@ class TaskManager():
|
||||
inventory_updates_qs = InventoryUpdate.objects.filter(
|
||||
status__in=status_list).exclude(source='file').prefetch_related('inventory_source', 'instance_group')
|
||||
inventory_updates = [i for i in inventory_updates_qs]
|
||||
project_updates = [p for p in ProjectUpdate.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||
# Notice the job_type='check': we want to prevent implicit project updates from blocking our jobs.
|
||||
project_updates = [p for p in ProjectUpdate.objects.filter(status__in=status_list, job_type='check').prefetch_related('instance_group')]
|
||||
system_jobs = [s for s in SystemJob.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||
ad_hoc_commands = [a for a in AdHocCommand.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||
workflow_jobs = [w for w in WorkflowJob.objects.filter(status__in=status_list)]
|
||||
@ -678,9 +679,9 @@ class TaskManager():
|
||||
return finished_wfjs
|
||||
|
||||
def schedule(self):
|
||||
with transaction.atomic():
|
||||
# Lock
|
||||
with advisory_lock('task_manager_lock', wait=False) as acquired:
|
||||
# Lock
|
||||
with advisory_lock('task_manager_lock', wait=False) as acquired:
|
||||
with transaction.atomic():
|
||||
if acquired is False:
|
||||
logger.debug("Not running scheduler, another task holds lock")
|
||||
return
|
||||
|
||||
@ -32,7 +32,7 @@ except Exception:
|
||||
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
|
||||
from celery.signals import celeryd_init, worker_shutdown
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -108,6 +108,31 @@ def log_celery_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
|
||||
@celeryd_init.connect
|
||||
def celery_startup(conf=None, **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 kwargs['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))
|
||||
|
||||
startup_logger = logging.getLogger('awx.main.tasks')
|
||||
startup_logger.info("Syncing Schedules")
|
||||
for sch in Schedule.objects.all():
|
||||
@ -147,9 +172,17 @@ def inform_cluster_of_shutdown(*args, **kwargs):
|
||||
|
||||
@shared_task(bind=True, queue=settings.CELERY_DEFAULT_QUEUE)
|
||||
def apply_cluster_membership_policies(self):
|
||||
started_waiting = time.time()
|
||||
with advisory_lock('cluster_policy_lock', wait=True):
|
||||
lock_time = time.time() - started_waiting
|
||||
if lock_time > 1.0:
|
||||
to_log = logger.info
|
||||
else:
|
||||
to_log = logger.debug
|
||||
to_log('Waited {} seconds to obtain lock name: cluster_policy_lock'.format(lock_time))
|
||||
started_compute = time.time()
|
||||
all_instances = list(Instance.objects.order_by('id'))
|
||||
all_groups = list(InstanceGroup.objects.all())
|
||||
all_groups = list(InstanceGroup.objects.prefetch_related('instances'))
|
||||
iso_hostnames = set([])
|
||||
for ig in all_groups:
|
||||
if ig.controller_id is not None:
|
||||
@ -159,28 +192,32 @@ def apply_cluster_membership_policies(self):
|
||||
total_instances = len(considered_instances)
|
||||
actual_groups = []
|
||||
actual_instances = []
|
||||
Group = namedtuple('Group', ['obj', 'instances'])
|
||||
Group = namedtuple('Group', ['obj', 'instances', 'prior_instances'])
|
||||
Node = namedtuple('Instance', ['obj', 'groups'])
|
||||
|
||||
# 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=[])
|
||||
group_actual = Group(obj=ig, instances=[], prior_instances=[
|
||||
instance.pk for instance in ig.instances.all() # obtained in prefetch
|
||||
])
|
||||
for hostname in ig.policy_instance_list:
|
||||
if hostname not in instance_hostnames_map:
|
||||
logger.info(six.text_type("Unknown instance {} in {} policy list").format(hostname, ig.name))
|
||||
continue
|
||||
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)
|
||||
# NOTE: arguable behavior: policy-list-group is not added to
|
||||
# instance's group count for consideration in minimum-policy rules
|
||||
if group_actual.instances:
|
||||
logger.info(six.text_type("Policy List, adding Instances {} to Group {}").format(group_actual.instances, ig.name))
|
||||
|
||||
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))
|
||||
logger.info('Committing instances to isolated group {}'.format(ig.name))
|
||||
ig.instances.set(group_actual.instances)
|
||||
|
||||
# Process Instance minimum policies next, since it represents a concrete lower bound to the
|
||||
@ -189,6 +226,7 @@ def apply_cluster_membership_policies(self):
|
||||
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)):
|
||||
policy_min_added = []
|
||||
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
|
||||
@ -196,12 +234,15 @@ def apply_cluster_membership_policies(self):
|
||||
# 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.instances.append(i.obj.id)
|
||||
i.groups.append(g.obj.id)
|
||||
policy_min_added.append(i.obj.id)
|
||||
if policy_min_added:
|
||||
logger.info(six.text_type("Policy minimum, adding Instances {} to Group {}").format(policy_min_added, g.obj.name))
|
||||
|
||||
# Finally, process instance policy percentages
|
||||
for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)):
|
||||
policy_per_added = []
|
||||
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
|
||||
@ -209,15 +250,34 @@ def apply_cluster_membership_policies(self):
|
||||
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)
|
||||
i.groups.append(g.obj.id)
|
||||
policy_per_added.append(i.obj.id)
|
||||
if policy_per_added:
|
||||
logger.info(six.text_type("Policy percentage, adding Instances {} to Group {}").format(policy_per_added, g.obj.name))
|
||||
|
||||
# Determine if any changes need to be made
|
||||
needs_change = False
|
||||
for g in actual_groups:
|
||||
if set(g.instances) != set(g.prior_instances):
|
||||
needs_change = True
|
||||
break
|
||||
if not needs_change:
|
||||
logger.info('Cluster policy no-op finished in {} seconds'.format(time.time() - started_compute))
|
||||
return
|
||||
|
||||
# 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)
|
||||
instances_to_add = set(g.instances) - set(g.prior_instances)
|
||||
instances_to_remove = set(g.prior_instances) - set(g.instances)
|
||||
if instances_to_add:
|
||||
logger.info('Adding instances {} to group {}'.format(list(instances_to_add), g.obj.name))
|
||||
g.obj.instances.add(*instances_to_add)
|
||||
if instances_to_remove:
|
||||
logger.info('Removing instances {} from group {}'.format(list(instances_to_remove), g.obj.name))
|
||||
g.obj.instances.remove(*instances_to_remove)
|
||||
logger.info('Cluster policy computation finished in {} seconds'.format(time.time() - started_compute))
|
||||
|
||||
|
||||
@shared_task(exchange='tower_broadcast_all', bind=True)
|
||||
@ -233,34 +293,6 @@ def handle_setting_changes(self, setting_keys):
|
||||
cache.delete_many(cache_keys)
|
||||
|
||||
|
||||
@celeryd_after_setup.connect
|
||||
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))
|
||||
|
||||
|
||||
@shared_task(queue=settings.CELERY_DEFAULT_QUEUE)
|
||||
def send_notifications(notification_list, job_id=None):
|
||||
if not isinstance(notification_list, list):
|
||||
@ -761,12 +793,12 @@ class BaseTask(Task):
|
||||
os.chmod(path, stat.S_IRUSR)
|
||||
return path
|
||||
|
||||
def add_ansible_venv(self, venv_path, env, add_awx_lib=True):
|
||||
def add_ansible_venv(self, venv_path, env, add_awx_lib=True, **kwargs):
|
||||
env['VIRTUAL_ENV'] = venv_path
|
||||
env['PATH'] = os.path.join(venv_path, "bin") + ":" + env['PATH']
|
||||
venv_libdir = os.path.join(venv_path, "lib")
|
||||
|
||||
if not os.path.exists(venv_libdir):
|
||||
if not kwargs.get('isolated', False) and not os.path.exists(venv_libdir):
|
||||
raise RuntimeError(
|
||||
'a valid Python virtualenv does not exist at {}'.format(venv_path)
|
||||
)
|
||||
@ -1179,7 +1211,7 @@ class RunJob(BaseTask):
|
||||
plugin_dirs.extend(settings.AWX_ANSIBLE_CALLBACK_PLUGINS)
|
||||
plugin_path = ':'.join(plugin_dirs)
|
||||
env = super(RunJob, self).build_env(job, **kwargs)
|
||||
env = self.add_ansible_venv(job.ansible_virtualenv_path, env, add_awx_lib=kwargs.get('isolated', False))
|
||||
env = self.add_ansible_venv(job.ansible_virtualenv_path, env, add_awx_lib=kwargs.get('isolated', False), **kwargs)
|
||||
# Set environment variables needed for inventory and job event
|
||||
# callbacks to work.
|
||||
env['JOB_ID'] = str(job.pk)
|
||||
@ -2129,8 +2161,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
elif src == 'scm':
|
||||
args.append(inventory_update.get_actual_source_path())
|
||||
elif src == 'custom':
|
||||
runpath = tempfile.mkdtemp(prefix='awx_inventory_', dir=settings.AWX_PROOT_BASE_PATH)
|
||||
handle, path = tempfile.mkstemp(dir=runpath)
|
||||
handle, path = tempfile.mkstemp(dir=kwargs['private_data_dir'])
|
||||
f = os.fdopen(handle, 'w')
|
||||
if inventory_update.source_script is None:
|
||||
raise RuntimeError('Inventory Script does not exist')
|
||||
@ -2139,7 +2170,6 @@ class RunInventoryUpdate(BaseTask):
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||
args.append(path)
|
||||
args.append("--custom")
|
||||
self.cleanup_paths.append(runpath)
|
||||
args.append('-v%d' % inventory_update.verbosity)
|
||||
if settings.DEBUG:
|
||||
args.append('--traceback')
|
||||
|
||||
@ -365,6 +365,116 @@ def test_inventory_source_vars_prohibition(post, inventory, admin_user):
|
||||
assert 'FOOBAR' in r.data['source_vars'][0]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestInventorySourceCredential:
|
||||
def test_need_cloud_credential(self, inventory, admin_user, post):
|
||||
"""Test that a cloud-based source requires credential"""
|
||||
r = post(
|
||||
url=reverse('api:inventory_source_list'),
|
||||
data={'inventory': inventory.pk, 'name': 'foo', 'source': 'openstack'},
|
||||
expect=400,
|
||||
user=admin_user
|
||||
)
|
||||
assert 'Credential is required for a cloud source' in r.data['credential'][0]
|
||||
|
||||
def test_ec2_no_credential(self, inventory, admin_user, post):
|
||||
"""Test that an ec2 inventory source can be added with no credential"""
|
||||
post(
|
||||
url=reverse('api:inventory_source_list'),
|
||||
data={'inventory': inventory.pk, 'name': 'fobar', 'source': 'ec2'},
|
||||
expect=201,
|
||||
user=admin_user
|
||||
)
|
||||
|
||||
def test_validating_credential_type(self, organization, inventory, admin_user, post):
|
||||
"""Test that cloud sources must use their respective credential type"""
|
||||
from awx.main.models.credential import Credential, CredentialType
|
||||
openstack = CredentialType.defaults['openstack']()
|
||||
openstack.save()
|
||||
os_cred = Credential.objects.create(
|
||||
credential_type=openstack, name='bar', organization=organization)
|
||||
r = post(
|
||||
url=reverse('api:inventory_source_list'),
|
||||
data={
|
||||
'inventory': inventory.pk, 'name': 'fobar', 'source': 'ec2',
|
||||
'credential': os_cred.pk
|
||||
},
|
||||
expect=400,
|
||||
user=admin_user
|
||||
)
|
||||
assert 'Cloud-based inventory sources (such as ec2)' in r.data['credential'][0]
|
||||
assert 'require credentials for the matching cloud service' in r.data['credential'][0]
|
||||
|
||||
def test_vault_credential_not_allowed(self, project, inventory, vault_credential, admin_user, post):
|
||||
"""Vault credentials cannot be associated via the deprecated field"""
|
||||
# TODO: when feature is added, add tests to use the related credentials
|
||||
# endpoint for multi-vault attachment
|
||||
r = post(
|
||||
url=reverse('api:inventory_source_list'),
|
||||
data={
|
||||
'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm',
|
||||
'source_project': project.pk, 'source_path': '',
|
||||
'credential': vault_credential.pk
|
||||
},
|
||||
expect=400,
|
||||
user=admin_user
|
||||
)
|
||||
assert 'Credentials of type insights and vault' in r.data['credential'][0]
|
||||
assert 'disallowed for scm inventory sources' in r.data['credential'][0]
|
||||
|
||||
def test_vault_credential_not_allowed_via_related(
|
||||
self, project, inventory, vault_credential, admin_user, post):
|
||||
"""Vault credentials cannot be associated via related endpoint"""
|
||||
inv_src = InventorySource.objects.create(
|
||||
inventory=inventory, name='foobar', source='scm',
|
||||
source_project=project, source_path=''
|
||||
)
|
||||
r = post(
|
||||
url=reverse('api:inventory_source_credentials_list', kwargs={'pk': inv_src.pk}),
|
||||
data={
|
||||
'id': vault_credential.pk
|
||||
},
|
||||
expect=400,
|
||||
user=admin_user
|
||||
)
|
||||
assert 'Credentials of type insights and vault' in r.data['msg']
|
||||
assert 'disallowed for scm inventory sources' in r.data['msg']
|
||||
|
||||
def test_credentials_relationship_mapping(self, project, inventory, organization, admin_user, post, patch):
|
||||
"""The credentials relationship is used to manage the cloud credential
|
||||
this test checks that replacement works"""
|
||||
from awx.main.models.credential import Credential, CredentialType
|
||||
openstack = CredentialType.defaults['openstack']()
|
||||
openstack.save()
|
||||
os_cred = Credential.objects.create(
|
||||
credential_type=openstack, name='bar', organization=organization)
|
||||
r = post(
|
||||
url=reverse('api:inventory_source_list'),
|
||||
data={
|
||||
'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm',
|
||||
'source_project': project.pk, 'source_path': '',
|
||||
'credential': os_cred.pk
|
||||
},
|
||||
expect=201,
|
||||
user=admin_user
|
||||
)
|
||||
aws = CredentialType.defaults['aws']()
|
||||
aws.save()
|
||||
aws_cred = Credential.objects.create(
|
||||
credential_type=aws, name='bar2', organization=organization)
|
||||
inv_src = InventorySource.objects.get(pk=r.data['id'])
|
||||
assert list(inv_src.credentials.values_list('id', flat=True)) == [os_cred.pk]
|
||||
patch(
|
||||
url=inv_src.get_absolute_url(),
|
||||
data={
|
||||
'credential': aws_cred.pk
|
||||
},
|
||||
expect=200,
|
||||
user=admin_user
|
||||
)
|
||||
assert list(inv_src.credentials.values_list('id', flat=True)) == [aws_cred.pk]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestControlledBySCM:
|
||||
'''
|
||||
|
||||
@ -5,7 +5,10 @@ import json
|
||||
from django.db import connection
|
||||
from django.test.utils import override_settings
|
||||
from django.test import Client
|
||||
from django.core.urlresolvers import resolve
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from awx.main.middleware import DeprecatedAuthTokenMiddleware
|
||||
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,
|
||||
@ -260,36 +263,6 @@ 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
|
||||
@ -314,3 +287,117 @@ def test_implicit_authorization(oauth_application, admin):
|
||||
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()
|
||||
|
||||
|
||||
@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
|
||||
)
|
||||
assert AccessToken.objects.count() == 1
|
||||
assert RefreshToken.objects.count() == 1
|
||||
token = AccessToken.objects.get(token=response.data['token'])
|
||||
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
||||
|
||||
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
|
||||
]))
|
||||
)
|
||||
assert RefreshToken.objects.filter(token=refresh_token).exists()
|
||||
original_refresh_token = RefreshToken.objects.get(token=refresh_token)
|
||||
assert token not in AccessToken.objects.all()
|
||||
assert AccessToken.objects.count() == 1
|
||||
# the same RefreshToken remains but is marked revoked
|
||||
assert RefreshToken.objects.count() == 2
|
||||
new_token = json.loads(response._container[0])['access_token']
|
||||
new_refresh_token = json.loads(response._container[0])['refresh_token']
|
||||
assert AccessToken.objects.filter(token=new_token).count() == 1
|
||||
# checks that RefreshTokens are rotated (new RefreshToken issued)
|
||||
assert RefreshToken.objects.filter(token=new_refresh_token).count() == 1
|
||||
assert original_refresh_token.revoked # is not None
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoke_access_then_refreshtoken(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
|
||||
|
||||
token.revoke()
|
||||
assert AccessToken.objects.count() == 0
|
||||
assert RefreshToken.objects.count() == 1
|
||||
assert not refresh_token.revoked
|
||||
|
||||
refresh_token.revoke()
|
||||
assert AccessToken.objects.count() == 0
|
||||
assert RefreshToken.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoke_refreshtoken(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
|
||||
)
|
||||
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
||||
assert AccessToken.objects.count() == 1
|
||||
assert RefreshToken.objects.count() == 1
|
||||
|
||||
refresh_token.revoke()
|
||||
assert AccessToken.objects.count() == 0
|
||||
# the same RefreshToken is recycled
|
||||
new_refresh_token = RefreshToken.objects.all().first()
|
||||
assert refresh_token == new_refresh_token
|
||||
assert new_refresh_token.revoked
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('fmt', ['json', 'multipart'])
|
||||
def test_deprecated_authtoken_support(alice, fmt):
|
||||
kwargs = {
|
||||
'data': {'username': 'alice', 'password': 'alice'},
|
||||
'format': fmt
|
||||
}
|
||||
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||
DeprecatedAuthTokenMiddleware().process_request(request)
|
||||
assert request.path == request.path_info == '/api/v2/users/{}/personal_tokens/'.format(alice.pk)
|
||||
view, view_args, view_kwargs = resolve(request.path)
|
||||
resp = view(request, *view_args, **view_kwargs)
|
||||
assert resp.status_code == 201
|
||||
assert 'token' in resp.data
|
||||
assert resp.data['refresh_token'] is None
|
||||
assert resp.data['scope'] == 'write'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deprecated_authtoken_invalid_username(alice):
|
||||
kwargs = {
|
||||
'data': {'username': 'nobody', 'password': 'nobody'},
|
||||
'format': 'json'
|
||||
}
|
||||
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||
resp = DeprecatedAuthTokenMiddleware().process_request(request)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_deprecated_authtoken_missing_credentials(alice):
|
||||
kwargs = {
|
||||
'data': {},
|
||||
'format': 'json'
|
||||
}
|
||||
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||
resp = DeprecatedAuthTokenMiddleware().process_request(request)
|
||||
assert resp.status_code == 401
|
||||
|
||||
@ -34,8 +34,17 @@ class TestOAuth2Application:
|
||||
client_type='confidential', authorization_grant_type='password', organization=organization
|
||||
)
|
||||
assert access.can_read(app) is can_access
|
||||
|
||||
|
||||
|
||||
def test_admin_only_can_read(self, user, organization):
|
||||
user = user('org-admin', False)
|
||||
organization.admin_role.members.add(user)
|
||||
access = OAuth2ApplicationAccess(user)
|
||||
app = Application.objects.create(
|
||||
name='test app for {}'.format(user.username), user=user,
|
||||
client_type='confidential', authorization_grant_type='password', organization=organization
|
||||
)
|
||||
assert access.can_read(app) is True
|
||||
|
||||
def test_app_activity_stream(self, org_admin, alice, organization):
|
||||
app = Application.objects.create(
|
||||
name='test app for {}'.format(org_admin.username), user=org_admin,
|
||||
|
||||
@ -53,9 +53,9 @@ def test_really_long_event_fields(field):
|
||||
with mock.patch.object(JobEvent, 'objects') as manager:
|
||||
JobEvent.create_from_data(**{
|
||||
'job_id': 123,
|
||||
field: 'X' * 4096
|
||||
'event_data': {field: 'X' * 4096}
|
||||
})
|
||||
manager.create.assert_called_with(**{
|
||||
'job_id': 123,
|
||||
field: 'X' * 1021 + '...'
|
||||
'event_data': {field: 'X' * 1021 + '...'}
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
from collections import namedtuple
|
||||
|
||||
# AWX
|
||||
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
||||
@ -44,8 +43,26 @@ def test_log_configurable_severity(level, expect, dummy_log_record):
|
||||
assert filter.filter(dummy_log_record) is expect
|
||||
|
||||
|
||||
Field = namedtuple('Field', 'name')
|
||||
Meta = namedtuple('Meta', 'fields')
|
||||
class Field(object):
|
||||
|
||||
def __init__(self, name, related_model=None, __prevent_search__=None):
|
||||
self.name = name
|
||||
self.related_model = related_model
|
||||
self.__prevent_search__ = __prevent_search__
|
||||
|
||||
|
||||
class Meta(object):
|
||||
|
||||
def __init__(self, fields):
|
||||
self._fields = {
|
||||
f.name: f for f in fields
|
||||
}
|
||||
self.object_name = 'Host'
|
||||
self.fields_map = {}
|
||||
self.fields = self._fields.values()
|
||||
|
||||
def get_field(self, f):
|
||||
return self._fields.get(f)
|
||||
|
||||
|
||||
class mockObjects:
|
||||
@ -53,15 +70,32 @@ class mockObjects:
|
||||
return Q(*args, **kwargs)
|
||||
|
||||
|
||||
class mockUser:
|
||||
def __init__(self):
|
||||
print("Host user created")
|
||||
self._meta = Meta(fields=[
|
||||
Field(name='password', __prevent_search__=True)
|
||||
])
|
||||
|
||||
|
||||
class mockHost:
|
||||
def __init__(self):
|
||||
print("Host mock created")
|
||||
self.objects = mockObjects()
|
||||
self._meta = Meta(fields=(Field(name='name'), Field(name='description')))
|
||||
fields = [
|
||||
Field(name='name'),
|
||||
Field(name='description'),
|
||||
Field(name='created_by', related_model=mockUser())
|
||||
]
|
||||
self._meta = Meta(fields=fields)
|
||||
|
||||
|
||||
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
||||
class TestSmartFilterQueryFromString():
|
||||
@mock.patch(
|
||||
'awx.api.filters.get_field_from_path',
|
||||
lambda model, path: (model, path) # disable field filtering, because a__b isn't a real Host field
|
||||
)
|
||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
|
||||
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
|
||||
@ -88,6 +122,16 @@ class TestSmartFilterQueryFromString():
|
||||
SmartFilter.query_from_string(filter_string)
|
||||
assert e.value.message == u"Invalid query " + filter_string
|
||||
|
||||
@pytest.mark.parametrize("filter_string", [
|
||||
'created_by__password__icontains=pbkdf2'
|
||||
'search=foo or created_by__password__icontains=pbkdf2',
|
||||
'created_by__password__icontains=pbkdf2 or search=foo',
|
||||
])
|
||||
def test_forbidden_filter_string(self, mock_get_host_model, filter_string):
|
||||
with pytest.raises(Exception) as e:
|
||||
SmartFilter.query_from_string(filter_string)
|
||||
"Filtering on password is not allowed." in str(e)
|
||||
|
||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||
(u'(a=abc\u1F5E3def)', Q(**{u"a": u"abc\u1F5E3def"})),
|
||||
(u'(ansible_facts__a=abc\u1F5E3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1F5E3def"}})),
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017 Ansible Tower by Red Hat
|
||||
# All Rights Reserved.
|
||||
|
||||
# python
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
# AWX
|
||||
from awx.main.utils.ha import (
|
||||
AWXCeleryRouter,
|
||||
)
|
||||
|
||||
|
||||
class TestAddRemoveCeleryWorkerQueues():
|
||||
@pytest.fixture
|
||||
def instance_generator(self, mocker):
|
||||
def fn(hostname='east-1'):
|
||||
groups=['east', 'west', 'north', 'south']
|
||||
instance = mocker.MagicMock()
|
||||
instance.hostname = hostname
|
||||
instance.rampart_groups = mocker.MagicMock()
|
||||
instance.rampart_groups.values_list = mocker.MagicMock(return_value=groups)
|
||||
|
||||
return instance
|
||||
return fn
|
||||
|
||||
@pytest.fixture
|
||||
def worker_queues_generator(self, mocker):
|
||||
def fn(queues=['east', 'west']):
|
||||
return [dict(name=n, alias='') for n in queues]
|
||||
return fn
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app(self, mocker):
|
||||
app = mocker.MagicMock()
|
||||
app.control = mocker.MagicMock()
|
||||
app.control.cancel_consumer = mocker.MagicMock()
|
||||
return app
|
||||
|
||||
|
||||
class TestUpdateCeleryWorkerRouter():
|
||||
|
||||
@pytest.mark.parametrize("is_controller,expected_routes", [
|
||||
(False, {
|
||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'}
|
||||
}),
|
||||
(True, {
|
||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.awx_isolated_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
}),
|
||||
])
|
||||
def test_update_celery_worker_routes(self, mocker, is_controller, expected_routes):
|
||||
def get_or_register():
|
||||
instance = mock.MagicMock()
|
||||
instance.hostname = 'east-1'
|
||||
instance.is_controller = mock.MagicMock(return_value=is_controller)
|
||||
return (False, instance)
|
||||
|
||||
with mock.patch('awx.main.models.Instance.objects.get_or_register', get_or_register):
|
||||
router = AWXCeleryRouter()
|
||||
|
||||
for k,v in expected_routes.iteritems():
|
||||
assert router.route_for_task(k) == v
|
||||
|
||||
@ -147,6 +147,10 @@ class SmartFilter(object):
|
||||
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:
|
||||
# detect loops and restrict access to sensitive fields
|
||||
# this import is intentional here to avoid a circular import
|
||||
from awx.api.filters import FieldLookupBackend
|
||||
FieldLookupBackend().get_field_from_lookup(Host, k)
|
||||
kwargs[k] = v
|
||||
self.result = Host.objects.filter(**kwargs)
|
||||
|
||||
|
||||
@ -3,21 +3,15 @@
|
||||
# Copyright (c) 2017 Ansible Tower by Red Hat
|
||||
# All Rights Reserved.
|
||||
|
||||
from awx.main.models import Instance
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class AWXCeleryRouter(object):
|
||||
def route_for_task(self, task, args=None, kwargs=None):
|
||||
(changed, instance) = Instance.objects.get_or_register()
|
||||
tasks = [
|
||||
'awx.main.tasks.cluster_node_heartbeat',
|
||||
'awx.main.tasks.purge_old_stdout_files',
|
||||
]
|
||||
isolated_tasks = [
|
||||
'awx.main.tasks.awx_isolated_heartbeat',
|
||||
]
|
||||
if task in tasks:
|
||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
||||
|
||||
if instance.is_controller() and task in isolated_tasks:
|
||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
||||
return {'queue': settings.CLUSTER_HOST_ID, 'routing_key': settings.CLUSTER_HOST_ID}
|
||||
|
||||
@ -261,6 +261,7 @@ MIDDLEWARE_CLASSES = ( # NOQA
|
||||
'awx.sso.middleware.SocialAuthMiddleware',
|
||||
'crum.CurrentRequestUserMiddleware',
|
||||
'awx.main.middleware.URLModificationMiddleware',
|
||||
'awx.main.middleware.DeprecatedAuthTokenMiddleware',
|
||||
'awx.main.middleware.SessionTimeoutMiddleware',
|
||||
)
|
||||
|
||||
@ -1177,16 +1178,13 @@ LOGGING = {
|
||||
'propagate': False
|
||||
},
|
||||
'awx.main.access': {
|
||||
'handlers': ['null'],
|
||||
'propagate': False,
|
||||
'level': 'INFO', # very verbose debug-level logs
|
||||
},
|
||||
'awx.main.signals': {
|
||||
'handlers': ['null'],
|
||||
'propagate': False,
|
||||
'level': 'INFO', # very verbose debug-level logs
|
||||
},
|
||||
'awx.api.permissions': {
|
||||
'handlers': ['null'],
|
||||
'propagate': False,
|
||||
'level': 'INFO', # very verbose debug-level logs
|
||||
},
|
||||
'awx.analytics': {
|
||||
'handlers': ['external_logger'],
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
@import 'portalMode/_index';
|
||||
@import 'output/_index';
|
||||
@import 'users/tokens/_index';
|
||||
|
||||
/** @define Popup Modal after create new token and applicaiton and save form */
|
||||
.PopupModal {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.PopupModal-label {
|
||||
font-weight: bold;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.PopupModal-value {
|
||||
width: 70%;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
function AddApplicationsController (models, $state, strings, $scope) {
|
||||
function AddApplicationsController (models, $state, strings, $scope, Alert, $filter) {
|
||||
const vm = this || {};
|
||||
|
||||
const { application, me, organization } = models;
|
||||
@ -60,6 +60,41 @@ function AddApplicationsController (models, $state, strings, $scope) {
|
||||
};
|
||||
|
||||
vm.form.onSaveSuccess = res => {
|
||||
if (res.data && res.data.client_id) {
|
||||
const name = res.data.name ?
|
||||
`<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.NAME_LABEL')}
|
||||
</div>
|
||||
<div class="PopupModal-value">
|
||||
${$filter('sanitize')(res.data.name)}
|
||||
</div>
|
||||
</div>` : '';
|
||||
const clientId = res.data.client_id ?
|
||||
`<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.CLIENT_ID_LABEL')}
|
||||
</div>
|
||||
<div class="PopupModal-value">
|
||||
${res.data.client_id}
|
||||
</div>
|
||||
</div>` : '';
|
||||
const clientSecret = res.data.client_secret ?
|
||||
`<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.CLIENT_SECRECT_LABEL')}
|
||||
</div>
|
||||
<div class="PopupModal-value">
|
||||
${res.data.client_secret}
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
Alert(strings.get('add.MODAL_HEADER'), `
|
||||
${name}
|
||||
${clientId}
|
||||
${clientSecret}
|
||||
`, null, null, null, null, null, true);
|
||||
}
|
||||
$state.go('applications.edit', { application_id: res.data.id }, { reload: true });
|
||||
};
|
||||
|
||||
@ -74,7 +109,9 @@ AddApplicationsController.$inject = [
|
||||
'resolvedModels',
|
||||
'$state',
|
||||
'ApplicationsStrings',
|
||||
'$scope'
|
||||
'$scope',
|
||||
'Alert',
|
||||
'$filter',
|
||||
];
|
||||
|
||||
export default AddApplicationsController;
|
||||
|
||||
@ -21,7 +21,11 @@ function ApplicationsStrings (BaseString) {
|
||||
};
|
||||
|
||||
ns.add = {
|
||||
PANEL_TITLE: t.s('NEW APPLICATION')
|
||||
PANEL_TITLE: t.s('NEW APPLICATION'),
|
||||
CLIENT_ID_LABEL: t.s('CLIENT ID'),
|
||||
CLIENT_SECRECT_LABEL: t.s('CLIENT SECRET'),
|
||||
MODAL_HEADER: t.s('APPLICATION INFORMATION'),
|
||||
NAME_LABEL: t.s('NAME'),
|
||||
};
|
||||
|
||||
ns.list = {
|
||||
|
||||
@ -38,6 +38,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIcon--md {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&-menuIcon--lg {
|
||||
font-size: 22px;
|
||||
line-height: 12px;
|
||||
@ -94,6 +104,10 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-line--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-event {
|
||||
.at-mixin-event();
|
||||
}
|
||||
@ -138,6 +152,10 @@
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
max-height: calc(100vh - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
&-borderHeader {
|
||||
|
||||
@ -13,9 +13,30 @@ 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_ANSI_COLORMAP = {
|
||||
0: '#000',
|
||||
1: '#A00',
|
||||
2: '#0A0',
|
||||
3: '#F0AD4E',
|
||||
4: '#00A',
|
||||
5: '#A0A',
|
||||
6: '#0AA',
|
||||
7: '#AAA',
|
||||
8: '#555',
|
||||
9: '#F55',
|
||||
10: '#5F5',
|
||||
11: '#FF5',
|
||||
12: '#55F',
|
||||
13: '#F5F',
|
||||
14: '#5FF',
|
||||
15: '#FFF'
|
||||
};
|
||||
export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||
export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable';
|
||||
export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast';
|
||||
export const OUTPUT_MAX_BUFFER_LENGTH = 1000;
|
||||
export const OUTPUT_MAX_LAG = 120;
|
||||
export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update'];
|
||||
export const OUTPUT_ORDER_BY = 'counter';
|
||||
export const OUTPUT_PAGE_CACHE = true;
|
||||
export const OUTPUT_PAGE_LIMIT = 5;
|
||||
@ -23,8 +44,8 @@ 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_SEARCH_FIELDS = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play', 'start_line', 'end_line'];
|
||||
export const OUTPUT_SEARCH_KEY_EXAMPLES = ['host_name:localhost', 'task:set', 'created:>=2000-01-01', 'start_line:>=9000'];
|
||||
export const OUTPUT_EVENT_LIMIT = OUTPUT_PAGE_LIMIT * OUTPUT_PAGE_SIZE;
|
||||
|
||||
export const WS_PREFIX = 'ws';
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import {
|
||||
EVENT_START_PLAY,
|
||||
EVENT_START_TASK,
|
||||
OUTPUT_ELEMENT_LAST,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
} from './constants';
|
||||
|
||||
@ -16,61 +17,21 @@ let scroll;
|
||||
let status;
|
||||
let slide;
|
||||
let stream;
|
||||
let page;
|
||||
|
||||
let vm;
|
||||
|
||||
const bufferState = [0, 0]; // [length, count]
|
||||
const listeners = [];
|
||||
const rx = [];
|
||||
let lockFrames = false;
|
||||
|
||||
function bufferInit () {
|
||||
rx.length = 0;
|
||||
|
||||
bufferState[0] = 0;
|
||||
bufferState[1] = 0;
|
||||
}
|
||||
|
||||
function bufferAdd (event) {
|
||||
rx.push(event);
|
||||
|
||||
bufferState[0] += 1;
|
||||
bufferState[1] += 1;
|
||||
|
||||
return bufferState[1];
|
||||
}
|
||||
|
||||
function bufferEmpty (min, max) {
|
||||
let count = 0;
|
||||
let removed = [];
|
||||
|
||||
for (let i = bufferState[0] - 1; i >= 0; i--) {
|
||||
if (rx[i].counter <= max) {
|
||||
removed = removed.concat(rx.splice(i, 1));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (lockFrames) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const popCount = events.length - render.getCapacity();
|
||||
|
||||
if (!vm.isFollowing && canStartFollowing()) {
|
||||
startFollowing();
|
||||
}
|
||||
@ -85,13 +46,13 @@ function onFrames (events) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.popBack(popCount)
|
||||
return render.popBack(popCount)
|
||||
.then(() => {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
}
|
||||
|
||||
return slide.pushFront(events);
|
||||
return render.pushFront(events);
|
||||
})
|
||||
.then(() => {
|
||||
if (vm.isFollowing) {
|
||||
@ -104,27 +65,44 @@ function onFrames (events) {
|
||||
});
|
||||
}
|
||||
|
||||
function first () {
|
||||
//
|
||||
// Menu Controls (Running)
|
||||
//
|
||||
|
||||
function firstRange () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
stopFollowing();
|
||||
lockFollow = true;
|
||||
|
||||
if (slide.isOnFirstPage()) {
|
||||
scroll.resetScrollPosition();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
stopFollowing();
|
||||
return render.clear()
|
||||
.then(() => slide.getFirst())
|
||||
.then(results => render.pushFront(results))
|
||||
.then(() => slide.getNext())
|
||||
.then(results => {
|
||||
const popCount = results.length - render.getCapacity();
|
||||
|
||||
return slide.getFirst()
|
||||
.then(() => {
|
||||
scroll.resetScrollPosition();
|
||||
return render.popBack(popCount)
|
||||
.then(() => render.pushFront(results));
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
lockFollow = false;
|
||||
});
|
||||
}
|
||||
|
||||
function next () {
|
||||
function nextRange () {
|
||||
if (vm.isFollowing) {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
@ -135,34 +113,49 @@ function next () {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
if (slide.getTailCounter() >= slide.getMaxCounter()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
return slide.getNext()
|
||||
.then(results => {
|
||||
const popCount = results.length - render.getCapacity();
|
||||
|
||||
return render.popBack(popCount)
|
||||
.then(() => render.pushFront(results));
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function previous () {
|
||||
function previousRange () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
stopFollowing();
|
||||
lockFrames = true;
|
||||
|
||||
stopFollowing();
|
||||
|
||||
const initialPosition = scroll.getScrollPosition();
|
||||
let initialPosition;
|
||||
let popHeight;
|
||||
|
||||
return slide.getPrevious()
|
||||
.then(popHeight => {
|
||||
.then(results => {
|
||||
const popCount = results.length - render.getCapacity();
|
||||
initialPosition = scroll.getScrollPosition();
|
||||
|
||||
return render.popFront(popCount)
|
||||
.then(() => {
|
||||
popHeight = scroll.getScrollHeight();
|
||||
|
||||
return render.pushBack(results);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
|
||||
|
||||
@ -171,10 +164,12 @@ function previous () {
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function last () {
|
||||
function lastRange () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
@ -182,16 +177,39 @@ function last () {
|
||||
scroll.pause();
|
||||
lockFrames = true;
|
||||
|
||||
return slide.getLast()
|
||||
return render.clear()
|
||||
.then(() => slide.getLast())
|
||||
.then(results => render.pushFront(results))
|
||||
.then(() => {
|
||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||
|
||||
scroll.scrollToBottom();
|
||||
lockFrames = false;
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
lockFrames = false;
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function menuLastRange () {
|
||||
if (vm.isFollowing) {
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
lockFollow = false;
|
||||
|
||||
return lastRange()
|
||||
.then(() => {
|
||||
startFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
@ -210,8 +228,7 @@ function canStartFollowing () {
|
||||
|
||||
if (followOnce && // one-time activation from top of first page
|
||||
scroll.isBeyondUpperThreshold() &&
|
||||
slide.getHeadCounter() === 1 &&
|
||||
slide.getTailCounter() >= OUTPUT_PAGE_SIZE) {
|
||||
slide.getTailCounter() - slide.getHeadCounter() >= OUTPUT_PAGE_SIZE) {
|
||||
followOnce = false;
|
||||
|
||||
return true;
|
||||
@ -234,27 +251,166 @@ function stopFollowing () {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.unlock();
|
||||
scroll.unhide();
|
||||
|
||||
vm.isFollowing = false;
|
||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||
}
|
||||
|
||||
//
|
||||
// Menu Controls (Page Mode)
|
||||
//
|
||||
|
||||
function firstPage () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return render.clear()
|
||||
.then(() => page.getFirst())
|
||||
.then(results => render.pushFront(results))
|
||||
.then(() => page.getNext())
|
||||
.then(results => {
|
||||
const popCount = page.trimHead();
|
||||
|
||||
return render.popBack(popCount)
|
||||
.then(() => render.pushFront(results));
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function lastPage () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return render.clear()
|
||||
.then(() => page.getLast())
|
||||
.then(results => render.pushBack(results))
|
||||
.then(() => page.getPrevious())
|
||||
.then(results => {
|
||||
const popCount = page.trimTail();
|
||||
|
||||
return render.popFront(popCount)
|
||||
.then(() => render.pushBack(results));
|
||||
})
|
||||
.then(() => {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function nextPage () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
return page.getNext()
|
||||
.then(results => {
|
||||
const popCount = page.trimHead();
|
||||
|
||||
return render.popBack(popCount)
|
||||
.then(() => render.pushFront(results));
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
});
|
||||
}
|
||||
|
||||
function previousPage () {
|
||||
if (scroll.isPaused()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
scroll.pause();
|
||||
|
||||
let initialPosition;
|
||||
let popHeight;
|
||||
|
||||
return page.getPrevious()
|
||||
.then(results => {
|
||||
const popCount = page.trimTail();
|
||||
initialPosition = scroll.getScrollPosition();
|
||||
|
||||
return render.popFront(popCount)
|
||||
.then(() => {
|
||||
popHeight = scroll.getScrollHeight();
|
||||
|
||||
return render.pushBack(results);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
const currentHeight = scroll.getScrollHeight();
|
||||
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.finally(() => {
|
||||
scroll.resume();
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Menu Controls
|
||||
//
|
||||
|
||||
function first () {
|
||||
if (vm.isProcessingFinished) {
|
||||
return firstPage();
|
||||
}
|
||||
|
||||
return firstRange();
|
||||
}
|
||||
|
||||
function last () {
|
||||
if (vm.isProcessingFinished) {
|
||||
return lastPage();
|
||||
}
|
||||
|
||||
return lastRange();
|
||||
}
|
||||
|
||||
function next () {
|
||||
if (vm.isProcessingFinished) {
|
||||
return nextPage();
|
||||
}
|
||||
|
||||
return nextRange();
|
||||
}
|
||||
|
||||
function previous () {
|
||||
if (vm.isProcessingFinished) {
|
||||
return previousPage();
|
||||
}
|
||||
|
||||
return previousRange();
|
||||
}
|
||||
|
||||
function menuLast () {
|
||||
if (vm.isFollowing) {
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
|
||||
return $q.resolve();
|
||||
if (vm.isProcessingFinished) {
|
||||
return lastPage();
|
||||
}
|
||||
|
||||
lockFollow = false;
|
||||
|
||||
if (slide.isOnLastPage()) {
|
||||
scroll.scrollToBottom();
|
||||
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return last();
|
||||
return menuLastRange();
|
||||
}
|
||||
|
||||
function down () {
|
||||
@ -269,64 +425,125 @@ function togglePanelExpand () {
|
||||
vm.isPanelExpanded = !vm.isPanelExpanded;
|
||||
}
|
||||
|
||||
function toggleMenuExpand () {
|
||||
//
|
||||
// Line Interaction
|
||||
//
|
||||
|
||||
const iconCollapsed = 'fa-angle-right';
|
||||
const iconExpanded = 'fa-angle-down';
|
||||
const iconSelector = '.at-Stdout-toggle > i';
|
||||
const lineCollapsed = 'hidden';
|
||||
|
||||
function toggleCollapseAll () {
|
||||
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);
|
||||
const records = Object.keys(render.records).map(key => render.records[key]);
|
||||
const plays = records.filter(({ name }) => name === EVENT_START_PLAY);
|
||||
const tasks = records.filter(({ name }) => name === EVENT_START_TASK);
|
||||
|
||||
// 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 orphanLines = records
|
||||
.filter(({ level }) => level === 3)
|
||||
.filter(({ parents }) => !records[parents[0]]);
|
||||
|
||||
const toggled = playRecords.concat(orphanTaskRecords)
|
||||
.map(({ uuid }) => getToggleElements(uuid))
|
||||
.filter(({ icon }) => icon.length > 0)
|
||||
.map(({ icon, lines }) => setExpanded(icon, lines, !vm.isMenuExpanded));
|
||||
const orphanLineParents = orphanLines
|
||||
.map(({ parents }) => ({ uuid: parents[0] }));
|
||||
|
||||
if (toggled.length > 0) {
|
||||
vm.isMenuExpanded = !vm.isMenuExpanded;
|
||||
plays.concat(tasks).forEach(({ uuid }) => {
|
||||
const icon = $(`#${uuid} ${iconSelector}`);
|
||||
|
||||
if (vm.isMenuCollapsed) {
|
||||
icon.removeClass(iconCollapsed);
|
||||
icon.addClass(iconExpanded);
|
||||
} else {
|
||||
icon.removeClass(iconExpanded);
|
||||
icon.addClass(iconCollapsed);
|
||||
}
|
||||
});
|
||||
|
||||
tasks.concat(orphanLineParents).forEach(({ uuid }) => {
|
||||
const lines = $(`.child-of-${uuid}`);
|
||||
|
||||
if (vm.isMenuCollapsed) {
|
||||
lines.removeClass(lineCollapsed);
|
||||
} else {
|
||||
lines.addClass(lineCollapsed);
|
||||
}
|
||||
});
|
||||
|
||||
vm.isMenuCollapsed = !vm.isMenuCollapsed;
|
||||
render.setCollapseAll(vm.isMenuCollapsed);
|
||||
}
|
||||
|
||||
function toggleCollapse (uuid) {
|
||||
if (scroll.isPaused()) return;
|
||||
|
||||
const record = render.records[uuid];
|
||||
|
||||
if (record.name === EVENT_START_PLAY) {
|
||||
togglePlayCollapse(uuid);
|
||||
}
|
||||
|
||||
if (record.name === EVENT_START_TASK) {
|
||||
toggleTaskCollapse(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLineExpand (uuid) {
|
||||
if (scroll.isPaused()) return;
|
||||
function togglePlayCollapse (uuid) {
|
||||
const record = render.records[uuid];
|
||||
const descendants = record.children || [];
|
||||
|
||||
const { icon, lines } = getToggleElements(uuid);
|
||||
const isExpanded = icon.hasClass('fa-angle-down');
|
||||
const icon = $(`#${uuid} ${iconSelector}`);
|
||||
const lines = $(`.child-of-${uuid}`);
|
||||
const taskIcons = $(`#${descendants.join(', #')}`).find(iconSelector);
|
||||
|
||||
setExpanded(icon, lines, !isExpanded);
|
||||
const isCollapsed = icon.hasClass(iconCollapsed);
|
||||
|
||||
vm.isMenuExpanded = !isExpanded;
|
||||
if (isCollapsed) {
|
||||
icon.removeClass(iconCollapsed);
|
||||
icon.addClass(iconExpanded);
|
||||
|
||||
taskIcons.removeClass(iconExpanded);
|
||||
taskIcons.addClass(iconCollapsed);
|
||||
lines.removeClass(lineCollapsed);
|
||||
|
||||
descendants
|
||||
.map(item => $(`.child-of-${item}`))
|
||||
.forEach(line => line.addClass(lineCollapsed));
|
||||
} else {
|
||||
icon.removeClass(iconExpanded);
|
||||
icon.addClass(iconCollapsed);
|
||||
|
||||
taskIcons.removeClass(iconExpanded);
|
||||
taskIcons.addClass(iconCollapsed);
|
||||
|
||||
lines.addClass(lineCollapsed);
|
||||
}
|
||||
|
||||
descendants
|
||||
.map(item => render.records[item])
|
||||
.filter(({ name }) => name === EVENT_START_TASK)
|
||||
.forEach(rec => { render.records[rec.uuid].isCollapsed = true; });
|
||||
|
||||
render.records[uuid].isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
||||
function getToggleElements (uuid) {
|
||||
const record = render.record[uuid];
|
||||
function toggleTaskCollapse (uuid) {
|
||||
const icon = $(`#${uuid} ${iconSelector}`);
|
||||
const lines = $(`.child-of-${uuid}`);
|
||||
|
||||
const iconSelector = '.at-Stdout-toggle > i';
|
||||
const additionalSelector = `#${(record.children || []).join(', #')}`;
|
||||
const isCollapsed = icon.hasClass(iconCollapsed);
|
||||
|
||||
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');
|
||||
if (isCollapsed) {
|
||||
icon.removeClass(iconCollapsed);
|
||||
icon.addClass(iconExpanded);
|
||||
lines.removeClass(lineCollapsed);
|
||||
} else {
|
||||
icon.removeClass('fa-angle-down');
|
||||
icon.addClass('fa-angle-right');
|
||||
lines.addClass('hidden');
|
||||
icon.removeClass(iconExpanded);
|
||||
icon.addClass(iconCollapsed);
|
||||
lines.addClass(lineCollapsed);
|
||||
}
|
||||
|
||||
render.records[uuid].isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
||||
function compile (html) {
|
||||
@ -337,6 +554,60 @@ function showHostDetails (id, uuid) {
|
||||
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
||||
}
|
||||
|
||||
function showMissingEvents (uuid) {
|
||||
const record = render.records[uuid];
|
||||
|
||||
const min = Math.min(...record.counters);
|
||||
const max = Math.min(Math.max(...record.counters), min + OUTPUT_PAGE_SIZE);
|
||||
|
||||
const selector = `#${uuid}`;
|
||||
const clicked = $(selector);
|
||||
|
||||
return resource.events.getRange([min, max])
|
||||
.then(results => {
|
||||
const counters = results.map(({ counter }) => counter);
|
||||
|
||||
for (let i = min; i <= max; i++) {
|
||||
if (counters.indexOf(i) < 0) {
|
||||
results = results.filter(({ counter }) => counter < i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let lines = 0;
|
||||
let untrusted = '';
|
||||
|
||||
for (let i = 0; i <= results.length - 1; i++) {
|
||||
const { html, count } = render.transformEvent(results[i]);
|
||||
|
||||
lines += count;
|
||||
untrusted += html;
|
||||
|
||||
const shifted = render.records[uuid].counters.shift();
|
||||
delete render.uuids[shifted];
|
||||
}
|
||||
|
||||
const trusted = render.trustHtml(untrusted);
|
||||
const elements = angular.element(trusted);
|
||||
|
||||
return render
|
||||
.requestAnimationFrame(() => {
|
||||
elements.insertBefore(clicked);
|
||||
|
||||
if (render.records[uuid].counters.length === 0) {
|
||||
clicked.remove();
|
||||
delete render.records[uuid];
|
||||
}
|
||||
})
|
||||
.then(() => render.compile(elements))
|
||||
.then(() => lines);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Event Handling
|
||||
//
|
||||
|
||||
let streaming;
|
||||
function stopListening () {
|
||||
streaming = null;
|
||||
@ -361,7 +632,7 @@ function handleJobEvent (data) {
|
||||
streaming = streaming || resource.events
|
||||
.getRange([Math.max(1, data.counter - 50), data.counter + 50])
|
||||
.then(results => {
|
||||
results = results.concat(data);
|
||||
results.push(data);
|
||||
|
||||
const counters = results.map(({ counter }) => counter);
|
||||
const min = Math.min(...counters);
|
||||
@ -379,12 +650,13 @@ function handleJobEvent (data) {
|
||||
results = results.filter(({ counter }) => counter > maxMissing);
|
||||
}
|
||||
|
||||
stream.setMissingCounterThreshold(max + 1);
|
||||
results.forEach(item => {
|
||||
stream.pushJobEvent(item);
|
||||
status.pushJobEvent(item);
|
||||
});
|
||||
|
||||
stream.setMissingCounterThreshold(min);
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
@ -406,12 +678,36 @@ function handleSummaryEvent (data) {
|
||||
stream.setFinalCounter(data.final_counter);
|
||||
}
|
||||
|
||||
//
|
||||
// Search
|
||||
//
|
||||
|
||||
function reloadState (params) {
|
||||
params.isPanelExpanded = vm.isPanelExpanded;
|
||||
|
||||
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
|
||||
}
|
||||
|
||||
//
|
||||
// Debug Mode
|
||||
//
|
||||
|
||||
function clear () {
|
||||
stopListening();
|
||||
render.clear();
|
||||
|
||||
followOnce = true;
|
||||
lockFollow = false;
|
||||
lockFrames = false;
|
||||
|
||||
stream.bufferInit();
|
||||
status.init(resource);
|
||||
slide.init(resource.events, render);
|
||||
status.subscribe(data => { vm.status = data.status; });
|
||||
|
||||
startListening();
|
||||
}
|
||||
|
||||
function OutputIndexController (
|
||||
_$compile_,
|
||||
_$q_,
|
||||
@ -428,7 +724,8 @@ function OutputIndexController (
|
||||
strings,
|
||||
$stateParams,
|
||||
) {
|
||||
const { isPanelExpanded } = $stateParams;
|
||||
const { isPanelExpanded, _debug } = $stateParams;
|
||||
const isProcessingFinished = !_debug && _resource_.model.get('event_processing_finished');
|
||||
|
||||
$compile = _$compile_;
|
||||
$q = _$q_;
|
||||
@ -440,7 +737,8 @@ function OutputIndexController (
|
||||
render = _render_;
|
||||
status = _status_;
|
||||
stream = _stream_;
|
||||
slide = resource.model.get('event_processing_finished') ? _page_ : _slide_;
|
||||
slide = _slide_;
|
||||
page = _page_;
|
||||
|
||||
vm = this || {};
|
||||
|
||||
@ -451,24 +749,27 @@ function OutputIndexController (
|
||||
vm.resource = resource;
|
||||
vm.reloadState = reloadState;
|
||||
vm.isPanelExpanded = isPanelExpanded;
|
||||
vm.isProcessingFinished = isProcessingFinished;
|
||||
vm.togglePanelExpand = togglePanelExpand;
|
||||
|
||||
// Stdout Navigation
|
||||
vm.menu = { last: menuLast, first, down, up };
|
||||
vm.isMenuExpanded = true;
|
||||
vm.menu = { last: menuLast, first, down, up, clear };
|
||||
vm.isMenuCollapsed = false;
|
||||
vm.isFollowing = false;
|
||||
vm.toggleMenuExpand = toggleMenuExpand;
|
||||
vm.toggleLineExpand = toggleLineExpand;
|
||||
vm.toggleCollapseAll = toggleCollapseAll;
|
||||
vm.toggleCollapse = toggleCollapse;
|
||||
vm.showHostDetails = showHostDetails;
|
||||
vm.showMissingEvents = showMissingEvents;
|
||||
vm.toggleLineEnabled = resource.model.get('type') === 'job';
|
||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||
vm.debug = _debug;
|
||||
|
||||
render.requestAnimationFrame(() => {
|
||||
bufferInit();
|
||||
render.init({ compile, toggles: vm.toggleLineEnabled });
|
||||
|
||||
status.init(resource);
|
||||
slide.init(render, resource.events, scroll);
|
||||
render.init({ compile, toggles: vm.toggleLineEnabled });
|
||||
page.init(resource.events);
|
||||
slide.init(resource.events, render);
|
||||
|
||||
scroll.init({
|
||||
next,
|
||||
@ -482,10 +783,29 @@ function OutputIndexController (
|
||||
},
|
||||
});
|
||||
|
||||
let showFollowTip = true;
|
||||
const rates = [];
|
||||
stream.init({
|
||||
bufferAdd,
|
||||
bufferEmpty,
|
||||
onFrames,
|
||||
onFrameRate (rate) {
|
||||
rates.push(rate);
|
||||
rates.splice(0, rates.length - 5);
|
||||
|
||||
if (rates.every(value => value === 1)) {
|
||||
scroll.unlock();
|
||||
scroll.unhide();
|
||||
}
|
||||
|
||||
if (rate > 1 && vm.isFollowing) {
|
||||
scroll.lock();
|
||||
scroll.hide();
|
||||
|
||||
if (showFollowTip) {
|
||||
showFollowTip = false;
|
||||
$(OUTPUT_ELEMENT_LAST).trigger('mouseenter');
|
||||
}
|
||||
}
|
||||
},
|
||||
onStop () {
|
||||
lockFollow = true;
|
||||
stopFollowing();
|
||||
@ -493,11 +813,12 @@ function OutputIndexController (
|
||||
status.updateStats();
|
||||
status.dispatch();
|
||||
status.sync();
|
||||
scroll.stop();
|
||||
scroll.unlock();
|
||||
scroll.unhide();
|
||||
}
|
||||
});
|
||||
|
||||
if (resource.model.get('event_processing_finished')) {
|
||||
if (isProcessingFinished) {
|
||||
followOnce = false;
|
||||
lockFollow = true;
|
||||
lockFrames = true;
|
||||
@ -511,8 +832,21 @@ function OutputIndexController (
|
||||
startListening();
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
return render.clear();
|
||||
}
|
||||
|
||||
return last();
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
stopListening();
|
||||
|
||||
render.clear();
|
||||
render.el.remove();
|
||||
slide.clear();
|
||||
stream.bufferInit();
|
||||
});
|
||||
}
|
||||
|
||||
OutputIndexController.$inject = [
|
||||
|
||||
@ -82,7 +82,7 @@ function resolveResource (
|
||||
order_by: OUTPUT_ORDER_BY,
|
||||
};
|
||||
|
||||
if (job_event_search) { // eslint-disable-line camelcase
|
||||
if (job_event_search) {
|
||||
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
||||
Object.assign(params, query);
|
||||
}
|
||||
@ -173,7 +173,7 @@ function JobsRun ($stateRegistry, $filter, strings) {
|
||||
const sanitize = $filter('sanitize');
|
||||
|
||||
const state = {
|
||||
url: '/:type/:id?job_event_search',
|
||||
url: '/:type/:id?job_event_search?_debug',
|
||||
name: 'output',
|
||||
parent,
|
||||
ncyBreadcrumb,
|
||||
|
||||
@ -21,13 +21,14 @@
|
||||
reload="vm.reloadState">
|
||||
</at-job-search>
|
||||
<div class="at-Stdout-menuTop">
|
||||
<div class="pull-left" ng-click="vm.toggleMenuExpand()">
|
||||
<div class="pull-left" ng-click="vm.toggleCollapseAll()">
|
||||
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
|
||||
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
|
||||
ng-class="{ 'fa-minus': !vm.isMenuCollapsed, 'fa-plus': vm.isMenuCollapsed }"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-click="vm.menu.last()">
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||
ng-class="{ 'at-Stdout-menuIcon--active': vm.isFollowing }"
|
||||
id="atStdoutMenuLast"
|
||||
data-placement="top"
|
||||
data-trigger="hover"
|
||||
data-tip-watch="vm.followTooltip"
|
||||
@ -46,6 +47,9 @@
|
||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"
|
||||
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_UP') }}"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-if="vm.debug" ng-click="vm.menu.clear()">
|
||||
<i class="at-Stdout-menuIcon--md fa fa-undo"></i>
|
||||
</div>
|
||||
<div class="at-u-clear"></div>
|
||||
</div>
|
||||
<div class="at-Stdout-container">
|
||||
|
||||
@ -2,244 +2,153 @@
|
||||
import { OUTPUT_PAGE_LIMIT } from './constants';
|
||||
|
||||
function PageService ($q) {
|
||||
this.init = (storage, api, { getScrollHeight }) => {
|
||||
const { prepend, append, shift, pop, deleteRecord } = storage;
|
||||
const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api;
|
||||
|
||||
this.init = ({ getPage, getFirst, getLast, getLastPageNumber }) => {
|
||||
this.api = {
|
||||
getPage,
|
||||
getFirst,
|
||||
getLast,
|
||||
getLastPageNumber,
|
||||
getMaxCounter,
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
prepend,
|
||||
append,
|
||||
shift,
|
||||
pop,
|
||||
deleteRecord,
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
getScrollHeight,
|
||||
};
|
||||
|
||||
this.records = {};
|
||||
this.uuids = {};
|
||||
|
||||
this.state = {
|
||||
head: 0,
|
||||
tail: 0,
|
||||
};
|
||||
|
||||
this.chain = $q.resolve();
|
||||
};
|
||||
|
||||
this.pushFront = (results, key) => {
|
||||
if (!results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return this.storage.append(results)
|
||||
.then(() => {
|
||||
const tail = key || ++this.state.tail;
|
||||
|
||||
this.records[tail] = {};
|
||||
results.forEach(({ counter, start_line, end_line, uuid }) => {
|
||||
this.records[tail][counter] = { start_line, end_line };
|
||||
this.uuids[counter] = uuid;
|
||||
});
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.pushBack = (results, key) => {
|
||||
if (!results) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
return this.storage.prepend(results)
|
||||
.then(() => {
|
||||
const head = key || --this.state.head;
|
||||
|
||||
this.records[head] = {};
|
||||
results.forEach(({ counter, start_line, end_line, uuid }) => {
|
||||
this.records[head][counter] = { start_line, end_line };
|
||||
this.uuids[counter] = uuid;
|
||||
});
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.popBack = () => {
|
||||
if (this.getRecordCount() === 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
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.pages = {};
|
||||
this.state = { head: 0, tail: 0 };
|
||||
};
|
||||
|
||||
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;
|
||||
if (number < 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
if (number > lastPageNumber) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getPage(number))
|
||||
.then(events => this.pushFront(events))
|
||||
.then(() => $q.resolve(popHeight));
|
||||
let promise;
|
||||
|
||||
return this.chain;
|
||||
if (this.pages[number]) {
|
||||
promise = $q.resolve(this.pages[number]);
|
||||
} else {
|
||||
promise = this.api.getPage(number);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(results => {
|
||||
if (results.length <= 0) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
this.state.tail = number;
|
||||
this.pages[number] = results;
|
||||
|
||||
return $q.resolve(results);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
if (number < 1) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
if (number > this.api.getLastPageNumber()) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
this.chain = this.chain
|
||||
.then(() => this.api.getPage(number))
|
||||
.then(events => this.pushBack(events))
|
||||
.then(() => $q.resolve(popHeight));
|
||||
let promise;
|
||||
|
||||
return this.chain;
|
||||
if (this.pages[number]) {
|
||||
promise = $q.resolve(this.pages[number]);
|
||||
} else {
|
||||
promise = this.api.getPage(number);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(results => {
|
||||
if (results.length <= 0) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
this.state.head = number;
|
||||
this.pages[number] = results;
|
||||
|
||||
return $q.resolve(results);
|
||||
});
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const count = this.getRecordCount();
|
||||
this.getLast = () => this.api.getLast()
|
||||
.then(results => {
|
||||
if (results.length <= 0) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
for (let i = 0; i <= count; ++i) {
|
||||
this.chain = this.chain.then(() => this.popBack());
|
||||
}
|
||||
const number = this.api.getLastPageNumber();
|
||||
|
||||
return this.chain;
|
||||
};
|
||||
this.state.head = number;
|
||||
this.state.tail = number;
|
||||
this.pages[number] = results;
|
||||
|
||||
this.getLast = () => this.clear()
|
||||
.then(() => this.api.getLast())
|
||||
.then(events => {
|
||||
const lastPage = this.api.getLastPageNumber();
|
||||
return $q.resolve(results);
|
||||
});
|
||||
|
||||
this.state.head = lastPage;
|
||||
this.state.tail = lastPage;
|
||||
this.getFirst = () => this.api.getFirst()
|
||||
.then(results => {
|
||||
if (results.length <= 0) {
|
||||
return $q.resolve([]);
|
||||
}
|
||||
|
||||
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;
|
||||
this.pages[1] = results;
|
||||
|
||||
return this.pushBack(events, 1);
|
||||
})
|
||||
.then(() => this.getNext());
|
||||
return $q.resolve(results);
|
||||
});
|
||||
|
||||
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();
|
||||
this.trimTail = () => {
|
||||
const { tail, head } = this.state;
|
||||
let popCount = 0;
|
||||
|
||||
for (let i = tail; i > head; i--) {
|
||||
if (!this.isOverCapacity()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.pages[i]) {
|
||||
popCount += this.pages[i].length;
|
||||
}
|
||||
|
||||
delete this.pages[i];
|
||||
|
||||
this.state.tail--;
|
||||
}
|
||||
|
||||
return popCount;
|
||||
};
|
||||
|
||||
this.trimHead = () => {
|
||||
const { head, tail } = this.state;
|
||||
let popCount = 0;
|
||||
|
||||
for (let i = head; i < tail; i++) {
|
||||
if (!this.isOverCapacity()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.pages[i]) {
|
||||
popCount += this.pages[i].length;
|
||||
}
|
||||
|
||||
delete this.pages[i];
|
||||
|
||||
this.state.head++;
|
||||
}
|
||||
|
||||
return popCount;
|
||||
};
|
||||
|
||||
this.isOverCapacity = () => this.state.tail - this.state.head > OUTPUT_PAGE_LIMIT;
|
||||
}
|
||||
|
||||
PageService.$inject = ['$q'];
|
||||
|
||||
@ -3,9 +3,12 @@ import Entities from 'html-entities';
|
||||
|
||||
import {
|
||||
EVENT_START_PLAY,
|
||||
EVENT_START_PLAYBOOK,
|
||||
EVENT_STATS_PLAY,
|
||||
EVENT_START_TASK,
|
||||
OUTPUT_ANSI_COLORMAP,
|
||||
OUTPUT_ELEMENT_TBODY,
|
||||
OUTPUT_EVENT_LIMIT,
|
||||
} from './constants';
|
||||
|
||||
const EVENT_GROUPS = [
|
||||
@ -19,7 +22,7 @@ const TIME_EVENTS = [
|
||||
EVENT_STATS_PLAY,
|
||||
];
|
||||
|
||||
const ansi = new Ansi();
|
||||
const ansi = new Ansi({ stream: true, colors: OUTPUT_ANSI_COLORMAP });
|
||||
const entities = new Entities.AllHtmlEntities();
|
||||
|
||||
// https://github.com/chalk/ansi-regex
|
||||
@ -33,98 +36,243 @@ const hasAnsi = input => re.test(input);
|
||||
|
||||
function JobRenderService ($q, $sce, $window) {
|
||||
this.init = ({ compile, toggles }) => {
|
||||
this.parent = null;
|
||||
this.record = {};
|
||||
this.el = $(OUTPUT_ELEMENT_TBODY);
|
||||
this.hooks = { compile };
|
||||
this.el = $(OUTPUT_ELEMENT_TBODY);
|
||||
this.parent = null;
|
||||
|
||||
this.createToggles = toggles;
|
||||
this.state = {
|
||||
head: 0,
|
||||
tail: 0,
|
||||
collapseAll: false,
|
||||
toggleMode: toggles,
|
||||
};
|
||||
|
||||
this.records = {};
|
||||
this.uuids = {};
|
||||
};
|
||||
|
||||
this.sortByLineNumber = (a, b) => {
|
||||
if (a.start_line > b.start_line) {
|
||||
this.setCollapseAll = value => {
|
||||
this.state.collapseAll = value;
|
||||
Object.keys(this.records).forEach(key => {
|
||||
this.records[key].isCollapsed = value;
|
||||
});
|
||||
};
|
||||
|
||||
this.sortByCounter = (a, b) => {
|
||||
if (a.counter > b.counter) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.start_line < b.start_line) {
|
||||
if (a.counter < b.counter) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
this.transformEventGroup = events => {
|
||||
//
|
||||
// Event Data Transformation / HTML Building
|
||||
//
|
||||
|
||||
this.appendEventGroup = events => {
|
||||
let lines = 0;
|
||||
let html = '';
|
||||
|
||||
events.sort(this.sortByLineNumber);
|
||||
events.sort(this.sortByCounter);
|
||||
|
||||
for (let i = 0; i < events.length; ++i) {
|
||||
const line = this.transformEvent(events[i]);
|
||||
html += line.html;
|
||||
lines += line.count;
|
||||
for (let i = 0; i <= events.length - 1; i++) {
|
||||
const current = events[i];
|
||||
|
||||
if (this.state.tail && current.counter !== this.state.tail + 1) {
|
||||
const missing = this.appendMissingEventGroup(current);
|
||||
|
||||
html += missing.html;
|
||||
lines += missing.count;
|
||||
}
|
||||
|
||||
const eventLines = this.transformEvent(current);
|
||||
|
||||
html += eventLines.html;
|
||||
lines += eventLines.count;
|
||||
}
|
||||
|
||||
return { html, lines };
|
||||
};
|
||||
|
||||
this.transformEvent = event => {
|
||||
if (this.record[event.uuid]) {
|
||||
this.appendMissingEventGroup = event => {
|
||||
const tailUUID = this.uuids[this.state.tail];
|
||||
const tailRecord = this.records[tailUUID];
|
||||
|
||||
if (!tailRecord) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
if (!event || !event.stdout) {
|
||||
let uuid;
|
||||
|
||||
if (tailRecord.isMissing) {
|
||||
uuid = tailUUID;
|
||||
} else {
|
||||
uuid = `${event.counter}-${tailUUID}`;
|
||||
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
|
||||
}
|
||||
|
||||
for (let i = this.state.tail + 1; i < event.counter; i++) {
|
||||
this.records[uuid].counters.push(i);
|
||||
this.uuids[i] = uuid;
|
||||
}
|
||||
|
||||
if (tailRecord.isMissing) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
if (tailRecord.end === event.start_line) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const html = this.buildRowHTML(this.records[uuid]);
|
||||
const count = 1;
|
||||
|
||||
return { html, count };
|
||||
};
|
||||
|
||||
this.prependEventGroup = events => {
|
||||
let lines = 0;
|
||||
let html = '';
|
||||
|
||||
events.sort(this.sortByCounter);
|
||||
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const current = events[i];
|
||||
|
||||
if (this.state.head && current.counter !== this.state.head - 1) {
|
||||
const missing = this.prependMissingEventGroup(current);
|
||||
|
||||
html = missing.html + html;
|
||||
lines += missing.count;
|
||||
}
|
||||
|
||||
const eventLines = this.transformEvent(current);
|
||||
|
||||
html = eventLines.html + html;
|
||||
lines += eventLines.count;
|
||||
}
|
||||
|
||||
return { html, lines };
|
||||
};
|
||||
|
||||
this.prependMissingEventGroup = event => {
|
||||
const headUUID = this.uuids[this.state.head];
|
||||
const headRecord = this.records[headUUID];
|
||||
|
||||
if (!headRecord) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
let uuid;
|
||||
|
||||
if (headRecord.isMissing) {
|
||||
uuid = headUUID;
|
||||
} else {
|
||||
uuid = `${headUUID}-${event.counter}`;
|
||||
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
|
||||
}
|
||||
|
||||
for (let i = this.state.head - 1; i > event.counter; i--) {
|
||||
this.records[uuid].counters.unshift(i);
|
||||
this.uuids[i] = uuid;
|
||||
}
|
||||
|
||||
if (headRecord.isMissing) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
if (event.end_line === headRecord.start) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const html = this.buildRowHTML(this.records[uuid]);
|
||||
const count = 1;
|
||||
|
||||
return { html, count };
|
||||
};
|
||||
|
||||
this.transformEvent = event => {
|
||||
if (!event || event.stdout === null || event.stdout === undefined) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
if (event.uuid && this.records[event.uuid]) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const stdout = this.sanitize(event.stdout);
|
||||
const lines = stdout.split('\r\n');
|
||||
const record = this.createRecord(event, lines);
|
||||
|
||||
if (event.event === EVENT_START_PLAYBOOK) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let count = lines.length;
|
||||
let ln = event.start_line;
|
||||
|
||||
const current = this.createRecord(ln, lines, event);
|
||||
|
||||
const html = lines.reduce((concat, line, i) => {
|
||||
for (let i = 0; i <= lines.length - 1; i++) {
|
||||
ln++;
|
||||
|
||||
const line = lines[i];
|
||||
const isLastLine = i === lines.length - 1;
|
||||
|
||||
let row = this.createRow(current, ln, line);
|
||||
let row = this.buildRowHTML(record, ln, line);
|
||||
|
||||
if (current && current.isTruncated && isLastLine) {
|
||||
row += this.createRow(current);
|
||||
if (record && record.isTruncated && isLastLine) {
|
||||
row += this.buildRowHTML(record);
|
||||
count++;
|
||||
}
|
||||
|
||||
return `${concat}${row}`;
|
||||
}, '');
|
||||
html += row;
|
||||
}
|
||||
|
||||
if (this.records[event.uuid]) {
|
||||
this.records[event.uuid].lineCount = count;
|
||||
}
|
||||
|
||||
return { html, count };
|
||||
};
|
||||
|
||||
this.isHostEvent = (event) => {
|
||||
if (typeof event.host === 'number') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type === 'project_update_event' &&
|
||||
event.event !== 'runner_on_skipped' &&
|
||||
event.event_data.host) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.createRecord = (ln, lines, event) => {
|
||||
if (!event.uuid) {
|
||||
this.createRecord = (event, lines) => {
|
||||
if (!event.counter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = {
|
||||
if (!this.state.head || event.counter < this.state.head) {
|
||||
this.state.head = event.counter;
|
||||
}
|
||||
|
||||
if (!this.state.tail || event.counter > this.state.tail) {
|
||||
this.state.tail = event.counter;
|
||||
}
|
||||
|
||||
if (!event.uuid) {
|
||||
this.uuids[event.counter] = event.counter;
|
||||
this.records[event.counter] = { counters: [event.counter], lineCount: lines.length };
|
||||
|
||||
return this.records[event.counter];
|
||||
}
|
||||
|
||||
let isHost = false;
|
||||
if (typeof event.host === 'number') {
|
||||
isHost = true;
|
||||
} else if (event.type === 'project_update_event' &&
|
||||
event.event !== 'runner_on_skipped' &&
|
||||
event.event_data.host) {
|
||||
isHost = true;
|
||||
}
|
||||
|
||||
const record = {
|
||||
isHost,
|
||||
id: event.id,
|
||||
line: ln + 1,
|
||||
line: event.start_line + 1,
|
||||
name: event.event,
|
||||
uuid: event.uuid,
|
||||
level: event.event_level,
|
||||
@ -132,50 +280,49 @@ function JobRenderService ($q, $sce, $window) {
|
||||
end: event.end_line,
|
||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||
lineCount: lines.length,
|
||||
isHost: this.isHostEvent(event),
|
||||
isCollapsed: this.state.collapseAll,
|
||||
counters: [event.counter],
|
||||
};
|
||||
|
||||
if (event.parent_uuid) {
|
||||
info.parents = this.getParentEvents(event.parent_uuid);
|
||||
record.parents = this.getParentEvents(event.parent_uuid);
|
||||
if (this.records[event.parent_uuid]) {
|
||||
record.isCollapsed = this.records[event.parent_uuid].isCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.isTruncated) {
|
||||
info.truncatedAt = event.start_line + lines.length;
|
||||
if (record.isTruncated) {
|
||||
record.truncatedAt = event.start_line + lines.length;
|
||||
}
|
||||
|
||||
if (EVENT_GROUPS.includes(event.event)) {
|
||||
info.isParent = true;
|
||||
record.isParent = true;
|
||||
|
||||
if (event.event_level === 1) {
|
||||
this.parent = event.uuid;
|
||||
}
|
||||
|
||||
if (event.parent_uuid) {
|
||||
if (this.record[event.parent_uuid]) {
|
||||
if (this.record[event.parent_uuid].children &&
|
||||
!this.record[event.parent_uuid].children.includes(event.uuid)) {
|
||||
this.record[event.parent_uuid].children.push(event.uuid);
|
||||
if (this.records[event.parent_uuid]) {
|
||||
if (this.records[event.parent_uuid].children &&
|
||||
!this.records[event.parent_uuid].children.includes(event.uuid)) {
|
||||
this.records[event.parent_uuid].children.push(event.uuid);
|
||||
} else {
|
||||
this.record[event.parent_uuid].children = [event.uuid];
|
||||
this.records[event.parent_uuid].children = [event.uuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TIME_EVENTS.includes(event.event)) {
|
||||
info.time = this.getTimestamp(event.created);
|
||||
info.line++;
|
||||
record.time = this.getTimestamp(event.created);
|
||||
record.line++;
|
||||
}
|
||||
|
||||
this.record[event.uuid] = info;
|
||||
this.records[event.uuid] = record;
|
||||
this.uuids[event.counter] = event.uuid;
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
this.getRecord = uuid => this.record[uuid];
|
||||
|
||||
this.deleteRecord = uuid => {
|
||||
delete this.record[uuid];
|
||||
return record;
|
||||
};
|
||||
|
||||
this.getParentEvents = (uuid, list) => {
|
||||
@ -183,42 +330,56 @@ function JobRenderService ($q, $sce, $window) {
|
||||
// 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);
|
||||
if (this.records[uuid] && this.records[uuid].parents) {
|
||||
list = list.concat(this.records[uuid].parents);
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
this.createRow = (current, ln, content) => {
|
||||
this.buildRowHTML = (record, ln, content) => {
|
||||
let id = '';
|
||||
let icon = '';
|
||||
let timestamp = '';
|
||||
let tdToggle = '';
|
||||
let tdEvent = '';
|
||||
let classList = '';
|
||||
|
||||
if (record.isMissing) {
|
||||
return `<div id="${record.uuid}" class="at-Stdout-row">
|
||||
<div class="at-Stdout-toggle"></div>
|
||||
<div class="at-Stdout-line at-Stdout-line--clickable" ng-click="vm.showMissingEvents('${record.uuid}')">...</div></div>`;
|
||||
}
|
||||
|
||||
content = content || '';
|
||||
|
||||
if (hasAnsi(content)) {
|
||||
content = ansi.toHtml(content);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
if (this.createToggles && current.isParent && current.line === ln) {
|
||||
id = current.uuid;
|
||||
tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleLineExpand('${id}')"><i class="fa fa-angle-down can-toggle"></i></div>`;
|
||||
if (record) {
|
||||
if (this.state.toggleMode && record.isParent && record.line === ln) {
|
||||
id = record.uuid;
|
||||
|
||||
if (record.isCollapsed) {
|
||||
icon = 'fa-angle-right';
|
||||
} else {
|
||||
icon = 'fa-angle-down';
|
||||
}
|
||||
|
||||
tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleCollapse('${id}')"><i class="fa ${icon} can-toggle"></i></div>`;
|
||||
}
|
||||
|
||||
if (current.isHost) {
|
||||
tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}', '${current.uuid}')"><span ng-non-bindable>${content}</span></div>`;
|
||||
if (record.isHost) {
|
||||
tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${record.id}', '${record.uuid}')"><span ng-non-bindable>${content}</span></div>`;
|
||||
}
|
||||
|
||||
if (current.time && current.line === ln) {
|
||||
timestamp = `<span>${current.time}</span>`;
|
||||
if (record.time && record.line === ln) {
|
||||
timestamp = `<span>${record.time}</span>`;
|
||||
}
|
||||
|
||||
if (current.parents) {
|
||||
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||
if (record.parents) {
|
||||
classList = record.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,6 +395,12 @@ function JobRenderService ($q, $sce, $window) {
|
||||
ln = '...';
|
||||
}
|
||||
|
||||
if (record && record.isCollapsed) {
|
||||
if (record.level === 3 || record.level === 0) {
|
||||
classList += ' hidden';
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div id="${id}" class="at-Stdout-row ${classList}">
|
||||
${tdToggle}
|
||||
@ -252,6 +419,10 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return `${hour}:${minute}:${second}`;
|
||||
};
|
||||
|
||||
//
|
||||
// Element Operations
|
||||
//
|
||||
|
||||
this.remove = elements => this.requestAnimationFrame(() => elements.remove());
|
||||
|
||||
this.requestAnimationFrame = fn => $q(resolve => {
|
||||
@ -270,19 +441,25 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return this.requestAnimationFrame();
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
const elements = this.el.children();
|
||||
this.removeAll = () => {
|
||||
const elements = this.el.contents();
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.shift = lines => {
|
||||
const elements = this.el.children().slice(0, lines);
|
||||
// We multiply by two here under the assumption that one element and one text node
|
||||
// is generated for each line of output.
|
||||
const count = 2 * lines;
|
||||
const elements = this.el.contents().slice(0, count);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
|
||||
this.pop = lines => {
|
||||
const elements = this.el.children().slice(-lines);
|
||||
// We multiply by two here under the assumption that one element and one text node
|
||||
// is generated for each line of output.
|
||||
const count = 2 * lines;
|
||||
const elements = this.el.contents().slice(-count);
|
||||
|
||||
return this.remove(elements);
|
||||
};
|
||||
@ -292,7 +469,7 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const result = this.transformEventGroup(events);
|
||||
const result = this.prependEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
const newElements = angular.element(html);
|
||||
@ -307,7 +484,7 @@ function JobRenderService ($q, $sce, $window) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const result = this.transformEventGroup(events);
|
||||
const result = this.appendEventGroup(events);
|
||||
const html = this.trustHtml(result.html);
|
||||
|
||||
const newElements = angular.element(html);
|
||||
@ -318,8 +495,110 @@ function JobRenderService ($q, $sce, $window) {
|
||||
};
|
||||
|
||||
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
||||
|
||||
this.sanitize = html => entities.encode(html);
|
||||
|
||||
//
|
||||
// Event Counter Methods - External code should prefer these.
|
||||
//
|
||||
|
||||
this.clear = () => this.removeAll()
|
||||
.then(() => {
|
||||
const head = this.getHeadCounter();
|
||||
const tail = this.getTailCounter();
|
||||
|
||||
for (let i = head; i <= tail; ++i) {
|
||||
const uuid = this.uuids[i];
|
||||
|
||||
if (uuid) {
|
||||
delete this.records[uuid];
|
||||
delete this.uuids[i];
|
||||
}
|
||||
}
|
||||
|
||||
this.state.head = 0;
|
||||
this.state.tail = 0;
|
||||
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
this.pushFront = events => {
|
||||
const tail = this.getTailCounter();
|
||||
|
||||
return this.append(events.filter(({ counter }) => counter > tail));
|
||||
};
|
||||
|
||||
this.pushBack = events => {
|
||||
const head = this.getHeadCounter();
|
||||
const tail = this.getTailCounter();
|
||||
|
||||
return this.prepend(events.filter(({ counter }) => counter < head || counter > tail));
|
||||
};
|
||||
|
||||
this.popFront = count => {
|
||||
if (!count || count <= 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const max = this.state.tail;
|
||||
const min = max - count;
|
||||
|
||||
let lines = 0;
|
||||
|
||||
for (let i = max; i >= min; --i) {
|
||||
const uuid = this.uuids[i];
|
||||
|
||||
if (!uuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.records[uuid].counters.pop();
|
||||
delete this.uuids[i];
|
||||
|
||||
if (this.records[uuid].counters.length === 0) {
|
||||
lines += this.records[uuid].lineCount;
|
||||
|
||||
delete this.records[uuid];
|
||||
this.state.tail--;
|
||||
}
|
||||
}
|
||||
|
||||
return this.pop(lines);
|
||||
};
|
||||
|
||||
this.popBack = count => {
|
||||
if (!count || count <= 0) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const min = this.state.head;
|
||||
const max = min + count;
|
||||
|
||||
let lines = 0;
|
||||
|
||||
for (let i = min; i <= max; ++i) {
|
||||
const uuid = this.uuids[i];
|
||||
|
||||
if (!uuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.records[uuid].counters.shift();
|
||||
delete this.uuids[i];
|
||||
|
||||
if (this.records[uuid].counters.length === 0) {
|
||||
lines += this.records[uuid].lineCount;
|
||||
|
||||
delete this.records[uuid];
|
||||
this.state.head++;
|
||||
}
|
||||
}
|
||||
|
||||
return this.shift(lines);
|
||||
};
|
||||
|
||||
this.getHeadCounter = () => this.state.head;
|
||||
this.getTailCounter = () => this.state.tail;
|
||||
this.getCapacity = () => OUTPUT_EVENT_LIMIT - (this.getTailCounter() - this.getHeadCounter());
|
||||
}
|
||||
|
||||
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
||||
|
||||
@ -5,8 +5,6 @@ import {
|
||||
OUTPUT_SCROLL_THRESHOLD,
|
||||
} from './constants';
|
||||
|
||||
const MAX_THRASH = 20;
|
||||
|
||||
function JobScrollService ($q, $timeout) {
|
||||
this.init = ({ next, previous, onThresholdLeave }) => {
|
||||
this.el = $(OUTPUT_ELEMENT_CONTAINER);
|
||||
@ -33,7 +31,6 @@ function JobScrollService ($q, $timeout) {
|
||||
paused: false,
|
||||
locked: false,
|
||||
hover: false,
|
||||
running: true,
|
||||
thrash: 0,
|
||||
};
|
||||
|
||||
@ -44,13 +41,6 @@ function JobScrollService ($q, $timeout) {
|
||||
|
||||
this.onMouseEnter = () => {
|
||||
this.state.hover = true;
|
||||
|
||||
if (this.state.thrash >= MAX_THRASH) {
|
||||
this.state.thrash = MAX_THRASH - 1;
|
||||
}
|
||||
|
||||
this.unlock();
|
||||
this.unhide();
|
||||
};
|
||||
|
||||
this.onMouseLeave = () => {
|
||||
@ -62,23 +52,6 @@ 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;
|
||||
}
|
||||
@ -195,16 +168,6 @@ function JobScrollService ($q, $timeout) {
|
||||
this.setScrollPosition(this.getScrollHeight());
|
||||
};
|
||||
|
||||
this.start = () => {
|
||||
this.state.running = true;
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
this.unlock();
|
||||
this.unhide();
|
||||
this.state.running = false;
|
||||
};
|
||||
|
||||
this.lock = () => {
|
||||
this.state.locked = true;
|
||||
};
|
||||
@ -256,7 +219,6 @@ function JobScrollService ($q, $timeout) {
|
||||
};
|
||||
|
||||
this.isPaused = () => this.state.paused;
|
||||
this.isRunning = () => this.state.running;
|
||||
this.isLocked = () => this.state.locked;
|
||||
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||
}
|
||||
|
||||
@ -1,42 +1,12 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import {
|
||||
API_MAX_PAGE_SIZE,
|
||||
OUTPUT_EVENT_LIMIT,
|
||||
OUTPUT_MAX_BUFFER_LENGTH,
|
||||
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.init = ({ getRange, getFirst, getLast, getMaxCounter }, storage) => {
|
||||
const { getHeadCounter, getTailCounter } = storage;
|
||||
|
||||
this.api = {
|
||||
getRange,
|
||||
@ -46,32 +16,20 @@ function SlidingWindowService ($q) {
|
||||
};
|
||||
|
||||
this.storage = {
|
||||
clear,
|
||||
prepend,
|
||||
append,
|
||||
shift,
|
||||
pop,
|
||||
getRecord,
|
||||
deleteRecord,
|
||||
getHeadCounter,
|
||||
getTailCounter,
|
||||
};
|
||||
|
||||
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.cache = {
|
||||
first: null
|
||||
};
|
||||
};
|
||||
|
||||
this.getBoundedRange = range => {
|
||||
@ -92,273 +50,46 @@ function SlidingWindowService ($q) {
|
||||
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;
|
||||
return this.api.getRange(next);
|
||||
};
|
||||
|
||||
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;
|
||||
return this.api.getRange(previous);
|
||||
};
|
||||
|
||||
this.getFirst = () => {
|
||||
this.chain = this.chain
|
||||
.then(() => this.clear())
|
||||
.then(() => {
|
||||
if (this.cache.first) {
|
||||
return $q.resolve(this.cache.first);
|
||||
}
|
||||
if (this.cache.first) {
|
||||
return $q.resolve(this.cache.first);
|
||||
}
|
||||
|
||||
return this.api.getFirst();
|
||||
})
|
||||
return this.api.getFirst()
|
||||
.then(events => {
|
||||
if (events.length === OUTPUT_PAGE_SIZE) {
|
||||
this.cache.first = events;
|
||||
}
|
||||
|
||||
return this.pushFront(events);
|
||||
return $q.resolve(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);
|
||||
}
|
||||
this.getLast = () => 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;
|
||||
};
|
||||
return this.api.getLast();
|
||||
});
|
||||
|
||||
this.pushFrames = events => {
|
||||
const head = this.getHeadCounter();
|
||||
const tail = this.getTailCounter();
|
||||
const frames = this.buffer.events.concat(events);
|
||||
const [head, tail] = this.getRange();
|
||||
|
||||
let min;
|
||||
let max;
|
||||
@ -367,7 +98,7 @@ function SlidingWindowService ($q) {
|
||||
for (let i = frames.length - 1; i >= 0; i--) {
|
||||
count++;
|
||||
|
||||
if (count > API_MAX_PAGE_SIZE) {
|
||||
if (count > OUTPUT_MAX_BUFFER_LENGTH) {
|
||||
frames.splice(i, 1);
|
||||
|
||||
count--;
|
||||
@ -388,27 +119,41 @@ function SlidingWindowService ($q) {
|
||||
this.buffer.max = max;
|
||||
this.buffer.count = count;
|
||||
|
||||
if (min >= head && min <= tail + 1) {
|
||||
return frames.filter(({ counter }) => counter > tail);
|
||||
if (tail - head === 0) {
|
||||
return frames;
|
||||
}
|
||||
|
||||
return [];
|
||||
return frames.filter(({ counter }) => counter > tail);
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
this.buffer.events.length = 0;
|
||||
this.buffer.min = 0;
|
||||
this.buffer.max = 0;
|
||||
this.buffer.count = 0;
|
||||
};
|
||||
|
||||
this.getFrames = () => $q.resolve(this.buffer.events);
|
||||
|
||||
this.getMaxCounter = () => {
|
||||
if (this.buffer.min) {
|
||||
return this.buffer.min;
|
||||
if (this.buffer.max && this.buffer.max > 1) {
|
||||
return this.buffer.max;
|
||||
}
|
||||
|
||||
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();
|
||||
this.isOnLastPage = () => {
|
||||
if (this.buffer.min) {
|
||||
return this.getTailCounter() >= this.buffer.min - 1;
|
||||
}
|
||||
|
||||
return this.getTailCounter() >= this.getMaxCounter() - OUTPUT_PAGE_SIZE;
|
||||
};
|
||||
|
||||
this.isOnFirstPage = () => this.getHeadCounter() === 1;
|
||||
this.getTailCounter = () => this.storage.getTailCounter();
|
||||
this.getHeadCounter = () => this.storage.getHeadCounter();
|
||||
}
|
||||
|
||||
SlidingWindowService.$inject = ['$q'];
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { OUTPUT_NO_COUNT_JOB_TYPES } from './constants';
|
||||
|
||||
const templateUrl = require('~features/output/stats.partial.html');
|
||||
|
||||
let vm;
|
||||
@ -21,6 +23,7 @@ function JobStatsController (strings, { subscribe }) {
|
||||
};
|
||||
|
||||
vm.$onInit = () => {
|
||||
vm.hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(vm.resource.model.get('type'));
|
||||
vm.download = vm.resource.model.get('related.stdout');
|
||||
vm.tooltips.toggleExpand = vm.expanded ?
|
||||
strings.get('tooltips.COLLAPSE_OUTPUT') :
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<!-- todo: styling, markup, css etc. - disposition according to project lib conventions -->
|
||||
<div class="at-u-floatRight">
|
||||
<span class="at-Panel-label">plays</span>
|
||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.plays || 0 }}</span>
|
||||
<span ng-show="!vm.hideCounts" class="at-Panel-label">plays</span>
|
||||
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.plays || 0 }}</span>
|
||||
|
||||
<span class="at-Panel-label">tasks</span>
|
||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.tasks || 0 }}</span>
|
||||
<span ng-show="!vm.hideCounts" class="at-Panel-label">tasks</span>
|
||||
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.tasks || 0 }}</span>
|
||||
|
||||
<span class="at-Panel-label">{{:: vm.strings.get('stats.HOSTS')}}</span>
|
||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.hosts || 1 }}</span>
|
||||
<span ng-show="!vm.hideCounts" class="at-Panel-label">{{:: vm.strings.get('stats.HOSTS')}}</span>
|
||||
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.hosts || 1 }}</span>
|
||||
|
||||
<span class="at-Panel-label">{{:: vm.strings.get('stats.ELAPSED') }}</span>
|
||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">
|
||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">
|
||||
{{ (vm.elapsed * 1000 || 0) | duration: "hh:mm:ss"}}
|
||||
</span>
|
||||
|
||||
|
||||
@ -1,36 +1,49 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import {
|
||||
EVENT_STATS_PLAY,
|
||||
OUTPUT_MAX_BUFFER_LENGTH,
|
||||
OUTPUT_MAX_LAG,
|
||||
OUTPUT_PAGE_SIZE,
|
||||
OUTPUT_EVENT_LIMIT,
|
||||
} from './constants';
|
||||
|
||||
const rx = [];
|
||||
|
||||
function OutputStream ($q) {
|
||||
this.init = ({ bufferAdd, bufferEmpty, onFrames, onStop }) => {
|
||||
this.init = ({ onFrames, onFrameRate, onStop }) => {
|
||||
this.hooks = {
|
||||
bufferAdd,
|
||||
bufferEmpty,
|
||||
onFrames,
|
||||
onFrameRate,
|
||||
onStop,
|
||||
};
|
||||
|
||||
this.bufferInit();
|
||||
};
|
||||
|
||||
this.bufferInit = () => {
|
||||
rx.length = 0;
|
||||
|
||||
this.counters = {
|
||||
used: [],
|
||||
ready: [],
|
||||
min: 1,
|
||||
max: 0,
|
||||
max: -1,
|
||||
ready: -1,
|
||||
final: null,
|
||||
used: [],
|
||||
missing: [],
|
||||
total: 0,
|
||||
length: 0,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
ending: false,
|
||||
ended: false,
|
||||
overflow: false,
|
||||
};
|
||||
|
||||
this.lag = 0;
|
||||
this.chain = $q.resolve();
|
||||
|
||||
this.factors = this.calcFactors(OUTPUT_PAGE_SIZE);
|
||||
this.factors = this.calcFactors(OUTPUT_EVENT_LIMIT);
|
||||
this.setFramesPerRender();
|
||||
};
|
||||
|
||||
@ -53,6 +66,7 @@ function OutputStream ($q) {
|
||||
const boundedIndex = Math.min(this.factors.length - 1, index);
|
||||
|
||||
this.framesPerRender = this.factors[boundedIndex];
|
||||
this.hooks.onFrameRate(this.framesPerRender);
|
||||
};
|
||||
|
||||
this.setMissingCounterThreshold = counter => {
|
||||
@ -61,36 +75,87 @@ function OutputStream ($q) {
|
||||
}
|
||||
};
|
||||
|
||||
this.updateCounterState = ({ counter }) => {
|
||||
this.counters.used.push(counter);
|
||||
this.bufferAdd = event => {
|
||||
const { counter } = event;
|
||||
|
||||
if (counter > this.counters.max) {
|
||||
this.counters.max = counter;
|
||||
}
|
||||
|
||||
let ready;
|
||||
const used = [];
|
||||
const missing = [];
|
||||
let minReady;
|
||||
let maxReady;
|
||||
|
||||
for (let i = this.counters.min; i <= this.counters.max; i++) {
|
||||
if (this.counters.used.indexOf(i) === -1) {
|
||||
missing.push(i);
|
||||
} else if (missing.length === 0) {
|
||||
maxReady = i;
|
||||
if (i === counter) {
|
||||
rx.push(event);
|
||||
used.push(i);
|
||||
this.counters.length += 1;
|
||||
} else {
|
||||
missing.push(i);
|
||||
}
|
||||
} else {
|
||||
used.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (maxReady) {
|
||||
minReady = this.counters.min;
|
||||
const excess = this.counters.length - OUTPUT_MAX_BUFFER_LENGTH;
|
||||
this.state.overflow = (excess > 0);
|
||||
|
||||
this.counters.min = maxReady + 1;
|
||||
this.counters.used = this.counters.used.filter(c => c > maxReady);
|
||||
if (missing.length === 0) {
|
||||
ready = this.counters.max;
|
||||
} else if (this.state.overflow) {
|
||||
ready = this.counters.min + this.framesPerRender;
|
||||
} else {
|
||||
ready = missing[0] - 1;
|
||||
}
|
||||
|
||||
this.counters.total += 1;
|
||||
this.counters.ready = ready;
|
||||
this.counters.used = used;
|
||||
this.counters.missing = missing;
|
||||
this.counters.ready = [minReady, maxReady];
|
||||
};
|
||||
|
||||
return this.counters.ready;
|
||||
this.bufferEmpty = threshold => {
|
||||
let removed = [];
|
||||
|
||||
for (let i = rx.length - 1; i >= 0; i--) {
|
||||
if (rx[i].counter <= threshold) {
|
||||
removed = removed.concat(rx.splice(i, 1));
|
||||
}
|
||||
}
|
||||
|
||||
this.counters.min = threshold + 1;
|
||||
this.counters.used = this.counters.used.filter(c => c > threshold);
|
||||
this.counters.length = rx.length;
|
||||
|
||||
return removed;
|
||||
};
|
||||
|
||||
this.isReadyToRender = () => {
|
||||
const { total } = this.counters;
|
||||
const readyCount = this.counters.ready - this.counters.min;
|
||||
|
||||
if (readyCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.ending) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (total % this.framesPerRender === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (total < OUTPUT_PAGE_SIZE) {
|
||||
if (readyCount % this.framesPerRender === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.pushJobEvent = data => {
|
||||
@ -103,24 +168,24 @@ function OutputStream ($q) {
|
||||
this.counters.final = data.counter;
|
||||
}
|
||||
|
||||
const [minReady, maxReady] = this.updateCounterState(data);
|
||||
const count = this.hooks.bufferAdd(data);
|
||||
this.bufferAdd(data);
|
||||
|
||||
if (count % OUTPUT_PAGE_SIZE === 0) {
|
||||
if (this.counters.total % OUTPUT_PAGE_SIZE === 0) {
|
||||
this.setFramesPerRender();
|
||||
}
|
||||
|
||||
const isReady = maxReady && (this.state.ending ||
|
||||
(maxReady - minReady) % this.framesPerRender === 0);
|
||||
|
||||
if (!isReady) {
|
||||
if (!this.isReadyToRender()) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const isLastFrame = this.state.ending && (maxReady >= this.counters.final);
|
||||
const events = this.hooks.bufferEmpty(minReady, maxReady);
|
||||
const isLast = this.state.ending && (this.counters.ready >= this.counters.final);
|
||||
const events = this.bufferEmpty(this.counters.ready);
|
||||
|
||||
return this.emitFrames(events, isLastFrame);
|
||||
if (events.length > 0) {
|
||||
return this.emitFrames(events, isLast);
|
||||
}
|
||||
|
||||
return $q.resolve();
|
||||
})
|
||||
.then(() => --this.lag);
|
||||
|
||||
@ -133,16 +198,20 @@ function OutputStream ($q) {
|
||||
this.state.ending = true;
|
||||
this.counters.final = counter;
|
||||
|
||||
if (counter >= this.counters.min) {
|
||||
if (counter > this.counters.ready) {
|
||||
return $q.resolve();
|
||||
}
|
||||
|
||||
const readyCount = this.counters.ready - this.counters.min;
|
||||
|
||||
let events = [];
|
||||
if (this.counters.ready.length > 0) {
|
||||
events = this.hooks.bufferEmpty(...this.counters.ready);
|
||||
if (readyCount > 0) {
|
||||
events = this.bufferEmpty(this.counters.ready);
|
||||
|
||||
return this.emitFrames(events, true);
|
||||
}
|
||||
|
||||
return this.emitFrames(events, true);
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
return this.chain;
|
||||
@ -157,7 +226,6 @@ function OutputStream ($q) {
|
||||
this.hooks.onStop();
|
||||
}
|
||||
|
||||
this.counters.ready.length = 0;
|
||||
return $q.resolve();
|
||||
});
|
||||
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
/** @define TokenModal */
|
||||
.TokenModal {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.TokenModal-label {
|
||||
font-weight: bold;
|
||||
width: 130px;
|
||||
}
|
||||
@ -58,30 +58,30 @@ function AddTokensController (
|
||||
return postToken
|
||||
.then(({ data }) => {
|
||||
const refreshHTML = data.refresh_token ?
|
||||
`<div class="TokenModal">
|
||||
<div class="TokenModal-label">
|
||||
`<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.REFRESH_TOKEN_LABEL')}
|
||||
</div>
|
||||
<div class="TokenModal-value">
|
||||
<div class="PopupModal-value">
|
||||
${data.refresh_token}
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
Alert(strings.get('add.TOKEN_MODAL_HEADER'), `
|
||||
<div class="TokenModal">
|
||||
<div class="TokenModal-label">
|
||||
<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.TOKEN_LABEL')}
|
||||
</div>
|
||||
<div class="TokenModal-value">
|
||||
<div class="PopupModal-value">
|
||||
${data.token}
|
||||
</div>
|
||||
</div>
|
||||
${refreshHTML}
|
||||
<div class="TokenModal">
|
||||
<div class="TokenModal-label">
|
||||
<div class="PopupModal">
|
||||
<div class="PopupModal-label">
|
||||
${strings.get('add.TOKEN_EXPIRES_LABEL')}
|
||||
</div>
|
||||
<div class="TokenModal-value">
|
||||
<div class="PopupModal-value">
|
||||
${$filter('longDate')(data.expires)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,6 +16,7 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
||||
|
||||
if (!vm.isSuperUser) {
|
||||
checkOrgAdmin();
|
||||
checkNotificationAdmin();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -54,6 +55,24 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkNotificationAdmin () {
|
||||
const usersPath = `/api/v2/users/${vm.currentUserId}/roles/?role_field=notification_admin_role`;
|
||||
$http.get(usersPath)
|
||||
.then(({ data }) => {
|
||||
if (data.count > 0) {
|
||||
vm.isNotificationAdmin = true;
|
||||
} else {
|
||||
vm.isNotificationAdmin = false;
|
||||
}
|
||||
})
|
||||
.catch(({ data, status }) => {
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: strings.get('error.HEADER'),
|
||||
msg: strings.get('error.CALL', { path: usersPath, action: 'GET', status })
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AtLayoutController.$inject = ['$scope', '$http', 'ComponentsStrings', 'ProcessErrors', '$transitions'];
|
||||
|
||||
@ -81,10 +81,10 @@
|
||||
<span>
|
||||
</div>
|
||||
<at-side-nav-item icon-class="fa-list-alt" route="credentialTypes" name="CREDENTIAL_TYPES"
|
||||
system-admin-only="true">
|
||||
system-admin-only="true">
|
||||
</at-side-nav-item>
|
||||
<at-side-nav-item icon-class="fa-bell" route="notifications" name="NOTIFICATIONS"
|
||||
system-admin-only="true">
|
||||
ng-show="$parent.layoutVm.isSuperUser || $parent.layoutVm.isOrgAdmin || $parent.layoutVm.isNotificationAdmin">
|
||||
</at-side-nav-item>
|
||||
<at-side-nav-item icon-class="fa-briefcase" route="managementJobsList" name="MANAGEMENT_JOBS"
|
||||
system-admin-only="true">
|
||||
|
||||
@ -46,6 +46,11 @@
|
||||
text-align: center;
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
|
||||
&--inline {
|
||||
margin-right: @at-space-2x;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.at-Panel-headingCustomContent {
|
||||
@ -59,6 +64,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: normal!important;
|
||||
width: 30%;
|
||||
margin: @at-space-2x;
|
||||
|
||||
@media screen and (max-width: @breakpoint-md) {
|
||||
flex: 2.5 0 auto;
|
||||
|
||||
@ -88,7 +88,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr
|
||||
};
|
||||
|
||||
scope.hasSelectedRows = function(){
|
||||
return _.any(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||
return _.some(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||
};
|
||||
|
||||
scope.selectTab = function(selected){
|
||||
|
||||
@ -94,7 +94,7 @@ function(scope, $state, i18n, CreateSelect2, Rest, $q, Wait, ProcessErrors) {
|
||||
};
|
||||
|
||||
scope.showSection2Container = function(){
|
||||
return _.any(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||
return _.some(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||
};
|
||||
|
||||
scope.showSection2Tab = function(tab){
|
||||
|
||||
@ -85,6 +85,9 @@ export default function BuildAnchor($log, $filter) {
|
||||
const inventoryId = _.get(obj, 'inventory', '').split('-').reverse()[0];
|
||||
url += `inventories/inventory/${inventoryId}/inventory_sources/edit/${obj.id}`;
|
||||
break;
|
||||
case 'o_auth2_application':
|
||||
url += `applications/${obj.id}`;
|
||||
break;
|
||||
default:
|
||||
url += resource + 's/' + obj.id + '/';
|
||||
}
|
||||
|
||||
@ -349,7 +349,7 @@ angular
|
||||
$rootScope.$broadcast("RemoveIndicator");
|
||||
}
|
||||
|
||||
if(_.contains(trans.from().name, 'output') && trans.to().name === 'jobs'){
|
||||
if(_.includes(trans.from().name, 'output') && trans.to().name === 'jobs'){
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
@ -375,7 +375,7 @@ angular
|
||||
$rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor');
|
||||
|
||||
// state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
|
||||
if (!_.contains($location.$$url, '/login')) {
|
||||
if (!_.includes($location.$$url, '/login')) {
|
||||
ConfigService.getConfig().then(function() {
|
||||
Timer.init().then(function(timer) {
|
||||
$rootScope.sessionTimer = timer;
|
||||
|
||||
@ -90,7 +90,7 @@ export default
|
||||
if(streamConfig && streamConfig.activityStream) {
|
||||
if(streamConfig.activityStreamTarget) {
|
||||
stateGoParams.target = streamConfig.activityStreamTarget;
|
||||
let isTemplateTarget = _.contains(['template', 'job_template', 'workflow_job_template'], streamConfig.activityStreamTarget);
|
||||
let isTemplateTarget = _.includes(['template', 'job_template', 'workflow_job_template'], streamConfig.activityStreamTarget);
|
||||
stateGoParams.activity_search = {
|
||||
or__object1__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||
or__object2__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||
|
||||
@ -25,7 +25,7 @@ export default
|
||||
if(expandedBreadcrumbWidth > availableWidth) {
|
||||
let widthToTrim = expandedBreadcrumbWidth - availableWidth;
|
||||
// Sort the crumbs from biggest to smallest
|
||||
let sortedCrumbs = _.sortByOrder(crumbs, ["origWidth"], ["desc"]);
|
||||
let sortedCrumbs = _.orderBy(crumbs, ["origWidth"], ["desc"]);
|
||||
let maxWidth;
|
||||
for(let i=0; i<sortedCrumbs.length; i++) {
|
||||
if(sortedCrumbs[i+1]) {
|
||||
|
||||
@ -252,7 +252,7 @@ export default [
|
||||
},
|
||||
];
|
||||
|
||||
var forms = _.pluck(authForms, 'formDef');
|
||||
var forms = _.map(authForms, 'formDef');
|
||||
_.each(forms, function(form) {
|
||||
var keys = _.keys(form.fields);
|
||||
_.each(keys, function(key) {
|
||||
|
||||
@ -119,7 +119,7 @@ export default [
|
||||
}));
|
||||
|
||||
$('.select2-selection__choice').each(function(i, element){
|
||||
if(!_.contains($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, element.title)){
|
||||
if(!_.includes($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, element.title)){
|
||||
$(`#configuration_jobs_template_AD_HOC_COMMANDS option[value='${element.title}']`).remove();
|
||||
element.remove();
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ export default [
|
||||
id: 'system-misc-form'
|
||||
}];
|
||||
|
||||
var forms = _.pluck(systemForms, 'formDef');
|
||||
var forms = _.map(systemForms, 'formDef');
|
||||
_.each(forms, function(form) {
|
||||
var keys = _.keys(form.fields);
|
||||
_.each(keys, function(key) {
|
||||
|
||||
@ -27,7 +27,7 @@ export default ['i18n', function(i18n) {
|
||||
SESSION_COOKIE_AGE: {
|
||||
type: 'number',
|
||||
integer: true,
|
||||
min: 60,
|
||||
min: 61,
|
||||
reset: 'SESSION_COOKIE_AGE',
|
||||
},
|
||||
SESSIONS_PER_USER: {
|
||||
|
||||
@ -17,8 +17,8 @@ function CapacityAdjuster (templateUrl, ProcessErrors, Wait, strings) {
|
||||
value: scope.state.mem_capacity
|
||||
}];
|
||||
|
||||
scope.min_capacity = _.min(adjustment_values, 'value');
|
||||
scope.max_capacity = _.max(adjustment_values, 'value');
|
||||
scope.min_capacity = _.minBy(adjustment_values, 'value');
|
||||
scope.max_capacity = _.maxBy(adjustment_values, 'value');
|
||||
|
||||
capacityAdjusterController.init();
|
||||
},
|
||||
@ -72,4 +72,4 @@ CapacityAdjuster.$inject = [
|
||||
'InstanceGroupsStrings'
|
||||
];
|
||||
|
||||
export default CapacityAdjuster;
|
||||
export default CapacityAdjuster;
|
||||
|
||||
@ -22,7 +22,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
|
||||
init();
|
||||
|
||||
function init() {
|
||||
Rest.setUrl(GetBasePath('projects'));
|
||||
Rest.setUrl(GetBasePath('notification_templates'));
|
||||
Rest.options()
|
||||
.then(({data}) => {
|
||||
if (!data.actions.POST) {
|
||||
@ -205,7 +205,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
|
||||
return $scope[i];
|
||||
}
|
||||
|
||||
params.notification_configuration = _.object(Object.keys(form.fields)
|
||||
params.notification_configuration = _.fromPairs(Object.keys(form.fields)
|
||||
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
||||
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
||||
|
||||
|
||||
@ -275,7 +275,7 @@ export default ['Rest', 'Wait',
|
||||
return $scope[i];
|
||||
}
|
||||
|
||||
params.notification_configuration = _.object(Object.keys(form.fields)
|
||||
params.notification_configuration = _.fromPairs(Object.keys(form.fields)
|
||||
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
||||
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
||||
hover: false,
|
||||
emptyListText: i18n.sprintf(i18n._("This list is populated by notification templates added from the %sNotifications%s section"), " <a ui-sref='notifications.add'>", "</a> "),
|
||||
basePath: 'notification_templates',
|
||||
ngIf: 'current_user.is_superuser || isOrgAdmin',
|
||||
ngIf: 'current_user.is_superuser || isOrgAdmin || isNotificationAdmin',
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
@ -40,6 +40,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
||||
flag: 'notification_templates_success',
|
||||
type: "toggle",
|
||||
ngClick: "toggleNotification($event, notification.id, \"notification_templates_success\")",
|
||||
ngDisabled: "!(current_user.is_superuser || isOrgAdmin)",
|
||||
awToolTip: "{{ schedule.play_tip }}",
|
||||
dataTipWatch: "schedule.play_tip",
|
||||
dataPlacement: "right",
|
||||
@ -51,6 +52,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
||||
flag: 'notification_templates_error',
|
||||
type: "toggle",
|
||||
ngClick: "toggleNotification($event, notification.id, \"notification_templates_error\")",
|
||||
ngDisabled: "!(current_user.is_superuser || isOrgAdmin)",
|
||||
awToolTip: "{{ schedule.play_tip }}",
|
||||
dataTipWatch: "schedule.play_tip",
|
||||
dataPlacement: "right",
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
|
||||
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt',
|
||||
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt', '$rootScope',
|
||||
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService', 'InstanceGroupsData', 'ConfigData',
|
||||
function($scope, $location, $stateParams, OrgAdminLookup,
|
||||
OrganizationForm, Rest, ProcessErrors, Prompt,
|
||||
OrganizationForm, Rest, ProcessErrors, Prompt, $rootScope,
|
||||
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService, InstanceGroupsData, ConfigData) {
|
||||
|
||||
let form = OrganizationForm(),
|
||||
@ -26,6 +26,12 @@ export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
|
||||
$scope.isOrgAdmin = isOrgAdmin;
|
||||
});
|
||||
|
||||
Rest.setUrl(GetBasePath('users') + $rootScope.current_user.id + '/roles/?role_field=notification_admin_role');
|
||||
Rest.get()
|
||||
.then(({data}) => {
|
||||
$scope.isNotificationAdmin = (data.count && data.count > 0);
|
||||
});
|
||||
|
||||
$scope.$watch('organization_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||
if (val === false) {
|
||||
$scope.canAdd = false;
|
||||
|
||||
@ -133,7 +133,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
}
|
||||
switch ($scope.scm_type.value) {
|
||||
case 'git':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' +
|
||||
i18n._('Example URLs for GIT SCM include:') +
|
||||
'</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||
@ -146,7 +146,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
||||
break;
|
||||
case 'svn':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
||||
@ -155,7 +155,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
$scope.scmBranchLabel = i18n._('Revision #');
|
||||
break;
|
||||
case 'hg':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||
'<li>ssh://server.example.com/path</li></ul>' +
|
||||
@ -174,7 +174,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
$scope.lookupType = 'insights_credential';
|
||||
break;
|
||||
default:
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p> ' + i18n._('URL popover text') + '</p>';
|
||||
$scope.credRequired = false;
|
||||
$scope.lookupType = 'scm_credential';
|
||||
|
||||
@ -270,7 +270,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
}
|
||||
switch ($scope.scm_type.value) {
|
||||
case 'git':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for GIT SCM include:') + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
|
||||
'<p>' + i18n.sprintf(i18n._('%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
|
||||
@ -281,7 +281,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
||||
break;
|
||||
case 'svn':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
||||
@ -290,7 +290,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.scmBranchLabel = i18n._('Revision #');
|
||||
break;
|
||||
case 'hg':
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||
'<li>ssh://server.example.com/path</li></ul>' +
|
||||
@ -309,7 +309,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.lookupType = 'insights_credential';
|
||||
break;
|
||||
default:
|
||||
$scope.credentialLabel = "SCM Credential";
|
||||
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||
$scope.urlPopover = '<p> ' + i18n._('URL popover text');
|
||||
$scope.credRequired = false;
|
||||
$scope.lookupType = 'scm_credential';
|
||||
|
||||
@ -670,6 +670,8 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
||||
query += '&role_level=workflow_admin_role';
|
||||
} else if ($state.current.name.includes('projects')) {
|
||||
query += '&role_level=project_admin_role';
|
||||
} else if ($state.current.name.includes('notifications')) {
|
||||
query += '&role_level=notification_admin_role';
|
||||
} else {
|
||||
query += '&role_level=admin_role';
|
||||
}
|
||||
|
||||
@ -36,10 +36,10 @@ export default ['$scope',
|
||||
$scope.selection.selectedItems =
|
||||
_items.filter(function(item) {
|
||||
return item.isSelected;
|
||||
}).pluck('value').value();
|
||||
}).map('value').value();
|
||||
|
||||
$scope.selection.deselectedItems =
|
||||
_items.pluck('value').difference($scope.selection.selectedItems)
|
||||
_items.map('value').difference($scope.selection.selectedItems)
|
||||
.value();
|
||||
|
||||
/**
|
||||
|
||||
@ -310,12 +310,12 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
||||
return [];
|
||||
}
|
||||
if(defaultParams) {
|
||||
let stripped =_.pick(params, (value, key) => {
|
||||
let stripped =_.pickBy(params, (value, key) => {
|
||||
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
||||
return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null;
|
||||
});
|
||||
let strippedCopy = _.cloneDeep(stripped);
|
||||
if(_.keys(_.pick(defaultParams, _.keys(strippedCopy))).length > 0){
|
||||
if(_.keys(_.pickBy(defaultParams, _.keys(strippedCopy))).length > 0){
|
||||
for (var key in strippedCopy) {
|
||||
if (strippedCopy.hasOwnProperty(key)) {
|
||||
let value = strippedCopy[key];
|
||||
@ -336,7 +336,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
||||
mergeQueryset (queryset, additional, singleSearchParam) {
|
||||
const space = '%20and%20';
|
||||
|
||||
const merged = _.merge({}, queryset, additional, (objectValue, sourceValue, key, object) => {
|
||||
const merged = _.mergeWith({}, queryset, additional, (objectValue, sourceValue, key, object) => {
|
||||
if (!(object[key] && object[key] !== sourceValue)) {
|
||||
// // https://lodash.com/docs/3.10.1#each
|
||||
// If this returns undefined merging is handled by default _.merge algorithm
|
||||
@ -418,7 +418,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
||||
termParams = searchWithoutKey(term, singleSearchParam);
|
||||
}
|
||||
|
||||
params = _.merge(params, termParams, combineSameSearches);
|
||||
params = _.mergeWith(params, termParams, combineSameSearches);
|
||||
});
|
||||
|
||||
return params;
|
||||
|
||||
@ -102,7 +102,7 @@ function SmartSearchController (
|
||||
const listName = $scope.list.name;
|
||||
const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`;
|
||||
|
||||
const isRelatedSearchTermField = (_.contains($scope.models[listName].related, rootField));
|
||||
const isRelatedSearchTermField = (_.includes($scope.models[listName].related, rootField));
|
||||
const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field');
|
||||
|
||||
return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField);
|
||||
@ -254,7 +254,7 @@ function SmartSearchController (
|
||||
defaults[key] = queryset[key];
|
||||
}
|
||||
});
|
||||
const cleared = _(defaults).omit(_.isNull).value();
|
||||
const cleared = _(defaults).omitBy(_.isNull).value();
|
||||
delete cleared.page;
|
||||
queryset = cleared;
|
||||
|
||||
|
||||
@ -744,10 +744,20 @@ function($injector, $stateExtender, $log, i18n) {
|
||||
// search will think they need to be set as search tags.
|
||||
var params;
|
||||
if(field.sourceModel === "organization"){
|
||||
params = {
|
||||
page_size: '5',
|
||||
role_level: 'admin_role'
|
||||
};
|
||||
if (form.name === "notification_template") {
|
||||
// Users with admin_role role level should also have
|
||||
// notification_admin_role so this should handle regular admin
|
||||
// users as well as notification admin users
|
||||
params = {
|
||||
page_size: '5',
|
||||
role_level: 'notification_admin_role'
|
||||
};
|
||||
} else {
|
||||
params = {
|
||||
page_size: '5',
|
||||
role_level: 'admin_role'
|
||||
};
|
||||
}
|
||||
}
|
||||
else if(field.sourceModel === "inventory_script"){
|
||||
params = {
|
||||
|
||||
@ -21,7 +21,9 @@ 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;
|
||||
setScopeFields(data);
|
||||
_.forEach(form.fields, (value, key) => {
|
||||
$scope[key] = data[key];
|
||||
});
|
||||
$scope.organization_name = data.summary_fields.organization.name;
|
||||
|
||||
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
|
||||
@ -36,19 +38,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest',
|
||||
});
|
||||
}
|
||||
|
||||
// @issue I think all this really want to do is _.forEach(form.fields, (field) =>{ $scope[field] = data[field]})
|
||||
function setScopeFields(data) {
|
||||
_(data)
|
||||
.pick(function(value, key) {
|
||||
return form.fields.hasOwnProperty(key) === true;
|
||||
})
|
||||
.forEach(function(value, key) {
|
||||
$scope[key] = value;
|
||||
})
|
||||
.value();
|
||||
return;
|
||||
}
|
||||
|
||||
// prepares a data payload for a PUT request to the API
|
||||
function processNewData(fields) {
|
||||
var data = {};
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
</div>
|
||||
<div class="Prompt-previewTags--outer">
|
||||
<div ng-show="promptData.launchConf.defaults.inventory.id && !promptData.prompts.inventory.value.id" class="Prompt-noSelectedItem">{{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}</div>
|
||||
<at-tag tag="promptData.prompts.inventory.value.name" remove-tag="vm.deleteSelectedInventory()" ng-show="!readOnlyPrompts"></at-tag>
|
||||
<at-tag tag="promptData.prompts.inventory.value.name" ng-show="readOnlyPrompts"></at-tag>
|
||||
<at-tag tag="promptData.prompts.inventory.value.name" remove-tag="vm.deleteSelectedInventory()" ng-show="!readOnlyPrompts && promptData.prompts.inventory.value.id"></at-tag>
|
||||
<at-tag tag="promptData.prompts.inventory.value.name" ng-show="readOnlyPrompts && promptData.prompts.inventory.value.id"></at-tag>
|
||||
</div>
|
||||
<div class="Prompt-previewTagRevert">
|
||||
<a class="Prompt-revertLink" href="" ng-hide="readOnlyPrompts || promptData.prompts.inventory.value.id === promptData.launchConf.defaults.inventory.id" ng-click="vm.revert()">{{:: vm.strings.get('prompt.REVERT') }}</a>
|
||||
|
||||
@ -261,7 +261,7 @@ export default
|
||||
scope.maxTextError = false;
|
||||
|
||||
if(scope.type.type==="text"){
|
||||
if(scope.default.trim() !== ""){
|
||||
if(scope.default && scope.default.trim() !== ""){
|
||||
if(scope.default.trim().length < scope.text_min && scope.text_min !== "" ){
|
||||
scope.minTextError = true;
|
||||
}
|
||||
@ -272,7 +272,7 @@ export default
|
||||
}
|
||||
|
||||
if(scope.type.type==="textarea"){
|
||||
if(scope.default_textarea.trim() !== ""){
|
||||
if(scope.default_textarea && scope.default_textarea.trim() !== ""){
|
||||
if(scope.default_textarea.trim().length < scope.textarea_min && scope.textarea_min !== "" ){
|
||||
scope.minTextError = true;
|
||||
}
|
||||
@ -283,7 +283,7 @@ export default
|
||||
}
|
||||
|
||||
if(scope.type.type==="password"){
|
||||
if(scope.default_password.trim() !== ""){
|
||||
if(scope.default_password && scope.default_password.trim() !== ""){
|
||||
if(scope.default_password.trim().length < scope.password_min && scope.password_min !== "" ){
|
||||
scope.minTextError = true;
|
||||
}
|
||||
@ -293,7 +293,7 @@ export default
|
||||
}
|
||||
}
|
||||
|
||||
if(scope.type.type==="multiselect" && scope.default_multiselect.trim() !== ""){
|
||||
if(scope.type.type==="multiselect" && scope.default_multiselect && scope.default_multiselect.trim() !== ""){
|
||||
choiceArray = scope.choices.split(/\n/);
|
||||
answerArray = scope.default_multiselect.split(/\n/);
|
||||
|
||||
@ -306,7 +306,7 @@ export default
|
||||
}
|
||||
}
|
||||
|
||||
if(scope.type.type==="multiplechoice" && scope.default.trim() !== ""){
|
||||
if(scope.type.type==="multiplechoice" && scope.default && scope.default.trim() !== ""){
|
||||
choiceArray = scope.choices.split(/\n/);
|
||||
if($.inArray(scope.default, choiceArray)===-1){
|
||||
scope.invalidChoice = true;
|
||||
|
||||
@ -7,9 +7,10 @@
|
||||
export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
'$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
|
||||
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
|
||||
'i18n',
|
||||
function($scope, WorkflowService, GetBasePath, TemplatesService,
|
||||
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
|
||||
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
||||
Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) {
|
||||
|
||||
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
||||
|
||||
@ -301,15 +302,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
if (!optionsToInclude) {
|
||||
$scope.edgeTypeOptions = [
|
||||
{
|
||||
label: 'Always',
|
||||
label: i18n._('Always'),
|
||||
value: 'always'
|
||||
},
|
||||
{
|
||||
label: 'On Success',
|
||||
label: i18n._('On Success'),
|
||||
value: 'success'
|
||||
},
|
||||
{
|
||||
label: 'On Failure',
|
||||
label: i18n._('On Failure'),
|
||||
value: 'failure'
|
||||
}
|
||||
];
|
||||
@ -641,6 +642,31 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
|
||||
if (!_.isEmpty($scope.nodeBeingEdited.promptData)) {
|
||||
$scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData);
|
||||
const launchConf = $scope.promptData.launchConf;
|
||||
|
||||
if (!launchConf.survey_enabled &&
|
||||
!launchConf.ask_inventory_on_launch &&
|
||||
!launchConf.ask_credential_on_launch &&
|
||||
!launchConf.ask_verbosity_on_launch &&
|
||||
!launchConf.ask_job_type_on_launch &&
|
||||
!launchConf.ask_limit_on_launch &&
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.ask_variables_on_launch &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
} else {
|
||||
$scope.showPromptButton = true;
|
||||
|
||||
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
|
||||
$scope.promptModalMissingReqFields = true;
|
||||
} else {
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job_template' ||
|
||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template'
|
||||
@ -727,8 +753,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.survey_enabled &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.ask_variables_on_launch &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
@ -839,19 +865,19 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
|
||||
switch($scope.nodeBeingEdited.edgeType) {
|
||||
case "always":
|
||||
$scope.edgeType = {label: "Always", value: "always"};
|
||||
$scope.edgeType = {label: i18n._("Always"), value: "always"};
|
||||
if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) {
|
||||
edgeDropdownOptions = ["always"];
|
||||
}
|
||||
break;
|
||||
case "success":
|
||||
$scope.edgeType = {label: "On Success", value: "success"};
|
||||
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
|
||||
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
||||
edgeDropdownOptions = ["success", "failure"];
|
||||
}
|
||||
break;
|
||||
case "failure":
|
||||
$scope.edgeType = {label: "On Failure", value: "failure"};
|
||||
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
|
||||
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
||||
edgeDropdownOptions = ["success", "failure"];
|
||||
}
|
||||
@ -978,7 +1004,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
|
||||
switch($scope.nodeBeingEdited.edgeType) {
|
||||
case "always":
|
||||
$scope.edgeType = {label: "Always", value: "always"};
|
||||
$scope.edgeType = {label: i18n._("Always"), value: "always"};
|
||||
if (
|
||||
_.includes(siblingConnectionTypes, "always") &&
|
||||
!_.includes(siblingConnectionTypes, "success") &&
|
||||
@ -990,7 +1016,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
}
|
||||
break;
|
||||
case "success":
|
||||
$scope.edgeType = {label: "On Success", value: "success"};
|
||||
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
|
||||
if (
|
||||
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
||||
!_.includes(siblingConnectionTypes, "always")
|
||||
@ -1001,7 +1027,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
}
|
||||
break;
|
||||
case "failure":
|
||||
$scope.edgeType = {label: "On Failure", value: "failure"};
|
||||
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
|
||||
if (
|
||||
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
||||
!_.includes(siblingConnectionTypes, "always")
|
||||
@ -1071,8 +1097,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
!launchConf.ask_tags_on_launch &&
|
||||
!launchConf.ask_skip_tags_on_launch &&
|
||||
!launchConf.ask_diff_mode_on_launch &&
|
||||
!launchConf.survey_enabled &&
|
||||
!launchConf.credential_needed_to_start &&
|
||||
!launchConf.ask_variables_on_launch &&
|
||||
launchConf.variables_needed_to_start.length === 0) {
|
||||
$scope.showPromptButton = false;
|
||||
$scope.promptModalMissingReqFields = false;
|
||||
|
||||
@ -17,7 +17,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
'$state', 'i18n', 'resolvedModels', 'resourceData',
|
||||
function($scope, $rootScope, $stateParams, UserForm, Rest, ProcessErrors,
|
||||
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);
|
||||
}
|
||||
@ -28,12 +28,16 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
id = $stateParams.user_id,
|
||||
defaultUrl = GetBasePath('users') + id,
|
||||
user_obj = resourceData.data;
|
||||
|
||||
|
||||
$scope.breadcrumb.user_name = user_obj.username;
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
_.forEach(form.fields, (value, key) => {
|
||||
$scope[key] = user_obj[key];
|
||||
});
|
||||
|
||||
$scope.canEdit = me.get('summary_fields.user_capabilities.edit');
|
||||
$scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0;
|
||||
$scope.isCurrentlyLoggedInUser = (parseInt(id) === $rootScope.current_user.id);
|
||||
@ -73,9 +77,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
$scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||
$scope.canAdd = (val === false) ? false : true;
|
||||
});
|
||||
|
||||
setScopeFields(user_obj);
|
||||
|
||||
}
|
||||
|
||||
function user_type_sync($scope) {
|
||||
@ -107,19 +108,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function setScopeFields(data) {
|
||||
_(data)
|
||||
.pick(function(value, key) {
|
||||
return form.fields.hasOwnProperty(key) === true;
|
||||
})
|
||||
.forEach(function(value, key) {
|
||||
$scope[key] = value;
|
||||
})
|
||||
.value();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.redirectToResource = function(resource) {
|
||||
let type = resource.summary_fields.resource_type.replace(/ /g , "_");
|
||||
var id = resource.related[type].split("/")[4];
|
||||
@ -152,7 +140,11 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
var processNewData = function(fields) {
|
||||
var data = {};
|
||||
_.forEach(fields, function(value, key) {
|
||||
if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) {
|
||||
if (value.type === 'sensitive') {
|
||||
if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) {
|
||||
data[key] = $scope[key];
|
||||
}
|
||||
} else {
|
||||
data[key] = $scope[key];
|
||||
}
|
||||
});
|
||||
|
||||
41
awx/ui/npm-shrinkwrap.json
generated
41
awx/ui/npm-shrinkwrap.json
generated
@ -223,6 +223,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.8.0",
|
||||
"from": "lodash@>=3.8.0 <3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz"
|
||||
},
|
||||
"rrule": {
|
||||
"version": "2.2.0-dev",
|
||||
"from": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
|
||||
@ -233,7 +238,7 @@
|
||||
"angular-tz-extensions": {
|
||||
"version": "0.5.2",
|
||||
"from": "git+https://git@github.com/ansible/angular-tz-extensions.git#v0.5.2",
|
||||
"resolved": "git+https://git@github.com/ansible/angular-tz-extensions.git#9cabb05d58079092bfb29ccae721b35b46f28af6",
|
||||
"resolved": "git://github.com/ansible/angular-tz-extensions.git#9cabb05d58079092bfb29ccae721b35b46f28af6",
|
||||
"dependencies": {
|
||||
"jquery": {
|
||||
"version": "3.3.1",
|
||||
@ -1496,7 +1501,15 @@
|
||||
"version": "0.19.0",
|
||||
"from": "cheerio@>=0.19.0 <0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "3.10.1",
|
||||
"from": "lodash@>=3.2.0 <4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "1.7.0",
|
||||
@ -5420,6 +5433,12 @@
|
||||
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "3.10.1",
|
||||
"from": "lodash@>=3.8.0 <4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"from": "source-map@>=0.5.3 <0.6.0",
|
||||
@ -5723,6 +5742,12 @@
|
||||
"from": "inquirer@>=0.8.2 <0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.8.5.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.10.1",
|
||||
"from": "lodash@>=3.6.0 <4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5796,9 +5821,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.8.0",
|
||||
"from": "lodash@>=3.8.0 <3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz"
|
||||
"version": "4.17.10",
|
||||
"from": "lodash@>=4.17.10 <4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz"
|
||||
},
|
||||
"lodash._arraycopy": {
|
||||
"version": "3.0.0",
|
||||
@ -6317,6 +6342,12 @@
|
||||
"from": "glob@>=5.0.0 <6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "3.10.1",
|
||||
"from": "lodash@>=3.0.0 <4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,7 +29,8 @@
|
||||
"lint": "eslint .",
|
||||
"dev": "webpack --config build/webpack.development.js --progress",
|
||||
"watch": "webpack-dev-server --config build/webpack.watch.js --progress --https",
|
||||
"production": "webpack --config build/webpack.production.js"
|
||||
"production": "webpack --config build/webpack.production.js",
|
||||
"grab-licenses": "./utils/get_licenses.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.6.6",
|
||||
@ -120,7 +121,7 @@
|
||||
"jquery-ui": "^1.12.1",
|
||||
"js-yaml": "^3.2.7",
|
||||
"legacy-loader": "0.0.2",
|
||||
"lodash": "~3.8.0",
|
||||
"lodash": "~4.17.10",
|
||||
"lr-infinite-scroll": "git+https://git@github.com/lorenzofox3/lrInfiniteScroll",
|
||||
"moment": "^2.19.4",
|
||||
"ng-toast": "git+https://git@github.com/ansible/ngToast#v2.1.1",
|
||||
|
||||
@ -236,8 +236,8 @@ msgstr ""
|
||||
msgid "Add Project"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1718
|
||||
#: client/src/templates/job_templates/job-template.form.js:464
|
||||
#: client/src/shared/form-generator.js:1731
|
||||
#: client/src/templates/job_templates/job-template.form.js:468
|
||||
#: client/src/templates/workflows.form.js:205
|
||||
msgid "Add Survey"
|
||||
msgstr ""
|
||||
@ -277,12 +277,12 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:115
|
||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:117
|
||||
#: client/src/projects/projects.form.js:255
|
||||
#: client/src/templates/job_templates/job-template.form.js:407
|
||||
#: client/src/templates/job_templates/job-template.form.js:411
|
||||
#: client/src/templates/workflows.form.js:148
|
||||
msgid "Add a permission"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1453
|
||||
#: client/src/shared/form-generator.js:1466
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
@ -315,8 +315,8 @@ msgstr ""
|
||||
msgid "All Jobs"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:286
|
||||
#: client/src/templates/job_templates/job-template.form.js:293
|
||||
#: client/src/templates/job_templates/job-template.form.js:290
|
||||
#: client/src/templates/job_templates/job-template.form.js:297
|
||||
msgid "Allow Provisioning Callbacks"
|
||||
msgstr ""
|
||||
|
||||
@ -337,8 +337,8 @@ msgstr ""
|
||||
#: client/src/organizations/organizations.form.js:52
|
||||
#: client/src/projects/projects.form.js:207
|
||||
#: client/src/projects/projects.form.js:212
|
||||
#: client/src/templates/job_templates/job-template.form.js:235
|
||||
#: client/src/templates/job_templates/job-template.form.js:241
|
||||
#: client/src/templates/job_templates/job-template.form.js:239
|
||||
#: client/src/templates/job_templates/job-template.form.js:245
|
||||
msgid "Ansible Environment"
|
||||
msgstr ""
|
||||
|
||||
@ -434,7 +434,7 @@ msgstr ""
|
||||
msgid "Associate this host with a new group"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1455
|
||||
#: client/src/shared/form-generator.js:1468
|
||||
msgid "Auditor"
|
||||
msgstr ""
|
||||
|
||||
@ -703,7 +703,7 @@ msgstr ""
|
||||
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:188
|
||||
#: client/src/configuration/configuration.controller.js:617
|
||||
#: client/src/scheduler/scheduler.strings.js:56
|
||||
#: client/src/shared/form-generator.js:1706
|
||||
#: client/src/shared/form-generator.js:1719
|
||||
#: client/src/shared/lookup/lookup-modal.partial.html:19
|
||||
#: client/src/workflow-results/workflow-results.controller.js:38
|
||||
msgid "Cancel"
|
||||
@ -759,7 +759,7 @@ msgstr ""
|
||||
msgid "Check"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1078
|
||||
#: client/src/shared/form-generator.js:1087
|
||||
msgid "Choose a %s"
|
||||
msgstr ""
|
||||
|
||||
@ -844,7 +844,7 @@ msgid "Client Secret"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/scheduler/scheduler.strings.js:55
|
||||
#: client/src/shared/form-generator.js:1710
|
||||
#: client/src/shared/form-generator.js:1723
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
@ -870,7 +870,7 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/related/hosts/related-host.form.js:128
|
||||
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:155
|
||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:172
|
||||
#: client/src/templates/job_templates/job-template.form.js:439
|
||||
#: client/src/templates/job_templates/job-template.form.js:443
|
||||
#: client/src/templates/workflows.form.js:180
|
||||
msgid "Completed Jobs"
|
||||
msgstr ""
|
||||
@ -1338,7 +1338,7 @@ msgstr ""
|
||||
#: client/features/output/output.strings.js:34
|
||||
#: client/features/users/tokens/tokens.strings.js:14
|
||||
#: client/src/license/license.partial.html:5
|
||||
#: client/src/shared/form-generator.js:1488
|
||||
#: client/src/shared/form-generator.js:1501
|
||||
msgid "Details"
|
||||
msgstr ""
|
||||
|
||||
@ -1514,8 +1514,8 @@ msgstr ""
|
||||
msgid "Edit Question"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1722
|
||||
#: client/src/templates/job_templates/job-template.form.js:471
|
||||
#: client/src/shared/form-generator.js:1735
|
||||
#: client/src/templates/job_templates/job-template.form.js:475
|
||||
#: client/src/templates/workflows.form.js:212
|
||||
msgid "Edit Survey"
|
||||
msgstr ""
|
||||
@ -1608,8 +1608,8 @@ msgstr ""
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:299
|
||||
#: client/src/templates/job_templates/job-template.form.js:304
|
||||
#: client/src/templates/job_templates/job-template.form.js:303
|
||||
#: client/src/templates/job_templates/job-template.form.js:308
|
||||
#: client/src/templates/workflows.form.js:100
|
||||
#: client/src/templates/workflows.form.js:105
|
||||
msgid "Enable Concurrent Jobs"
|
||||
@ -1620,8 +1620,8 @@ msgid "Enable External Logging"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:124
|
||||
#: client/src/templates/job_templates/job-template.form.js:275
|
||||
#: client/src/templates/job_templates/job-template.form.js:280
|
||||
#: client/src/templates/job_templates/job-template.form.js:279
|
||||
#: client/src/templates/job_templates/job-template.form.js:284
|
||||
msgid "Enable Privilege Escalation"
|
||||
msgstr ""
|
||||
|
||||
@ -1629,7 +1629,7 @@ msgstr ""
|
||||
msgid "Enable survey"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:290
|
||||
#: client/src/templates/job_templates/job-template.form.js:294
|
||||
msgid "Enables creation of a provisioning callback URL. Using the URL a host can contact {{BRAND_NAME}} and request a configuration update using this job template."
|
||||
msgstr ""
|
||||
|
||||
@ -1815,8 +1815,8 @@ msgstr ""
|
||||
#: client/src/job-submission/job-submission.partial.html:165
|
||||
#: client/src/partials/logviewer.html:8
|
||||
#: client/src/scheduler/scheduler.strings.js:53
|
||||
#: client/src/templates/job_templates/job-template.form.js:353
|
||||
#: client/src/templates/job_templates/job-template.form.js:360
|
||||
#: client/src/templates/job_templates/job-template.form.js:357
|
||||
#: client/src/templates/job_templates/job-template.form.js:364
|
||||
#: client/src/templates/workflows.form.js:83
|
||||
#: client/src/templates/workflows.form.js:90
|
||||
#: client/src/workflow-results/workflow-results.controller.js:122
|
||||
@ -2091,8 +2091,8 @@ msgstr ""
|
||||
msgid "Host (Authentication URL)"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:335
|
||||
#: client/src/templates/job_templates/job-template.form.js:344
|
||||
#: client/src/templates/job_templates/job-template.form.js:339
|
||||
#: client/src/templates/job_templates/job-template.form.js:348
|
||||
msgid "Host Config Key"
|
||||
msgstr ""
|
||||
|
||||
@ -2269,7 +2269,7 @@ msgstr ""
|
||||
msgid "If checked, any hosts and groups that were previously present on the external source but are now removed will be removed from the Tower inventory. Hosts and groups that were not managed by the inventory source will be promoted to the next manually created group or if there is no manually created group to promote them into, they will be left in the \"all\" default group for the inventory."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:278
|
||||
#: client/src/templates/job_templates/job-template.form.js:282
|
||||
msgid "If enabled, run this playbook as an administrator."
|
||||
msgstr ""
|
||||
|
||||
@ -2277,11 +2277,11 @@ msgstr ""
|
||||
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:263
|
||||
#: client/src/templates/job_templates/job-template.form.js:267
|
||||
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:302
|
||||
#: client/src/templates/job_templates/job-template.form.js:306
|
||||
msgid "If enabled, simultaneous runs of this job template will be allowed."
|
||||
msgstr ""
|
||||
|
||||
@ -2289,7 +2289,7 @@ msgstr ""
|
||||
msgid "If enabled, simultaneous runs of this workflow job template will be allowed."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:313
|
||||
#: client/src/templates/job_templates/job-template.form.js:317
|
||||
msgid "If enabled, use cached facts if available and store discovered facts in the cache."
|
||||
msgstr ""
|
||||
|
||||
@ -2360,8 +2360,8 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:64
|
||||
#: client/src/organizations/organizations.form.js:38
|
||||
#: client/src/organizations/organizations.form.js:41
|
||||
#: client/src/templates/job_templates/job-template.form.js:248
|
||||
#: client/src/templates/job_templates/job-template.form.js:251
|
||||
#: client/src/templates/job_templates/job-template.form.js:252
|
||||
#: client/src/templates/job_templates/job-template.form.js:255
|
||||
msgid "Instance Groups"
|
||||
msgstr ""
|
||||
|
||||
@ -2679,7 +2679,7 @@ msgstr ""
|
||||
msgid "Last Updated"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1714
|
||||
#: client/src/shared/form-generator.js:1727
|
||||
msgid "Launch"
|
||||
msgstr ""
|
||||
|
||||
@ -2773,7 +2773,7 @@ msgstr ""
|
||||
msgid "Live events: error connecting to the server."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1992
|
||||
#: client/src/shared/form-generator.js:2005
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
@ -2843,6 +2843,10 @@ msgstr ""
|
||||
msgid "Manual projects do not require an SCM update"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:234
|
||||
msgid "Max 512 characters per label."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/login/loginModal/loginModal.partial.html:28
|
||||
msgid "Maximum per-user sessions reached. Please sign in."
|
||||
msgstr ""
|
||||
@ -3153,7 +3157,7 @@ msgid "No recent notifications."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/inventories-hosts/hosts/hosts.partial.html:36
|
||||
#: client/src/shared/form-generator.js:1886
|
||||
#: client/src/shared/form-generator.js:1899
|
||||
#: client/src/shared/list-generator/list-generator.factory.js:240
|
||||
msgid "No records matched your search."
|
||||
msgstr ""
|
||||
@ -3300,7 +3304,7 @@ msgstr ""
|
||||
|
||||
#: client/src/notifications/notificationTemplates.form.js:453
|
||||
#: client/src/partials/logviewer.html:7
|
||||
#: client/src/templates/job_templates/job-template.form.js:271
|
||||
#: client/src/templates/job_templates/job-template.form.js:275
|
||||
#: client/src/templates/workflows.form.js:96
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
@ -3414,7 +3418,7 @@ msgid "PLEASE ADD A SURVEY PROMPT."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/organizations/list/organizations-list.partial.html:37
|
||||
#: client/src/shared/form-generator.js:1892
|
||||
#: client/src/shared/form-generator.js:1905
|
||||
#: client/src/shared/list-generator/list-generator.factory.js:248
|
||||
msgid "PLEASE ADD ITEMS TO THIS LIST"
|
||||
msgstr ""
|
||||
@ -3448,7 +3452,7 @@ msgstr ""
|
||||
msgid "Pagerduty subdomain"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:359
|
||||
#: client/src/templates/job_templates/job-template.form.js:363
|
||||
msgid "Pass extra command line variables to the playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax."
|
||||
msgstr ""
|
||||
|
||||
@ -3531,7 +3535,7 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:106
|
||||
#: client/src/projects/projects.form.js:247
|
||||
#: client/src/teams/teams.form.js:120
|
||||
#: client/src/templates/job_templates/job-template.form.js:398
|
||||
#: client/src/templates/job_templates/job-template.form.js:402
|
||||
#: client/src/templates/workflows.form.js:139
|
||||
#: client/src/users/users.form.js:189
|
||||
msgid "Permissions"
|
||||
@ -3542,7 +3546,7 @@ msgid "Personal Access Token"
|
||||
msgstr ""
|
||||
|
||||
#: client/features/output/output.strings.js:63
|
||||
#: client/src/shared/form-generator.js:1076
|
||||
#: client/src/shared/form-generator.js:1085
|
||||
#: client/src/templates/job_templates/job-template.form.js:107
|
||||
#: client/src/templates/job_templates/job-template.form.js:115
|
||||
msgid "Playbook"
|
||||
@ -3598,15 +3602,15 @@ msgstr ""
|
||||
msgid "Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1165
|
||||
#: client/src/shared/form-generator.js:1178
|
||||
msgid "Please enter a number greater than %d and less than %d."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1167
|
||||
#: client/src/shared/form-generator.js:1180
|
||||
msgid "Please enter a number greater than %d."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1159
|
||||
#: client/src/shared/form-generator.js:1172
|
||||
msgid "Please enter a number."
|
||||
msgstr ""
|
||||
|
||||
@ -3708,7 +3712,7 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:102
|
||||
#: client/src/projects/projects.form.js:239
|
||||
#: client/src/teams/teams.form.js:116
|
||||
#: client/src/templates/job_templates/job-template.form.js:391
|
||||
#: client/src/templates/job_templates/job-template.form.js:395
|
||||
#: client/src/templates/workflows.form.js:132
|
||||
msgid "Please save before assigning permissions."
|
||||
msgstr ""
|
||||
@ -3772,11 +3776,11 @@ msgstr ""
|
||||
msgid "Please select Users from the list below."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1200
|
||||
#: client/src/shared/form-generator.js:1213
|
||||
msgid "Please select a number between"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1196
|
||||
#: client/src/shared/form-generator.js:1209
|
||||
msgid "Please select a number."
|
||||
msgstr ""
|
||||
|
||||
@ -3784,10 +3788,10 @@ msgstr ""
|
||||
msgid "Please select a value"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1088
|
||||
#: client/src/shared/form-generator.js:1156
|
||||
#: client/src/shared/form-generator.js:1277
|
||||
#: client/src/shared/form-generator.js:1385
|
||||
#: client/src/shared/form-generator.js:1097
|
||||
#: client/src/shared/form-generator.js:1169
|
||||
#: client/src/shared/form-generator.js:1290
|
||||
#: client/src/shared/form-generator.js:1398
|
||||
msgid "Please select a value."
|
||||
msgstr ""
|
||||
|
||||
@ -3799,7 +3803,7 @@ msgstr ""
|
||||
msgid "Please select an organization before editing the host filter."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1193
|
||||
#: client/src/shared/form-generator.js:1206
|
||||
msgid "Please select at least one value."
|
||||
msgstr ""
|
||||
|
||||
@ -3942,8 +3946,8 @@ msgstr ""
|
||||
#: client/src/templates/job_templates/job-template.form.js:185
|
||||
#: client/src/templates/job_templates/job-template.form.js:202
|
||||
#: client/src/templates/job_templates/job-template.form.js:219
|
||||
#: client/src/templates/job_templates/job-template.form.js:266
|
||||
#: client/src/templates/job_templates/job-template.form.js:366
|
||||
#: client/src/templates/job_templates/job-template.form.js:270
|
||||
#: client/src/templates/job_templates/job-template.form.js:370
|
||||
#: client/src/templates/job_templates/job-template.form.js:60
|
||||
#: client/src/templates/job_templates/job-template.form.js:86
|
||||
msgid "Prompt on launch"
|
||||
@ -3982,8 +3986,8 @@ msgstr ""
|
||||
msgid "Provide the named URL encoded name or id of the remote Tower inventory to be imported."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:322
|
||||
#: client/src/templates/job_templates/job-template.form.js:330
|
||||
#: client/src/templates/job_templates/job-template.form.js:326
|
||||
#: client/src/templates/job_templates/job-template.form.js:334
|
||||
msgid "Provisioning Callback URL"
|
||||
msgstr ""
|
||||
|
||||
@ -4492,7 +4496,7 @@ msgstr ""
|
||||
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:193
|
||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:158
|
||||
#: client/src/scheduler/scheduler.strings.js:57
|
||||
#: client/src/shared/form-generator.js:1698
|
||||
#: client/src/shared/form-generator.js:1711
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@ -4543,7 +4547,7 @@ msgstr ""
|
||||
#: client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js:35
|
||||
#: client/src/inventories-hosts/inventories/related/sources/sources.form.js:440
|
||||
#: client/src/projects/projects.form.js:290
|
||||
#: client/src/templates/job_templates/job-template.form.js:444
|
||||
#: client/src/templates/job_templates/job-template.form.js:448
|
||||
#: client/src/templates/workflows.form.js:185
|
||||
msgid "Schedules"
|
||||
msgstr ""
|
||||
@ -4566,7 +4570,7 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1702
|
||||
#: client/src/shared/form-generator.js:1715
|
||||
#: client/src/shared/lookup/lookup-modal.directive.js:59
|
||||
#: client/src/shared/lookup/lookup-modal.partial.html:20
|
||||
msgid "Select"
|
||||
@ -4634,7 +4638,7 @@ msgstr ""
|
||||
msgid "Select the Instance Groups for this Inventory to run on. Refer to the Ansible Tower documentation for more detail."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:250
|
||||
#: client/src/templates/job_templates/job-template.form.js:254
|
||||
msgid "Select the Instance Groups for this Job Template to run on."
|
||||
msgstr ""
|
||||
|
||||
@ -4642,7 +4646,7 @@ msgstr ""
|
||||
msgid "Select the Instance Groups for this Organization to run on."
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:240
|
||||
#: client/src/templates/job_templates/job-template.form.js:244
|
||||
msgid "Select the custom Python virtual environment for this job template to run on."
|
||||
msgstr ""
|
||||
|
||||
@ -4709,8 +4713,8 @@ msgstr ""
|
||||
#: client/features/templates/templates.strings.js:46
|
||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:115
|
||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:118
|
||||
#: client/src/templates/job_templates/job-template.form.js:257
|
||||
#: client/src/templates/job_templates/job-template.form.js:260
|
||||
#: client/src/templates/job_templates/job-template.form.js:261
|
||||
#: client/src/templates/job_templates/job-template.form.js:264
|
||||
msgid "Show Changes"
|
||||
msgstr ""
|
||||
|
||||
@ -4757,7 +4761,7 @@ msgstr ""
|
||||
#: client/src/inventories-hosts/inventories/inventory.list.js:86
|
||||
#: client/src/inventories-hosts/inventories/list/inventory-list.controller.js:76
|
||||
#: client/src/organizations/linkout/controllers/organizations-inventories.controller.js:70
|
||||
#: client/src/shared/form-generator.js:1463
|
||||
#: client/src/shared/form-generator.js:1476
|
||||
msgid "Smart Inventory"
|
||||
msgstr ""
|
||||
|
||||
@ -5111,8 +5115,8 @@ msgstr ""
|
||||
msgid "Textarea"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1393
|
||||
#: client/src/shared/form-generator.js:1399
|
||||
#: client/src/shared/form-generator.js:1406
|
||||
#: client/src/shared/form-generator.js:1412
|
||||
msgid "That value was not found. Please enter or select a valid value."
|
||||
msgstr ""
|
||||
|
||||
@ -5564,12 +5568,12 @@ msgstr ""
|
||||
|
||||
#: client/src/organizations/organizations.form.js:48
|
||||
#: client/src/projects/projects.form.js:209
|
||||
#: client/src/templates/job_templates/job-template.form.js:237
|
||||
#: client/src/templates/job_templates/job-template.form.js:241
|
||||
msgid "Use Default Environment"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/templates/job_templates/job-template.form.js:310
|
||||
#: client/src/templates/job_templates/job-template.form.js:315
|
||||
#: client/src/templates/job_templates/job-template.form.js:314
|
||||
#: client/src/templates/job_templates/job-template.form.js:319
|
||||
msgid "Use Fact Cache"
|
||||
msgstr ""
|
||||
|
||||
@ -5764,8 +5768,8 @@ msgstr ""
|
||||
msgid "View Project checkout results"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1726
|
||||
#: client/src/templates/job_templates/job-template.form.js:455
|
||||
#: client/src/shared/form-generator.js:1739
|
||||
#: client/src/templates/job_templates/job-template.form.js:459
|
||||
#: client/src/templates/workflows.form.js:196
|
||||
msgid "View Survey"
|
||||
msgstr ""
|
||||
@ -5946,7 +5950,7 @@ msgstr ""
|
||||
msgid "Workflow Templates"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1730
|
||||
#: client/src/shared/form-generator.js:1743
|
||||
#: client/src/templates/workflows.form.js:222
|
||||
msgid "Workflow Visualizer"
|
||||
msgstr ""
|
||||
@ -6032,7 +6036,7 @@ msgstr ""
|
||||
|
||||
#: client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html:24
|
||||
#: client/src/job-submission/job-submission.partial.html:317
|
||||
#: client/src/shared/form-generator.js:1200
|
||||
#: client/src/shared/form-generator.js:1213
|
||||
#: client/src/templates/prompt/steps/survey/prompt-survey.partial.html:42
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
@ -6144,7 +6148,7 @@ msgstr ""
|
||||
msgid "organization"
|
||||
msgstr ""
|
||||
|
||||
#: client/src/shared/form-generator.js:1076
|
||||
#: client/src/shared/form-generator.js:1085
|
||||
msgid "playbook"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user