Merge pull request #3657 from wwitzel3/jtabor-sockets

Switch to Django Channels
This commit is contained in:
Wayne Witzel III 2016-10-07 10:39:04 -04:00 committed by GitHub
commit 594fa8cf89
73 changed files with 817 additions and 5847 deletions

View File

@ -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

View File

@ -1,6 +1,5 @@
runserver: make runserver
celeryd: make celeryd
receiver: make receiver
socketservice: make socketservice
factcacher: make factcacher
flower: make flower

View File

@ -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
View 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
View 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)})

View 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()),
],
),
]

View File

@ -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).

View 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()

View File

@ -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 {}

View File

@ -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"

View File

@ -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

View File

@ -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()

View File

@ -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
View 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/$'),
]

View File

@ -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]

View File

@ -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")

View File

@ -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)

View File

@ -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()

View File

@ -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 = {
},
}
}

View File

@ -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

View File

@ -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/');

View File

@ -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) {

View File

@ -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');
}

View File

@ -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) {

View File

@ -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',

View File

@ -1,10 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import "./help/ChromeSocketHelp";
import "./help/FirefoxSocketHelp";
import "./help/InventoryGroups";
import "./help/SafariSocketHelp";

View File

@ -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."
}]
}
});

View File

@ -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."
}]
}
});

View File

@ -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>"
}]
}
});

View File

@ -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>"
}]
}
});

View File

@ -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,

View File

@ -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 });
}
};
}]);

View File

@ -23,5 +23,10 @@ export default {
},
ncyBreadcrumb: {
label: "RUN COMMAND"
},
socket: {
"groups":{
"jobs": ["status_changed"]
}
}
};

View File

@ -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'),

View File

@ -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 });
};

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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": []
}
}
};

View File

@ -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, {

View File

@ -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;
}

View File

@ -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',

View File

@ -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');

View File

@ -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'

View File

@ -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"

View File

@ -14,6 +14,11 @@ export default {
data: {
activityStreamId: 'id'
},
socket:{
"groups":{
"jobs": ["status_changed"]
}
},
ncyBreadcrumb: {
parent: 'jobTemplates',
label: "{{name}}"

View File

@ -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();
}

View File

@ -17,5 +17,10 @@ export default {
},
ncyBreadcrumb: {
label: "JOB TEMPLATES"
},
socket:{
"groups":{
"jobs": ["status_changed"]
}
}
};

View File

@ -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');

View File

@ -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',

View File

@ -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');

View File

@ -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) {

View File

@ -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) {

View File

@ -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"]
}
}
},
{

View File

@ -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';

View File

@ -9,6 +9,11 @@ export default {
url: '/portal',
ncyBreadcrumb: {
label: "MY VIEW"
},
socket: {
"groups":{
"jobs": ["status_changed"]
}
},
views: {
// the empty parent ui-view

View File

@ -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);
}
}
},
};
};
}]);

View File

@ -3,5 +3,5 @@ import icon from './icon.directive';
export default
angular.module('awIcon', [])
.directive('awIcon', icon)
.directive('awIcon', icon);
//.directive('includeSvg', includeSvg);

View File

@ -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);

View 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);

View 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;
});
}
};
}];

View File

@ -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,

View File

@ -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;
}
}]
}
};

View File

@ -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;
}]
}
};

View File

@ -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') {

View File

@ -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;
}]
}
};

View File

@ -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;
}]
}
};

View File

@ -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';

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View 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);
});
});
});

View File

@ -27,7 +27,7 @@ var vendorPkgs = [
'ng-toast',
'nvd3',
'select2',
'socket.io-client',
'reconnectingwebsocket'
];
var dev = {

View File

@ -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