mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 20:30:46 -03:30
Merge pull request #3657 from wwitzel3/jtabor-sockets
Switch to Django Channels
This commit is contained in:
commit
594fa8cf89
1
Makefile
1
Makefile
@ -364,7 +364,6 @@ server_noattach:
|
||||
tmux new-window 'exec make receiver'
|
||||
tmux select-window -t tower:1
|
||||
tmux rename-window 'Extra Services'
|
||||
tmux split-window -v 'exec make socketservice'
|
||||
tmux split-window -h 'exec make factcacher'
|
||||
|
||||
server: server_noattach
|
||||
|
||||
1
Procfile
1
Procfile
@ -1,6 +1,5 @@
|
||||
runserver: make runserver
|
||||
celeryd: make celeryd
|
||||
receiver: make receiver
|
||||
socketservice: make socketservice
|
||||
factcacher: make factcacher
|
||||
flower: make flower
|
||||
|
||||
@ -68,7 +68,7 @@ from awx.api.permissions import * # noqa
|
||||
from awx.api.renderers import * # noqa
|
||||
from awx.api.serializers import * # noqa
|
||||
from awx.api.metadata import RoleMetadata
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
logger = logging.getLogger('awx.api.views')
|
||||
|
||||
@ -532,11 +532,9 @@ class AuthTokenView(APIView):
|
||||
# Mark them as invalid and inform the user
|
||||
invalid_tokens = AuthToken.get_tokens_over_limit(serializer.validated_data['user'])
|
||||
for t in invalid_tokens:
|
||||
# TODO: send socket notification
|
||||
emit_websocket_notification('/socket.io/control',
|
||||
'limit_reached',
|
||||
dict(reason=force_text(AuthToken.reason_long('limit_reached'))),
|
||||
token_key=t.key)
|
||||
emit_channel_notification('control-limit_reached', dict(group_name='control',
|
||||
reason=force_text(AuthToken.reason_long('limit_reached')),
|
||||
token_key=t.key))
|
||||
t.invalidate(reason='limit_reached')
|
||||
|
||||
# Note: This header is normally added in the middleware whenever an
|
||||
@ -2654,7 +2652,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
|
||||
relationship = ''
|
||||
enforce_parent_relationship = 'workflow_job_template'
|
||||
new_in_310 = True
|
||||
|
||||
|
||||
'''
|
||||
Limit the set of WorkflowJobTemplateNodes to the related nodes of specified by
|
||||
'relationship'
|
||||
@ -2663,7 +2661,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
return getattr(parent, self.relationship).all()
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
||||
relationship = 'success_nodes'
|
||||
|
||||
@ -2684,7 +2682,7 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView):
|
||||
enforce_parent_relationship = 'workflow_job_template'
|
||||
new_in_310 = True
|
||||
'''
|
||||
|
||||
|
||||
#
|
||||
#Limit the set of WorkflowJobeNodes to the related nodes of specified by
|
||||
#'relationship'
|
||||
@ -2693,7 +2691,7 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
return getattr(parent, self.relationship).all()
|
||||
|
||||
|
||||
class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList):
|
||||
relationship = 'success_nodes'
|
||||
|
||||
@ -2768,6 +2766,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView):
|
||||
relationship = 'jobs'
|
||||
parent_key = 'workflow_job_template'
|
||||
|
||||
# TODO:
|
||||
class WorkflowJobList(ListCreateAPIView):
|
||||
|
||||
model = WorkflowJob
|
||||
@ -3174,21 +3173,8 @@ class JobJobTasksList(BaseJobEventsList):
|
||||
return ({'detail': 'Parent event not found.'}, -1, status.HTTP_404_NOT_FOUND)
|
||||
parent_task = parent_task[0]
|
||||
|
||||
# Some events correspond to a playbook or task starting up,
|
||||
# and these are what we're interested in here.
|
||||
STARTING_EVENTS = ('playbook_on_task_start', 'playbook_on_setup')
|
||||
|
||||
# We need to pull information about each start event.
|
||||
#
|
||||
# This is super tricky, because this table has a one-to-many
|
||||
# relationship with itself (parent-child), and we're getting
|
||||
# information for an arbitrary number of children. This means we
|
||||
# need stats on grandchildren, sorted by child.
|
||||
queryset = (JobEvent.objects.filter(parent__parent=parent_task,
|
||||
parent__event__in=STARTING_EVENTS)
|
||||
.values('parent__id', 'event', 'changed')
|
||||
.annotate(num=Count('event'))
|
||||
.order_by('parent__id'))
|
||||
queryset = JobEvent.get_startevent_queryset(parent_task, STARTING_EVENTS)
|
||||
|
||||
# The data above will come back in a list, but we are going to
|
||||
# want to access it based on the parent id, so map it into a
|
||||
@ -3766,7 +3752,7 @@ class RoleList(ListAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
result = Role.visible_roles(self.request.user)
|
||||
# Sanity check: is the requesting user an orphaned non-admin/auditor?
|
||||
# Sanity check: is the requesting user an orphaned non-admin/auditor?
|
||||
# if yes, make system admin/auditor mandatorily visible.
|
||||
if not self.request.user.organizations.exists() and\
|
||||
not self.request.user.is_superuser and\
|
||||
|
||||
37
awx/asgi.py
Normal file
37
awx/asgi.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
import os
|
||||
import logging
|
||||
from awx import __version__ as tower_version
|
||||
|
||||
# Prepare the AWX environment.
|
||||
from awx import prepare_env, MODE
|
||||
prepare_env()
|
||||
|
||||
from django.core.wsgi import get_wsgi_application # NOQA
|
||||
|
||||
"""
|
||||
ASGI config for AWX project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``channel_layer``.
|
||||
|
||||
For more information on this file, see
|
||||
https://channels.readthedocs.io/en/latest/deploying.html
|
||||
"""
|
||||
|
||||
if MODE == 'production':
|
||||
logger = logging.getLogger('awx.main.models.jobs')
|
||||
try:
|
||||
fd = open("/var/lib/awx/.tower_version", "r")
|
||||
if fd.read().strip() != tower_version:
|
||||
raise Exception()
|
||||
except Exception:
|
||||
logger.error("Missing or incorrect metadata for Tower version. Ensure Tower was installed using the setup playbook.")
|
||||
raise Exception("Missing or incorrect metadata for Tower version. Ensure Tower was installed using the setup playbook.")
|
||||
|
||||
from channels.asgi import get_channel_layer
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings")
|
||||
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
39
awx/main/consumers.py
Normal file
39
awx/main/consumers.py
Normal file
@ -0,0 +1,39 @@
|
||||
import json
|
||||
|
||||
from channels import Group
|
||||
from channels.sessions import channel_session
|
||||
|
||||
|
||||
def discard_groups(message):
|
||||
if 'groups' in message.channel_session:
|
||||
for group in message.channel_session['groups']:
|
||||
Group(group).discard(message.reply_channel)
|
||||
|
||||
@channel_session
|
||||
def ws_disconnect(message):
|
||||
discard_groups(message)
|
||||
|
||||
@channel_session
|
||||
def ws_receive(message):
|
||||
raw_data = message.content['text']
|
||||
data = json.loads(raw_data)
|
||||
|
||||
if 'groups' in data:
|
||||
discard_groups(message)
|
||||
groups = data['groups']
|
||||
current_groups = message.channel_session.pop('groups') if 'groups' in message.channel_session else []
|
||||
for group_name,v in groups.items():
|
||||
if type(v) is list:
|
||||
for oid in v:
|
||||
name = '{}-{}'.format(group_name, oid)
|
||||
current_groups.append(name)
|
||||
Group(name).add(message.reply_channel)
|
||||
else:
|
||||
current_groups.append(group_name)
|
||||
Group(group_name).add(message.reply_channel)
|
||||
message.channel_session['groups'] = current_groups
|
||||
|
||||
|
||||
def emit_channel_notification(group, payload):
|
||||
payload = json.dumps(payload)
|
||||
Group(group).send({"text": json.dumps(payload)})
|
||||
22
awx/main/migrations/0039_v310_channelgroup.py
Normal file
22
awx/main/migrations/0039_v310_channelgroup.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0038_v310_workflow_rbac_prompts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChannelGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('group', models.CharField(unique=True, max_length=200)),
|
||||
('channels', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -22,6 +22,7 @@ from awx.main.models.notifications import * # noqa
|
||||
from awx.main.models.fact import * # noqa
|
||||
from awx.main.models.label import * # noqa
|
||||
from awx.main.models.workflow import * # noqa
|
||||
from awx.main.models.channels import * # noqa
|
||||
|
||||
# Monkeypatch Django serializer to ignore django-taggit fields (which break
|
||||
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
||||
|
||||
5
awx/main/models/channels.py
Normal file
5
awx/main/models/channels.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
class ChannelGroup(models.Model):
|
||||
group = models.CharField(max_length=200, unique=True)
|
||||
channels = models.TextField()
|
||||
@ -1222,7 +1222,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
||||
from awx.main.tasks import RunInventoryUpdate
|
||||
return RunInventoryUpdate
|
||||
|
||||
def socketio_emit_data(self):
|
||||
def websocket_emit_data(self):
|
||||
if self.inventory_source.group is not None:
|
||||
return dict(group_id=self.inventory_source.group.id)
|
||||
return {}
|
||||
|
||||
@ -29,12 +29,13 @@ from awx.main.models.notifications import (
|
||||
JobNotificationMixin,
|
||||
)
|
||||
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.redact import PlainTextCleaner
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
from awx.main.models.base import PERM_INVENTORY_SCAN
|
||||
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.models.jobs')
|
||||
|
||||
@ -1270,11 +1271,10 @@ class JobEvent(CreatedModifiedModel):
|
||||
if update_fields:
|
||||
host_summary.save(update_fields=update_fields)
|
||||
job.inventory.update_computed_fields()
|
||||
emit_websocket_notification('/socket.io/jobs', 'summary_complete', dict(unified_job_id=job.id))
|
||||
|
||||
emit_channel_notification('jobs-summary', dict(group_name='jobs', unified_job_id=job.id))
|
||||
|
||||
@classmethod
|
||||
def start_event_queryset(cls, parent_task, starting_events, ordering=None):
|
||||
def get_startevent_queryset(cls, parent_task, starting_events, ordering=None):
|
||||
'''
|
||||
We need to pull information about each start event.
|
||||
|
||||
@ -1380,7 +1380,7 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
|
||||
from awx.main.tasks import RunSystemJob
|
||||
return RunSystemJob
|
||||
|
||||
def socketio_emit_data(self):
|
||||
def websocket_emit_data(self):
|
||||
return {}
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -1421,4 +1421,3 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
|
||||
|
||||
def get_notification_friendly_name(self):
|
||||
return "System Job"
|
||||
|
||||
|
||||
@ -407,7 +407,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def socketio_emit_data(self):
|
||||
def websocket_emit_data(self):
|
||||
return dict(project_id=self.project.id)
|
||||
|
||||
@property
|
||||
|
||||
@ -16,7 +16,8 @@ from jsonfield import JSONField
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.utils import ignore_inventory_computed_fields, emit_websocket_notification
|
||||
from awx.main.utils import ignore_inventory_computed_fields
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
logger = logging.getLogger('awx.main.models.schedule')
|
||||
@ -112,7 +113,7 @@ class Schedule(CommonModel):
|
||||
self.dtend = make_aware(datetime.datetime.strptime(until_date, "%Y%m%dT%H%M%SZ"), get_default_timezone())
|
||||
if 'count' in self.rrule.lower():
|
||||
self.dtend = future_rs[-1]
|
||||
emit_websocket_notification('/socket.io/schedules', 'schedule_changed', dict(id=self.id))
|
||||
emit_channel_notification('schedules-changed', dict(id=self.id, group_name='schedules'))
|
||||
with ignore_inventory_computed_fields():
|
||||
self.unified_job_template.update_computed_fields()
|
||||
|
||||
|
||||
@ -32,8 +32,9 @@ from djcelery.models import TaskMeta
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.schedules import Schedule
|
||||
from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates
|
||||
from awx.main.redact import UriCleaner, REPLACE_STR
|
||||
from awx.main.utils import decrypt_field, _inventory_updates
|
||||
from awx.main.redact import UriCleaner
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
__all__ = ['UnifiedJobTemplate', 'UnifiedJob']
|
||||
|
||||
@ -774,14 +775,15 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
''' Given another task object determine if this task would be blocked by it '''
|
||||
raise NotImplementedError # Implement in subclass.
|
||||
|
||||
def socketio_emit_data(self):
|
||||
def websocket_emit_data(self):
|
||||
''' Return extra data that should be included when submitting data to the browser over the websocket connection '''
|
||||
return {}
|
||||
|
||||
def socketio_emit_status(self, status):
|
||||
def websocket_emit_status(self, status):
|
||||
status_data = dict(unified_job_id=self.id, status=status)
|
||||
status_data.update(self.socketio_emit_data())
|
||||
emit_websocket_notification('/socket.io/jobs', 'status_changed', status_data)
|
||||
status_data.update(self.websocket_emit_data())
|
||||
status_data['group_name'] = 'jobs'
|
||||
emit_channel_notification('jobs-status_changed', status_data)
|
||||
|
||||
def generate_dependencies(self, active_tasks):
|
||||
''' Generate any tasks that the current task might be dependent on given a list of active
|
||||
@ -859,7 +861,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
# Save the pending status, and inform the SocketIO listener.
|
||||
self.update_fields(start_args=json.dumps(kwargs), status='pending')
|
||||
self.socketio_emit_status("pending")
|
||||
self.websocket_emit_status("pending")
|
||||
|
||||
from awx.main.scheduler.tasks import run_job_launch
|
||||
run_job_launch.delay(self.id)
|
||||
@ -912,7 +914,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
instance.job_explanation = 'Forced cancel'
|
||||
update_fields.append('job_explanation')
|
||||
instance.save(update_fields=update_fields)
|
||||
self.socketio_emit_status("canceled")
|
||||
self.websocket_emit_status("canceled")
|
||||
except: # FIXME: Log this exception!
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
@ -926,8 +928,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
self.status = 'canceled'
|
||||
cancel_fields.append('status')
|
||||
self.save(update_fields=cancel_fields)
|
||||
self.socketio_emit_status("canceled")
|
||||
self.websocket_emit_status("canceled")
|
||||
if settings.BROKER_URL.startswith('amqp://'):
|
||||
self._force_cancel()
|
||||
return self.cancel_flag
|
||||
|
||||
|
||||
7
awx/main/routing.py
Normal file
7
awx/main/routing.py
Normal file
@ -0,0 +1,7 @@
|
||||
from channels.routing import route
|
||||
|
||||
|
||||
channel_routing = [
|
||||
route("websocket.disconnect", "awx.main.consumers.ws_disconnect", path=r'^/websocket/$'),
|
||||
route("websocket.receive", "awx.main.consumers.ws_receive", path=r'^/websocket/$'),
|
||||
]
|
||||
@ -62,7 +62,7 @@ def spawn_workflow_graph_jobs(workflow_jobs):
|
||||
job.status = 'failed'
|
||||
job.job_explanation = "Workflow job could not start because it was not in the right state or required manual credentials"
|
||||
job.save(update_fields=['status', 'job_explanation'])
|
||||
job.socketio_emit_status("failed")
|
||||
job.websocket_emit_status("failed")
|
||||
|
||||
# TODO: should we emit a status on the socket here similar to tasks.py tower_periodic_scheduler() ?
|
||||
#emit_websocket_notification('/socket.io/jobs', '', dict(id=))
|
||||
@ -76,7 +76,7 @@ def process_finished_workflow_jobs(workflow_jobs):
|
||||
# TODO: detect if wfj failed
|
||||
workflow_job.status = 'completed'
|
||||
workflow_job.save()
|
||||
workflow_job.socketio_emit_status('completed')
|
||||
workflow_job.websocket_emit_status('completed')
|
||||
|
||||
def rebuild_graph():
|
||||
"""Regenerate the task graph by refreshing known tasks from Tower, purging
|
||||
@ -120,7 +120,7 @@ def rebuild_graph():
|
||||
logger.debug("Active celery tasks: " + str(active_tasks))
|
||||
for task in list(running_celery_tasks):
|
||||
if (task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR')):
|
||||
# NOTE: Pull status again and make sure it didn't finish in
|
||||
# NOTE: Pull status again and make sure it didn't finish in
|
||||
# the meantime?
|
||||
task.status = 'failed'
|
||||
task.job_explanation += ' '.join((
|
||||
@ -128,8 +128,8 @@ def rebuild_graph():
|
||||
'Celery, so it has been marked as failed.',
|
||||
))
|
||||
task.save()
|
||||
task.socketio_emit_status("failed")
|
||||
running_tasks.pop(task)
|
||||
task.websocket_emit_status("failed")
|
||||
running_tasks.pop(running_tasks.index(task))
|
||||
logger.error("Task %s appears orphaned... marking as failed" % task)
|
||||
|
||||
# Create and process dependencies for new tasks
|
||||
@ -142,7 +142,7 @@ def rebuild_graph():
|
||||
task.status = 'failed'
|
||||
task.job_explanation += 'Task failed to generate dependencies: {}'.format(e)
|
||||
task.save()
|
||||
task.socketio_emit_status("failed")
|
||||
task.websocket_emit_status("failed")
|
||||
continue
|
||||
logger.debug("New dependencies: %s" % str(task_dependencies))
|
||||
for dep in task_dependencies:
|
||||
@ -202,7 +202,7 @@ def process_graph(graph, task_capacity):
|
||||
|
||||
node_type = graph.get_node_type(node_obj)
|
||||
if node_type == 'job':
|
||||
# clear dependencies because a job can block (not necessarily
|
||||
# clear dependencies because a job can block (not necessarily
|
||||
# depend) on other jobs that share the same job template
|
||||
node_dependencies = []
|
||||
|
||||
@ -215,7 +215,7 @@ def process_graph(graph, task_capacity):
|
||||
node_obj.start()
|
||||
spawn_workflow_graph_jobs([node_obj])
|
||||
return process_graph(graph, task_capacity)
|
||||
|
||||
|
||||
dependent_nodes = [{'type': graph.get_node_type(node_obj), 'id': node_obj.id}] + \
|
||||
[{'type': graph.get_node_type(n['node_object']),
|
||||
'id': n['node_object'].id} for n in node_dependencies]
|
||||
|
||||
@ -19,10 +19,12 @@ from crum.signals import current_user_getter
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.api.serializers import * # noqa
|
||||
from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, emit_websocket_notification
|
||||
from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore
|
||||
from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates
|
||||
from awx.main.tasks import update_inventory_computed_fields
|
||||
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
__all__ = []
|
||||
|
||||
logger = logging.getLogger('awx.main.signals')
|
||||
@ -33,13 +35,15 @@ logger = logging.getLogger('awx.main.signals')
|
||||
def emit_job_event_detail(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
created = kwargs['created']
|
||||
print("before created job_event_detail")
|
||||
if created:
|
||||
event_serialized = JobEventSerializer(instance).data
|
||||
event_serialized['id'] = instance.id
|
||||
event_serialized["created"] = event_serialized["created"].isoformat()
|
||||
event_serialized["modified"] = event_serialized["modified"].isoformat()
|
||||
event_serialized["event_name"] = instance.event
|
||||
emit_websocket_notification('/socket.io/job_events', 'job_events-' + str(instance.job.id), event_serialized)
|
||||
event_serialized["group_name"] = "job_events"
|
||||
emit_channel_notification('job_events-' + str(instance.job.id), event_serialized)
|
||||
|
||||
def emit_ad_hoc_command_event_detail(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
@ -50,7 +54,8 @@ def emit_ad_hoc_command_event_detail(sender, **kwargs):
|
||||
event_serialized["created"] = event_serialized["created"].isoformat()
|
||||
event_serialized["modified"] = event_serialized["modified"].isoformat()
|
||||
event_serialized["event_name"] = instance.event
|
||||
emit_websocket_notification('/socket.io/ad_hoc_command_events', 'ad_hoc_command_events-' + str(instance.ad_hoc_command_id), event_serialized)
|
||||
event_serialized["group_name"] = "ad_hoc_command_events"
|
||||
emit_channel_notification('ad_hoc_command_events-' + str(instance.ad_hoc_command_id), event_serialized)
|
||||
|
||||
def emit_update_inventory_computed_fields(sender, **kwargs):
|
||||
logger.debug("In update inventory computed fields")
|
||||
|
||||
@ -49,13 +49,13 @@ from awx.main.models import * # noqa
|
||||
from awx.main.models import UnifiedJob
|
||||
from awx.main.task_engine import TaskEnhancer
|
||||
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
|
||||
emit_websocket_notification,
|
||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
'RunAdHocCommand', 'RunWorkflowJob', 'handle_work_error',
|
||||
'handle_work_success', 'update_inventory_computed_fields',
|
||||
'send_notifications', 'run_administrative_checks',
|
||||
'RunAdHocCommand', 'RunWorkflowJob', 'handle_work_error',
|
||||
'handle_work_success', 'update_inventory_computed_fields',
|
||||
'send_notifications', 'run_administrative_checks',
|
||||
'RunJobLaunch']
|
||||
|
||||
HIDDEN_PASSWORD = '**********'
|
||||
@ -183,8 +183,8 @@ def tower_periodic_scheduler(self):
|
||||
new_unified_job.status = 'failed'
|
||||
new_unified_job.job_explanation = "Scheduled job could not start because it was not in the right state or required manual credentials"
|
||||
new_unified_job.save(update_fields=['status', 'job_explanation'])
|
||||
new_unified_job.socketio_emit_status("failed")
|
||||
emit_websocket_notification('/socket.io/schedules', 'schedule_changed', dict(id=schedule.id))
|
||||
new_unified_job.websocket_emit_status("failed")
|
||||
emit_channel_notification('schedules-changed', dict(id=schedule.id, group_name="schedules"))
|
||||
|
||||
def _send_notification_templates(instance, status_str):
|
||||
if status_str not in ['succeeded', 'failed']:
|
||||
@ -237,11 +237,11 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
|
||||
(first_instance_type, first_instance.name, first_instance.id)
|
||||
instance.save()
|
||||
instance.socketio_emit_status("failed")
|
||||
instance.websocket_emit_status("failed")
|
||||
|
||||
if first_instance:
|
||||
_send_notification_templates(first_instance, 'failed')
|
||||
|
||||
|
||||
# We only send 1 job complete message since all the job completion message
|
||||
# handling does is trigger the scheduler. If we extend the functionality of
|
||||
# what the job complete message handler does then we may want to send a
|
||||
@ -590,7 +590,7 @@ class BaseTask(Task):
|
||||
'''
|
||||
instance = self.update_model(pk, status='running', celery_task_id=self.request.id)
|
||||
|
||||
instance.socketio_emit_status("running")
|
||||
instance.websocket_emit_status("running")
|
||||
status, rc, tb = 'error', None, ''
|
||||
output_replacements = []
|
||||
try:
|
||||
@ -659,7 +659,7 @@ class BaseTask(Task):
|
||||
instance = self.update_model(pk, status=status, result_traceback=tb,
|
||||
output_replacements=output_replacements)
|
||||
self.post_run_hook(instance, **kwargs)
|
||||
instance.socketio_emit_status(status)
|
||||
instance.websocket_emit_status(status)
|
||||
if status != 'successful' and not hasattr(settings, 'CELERY_UNIT_TEST'):
|
||||
# Raising an exception will mark the job as 'failed' in celery
|
||||
# and will stop a task chain from continuing to execute
|
||||
@ -1678,7 +1678,7 @@ class RunSystemJob(BaseTask):
|
||||
|
||||
'''
|
||||
class RunWorkflowJob(BaseTask):
|
||||
|
||||
|
||||
name = 'awx.main.tasks.run_workflow_job'
|
||||
model = WorkflowJob
|
||||
|
||||
@ -1691,14 +1691,14 @@ class RunWorkflowJob(BaseTask):
|
||||
# complete. Instead, the workflow job should return or never even run,
|
||||
# because all of the "launch logic" can be done schedule().
|
||||
|
||||
# However, other aspects of our system depend on a 1-1 relationship
|
||||
# However, other aspects of our system depend on a 1-1 relationship
|
||||
# between a Job and a Celery Task.
|
||||
#
|
||||
#
|
||||
# * If we let the workflow job task (RunWorkflowJob.run()) complete
|
||||
# then how do we trigger the handle_work_error and
|
||||
# then how do we trigger the handle_work_error and
|
||||
# handle_work_success subtasks?
|
||||
#
|
||||
# * How do we handle the recovery process? (i.e. there is an entry in
|
||||
# * How do we handle the recovery process? (i.e. there is an entry in
|
||||
# the database but not in celery).
|
||||
while True:
|
||||
dag = WorkflowDAG(instance)
|
||||
|
||||
@ -491,19 +491,6 @@ def get_system_task_capacity():
|
||||
return 50 + ((int(total_mem_value) / 1024) - 2) * 75
|
||||
|
||||
|
||||
def emit_websocket_notification(endpoint, event, payload, token_key=None):
|
||||
from awx.main.socket_queue import Socket
|
||||
|
||||
try:
|
||||
with Socket('websocket', 'w', nowait=True, logger=logger) as websocket:
|
||||
if token_key:
|
||||
payload['token_key'] = token_key
|
||||
payload['event'] = event
|
||||
payload['endpoint'] = endpoint
|
||||
websocket.publish(payload)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_inventory_updates = threading.local()
|
||||
|
||||
|
||||
|
||||
@ -192,6 +192,7 @@ INSTALLED_APPS = (
|
||||
'django_extensions',
|
||||
'djcelery',
|
||||
'kombu.transport.django',
|
||||
'channels',
|
||||
'polymorphic',
|
||||
'taggit',
|
||||
'social.apps.django_app.default',
|
||||
@ -375,7 +376,7 @@ CELERY_ROUTES = {'awx.main.tasks.run_job': {'queue': 'jobs',
|
||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'default',
|
||||
'routing_key': 'cluster.heartbeat'},
|
||||
}
|
||||
|
||||
|
||||
CELERYBEAT_SCHEDULE = {
|
||||
'tower_scheduler': {
|
||||
'task': 'awx.main.tasks.tower_periodic_scheduler',
|
||||
@ -976,4 +977,3 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ if is_testing(sys.argv):
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MONGO_DB = 'system_tracking_test'
|
||||
|
||||
# Celery AMQP configuration.
|
||||
@ -84,6 +84,12 @@ BROKER_URL = "amqp://{}:{}@{}/{}".format(os.environ.get("RABBITMQ_USER"),
|
||||
os.environ.get("RABBITMQ_HOST"),
|
||||
os.environ.get("RABBITMQ_VHOST"))
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {'BACKEND': 'asgi_amqp.AMQPChannelLayer',
|
||||
'ROUTING': 'awx.main.routing.channel_routing',
|
||||
'CONFIG': {'url': BROKER_URL}}
|
||||
}
|
||||
|
||||
# Mongo host configuration
|
||||
MONGO_HOST = NotImplemented
|
||||
|
||||
|
||||
@ -29,7 +29,6 @@ import './helpers';
|
||||
import './forms';
|
||||
import './lists';
|
||||
import './widgets';
|
||||
import './help';
|
||||
import './filters';
|
||||
import { Home } from './controllers/Home';
|
||||
import { SocketsController } from './controllers/Sockets';
|
||||
@ -75,7 +74,6 @@ import './shared/Modal';
|
||||
import './shared/prompt-dialog';
|
||||
import './shared/directives';
|
||||
import './shared/filters';
|
||||
import './shared/Socket';
|
||||
import './shared/features/main';
|
||||
import config from './shared/config/main';
|
||||
import './login/authenticationServices/pendo/ng-pendo';
|
||||
@ -183,7 +181,6 @@ var tower = angular.module('Tower', [
|
||||
'HostGroupsFormDefinition',
|
||||
'StreamWidget',
|
||||
'JobsHelper',
|
||||
'InventoryGroupsHelpDefinition',
|
||||
'CredentialsHelper',
|
||||
'StreamListDefinition',
|
||||
'ActivityDetailDefinition',
|
||||
@ -197,10 +194,8 @@ var tower = angular.module('Tower', [
|
||||
'StandardOutHelper',
|
||||
'LogViewerOptionsDefinition',
|
||||
'JobDetailHelper',
|
||||
'SocketIO',
|
||||
'lrInfiniteScroll',
|
||||
'LoadConfigHelper',
|
||||
'SocketHelper',
|
||||
'PortalJobsListDefinition',
|
||||
'features',
|
||||
'longDateFilter',
|
||||
@ -243,88 +238,6 @@ var tower = angular.module('Tower', [
|
||||
});
|
||||
|
||||
$stateProvider.
|
||||
state('dashboard', {
|
||||
url: '/home',
|
||||
templateUrl: urlPrefix + 'partials/home.html',
|
||||
controller: Home,
|
||||
params: { licenseMissing: null },
|
||||
data: {
|
||||
activityStream: true,
|
||||
refreshButton: true
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "DASHBOARD"
|
||||
},
|
||||
resolve: {
|
||||
graphData: ['$q', 'jobStatusGraphData', '$rootScope',
|
||||
function($q, jobStatusGraphData, $rootScope) {
|
||||
return $rootScope.featuresConfigured.promise.then(function() {
|
||||
return $q.all({
|
||||
jobStatus: jobStatusGraphData.get("month", "all"),
|
||||
});
|
||||
});
|
||||
}
|
||||
]
|
||||
}
|
||||
}).
|
||||
|
||||
state('jobs', {
|
||||
url: '/jobs',
|
||||
templateUrl: urlPrefix + 'partials/jobs.html',
|
||||
controller: JobsListController,
|
||||
ncyBreadcrumb: {
|
||||
label: "JOBS"
|
||||
}
|
||||
}).
|
||||
|
||||
state('projects', {
|
||||
url: '/projects?{status}',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsList,
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'project'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "PROJECTS"
|
||||
}
|
||||
}).
|
||||
|
||||
state('projects.add', {
|
||||
url: '/add',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsAdd,
|
||||
ncyBreadcrumb: {
|
||||
parent: "projects",
|
||||
label: "CREATE PROJECT"
|
||||
}
|
||||
}).
|
||||
|
||||
state('projects.edit', {
|
||||
url: '/:id',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsEdit,
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'projects',
|
||||
label: '{{name}}'
|
||||
}
|
||||
}).
|
||||
|
||||
state('projectOrganizations', {
|
||||
url: '/projects/:project_id/organizations',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsList
|
||||
}).
|
||||
|
||||
state('projectOrganizationAdd', {
|
||||
url: '/projects/:project_id/organizations/add',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsAdd
|
||||
}).
|
||||
|
||||
state('teams', {
|
||||
url: '/teams',
|
||||
templateUrl: urlPrefix + 'partials/teams.html',
|
||||
@ -526,18 +439,137 @@ var tower = angular.module('Tower', [
|
||||
}]);
|
||||
}])
|
||||
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log',
|
||||
.run(['$stateExtender', '$q', '$compile', '$cookieStore', '$rootScope', '$log',
|
||||
'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer',
|
||||
'ClearScope', 'Socket', 'LoadConfig', 'Store',
|
||||
'ShowSocketHelp', 'pendoService', 'Prompt', 'Rest', 'Wait',
|
||||
'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
|
||||
'FeaturesService', '$filter',
|
||||
function($q, $compile, $cookieStore, $rootScope, $log, CheckLicense,
|
||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait,
|
||||
'ClearScope', 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest',
|
||||
'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
|
||||
'FeaturesService', '$filter', 'SocketService',
|
||||
function($stateExtender, $q, $compile, $cookieStore, $rootScope, $log,
|
||||
CheckLicense, $location, Authorization, LoadBasePaths, Timer,
|
||||
ClearScope, LoadConfig, Store, pendoService, Prompt, Rest, Wait,
|
||||
ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService,
|
||||
$filter) {
|
||||
var sock;
|
||||
$filter, SocketService) {
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'dashboard',
|
||||
url: '/home',
|
||||
templateUrl: urlPrefix + 'partials/home.html',
|
||||
controller: Home,
|
||||
params: { licenseMissing: null },
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
refreshButton: true
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "DASHBOARD"
|
||||
},
|
||||
resolve: {
|
||||
graphData: ['$q', 'jobStatusGraphData', '$rootScope',
|
||||
function($q, jobStatusGraphData, $rootScope) {
|
||||
return $rootScope.featuresConfigured.promise.then(function() {
|
||||
return $q.all({
|
||||
jobStatus: jobStatusGraphData.get("month", "all"),
|
||||
});
|
||||
});
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'jobs',
|
||||
url: '/jobs',
|
||||
templateUrl: urlPrefix + 'partials/jobs.html',
|
||||
controller: JobsListController,
|
||||
ncyBreadcrumb: {
|
||||
label: "JOBS"
|
||||
},
|
||||
params: {
|
||||
search: {
|
||||
value: {order_by:'-finished'}
|
||||
}
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"],
|
||||
"schedules": ["changed"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'projects',
|
||||
url: '/projects?{status}',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsList,
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'project'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "PROJECTS"
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'projects.add',
|
||||
url: '/add',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsAdd,
|
||||
ncyBreadcrumb: {
|
||||
parent: "projects",
|
||||
label: "CREATE PROJECT"
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'projects.edit',
|
||||
url: '/:id',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsEdit,
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'projects',
|
||||
label: '{{name}}'
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'projectOrganizations',
|
||||
url: '/projects/:project_id/organizations',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsList
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'projectOrganizationAdd',
|
||||
url: '/projects/:project_id/organizations/add',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: OrganizationsAdd
|
||||
});
|
||||
|
||||
$rootScope.addPermission = function(scope) {
|
||||
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
|
||||
};
|
||||
@ -709,84 +741,9 @@ var tower = angular.module('Tower', [
|
||||
|
||||
$rootScope.crumbCache = [];
|
||||
|
||||
if ($rootScope.removeOpenSocket) {
|
||||
$rootScope.removeOpenSocket();
|
||||
}
|
||||
$rootScope.removeOpenSocket = $rootScope.$on('OpenSocket', function() {
|
||||
// Listen for job changes and issue callbacks to initiate
|
||||
// DOM updates
|
||||
function openSocket() {
|
||||
var schedule_socket, control_socket;
|
||||
|
||||
sock = Socket({ scope: $rootScope, endpoint: "jobs" });
|
||||
sock.init();
|
||||
sock.on("status_changed", function(data) {
|
||||
$log.debug('Job ' + data.unified_job_id +
|
||||
' status changed to ' + data.status +
|
||||
' send to ' + $location.$$url);
|
||||
|
||||
// this acts as a router...it emits the proper
|
||||
// value based on what URL the user is currently
|
||||
// accessing.
|
||||
if ($state.is('jobs')) {
|
||||
$rootScope.$emit('JobStatusChange-jobs', data);
|
||||
} else if ($state.includes('jobDetail') ||
|
||||
$state.is('adHocJobStdout') ||
|
||||
$state.is('inventorySyncStdout') ||
|
||||
$state.is('managementJobStdout') ||
|
||||
$state.is('scmUpdateStdout')) {
|
||||
|
||||
$log.debug("sending status to standard out");
|
||||
$rootScope.$emit('JobStatusChange-jobStdout', data);
|
||||
}
|
||||
if ($state.includes('jobDetail')) {
|
||||
$rootScope.$emit('JobStatusChange-jobDetails', data);
|
||||
} else if ($state.is('dashboard')) {
|
||||
$rootScope.$emit('JobStatusChange-home', data);
|
||||
} else if ($state.is('portalMode')) {
|
||||
$rootScope.$emit('JobStatusChange-portal', data);
|
||||
} else if ($state.is('projects')) {
|
||||
$rootScope.$emit('JobStatusChange-projects', data);
|
||||
} else if ($state.is('inventoryManage')) {
|
||||
$rootScope.$emit('JobStatusChange-inventory', data);
|
||||
}
|
||||
});
|
||||
sock.on("summary_complete", function(data) {
|
||||
$log.debug('Job summary_complete ' + data.unified_job_id);
|
||||
$rootScope.$emit('JobSummaryComplete', data);
|
||||
});
|
||||
|
||||
schedule_socket = Socket({
|
||||
scope: $rootScope,
|
||||
endpoint: "schedules"
|
||||
});
|
||||
schedule_socket.init();
|
||||
schedule_socket.on("schedule_changed", function(data) {
|
||||
$log.debug('Schedule ' + data.unified_job_id + ' status changed to ' + data.status);
|
||||
$rootScope.$emit('ScheduleStatusChange', data);
|
||||
});
|
||||
|
||||
control_socket = Socket({
|
||||
scope: $rootScope,
|
||||
endpoint: "control"
|
||||
});
|
||||
control_socket.init();
|
||||
control_socket.on("limit_reached", function(data) {
|
||||
$log.debug(data.reason);
|
||||
$rootScope.sessionTimer.expireSession('session_limit');
|
||||
$state.go('signOut');
|
||||
});
|
||||
}
|
||||
openSocket();
|
||||
|
||||
setTimeout(function() {
|
||||
$rootScope.$apply(function() {
|
||||
sock.checkStatus();
|
||||
$log.debug('socket status: ' + $rootScope.socketStatus);
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
|
||||
// SocketService.subscribe(toState, toParams);
|
||||
// });
|
||||
|
||||
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState) {
|
||||
|
||||
@ -845,7 +802,7 @@ var tower = angular.module('Tower', [
|
||||
ConfigService.getConfig().then(function() {
|
||||
Timer.init().then(function(timer) {
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
SocketService.init();
|
||||
pendoService.issuePendoIdentity();
|
||||
CheckLicense.test();
|
||||
FeaturesService.get();
|
||||
@ -873,10 +830,6 @@ var tower = angular.module('Tower', [
|
||||
$('#' + tabs + ' #' + tab).tab('show');
|
||||
};
|
||||
|
||||
$rootScope.socketHelp = function() {
|
||||
ShowSocketHelp();
|
||||
};
|
||||
|
||||
$rootScope.leavePortal = function() {
|
||||
$rootScope.portalMode = false;
|
||||
$location.path('/home/');
|
||||
|
||||
@ -28,7 +28,7 @@ export function Home($scope, $compile, $stateParams, $rootScope, $location, $log
|
||||
|
||||
var dataCount = 0;
|
||||
|
||||
$rootScope.$on('JobStatusChange-home', function () {
|
||||
$scope.$on('ws-jobs', function () {
|
||||
Rest.setUrl(GetBasePath('dashboard'));
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
|
||||
@ -65,7 +65,7 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
|
||||
jobs_scope.viewJob = function (id) {
|
||||
$state.transitionTo('jobDetail', {id: id});
|
||||
};
|
||||
|
||||
|
||||
jobs_scope.showJobType = true;
|
||||
LoadJobsScope({
|
||||
parent_scope: $scope,
|
||||
@ -110,17 +110,11 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
|
||||
}
|
||||
};
|
||||
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
}
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobs', function() {
|
||||
$scope.$on('ws-jobs', function() {
|
||||
$scope.refreshJobs();
|
||||
});
|
||||
|
||||
if ($rootScope.removeScheduleStatusChange) {
|
||||
$rootScope.removeScheduleStatusChange();
|
||||
}
|
||||
$rootScope.removeScheduleStatusChange = $rootScope.$on('ScheduleStatusChange', function() {
|
||||
$scope.$on('ws-schedules', function() {
|
||||
if (api_complete) {
|
||||
scheduled_scope.search('schedule');
|
||||
}
|
||||
|
||||
@ -86,11 +86,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
}
|
||||
});
|
||||
|
||||
// Handle project update status changes
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
}
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-projects', function(e, data) {
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
var project;
|
||||
$log.debug(data);
|
||||
if ($scope.projects) {
|
||||
|
||||
@ -58,7 +58,7 @@ function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q) {
|
||||
destroyWatcher: angular.noop,
|
||||
setupWatcher: function(period, jobType) {
|
||||
this.destroyWatcher =
|
||||
$rootScope.$on('JobStatusChange-home', function() {
|
||||
$rootScope.$on('ws-jobs', function() {
|
||||
getData(period, jobType).then(function(result) {
|
||||
$rootScope.
|
||||
$broadcast('DataReceived:JobStatusGraph',
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import "./help/ChromeSocketHelp";
|
||||
import "./help/FirefoxSocketHelp";
|
||||
import "./help/InventoryGroups";
|
||||
import "./help/SafariSocketHelp";
|
||||
@ -1,47 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name help
|
||||
* @description These are the modal windows that are shown to the user to give additional guidance on certain tasks that might not be straightforward.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name help.function:ChromeSocketHelp
|
||||
* @description This help modal gives instructions on what the user should do if not connected to the web sockets while using Chrome.
|
||||
*/
|
||||
|
||||
|
||||
angular.module('ChromeSocketHelpDefinition', [])
|
||||
.value('ChromeSocketHelp', {
|
||||
story: {
|
||||
hdr: 'Live Events',
|
||||
width: 510,
|
||||
height: 560,
|
||||
steps: [{
|
||||
intro: 'Connection status indicator:',
|
||||
img: {
|
||||
src: 'socket_indicator.png',
|
||||
maxWidth: 360
|
||||
},
|
||||
box: "<p><i class=\"fa icon-socket-ok\"></i> indicates live events are streaming and the browser is connected to the live events server.</p><p>If the indicator continually shows <i class=\"fa icon-socket-error\"></i> " +
|
||||
"or <i class=\"fa icon-socket-connecting\"></i>, then live events are not streaming, and the browser is having difficulty connecting to the live events server. In this case click Next for troubleshooting help.</p>"
|
||||
}, {
|
||||
intro: 'Live events connection:',
|
||||
icon: {
|
||||
"class": "fa fa-5x fa-rss {{ socketStatus }}-color",
|
||||
style: "margin-top: 75px;",
|
||||
containerHeight: 200
|
||||
},
|
||||
box: "<p><strong>{{ browserName }}</strong> is connecting to the live events server on port <strong>{{ socketPort }}</strong>. The current connection status is " +
|
||||
"<i class=\"fa icon-socket-{{ socketStatus }}\"></i> <strong>{{ socketStatus }}</strong>.</p><p>If the connection status indicator is not green, have the " +
|
||||
"system administrator verify this is the correct port and that access to the port is not blocked by a firewall."
|
||||
}]
|
||||
}
|
||||
});
|
||||
@ -1,78 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name help.function:FirefoxSocketHelp
|
||||
* @description This help modal gives instructions on what the user should do if not connected to the web sockets while using Firefox.
|
||||
*/
|
||||
|
||||
|
||||
angular.module('FFSocketHelpDefinition', [])
|
||||
.value('FFSocketHelp', {
|
||||
story: {
|
||||
hdr: 'Live Events',
|
||||
width: 510,
|
||||
height: 560,
|
||||
steps: [{
|
||||
intro: 'Connection status indicator:',
|
||||
img: {
|
||||
src: 'socket_indicator.png',
|
||||
maxWidth: 360
|
||||
},
|
||||
box: "<p><i class=\"fa icon-socket-ok\"></i> indicates live events are streaming and the browser is connected to the live events server.</p><p>If the indicator continually shows <i class=\"fa icon-socket-error\"></i> " +
|
||||
"or <i class=\"fa icon-socket-connecting\"></i>, then live events are not streaming, and the browser is having difficulty connecting to the live events server. In this case click Next for troubleshooting help.</p>"
|
||||
}, {
|
||||
intro: 'Live events connection:',
|
||||
icon: {
|
||||
"class": "fa fa-5x fa-rss {{ socketStatus }}-color",
|
||||
style: "margin-top: 75px;",
|
||||
containerHeight: 200
|
||||
},
|
||||
box: "<p><strong>{{ browserName }}</strong> is connecting to the live events server on port <strong>{{ socketPort }}</strong>. The current connection status is " +
|
||||
"<i class=\"fa icon-socket-{{ socketStatus }}\"></i> <strong>{{ socketStatus }}</strong>.</p><p>If the connection status indicator is not green, have the " +
|
||||
"system administrator verify this is the correct port and that access to the port is not blocked by a firewall.</p>"
|
||||
}, {
|
||||
intro: 'Self signed certificate:',
|
||||
icon: {
|
||||
"class": "fa fa-5x fa-check ok-color",
|
||||
style: "margin-top: 75px;",
|
||||
containerHeight: 200
|
||||
},
|
||||
box: "<p>If the Tower web server is using a self signed security certificate, Firefox needs to accept the certificate and allow the " +
|
||||
"connection.</p><p>Click Next for help accepting a self signed certificate.</p>"
|
||||
}, {
|
||||
intro: 'Accepting a self-signed certificate:',
|
||||
img: {
|
||||
src: 'understand_the_risk.png',
|
||||
maxWidth: 440
|
||||
},
|
||||
box: "<p>Navigate to <a href=\"{{ socketURL }}\" target=\"_blank\">{{ socketURL }}</a> The above warning will appear.</p><p>Click <i>I Understand the Risks</i></p>"
|
||||
}, {
|
||||
intro: 'Accepting a self-signed certificate:',
|
||||
img: {
|
||||
src: 'add_exception.png',
|
||||
maxWidth: 440
|
||||
},
|
||||
box: "<p>Click the <i>Add Exception</i> button."
|
||||
}, {
|
||||
intro: 'Accepting a self-signed certificate:',
|
||||
img: {
|
||||
src: 'confirm_exception.png',
|
||||
maxWidth: 340
|
||||
},
|
||||
box: "<p>Click the <i>Confirm the Security Exception</i> button. This will add the self signed certificate from the Tower server to Firefox's list of trusted certificates.<p>"
|
||||
}, {
|
||||
intro: 'Accepting a self-signed certificate:',
|
||||
img: {
|
||||
src: 'refresh_firefox.png',
|
||||
maxWidth: 480
|
||||
},
|
||||
box: "<p>Now that Firefox has accepted the security certificate the live event connection status indicator should turn green. If it does not, reload Tower by clicking the " +
|
||||
"Firefox refresh button."
|
||||
}]
|
||||
}
|
||||
});
|
||||
@ -1,86 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name help.function:InventoryGroups
|
||||
* @description This help modal walks the user how to add groups to an inventory or a subgroup to an existing group.
|
||||
*/
|
||||
|
||||
|
||||
angular.module('InventoryGroupsHelpDefinition', [])
|
||||
.value('InventoryGroupsHelp', {
|
||||
story: {
|
||||
hdr: 'Inventory Setup',
|
||||
width: 510,
|
||||
height: 560,
|
||||
steps: [{
|
||||
intro: 'Start by creating a group:',
|
||||
img: {
|
||||
src: 'groups001.png',
|
||||
maxWidth: 257,
|
||||
maxHeight: 114
|
||||
},
|
||||
box: "Click <i class=\"fa fa-plus\"></i> on the groups list (the left side of the page) to add a new group.",
|
||||
autoOffNotice: true
|
||||
}, {
|
||||
intro: 'Enter group properties:',
|
||||
img: {
|
||||
src: 'groups002.png',
|
||||
maxWidth: 443,
|
||||
maxHeight: 251
|
||||
},
|
||||
box: 'Enter the group name, a description and any inventory variables. Variables can be entered using either JSON or YAML syntax. ' +
|
||||
'For more on inventory variables, see <a href=\"http://docs.ansible.com/intro_inventory.html\" target="_blank"> ' +
|
||||
'docs.ansible.com/intro_inventory.html</a>'
|
||||
}, {
|
||||
intro: 'Cloud inventory: select cloud source',
|
||||
img: {
|
||||
src: 'groups003.png',
|
||||
maxWidth: 412,
|
||||
maxHeight: 215
|
||||
},
|
||||
box: "For a cloud inventory, choose the cloud provider from the list and select your credentials. If you have not already setup " +
|
||||
"credentials for the provider, you will need to do that first on the <a href=\"/#/credentials\" " +
|
||||
"target=\"_blank\">Credentials</a> tab."
|
||||
}, {
|
||||
intro: 'Cloud inventory: synchronize Tower with the cloud',
|
||||
img: {
|
||||
src: 'groups004.png',
|
||||
maxWidth: 187,
|
||||
maxHeight: 175
|
||||
},
|
||||
box: "To import a cloud inventory into Tower, initiate an inventory sync by clicking <i class=\"fa fa-refresh\"></i>."
|
||||
}, {
|
||||
intro: "Add subgroups:",
|
||||
img: {
|
||||
src: 'groups008.png',
|
||||
maxWidth: 469,
|
||||
maxHeight: 243
|
||||
},
|
||||
box: "<div class=\"text-left\">First, select an existing group.</div>"
|
||||
}, {
|
||||
intro: "Add subgroups:",
|
||||
img: {
|
||||
src: 'groups009.png',
|
||||
maxWidth: 475,
|
||||
maxHeight: 198
|
||||
},
|
||||
box: "<div class=\"text-left\">Then click <i class=\"fa fa-plus\"></i> to create a new group. The new group " +
|
||||
"will be added to the selected group.</div>"
|
||||
}, {
|
||||
intro: 'Add hosts:',
|
||||
img: {
|
||||
src: 'groups010.png',
|
||||
maxWidth: 475,
|
||||
maxHeight: 122
|
||||
},
|
||||
box: "<div class=\"text-left\"><p>First, select a Group. " +
|
||||
"Then click <i class=\"fa fa-plus\"></i> on the hosts list (the right side of the page) to create a host. " +
|
||||
"The new host will be part of the selected group.</p></div>"
|
||||
}]
|
||||
}
|
||||
});
|
||||
@ -1,50 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name help.function:SafariSocketHelp
|
||||
* @description This help modal gives instructions on what the user should do if not connected to the web sockets while using Safari. Safari does not support websockets.
|
||||
*/
|
||||
|
||||
|
||||
angular.module('SafariSocketHelpDefinition', [])
|
||||
.value('SafariSocketHelp', {
|
||||
story: {
|
||||
hdr: 'Live Events',
|
||||
width: 510,
|
||||
height: 560,
|
||||
steps: [{
|
||||
intro: 'Connection status indicator:',
|
||||
img: {
|
||||
src: 'socket_indicator.png',
|
||||
maxWidth: 360
|
||||
},
|
||||
box: "<p><i class=\"fa icon-socket-ok\"></i> indicates live events are streaming and the browser is connected to the live events server.</p><p>If the indicator continually shows <i class=\"fa icon-socket-error\"></i> " +
|
||||
"or <i class=\"fa icon-socket-connecting\"></i>, then live events are not streaming, and the browser is having difficulty connecting to the live events server. In this case click Next for troubleshooting help.</p>"
|
||||
}, {
|
||||
intro: 'Live events connection:',
|
||||
icon: {
|
||||
"class": "fa fa-5x fa-rss {{ socketStatus }}-color",
|
||||
style: "margin-top: 75px;",
|
||||
containerHeight: 200
|
||||
},
|
||||
box: "<p><strong>{{ browserName }}</strong> is connecting to the live events server on port <strong>{{ socketPort }}</strong>. The current connection status is " +
|
||||
"<i class=\"fa icon-socket-{{ socketStatus }}\"></i> <strong>{{ socketStatus }}</strong>.</p><p>If the connection status indicator is not green, have the " +
|
||||
"system administrator verify this is the correct port and that access to the port is not blocked by a firewall.</p>"
|
||||
}, {
|
||||
intro: 'Self signed certificate:',
|
||||
icon: {
|
||||
"class": "fa fa-5x fa-check ok-color",
|
||||
style: "margin-top: 75px;",
|
||||
containerHeight: 200
|
||||
},
|
||||
box: "<p>Safari will not connect to the live event port when the Tower web server is configured with a self signed certificate. Check with a system administrator to " +
|
||||
"determine if Tower is using a self signed certificate. Installing a signed certificate will fix the problem.</p>" +
|
||||
"<p>Switching browsers to either Chrome or Firefox will work as well.</p>"
|
||||
}]
|
||||
}
|
||||
});
|
||||
@ -23,7 +23,6 @@ import ProjectPath from "./helpers/ProjectPath";
|
||||
import Projects from "./helpers/Projects";
|
||||
import Schedules from "./helpers/Schedules";
|
||||
import Selection from "./helpers/Selection";
|
||||
import SocketHelper from "./helpers/SocketHelper";
|
||||
import Users from "./helpers/Users";
|
||||
import Variables from "./helpers/Variables";
|
||||
import ApiDefaults from "./helpers/api-defaults";
|
||||
@ -55,7 +54,6 @@ export
|
||||
Projects,
|
||||
Schedules,
|
||||
Selection,
|
||||
SocketHelper,
|
||||
Users,
|
||||
Variables,
|
||||
ApiDefaults,
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name helpers.function:SocketHelper
|
||||
* @description
|
||||
* SocketHelper.js
|
||||
*
|
||||
* Show web socket troubleshooting help
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export default
|
||||
angular.module('SocketHelper', ['Utilities', 'FFSocketHelpDefinition', 'SafariSocketHelpDefinition' , 'ChromeSocketHelpDefinition'])
|
||||
|
||||
.factory('ShowSocketHelp', ['$location', '$rootScope', 'FFSocketHelp', 'SafariSocketHelp', 'ChromeSocketHelp', 'HelpDialog', 'browserData',
|
||||
function($location, $rootScope, FFSocketHelp, SafariSocketHelp, ChromeSocketHelp, HelpDialog, browserData) {
|
||||
return function() {
|
||||
var scope = $rootScope.$new();
|
||||
scope.socketPort = $AnsibleConfig.websocket_port;
|
||||
scope.socketURL = 'https://' + $location.host() + ':' + scope.socketPort + '/';
|
||||
scope.browserName = browserData.name;
|
||||
|
||||
if (browserData.name === 'Firefox') {
|
||||
HelpDialog({ defn: FFSocketHelp, scope: scope });
|
||||
}
|
||||
else if (browserData.name === 'Safari') {
|
||||
HelpDialog({ defn: SafariSocketHelp, scope: scope });
|
||||
}
|
||||
else {
|
||||
HelpDialog({ defn: ChromeSocketHelp, scope: scope });
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@ -23,5 +23,10 @@ export default {
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "RUN COMMAND"
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -22,6 +22,11 @@ var copyMoveGroup = {
|
||||
return GroupManageService.get({id: $stateParams.group_id}).then(res => res.data.results[0]);
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'form@inventoryManage' : {
|
||||
controller: CopyMoveGroupsController,
|
||||
@ -40,6 +45,11 @@ var copyMoveHost = {
|
||||
return HostManageService.get({id: $stateParams.host_id}).then(res => res.data.results[0]);
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'form@inventoryManage': {
|
||||
templateUrl: templateUrl('inventories/manage/copy-move/copy-move'),
|
||||
|
||||
@ -5,12 +5,14 @@
|
||||
*************************************************/
|
||||
export default
|
||||
['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroups', 'generateList', 'InventoryUpdate', 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus',
|
||||
'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Rest', 'GetBasePath', 'rbacUiControlService',
|
||||
'InventoryManageService', 'groupsUrl', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Find', 'Rest', 'GetBasePath', 'rbacUiControlService',
|
||||
function($scope, $rootScope, $state, $stateParams, InventoryGroups, generateList, InventoryUpdate, GroupManageService, GroupsCancelUpdate, ViewUpdateStatus,
|
||||
InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Rest, GetBasePath, rbacUiControlService){
|
||||
InventoryManageService, groupsUrl, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, Find, Rest, GetBasePath, rbacUiControlService){
|
||||
var list = InventoryGroups,
|
||||
view = generateList,
|
||||
pageSize = 20;
|
||||
|
||||
|
||||
$scope.inventory_id = $stateParams.inventory_id;
|
||||
|
||||
$scope.canAdd = false;
|
||||
@ -97,26 +99,27 @@
|
||||
group_name: group.name,
|
||||
group_source: res.data.results[0].source
|
||||
}));
|
||||
$scope.$emit('WatchUpdateStatus'); // init socket io conneciton and start watching for status updates
|
||||
$rootScope.$on('JobStatusChange-inventory', (event, data) => {
|
||||
switch(data.status){
|
||||
case 'failed' || 'successful':
|
||||
$state.reload();
|
||||
break;
|
||||
default:
|
||||
var status = GetSyncStatusMsg({
|
||||
status: data.status,
|
||||
has_inventory_sources: group.has_inventory_sources,
|
||||
source: group.source
|
||||
});
|
||||
group.status = data.status;
|
||||
group.status_class = status.class;
|
||||
group.status_tooltip = status.tooltip;
|
||||
group.launch_tooltip = status.launch_tip;
|
||||
group.launch_class = status.launch_class;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on(`ws-jobs`, function(e, data){
|
||||
var group = Find({ list: $scope.groups, key: 'id', val: data.group_id });
|
||||
if(data.status === 'failed' || data.status === 'successful'){
|
||||
$state.reload();
|
||||
}
|
||||
else{
|
||||
var status = GetSyncStatusMsg({
|
||||
status: data.status,
|
||||
has_inventory_sources: group.has_inventory_sources,
|
||||
source: group.source
|
||||
});
|
||||
group.status = data.status;
|
||||
group.status_class = status.class;
|
||||
group.status_tooltip = status.tooltip;
|
||||
group.launch_tooltip = status.launch_tip;
|
||||
group.launch_class = status.launch_class;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.cancelUpdate = function (id) {
|
||||
GroupsCancelUpdate({ scope: $scope, id: id });
|
||||
};
|
||||
|
||||
@ -16,6 +16,11 @@ var ManageGroupsEdit = {
|
||||
data: {
|
||||
mode: 'edit'
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
groupData: ['$stateParams', 'GroupManageService', function($stateParams, GroupManageService){
|
||||
return GroupManageService.get({id: $stateParams.group_id}).then(res => res.data.results[0]);
|
||||
@ -41,6 +46,11 @@ var ManageGroupsAdd = {
|
||||
data: {
|
||||
mode: 'add'
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'form@inventoryManage': {
|
||||
controller: addController,
|
||||
|
||||
@ -23,6 +23,11 @@ var ManageHostsEdit = {
|
||||
});
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'form@inventoryManage': {
|
||||
controller: editController,
|
||||
@ -40,6 +45,11 @@ var ManageHostsAdd = {
|
||||
data: {
|
||||
mode: 'add'
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'form@inventoryManage': {
|
||||
controller: addController,
|
||||
|
||||
@ -13,6 +13,11 @@ import GroupsListController from './groups/groups-list.controller';
|
||||
export default {
|
||||
name: 'inventoryManage',
|
||||
url: '/inventories/:inventory_id/manage?{group:int}{failed}',
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
params:{
|
||||
group:{
|
||||
array: true
|
||||
|
||||
@ -22,6 +22,12 @@ var hostEventModal = {
|
||||
return JobDetailService.getJobEventChildren($stateParams.taskId).then(res => res.data.results);
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
},
|
||||
onExit: function() {
|
||||
// close the modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
@ -36,28 +42,52 @@ var hostEventModal = {
|
||||
name: 'jobDetail.host-event.details',
|
||||
url: '/details',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-details')
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-details'),
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hostEventJson = {
|
||||
name: 'jobDetail.host-event.json',
|
||||
url: '/json',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror')
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror'),
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hostEventStdout = {
|
||||
name: 'jobDetail.host-event.stdout',
|
||||
url: '/stdout',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror')
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror'),
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hostEventStderr = {
|
||||
name: 'jobDetail.host-event.stderr',
|
||||
url: '/stderr',
|
||||
controller: 'HostEventController',
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror')
|
||||
templateUrl: templateUrl('job-detail/host-event/host-event-codemirror'),
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -22,6 +22,12 @@ export default {
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
hosts: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) {
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
|
||||
@ -48,23 +48,19 @@
|
||||
$scope.status = res.results[0].status;
|
||||
});
|
||||
};
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
if ($rootScope.removeJobSummaryComplete) {
|
||||
$rootScope.removeJobSummaryComplete();
|
||||
}
|
||||
// emitted by the API in the same function used to persist host summary data
|
||||
// JobEvent.update_host_summary_from_stats() from /awx/main.models.jobs.py
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobSummaryComplete', function(e, data) {
|
||||
$scope.$on('ws-jobs-summary', function(e, data) {
|
||||
// discard socket msgs we don't care about in this context
|
||||
if (parseInt($stateParams.id) === data.unified_job_id){
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
// UnifiedJob.def socketio_emit_status() from /awx/main.models.unified_jobs.py
|
||||
if ($rootScope.removeJobSummaryComplete) {
|
||||
$rootScope.removeJobSummaryComplete();
|
||||
}
|
||||
$rootScope.removeJobSummaryComplete = $rootScope.$on('JobStatusChange-jobDetails', function(e, data) {
|
||||
$scope.$on('ws-jobs', function(e, data) {
|
||||
if (parseInt($stateParams.id) === data.unified_job_id){
|
||||
$scope.status = data.status;
|
||||
}
|
||||
|
||||
@ -9,6 +9,12 @@ import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
export default {
|
||||
name: 'jobDetail.host-summary',
|
||||
url: '/event-summary',
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
},
|
||||
views:{
|
||||
'host-summary': {
|
||||
controller: 'HostSummaryController',
|
||||
|
||||
@ -197,32 +197,22 @@ export default
|
||||
"<p><i class=\"fa fa-circle changed-hosts-color\"></i> Changed</p>\n" +
|
||||
"<p><i class=\"fa fa-circle unreachable-hosts-color\"></i> Unreachable</p>\n" +
|
||||
"<p><i class=\"fa fa-circle failed-hosts-color\"></i> Failed</p>\n";
|
||||
function openSocket() {
|
||||
$rootScope.event_socket.on("job_events-" + job_id, function(data) {
|
||||
// update elapsed time on each event received
|
||||
scope.job_status.elapsed = GetElapsed({
|
||||
start: scope.job.created,
|
||||
end: Date.now()
|
||||
});
|
||||
if (api_complete && data.id > lastEventId) {
|
||||
scope.waiting = false;
|
||||
data.event = data.event_name;
|
||||
DigestEvent({ scope: scope, event: data });
|
||||
}
|
||||
UpdateDOM({ scope: scope });
|
||||
});
|
||||
// Unbind $rootScope socket event binding(s) so that they don't get triggered
|
||||
// in another instance of this controller
|
||||
scope.$on('$destroy', function() {
|
||||
$rootScope.event_socket.removeAllListeners("job_events-" + job_id);
|
||||
});
|
||||
}
|
||||
openSocket();
|
||||
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
}
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobDetails', function(e, data) {
|
||||
scope.$on(`ws-job_events-${job_id}`, function(e, data) {
|
||||
// update elapsed time on each event received
|
||||
scope.job_status.elapsed = GetElapsed({
|
||||
start: scope.job.created,
|
||||
end: Date.now()
|
||||
});
|
||||
if (api_complete && data.id > lastEventId) {
|
||||
scope.waiting = false;
|
||||
data.event = data.event_name;
|
||||
DigestEvent({ scope: scope, event: data });
|
||||
}
|
||||
UpdateDOM({ scope: scope });
|
||||
});
|
||||
|
||||
scope.$on(`ws-jobs`, function(e, data) {
|
||||
// if we receive a status change event for the current job indicating the job
|
||||
// is finished, stop event queue processing and reload
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) {
|
||||
@ -236,10 +226,7 @@ export default
|
||||
}
|
||||
});
|
||||
|
||||
if ($rootScope.removeJobSummaryComplete) {
|
||||
$rootScope.removeJobSummaryComplete();
|
||||
}
|
||||
$rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() {
|
||||
scope.$on('ws-jobs-summary', function() {
|
||||
// the job host summary should now be available from the API
|
||||
$log.debug('Trigging reload of job_host_summaries');
|
||||
scope.$emit('InitialLoadComplete');
|
||||
|
||||
@ -13,21 +13,11 @@ export default {
|
||||
parent: 'jobs',
|
||||
label: "{{ job.id }} - {{ job.name }}"
|
||||
},
|
||||
resolve: {
|
||||
jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
|
||||
if (!$rootScope.event_socket) {
|
||||
$rootScope.event_socket = Socket({
|
||||
scope: $rootScope,
|
||||
endpoint: "job_events"
|
||||
});
|
||||
$rootScope.event_socket.init();
|
||||
// returns should really be providing $rootScope.event_socket
|
||||
// otherwise, we have to inject the entire $rootScope into the controller
|
||||
return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}]
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed", "summary"],
|
||||
"job_events": []
|
||||
}
|
||||
},
|
||||
templateUrl: templateUrl('job-detail/job-detail'),
|
||||
controller: 'JobDetailController'
|
||||
|
||||
@ -15,6 +15,11 @@ export default {
|
||||
parent: "jobTemplates",
|
||||
label: "CREATE JOB TEMPLATE"
|
||||
},
|
||||
socket:{
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
onExit: function(){
|
||||
// close the survey maker modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
|
||||
@ -14,6 +14,11 @@ export default {
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
socket:{
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobTemplates',
|
||||
label: "{{name}}"
|
||||
|
||||
@ -37,6 +37,10 @@ export default
|
||||
view.inject(list, { mode: mode, scope: $scope });
|
||||
$rootScope.flashMessage = null;
|
||||
|
||||
$scope.$on(`ws-jobs`, function () {
|
||||
$scope.search(list.iterator);
|
||||
});
|
||||
|
||||
if ($scope.removePostRefresh) {
|
||||
$scope.removePostRefresh();
|
||||
}
|
||||
|
||||
@ -17,5 +17,10 @@ export default {
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: "JOB TEMPLATES"
|
||||
},
|
||||
socket:{
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -65,6 +65,7 @@ export default
|
||||
|
||||
var x,
|
||||
ConfigService = $injector.get('ConfigService'),
|
||||
SocketService = $injector.get('SocketService'),
|
||||
scope = angular.element(document.getElementById('main-view')).scope();
|
||||
|
||||
if(scope){
|
||||
@ -94,6 +95,7 @@ export default
|
||||
$rootScope.lastUser = $cookieStore.get('current_user').id;
|
||||
}
|
||||
ConfigService.delete();
|
||||
SocketService.disconnect();
|
||||
$cookieStore.remove('token_expires');
|
||||
$cookieStore.remove('current_user');
|
||||
$cookieStore.remove('token');
|
||||
|
||||
@ -9,6 +9,7 @@ import {templateUrl} from '../shared/template-url/template-url.factory';
|
||||
export default {
|
||||
name: 'signIn',
|
||||
route: '/login',
|
||||
socket: null,
|
||||
templateUrl: templateUrl('login/loginBackDrop'),
|
||||
resolve: {
|
||||
obj: ['$rootScope', 'Authorization',
|
||||
|
||||
@ -57,10 +57,11 @@
|
||||
export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope',
|
||||
'$location', 'Authorization', 'ToggleClass', 'Alert', 'Wait', 'Timer',
|
||||
'Empty', 'ClearScope', '$scope', 'pendoService', 'ConfigService',
|
||||
'CheckLicense', 'FeaturesService',
|
||||
'CheckLicense', 'FeaturesService', 'SocketService',
|
||||
function ($log, $cookieStore, $compile, $window, $rootScope, $location,
|
||||
Authorization, ToggleClass, Alert, Wait, Timer, Empty, ClearScope,
|
||||
scope, pendoService, ConfigService, CheckLicense, FeaturesService) {
|
||||
scope, pendoService, ConfigService, CheckLicense, FeaturesService,
|
||||
SocketService) {
|
||||
var lastPath, lastUser, sessionExpired, loginAgain;
|
||||
|
||||
loginAgain = function() {
|
||||
@ -135,7 +136,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope',
|
||||
Authorization.setUserInfo(data);
|
||||
Timer.init().then(function(timer){
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
SocketService.init();
|
||||
$rootScope.user_is_superuser = data.results[0].is_superuser;
|
||||
$rootScope.user_is_system_auditor = data.results[0].is_system_auditor;
|
||||
scope.$emit('AuthorizationGetLicense');
|
||||
|
||||
@ -21,6 +21,10 @@ export default ['$scope', '$rootScope', '$location', '$log',
|
||||
generator = GenerateList,
|
||||
orgBase = GetBasePath('organizations');
|
||||
|
||||
$scope.$on(`ws-jobs`, function () {
|
||||
$scope.search(list.iterator);
|
||||
});
|
||||
|
||||
Rest.setUrl(orgBase + $stateParams.organization_id);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
|
||||
@ -86,11 +86,7 @@ export default ['$scope', '$rootScope', '$location', '$log',
|
||||
}
|
||||
});
|
||||
|
||||
// Handle project update status changes
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
}
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-projects', function(e, data) {
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
var project;
|
||||
$log.debug(data);
|
||||
if ($scope.projects) {
|
||||
|
||||
@ -99,6 +99,11 @@ export default [
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -121,6 +126,11 @@ export default [
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -13,12 +13,9 @@ export function PortalModeJobsController($scope, $rootScope, GetBasePath, Genera
|
||||
defaultUrl = GetBasePath('jobs') + '?created_by=' + $rootScope.current_user.id,
|
||||
pageSize = 12;
|
||||
|
||||
if ($rootScope.removeJobStatusChange) {
|
||||
$rootScope.removeJobStatusChange();
|
||||
}
|
||||
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-portal', function() {
|
||||
$scope.$on('ws-jobs', function() {
|
||||
$scope.search('job');
|
||||
});
|
||||
});
|
||||
|
||||
$scope.iterator = list.iterator;
|
||||
$scope.activeFilter = 'user';
|
||||
|
||||
@ -9,6 +9,11 @@ export default {
|
||||
url: '/portal',
|
||||
ncyBreadcrumb: {
|
||||
label: "MY VIEW"
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
views: {
|
||||
// the empty parent ui-view
|
||||
|
||||
@ -1,226 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name shared.function:Socket
|
||||
* @description
|
||||
* Socket.js
|
||||
*
|
||||
* Wrapper for lib/socket.io-client/dist/socket.io.js.
|
||||
*/
|
||||
|
||||
/* global io */
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name shared.function:Socket#SocketIO
|
||||
* @methodOf shared.function:Socket
|
||||
* @description
|
||||
*/
|
||||
export default
|
||||
angular.module('SocketIO', ['Utilities'])
|
||||
|
||||
.factory('Socket', ['$rootScope', '$location', '$log', 'Authorization', 'Store', function ($rootScope, $location, $log, Authorization, Store) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
host = $location.host(),
|
||||
endpoint = params.endpoint,
|
||||
protocol = $location.protocol(),
|
||||
io = require('socket.io-client'),
|
||||
config, socketPort,
|
||||
url;
|
||||
|
||||
// Since some pages are opened in a new tab, we might get here before AnsibleConfig is available.
|
||||
// In that case, load from local storage.
|
||||
if ($AnsibleConfig) {
|
||||
socketPort = $AnsibleConfig.websocket_port;
|
||||
}
|
||||
else {
|
||||
$log.debug('getting web socket port from local storage');
|
||||
config = Store('AnsibleConfig');
|
||||
socketPort = config.websocket_port;
|
||||
}
|
||||
url = protocol + '://' + host + ':' + socketPort + '/socket.io/' + endpoint;
|
||||
$log.debug('opening socket connection to: ' + url);
|
||||
|
||||
function getSocketTip(status) {
|
||||
var result = '';
|
||||
switch(status) {
|
||||
case 'error':
|
||||
result = "Live events: error connecting to the Tower server.";
|
||||
break;
|
||||
case 'connecting':
|
||||
result = "Live events: attempting to connect to the Tower server.";
|
||||
break;
|
||||
case "ok":
|
||||
result = "Live events: connected. Pages containing job status information will automatically update in real-time.";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: scope,
|
||||
url: url,
|
||||
socket: null,
|
||||
|
||||
init: function() {
|
||||
var self = this,
|
||||
token = Authorization.getToken();
|
||||
if (!$rootScope.sessionTimer || ($rootScope.sessionTimer && !$rootScope.sessionTimer.isExpired())) {
|
||||
// We have a valid session token, so attempt socket connection
|
||||
$log.debug('Socket connecting to: ' + url);
|
||||
self.scope.socket_url = url;
|
||||
self.socket = io.connect(url, {
|
||||
query: "Token="+token,
|
||||
headers:
|
||||
{
|
||||
'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt
|
||||
'X-Auth-Token': 'Token ' + token
|
||||
},
|
||||
'connect timeout': 3000,
|
||||
'try multiple transports': false,
|
||||
'max reconnection attempts': 10,
|
||||
'reconnection limit': 2000,
|
||||
'force new connection': true
|
||||
});
|
||||
|
||||
self.socket.on('connection', function() {
|
||||
$log.debug('Socket connecting...');
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('connect', function() {
|
||||
$log.debug('Socket connection established');
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'ok';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('connect_failed', function(reason) {
|
||||
var r = reason || 'connection refused by host',
|
||||
token_actual = Authorization.getToken();
|
||||
|
||||
$log.debug('Socket connection failed: ' + r);
|
||||
|
||||
if (token_actual === token) {
|
||||
self.socket.socket.disconnect();
|
||||
}
|
||||
|
||||
self.scope.$apply(function () {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
|
||||
});
|
||||
self.socket.on('diconnect', function() {
|
||||
$log.debug('Socket disconnected');
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('error', function(reason) {
|
||||
var r = reason || 'connection refused by host';
|
||||
$log.debug('Socket error: ' + r);
|
||||
$log.error('Socket error: ' + r);
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnecting', function() {
|
||||
$log.debug('Socket attempting reconnect...');
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnect', function() {
|
||||
$log.debug('Socket reconnected');
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'ok';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
self.socket.on('reconnect_failed', function(reason) {
|
||||
$log.error('Socket reconnect failed: ' + reason);
|
||||
self.scope.$apply(function() {
|
||||
self.scope.socketStatus = 'error';
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
// encountered expired token, redirect to login page
|
||||
$rootScope.sessionTimer.expireSession('idle');
|
||||
$location.url('/login');
|
||||
}
|
||||
},
|
||||
checkStatus: function() {
|
||||
// Check connection status
|
||||
var self = this;
|
||||
if(self){
|
||||
if(self.socket){
|
||||
if(self.socket.socket){
|
||||
if (self.socket.socket.connected) {
|
||||
self.scope.socketStatus = 'ok';
|
||||
}
|
||||
else if (self.socket.socket.connecting || self.socket.reconnecting) {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
}
|
||||
else {
|
||||
self.scope.socketStatus = 'error';
|
||||
}
|
||||
self.scope.socketTip = getSocketTip(self.scope.socketStatus);
|
||||
return self.scope.socketStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
on: function (eventName, callback) {
|
||||
var self = this;
|
||||
if(self){
|
||||
if(self.socket){
|
||||
self.socket.on(eventName, function () {
|
||||
var args = arguments;
|
||||
self.scope.$apply(function () {
|
||||
callback.apply(self.socket, args);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
emit: function (eventName, data, callback) {
|
||||
var self = this;
|
||||
self.socket.emit(eventName, data, function () {
|
||||
var args = arguments;
|
||||
self.scope.$apply(function () {
|
||||
if (callback) {
|
||||
callback.apply(self.socket, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getUrl: function() {
|
||||
return url;
|
||||
},
|
||||
removeAllListeners: function (eventName) {
|
||||
var self = this;
|
||||
if(self){
|
||||
if(self.socket){
|
||||
self.socket.removeAllListeners(eventName);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}]);
|
||||
@ -3,5 +3,5 @@ import icon from './icon.directive';
|
||||
|
||||
export default
|
||||
angular.module('awIcon', [])
|
||||
.directive('awIcon', icon)
|
||||
.directive('awIcon', icon);
|
||||
//.directive('includeSvg', includeSvg);
|
||||
|
||||
@ -6,21 +6,21 @@
|
||||
|
||||
import listGenerator from './list-generator/main';
|
||||
import pagination from './pagination/main';
|
||||
import title from './title.directive';
|
||||
import lodashAsPromised from './lodash-as-promised';
|
||||
import stringFilters from './string-filters/main';
|
||||
import truncatedText from './truncated-text.directive';
|
||||
import stateExtender from './stateExtender.provider';
|
||||
import rbacUiControl from './rbacUiControl';
|
||||
import socket from './socket/main';
|
||||
|
||||
export default
|
||||
angular.module('shared', [listGenerator.name,
|
||||
pagination.name,
|
||||
stringFilters.name,
|
||||
'ui.router',
|
||||
rbacUiControl.name
|
||||
rbacUiControl.name,
|
||||
socket.name
|
||||
])
|
||||
.factory('lodashAsPromised', lodashAsPromised)
|
||||
.directive('truncatedText', truncatedText)
|
||||
//.directive('title', title)
|
||||
.provider('$stateExtender', stateExtender);
|
||||
|
||||
13
awx/ui/client/src/shared/socket/main.js
Normal file
13
awx/ui/client/src/shared/socket/main.js
Normal file
@ -0,0 +1,13 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
// import awFeatureDirective from './features.directive';
|
||||
import socketService from './socket.service';
|
||||
|
||||
export default
|
||||
angular.module('socket', [])
|
||||
// .directive('awFeature', awFeatureDirective)
|
||||
.service('SocketService', socketService);
|
||||
216
awx/ui/client/src/shared/socket/socket.service.js
Normal file
216
awx/ui/client/src/shared/socket/socket.service.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
import ReconnectingWebSocket from 'reconnectingwebsocket';
|
||||
export default
|
||||
['$rootScope', '$location', '$log','$state', '$q',
|
||||
function ($rootScope, $location, $log, $state, $q) {
|
||||
var needsResubscribing = false,
|
||||
socketPromise = $q.defer();
|
||||
return {
|
||||
init: function() {
|
||||
var self = this,
|
||||
host = window.location.host,
|
||||
url = "ws://" + host + "/websocket/";
|
||||
if (!$rootScope.sessionTimer || ($rootScope.sessionTimer && !$rootScope.sessionTimer.isExpired())) {
|
||||
// We have a valid session token, so attempt socket connection
|
||||
$log.debug('Socket connecting to: ' + url);
|
||||
|
||||
self.socket = new ReconnectingWebSocket(url, null, {
|
||||
timeoutInterval: 3000,
|
||||
maxReconnectAttempts: 10
|
||||
});
|
||||
|
||||
self.socket.onopen = function () {
|
||||
$log.debug("Websocket connection opened.");
|
||||
socketPromise.resolve();
|
||||
self.checkStatus();
|
||||
if(needsResubscribing){
|
||||
self.subscribe(self.getLast());
|
||||
needsResubscribing = false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
self.socket.onerror = function (error) {
|
||||
self.checkStatus();
|
||||
$log.debug('Websocket Error Logged: ' + error); //log errors
|
||||
};
|
||||
|
||||
self.socket.onconnecting = function (event) {
|
||||
self.checkStatus();
|
||||
$log.debug('Websocket reconnecting');
|
||||
needsResubscribing = true;
|
||||
};
|
||||
|
||||
self.socket.onclose = function (event) {
|
||||
self.checkStatus();
|
||||
$log.debug(`Websocket disconnected`);
|
||||
};
|
||||
|
||||
self.socket.onmessage = this.onMessage;
|
||||
|
||||
return self.socket;
|
||||
}
|
||||
else {
|
||||
// encountered expired token, redirect to login page
|
||||
$rootScope.sessionTimer.expireSession('idle');
|
||||
$location.url('/login');
|
||||
}
|
||||
},
|
||||
onMessage: function(e){
|
||||
// Function called when messages are received on by the UI from
|
||||
// the API over the websocket. This will route each message to
|
||||
// the appropriate controller for the current $state.
|
||||
e.data = e.data.replace(/\\/g, '');
|
||||
e.data = e.data.substr(0, e.data.length-1);
|
||||
e.data = e.data.substr(1);
|
||||
$log.debug('Received From Server: ' + e.data);
|
||||
|
||||
var data = JSON.parse(e.data), str = "";
|
||||
if(data.group_name==="jobs" && !('status' in data)){
|
||||
// we know that this must have been a
|
||||
// summary complete message b/c status is missing.
|
||||
// A an object w/ group_name === "jobs" AND a 'status' key
|
||||
// means it was for the event: status_changed.
|
||||
$log.debug('Job summary_complete ' + data.unified_job_id);
|
||||
$rootScope.$broadcast('ws-jobs-summary', data);
|
||||
return;
|
||||
}
|
||||
else if(data.group_name==="job_events"){
|
||||
// The naming scheme is "ws" then a
|
||||
// dash (-) and the group_name, then the job ID
|
||||
// ex: 'ws-jobs-<jobId>'
|
||||
str = `ws-${data.group_name}-${data.job}`
|
||||
}
|
||||
else if(data.group_name==="ad_hoc_command_events"){
|
||||
// The naming scheme is "ws" then a
|
||||
// dash (-) and the group_name, then the job ID
|
||||
// ex: 'ws-jobs-<jobId>'
|
||||
str = `ws-${data.group_name}-${data.ad_hoc_command}`;
|
||||
}
|
||||
else if(data.group_name==="control"){
|
||||
// As of Tower v. 3.1.0, there is only 1 "control"
|
||||
// message, which is for expiring the session if the
|
||||
// session limit is breached.
|
||||
$log.debug(data.reason);
|
||||
$rootScope.sessionTimer.expireSession('session_limit');
|
||||
$state.go('signOut');
|
||||
}
|
||||
else {
|
||||
// The naming scheme is "ws" then a
|
||||
// dash (-) and the group_name.
|
||||
// ex: 'ws-jobs'
|
||||
str = `ws-${data.group_name}`;
|
||||
}
|
||||
$rootScope.$broadcast(str, data);
|
||||
},
|
||||
disconnect: function(){
|
||||
if(this.socket){
|
||||
this.socket.close();
|
||||
}
|
||||
},
|
||||
subscribe: function(state){
|
||||
// Subscribe is used to tell the API that the UI wants to
|
||||
// listen for specific messages. A subscription object could
|
||||
// look like {"groups":{"jobs": ["status_changed", "summary"]}.
|
||||
// This is used by all socket-enabled $states
|
||||
this.emit(JSON.stringify(state.socket));
|
||||
this.setLast(state);
|
||||
},
|
||||
unsubscribe: function(state){
|
||||
// Unsubscribing tells the API that the user is no longer on
|
||||
// on a socket-enabled page, and sends an empty groups object
|
||||
// to the API: {"groups": {}}.
|
||||
// This is used for all pages that are socket-disabled
|
||||
if(this.requiresNewSubscribe(state)){
|
||||
this.emit(JSON.stringify(state.socket));
|
||||
}
|
||||
this.setLast(state);
|
||||
},
|
||||
setLast: function(state){
|
||||
this.last = state;
|
||||
},
|
||||
getLast: function(){
|
||||
return this.last;
|
||||
},
|
||||
requiresNewSubscribe(state){
|
||||
// This function is used for unsubscribing. If the last $state
|
||||
// required an "unsubscribe", then we don't need to unsubscribe
|
||||
// again, b/c the UI is already unsubscribed from all groups
|
||||
if (this.getLast() !== undefined){
|
||||
if( _.isEmpty(state.socket.groups) && _.isEmpty(this.getLast().socket.groups)){
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
checkStatus: function() {
|
||||
// Function for changing the socket indicator icon in the nav bar
|
||||
var self = this;
|
||||
if(self){
|
||||
if(self.socket){
|
||||
if (self.socket.readyState === 0 ) {
|
||||
$rootScope.socketStatus = 'connecting';
|
||||
$rootScope.socketTip = "Live events: attempting to connect to the Tower server.";
|
||||
}
|
||||
else if (self.socket.readyState === 1){
|
||||
$rootScope.socketStatus = 'ok';
|
||||
$rootScope.socketTip = "Live events: connected. Pages containing job status information will automatically update in real-time.";
|
||||
}
|
||||
else if (self.socket.readyState === 2 || self.socket.readyState === 3 ){
|
||||
$rootScope.socketStatus = 'error';
|
||||
$rootScope.socketTip = "Live events: error connecting to the Tower server.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
emit: function(data, callback) {
|
||||
// Function used for sending objects to the API over the
|
||||
// websocket.
|
||||
var self = this;
|
||||
$log.debug('Sent to Websocket Server: ' + data);
|
||||
socketPromise.promise.then(function(){
|
||||
self.socket.send(data, function () {
|
||||
var args = arguments;
|
||||
self.scope.$apply(function () {
|
||||
if (callback) {
|
||||
callback.apply(self.socket, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
addStateResolve: function(state, id){
|
||||
// This function is used for add a state resolve to all states,
|
||||
// socket-enabled AND socket-disabled, and whether the $state
|
||||
// requires a subscribe or an unsubscribe
|
||||
self = this;
|
||||
socketPromise.promise.then(function(){
|
||||
if(!state.socket){
|
||||
state.socket = {groups: {}};
|
||||
self.unsubscribe(state);
|
||||
}
|
||||
else{
|
||||
if(state.socket.groups.hasOwnProperty( "job_events")){
|
||||
state.socket.groups.job_events = [id];
|
||||
}
|
||||
if(state.socket.groups.hasOwnProperty( "ad_hoc_command_events")){
|
||||
state.socket.groups.ad_hoc_command_events = [id];
|
||||
}
|
||||
self.subscribe(state);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -1,10 +1,24 @@
|
||||
export default function($stateProvider) {
|
||||
this.$get = function() {
|
||||
return {
|
||||
addSocket: function(state){
|
||||
// The login route has a 'null' socket because it should
|
||||
// neither subscribe or unsubscribe
|
||||
if(state.socket!==null){
|
||||
if(!state.resolve){
|
||||
state.resolve = {};
|
||||
}
|
||||
state.resolve.socket = ['SocketService', '$stateParams',
|
||||
function(SocketService, $stateParams) {
|
||||
SocketService.addStateResolve(state, $stateParams.id);
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
addState: function(state) {
|
||||
var route = state.route || state.url;
|
||||
|
||||
this.addSocket(state);
|
||||
$stateProvider.state(state.name, {
|
||||
url: route,
|
||||
controller: state.controller,
|
||||
|
||||
@ -11,25 +11,17 @@ export default {
|
||||
route: '/ad_hoc_commands/:id',
|
||||
templateUrl: templateUrl('standard-out/adhoc/standard-out-adhoc'),
|
||||
controller: 'JobStdoutController',
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"],
|
||||
"ad_hoc_command_events": []
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ job.module_name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'ad_hoc_commands'
|
||||
},
|
||||
resolve: {
|
||||
adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
|
||||
if (!$rootScope.adhoc_event_socket) {
|
||||
$rootScope.adhoc_event_socket = Socket({
|
||||
scope: $rootScope,
|
||||
endpoint: "ad_hoc_command_events"
|
||||
});
|
||||
$rootScope.adhoc_event_socket.init();
|
||||
return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -13,17 +13,16 @@ export default {
|
||||
route: '/inventory_sync/:id',
|
||||
templateUrl: templateUrl('standard-out/inventory-sync/standard-out-inventory-sync'),
|
||||
controller: 'JobStdoutController',
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ inventory_source_name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'inventory_updates'
|
||||
},
|
||||
resolve: {
|
||||
inventorySyncSocket: [function() {
|
||||
// TODO: determine whether or not we have socket support for inventory sync standard out
|
||||
return true;
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -22,31 +22,21 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
function openSockets() {
|
||||
if ($state.current.name === 'jobDetail') {
|
||||
$log.debug("socket watching on job_events-" + job_id);
|
||||
$rootScope.event_socket.on("job_events-" + job_id, function() {
|
||||
$scope.$on(`ws-job_events-${job_id}`, function() {
|
||||
$log.debug("socket fired on job_events-" + job_id);
|
||||
if (api_complete) {
|
||||
event_queue++;
|
||||
}
|
||||
});
|
||||
// Unbind $rootScope socket event binding(s) so that they don't get triggered
|
||||
// in another instance of this controller
|
||||
$scope.$on('$destroy', function() {
|
||||
$rootScope.event_socket.removeAllListeners("job_events-" + job_id);
|
||||
});
|
||||
}
|
||||
if ($state.current.name === 'adHocJobStdout') {
|
||||
$log.debug("socket watching on ad_hoc_command_events-" + job_id);
|
||||
$rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() {
|
||||
$scope.$on(`ws-ad_hoc_command_events-${job_id}`, function() {
|
||||
$log.debug("socket fired on ad_hoc_command_events-" + job_id);
|
||||
if (api_complete) {
|
||||
event_queue++;
|
||||
}
|
||||
});
|
||||
// Unbind $rootScope socket event binding(s) so that they don't get triggered
|
||||
// in another instance of this controller
|
||||
$scope.$on('$destroy', function() {
|
||||
$rootScope.adhoc_event_socket.removeAllListeners("ad_hoc_command_events-" + job_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,10 +179,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
|
||||
// We watch for job status changes here. If the job completes we want to clear out the
|
||||
// stdout interval and kill the live_event_processing flag.
|
||||
if ($scope.removeJobStatusChange) {
|
||||
$scope.removeJobStatusChange();
|
||||
}
|
||||
$scope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) {
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) {
|
||||
if (data.status === 'failed' || data.status === 'canceled' ||
|
||||
data.status === 'error' || data.status === 'successful') {
|
||||
|
||||
@ -11,17 +11,16 @@ export default {
|
||||
route: '/management_jobs/:id',
|
||||
templateUrl: templateUrl('standard-out/management-jobs/standard-out-management-jobs'),
|
||||
controller: 'JobStdoutController',
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "jobs",
|
||||
label: "{{ job.name }}"
|
||||
},
|
||||
data: {
|
||||
jobType: 'system_jobs'
|
||||
},
|
||||
resolve: {
|
||||
managementJobSocket: [function() {
|
||||
// TODO: determine whether or not we have socket support for management job standard out
|
||||
return true;
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -17,13 +17,12 @@ export default {
|
||||
parent: "jobs",
|
||||
label: "{{ project_name }}"
|
||||
},
|
||||
socket: {
|
||||
"groups":{
|
||||
"jobs": ["status_changed"]
|
||||
}
|
||||
},
|
||||
data: {
|
||||
jobType: 'project_updates'
|
||||
},
|
||||
resolve: {
|
||||
scmUpdateSocket: [function() {
|
||||
// TODO: determine whether or not we have socket support for scm update standard out
|
||||
return true;
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -25,10 +25,7 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
|
||||
// Listen for job status updates that may come across via sockets. We need to check the payload
|
||||
// to see whethere the updated job is the one that we're currently looking at.
|
||||
if ($scope.removeJobStatusChange) {
|
||||
$scope.removeJobStatusChange();
|
||||
}
|
||||
$scope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) {
|
||||
$scope.$on(`ws-jobs`, function(e, data) {
|
||||
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) {
|
||||
$scope.job.status = data.status;
|
||||
}
|
||||
@ -39,12 +36,6 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
}
|
||||
});
|
||||
|
||||
// Unbind $rootScope socket event binding(s) so that they don't get triggered
|
||||
// in another instance of this controller
|
||||
$scope.$on('$destroy', function() {
|
||||
$scope.removeJobStatusChange();
|
||||
});
|
||||
|
||||
// Set the parse type so that CodeMirror knows how to display extra params YAML/JSON
|
||||
$scope.parseType = 'yaml';
|
||||
|
||||
|
||||
4865
awx/ui/npm-shrinkwrap.json
generated
4865
awx/ui/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@
|
||||
},
|
||||
"config": {
|
||||
"django_port": "8013",
|
||||
"websocket_port": "8080",
|
||||
"django_host": "0.0.0.0",
|
||||
"sauce_username": "leigh-johnson",
|
||||
"sauce_access_key": "f740c3ad-c706-4e10-bb95-46e2cc50c2ac"
|
||||
@ -17,7 +16,7 @@
|
||||
"npm": "^3.10.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build-docker-machine": "docker-machine ssh $DOCKER_MACHINE_NAME -f -N -L ${npm_package_config_websocket_port}:localhost:${npm_package_config_websocket_port}; ip=$(docker-machine ip $DOCKER_MACHINE_NAME); npm set ansible-tower:django_host ${ip}; grunt dev",
|
||||
"build-docker-machine": "ip=$(docker-machine ip $DOCKER_MACHINE_NAME); npm set ansible-tower:django_host ${ip}; grunt dev",
|
||||
"build-docker-cid": "ip=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' $DOCkER_CID` | npm set config ansible-tower:django_host ${ip}; grunt dev",
|
||||
"build-release": "grunt release",
|
||||
"pretest": "grunt clean:coverage",
|
||||
@ -99,7 +98,7 @@
|
||||
"moment": "^2.10.2",
|
||||
"ng-toast": "leigh-johnson/ngToast#2.0.1",
|
||||
"nvd3": "leigh-johnson/nvd3#1.7.1",
|
||||
"select2": "^4.0.2",
|
||||
"socket.io-client": "^0.9.17"
|
||||
"reconnectingwebsocket": "^1.0.0",
|
||||
"select2": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
30
awx/ui/tests/spec/socket/socket.service-test.js
Normal file
30
awx/ui/tests/spec/socket/socket.service-test.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
describe('Service: SocketService', () => {
|
||||
|
||||
let SocketService,
|
||||
rootScope,
|
||||
event;
|
||||
|
||||
beforeEach(angular.mock.module('shared'));
|
||||
beforeEach(angular.mock.module('socket', function($provide){
|
||||
$provide.value('$rootScope', rootScope);
|
||||
$provide.value('$location', {url: function(){}});
|
||||
}));
|
||||
beforeEach(angular.mock.inject(($rootScope, _SocketService_) => {
|
||||
rootScope = $rootScope.$new();
|
||||
rootScope.$emit = jasmine.createSpy('$emit');
|
||||
SocketService = _SocketService_;
|
||||
}));
|
||||
|
||||
describe('socket onmessage() should broadcast to correct event listener', function(){
|
||||
|
||||
it('should send to ws-jobs-summary', function(){
|
||||
event = {data : {group_name: "jobs"}};
|
||||
event.data = JSON.stringify(event.data);
|
||||
SocketService.onMessage(event);
|
||||
expect(rootScope.$emit).toHaveBeenCalledWith('ws-jobs-summary', event.data);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -27,7 +27,7 @@ var vendorPkgs = [
|
||||
'ng-toast',
|
||||
'nvd3',
|
||||
'select2',
|
||||
'socket.io-client',
|
||||
'reconnectingwebsocket'
|
||||
];
|
||||
|
||||
var dev = {
|
||||
|
||||
@ -85,7 +85,8 @@ pycparser==2.14
|
||||
pygerduty==0.32.1
|
||||
PyJWT==1.4.0
|
||||
pymongo==2.8
|
||||
pyOpenSSL==0.15.1
|
||||
pyOpenSSL==16.0.0
|
||||
cffi==1.7.0
|
||||
pyparsing==2.0.7
|
||||
pyrad==2.0
|
||||
pyrax==1.9.7
|
||||
@ -131,3 +132,6 @@ wheel==0.24.0
|
||||
wrapt==1.10.6
|
||||
wsgiref==0.1.2
|
||||
xmltodict==0.9.2
|
||||
channels==0.17.2
|
||||
asgi_amqp==0.3
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user