diff --git a/awx/main/dispatch/control.py b/awx/main/dispatch/control.py index 9beb6b4da2..f836e0624c 100644 --- a/awx/main/dispatch/control.py +++ b/awx/main/dispatch/control.py @@ -14,18 +14,18 @@ class Control(object): services = ('dispatcher', 'callback_receiver') result = None - def __init__(self, service): + def __init__(self, service, host=None): if service not in self.services: raise RuntimeError('{} must be in {}'.format(service, self.services)) self.service = service - queuename = get_local_queuename() - self.queue = Queue(queuename, Exchange(queuename), routing_key=queuename) + self.queuename = host or get_local_queuename() + self.queue = Queue(self.queuename, Exchange(self.queuename), routing_key=self.queuename) - def publish(self, msg, conn, host, **kwargs): + def publish(self, msg, conn, **kwargs): producer = Producer( exchange=self.queue.exchange, channel=conn, - routing_key=get_local_queuename() + routing_key=self.queuename ) producer.publish(msg, expiration=5, **kwargs) @@ -35,14 +35,13 @@ class Control(object): def running(self, *args, **kwargs): return self.control_with_reply('running', *args, **kwargs) - def control_with_reply(self, command, host=None, timeout=5): - host = host or settings.CLUSTER_HOST_ID - logger.warn('checking {} {} for {}'.format(self.service, command, host)) + def control_with_reply(self, command, timeout=5): + logger.warn('checking {} {} for {}'.format(self.service, command, self.queuename)) reply_queue = Queue(name="amq.rabbitmq.reply-to") self.result = None with Connection(settings.BROKER_URL) as conn: with Consumer(conn, reply_queue, callbacks=[self.process_message], no_ack=True): - self.publish({'control': command}, conn, host, reply_to='amq.rabbitmq.reply-to') + self.publish({'control': command}, conn, reply_to='amq.rabbitmq.reply-to') try: conn.drain_events(timeout=timeout) except socket.timeout: @@ -50,10 +49,9 @@ class Control(object): raise return self.result - def control(self, msg, host=None, **kwargs): - host = host or settings.CLUSTER_HOST_ID + def control(self, msg, **kwargs): with Connection(settings.BROKER_URL) as conn: - self.publish(msg, conn, host) + self.publish(msg, conn) def process_message(self, body, message): self.result = body diff --git a/awx/main/dispatch/pool.py b/awx/main/dispatch/pool.py index b03bbf4c92..e93d2ffc90 100644 --- a/awx/main/dispatch/pool.py +++ b/awx/main/dispatch/pool.py @@ -1,5 +1,6 @@ import logging import os +import sys import random import traceback from uuid import uuid4 @@ -10,7 +11,7 @@ from multiprocessing import Queue as MPQueue from Queue import Full as QueueFull, Empty as QueueEmpty from django.conf import settings -from django.db import connection as django_connection +from django.db import connection as django_connection, connections from django.core.cache import cache as django_cache from jinja2 import Template import psutil @@ -319,6 +320,8 @@ class AutoscalePool(WorkerPool): 1. Discover worker processes that exited, and recover messages they were handling. 2. Clean up unnecessary, idle workers. + 3. Check to see if the database says this node is running any tasks + that aren't actually running. If so, reap them. """ orphaned = [] for w in self.workers[::]: @@ -354,6 +357,20 @@ class AutoscalePool(WorkerPool): idx = random.choice(range(len(self.workers))) self.write(idx, m) + # if the database says a job is running on this node, but it's *not*, + # then reap it + running_uuids = [] + for worker in self.workers: + worker.calculate_managed_tasks() + running_uuids.extend(worker.managed_tasks.keys()) + try: + reaper.reap(excluded_uuids=running_uuids) + except Exception: + # we _probably_ failed here due to DB connectivity issues, so + # don't use our logger (it accesses the database for configuration) + _, _, tb = sys.exc_info() + traceback.print_tb(tb) + def up(self): if self.full: # if we can't spawn more workers, just toss this message into a @@ -364,18 +381,25 @@ class AutoscalePool(WorkerPool): return super(AutoscalePool, self).up() def write(self, preferred_queue, body): - # when the cluster heartbeat occurs, clean up internally - if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']: - self.cleanup() - if self.should_grow: - self.up() - # we don't care about "preferred queue" round robin distribution, just - # find the first non-busy worker and claim it - workers = self.workers[:] - random.shuffle(workers) - for w in workers: - if not w.busy: - w.put(body) - break - else: - return super(AutoscalePool, self).write(preferred_queue, body) + try: + # when the cluster heartbeat occurs, clean up internally + if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']: + self.cleanup() + if self.should_grow: + self.up() + # we don't care about "preferred queue" round robin distribution, just + # find the first non-busy worker and claim it + workers = self.workers[:] + random.shuffle(workers) + for w in workers: + if not w.busy: + w.put(body) + break + else: + return super(AutoscalePool, self).write(preferred_queue, body) + except Exception: + for conn in connections.all(): + # If the database connection has a hiccup, re-establish a new + # connection + conn.close_if_unusable_or_obsolete() + logger.exception('failed to write inbound message') diff --git a/awx/main/dispatch/reaper.py b/awx/main/dispatch/reaper.py index 8e9a9d3d15..f37a62290f 100644 --- a/awx/main/dispatch/reaper.py +++ b/awx/main/dispatch/reaper.py @@ -26,7 +26,7 @@ def reap_job(j, status): ) -def reap(instance=None, status='failed'): +def reap(instance=None, status='failed', excluded_uuids=[]): ''' Reap all jobs in waiting|running for this instance. ''' @@ -41,6 +41,6 @@ def reap(instance=None, status='failed'): Q(execution_node=me.hostname) | Q(controller_node=me.hostname) ) & ~Q(polymorphic_ctype_id=workflow_ctype_id) - ) + ).exclude(celery_task_id__in=excluded_uuids) for j in jobs: reap_job(j, status) diff --git a/awx/main/dispatch/worker/task.py b/awx/main/dispatch/worker/task.py index d1273749a1..89298384bb 100644 --- a/awx/main/dispatch/worker/task.py +++ b/awx/main/dispatch/worker/task.py @@ -5,6 +5,7 @@ import sys import traceback import six +from django import db from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown @@ -74,6 +75,10 @@ class TaskWorker(BaseWorker): 'task': u'awx.main.tasks.RunProjectUpdate' } ''' + for conn in db.connections.all(): + # If the database connection has a hiccup during at task, close it + # so we can establish a new connection + conn.close_if_unusable_or_obsolete() result = None try: result = self.run_callable(body) diff --git a/awx/main/management/commands/run_dispatcher.py b/awx/main/management/commands/run_dispatcher.py index 53cfe05a29..312c146e20 100644 --- a/awx/main/management/commands/run_dispatcher.py +++ b/awx/main/management/commands/run_dispatcher.py @@ -7,7 +7,7 @@ from multiprocessing import Process from django.conf import settings from django.core.cache import cache as django_cache from django.core.management.base import BaseCommand -from django.db import connection as django_connection +from django.db import connection as django_connection, connections from kombu import Connection, Exchange, Queue from awx.main.dispatch import get_local_queuename, reaper @@ -57,6 +57,10 @@ class Command(BaseCommand): return super(AWXScheduler, self).tick(*args, **kwargs) def apply_async(self, entry, producer=None, advance=True, **kwargs): + for conn in connections.all(): + # If the database connection has a hiccup, re-establish a new + # connection + conn.close_if_unusable_or_obsolete() task = TaskWorker.resolve_callable(entry.task) result, queue = task.apply_async() diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 082fa8951d..28f348d1e6 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -7,9 +7,11 @@ import json import logging import os import re +import socket import subprocess import tempfile from collections import OrderedDict +import six # Django from django.conf import settings @@ -29,6 +31,7 @@ from polymorphic.models import PolymorphicModel # AWX from awx.main.models.base import * # noqa +from awx.main.dispatch.control import Control as ControlDispatcher from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin from awx.main.utils import ( encrypt_dict, decrypt_field, _inventory_updates, @@ -1248,6 +1251,31 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique # Done! return True + + @property + def actually_running(self): + # returns True if the job is running in the appropriate dispatcher process + running = False + if all([ + self.status == 'running', + self.celery_task_id, + self.execution_node + ]): + # If the job is marked as running, but the dispatcher + # doesn't know about it (or the dispatcher doesn't reply), + # then cancel the job + timeout = 5 + try: + running = self.celery_task_id in ControlDispatcher( + 'dispatcher', self.execution_node + ).running(timeout=timeout) + except socket.timeout: + logger.error(six.text_type( + 'could not reach dispatcher on {} within {}s' + ).format(self.execution_node, timeout)) + running = False + return running + @property def can_cancel(self): return bool(self.status in CAN_CANCEL) @@ -1270,6 +1298,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if self.status in ('pending', 'waiting', 'new'): self.status = 'canceled' cancel_fields.append('status') + if self.status == 'running' and not self.actually_running: + self.status = 'canceled' + cancel_fields.append('status') if job_explanation is not None: self.job_explanation = job_explanation cancel_fields.append('job_explanation') diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index d8698abada..357dd9eeb0 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -481,3 +481,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio @property def preferred_instance_groups(self): return [] + + @property + def actually_running(self): + # WorkflowJobs don't _actually_ run anything in the dispatcher, so + # there's no point in asking the dispatcher if it knows about this task + return self.status == 'running' diff --git a/awx/main/tests/functional/test_dispatch.py b/awx/main/tests/functional/test_dispatch.py index b7dc158a9b..3567d7d37a 100644 --- a/awx/main/tests/functional/test_dispatch.py +++ b/awx/main/tests/functional/test_dispatch.py @@ -348,6 +348,32 @@ class TestJobReaper(object): else: assert job.status == status + @pytest.mark.parametrize('excluded_uuids, fail', [ + (['abc123'], False), + ([], True), + ]) + def test_do_not_reap_excluded_uuids(self, excluded_uuids, fail): + i = Instance(hostname='awx') + i.save() + j = Job( + status='running', + execution_node='awx', + controller_node='', + start_args='SENSITIVE', + celery_task_id='abc123', + ) + j.save() + + # if the UUID is excluded, don't reap it + reaper.reap(i, excluded_uuids=excluded_uuids) + job = Job.objects.first() + if fail: + assert job.status == 'failed' + assert 'marked as failed' in job.job_explanation + assert job.start_args == '' + else: + assert job.status == 'running' + def test_workflow_does_not_reap(self): i = Instance(hostname='awx') i.save() diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index f206a206bc..b816d2c65d 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -9,6 +9,7 @@ import atFeaturesTemplates from '~features/templates'; import atFeaturesUsers from '~features/users'; import atFeaturesJobs from '~features/jobs'; import atFeaturesPortalMode from '~features/portalMode'; +import atFeaturesProjects from '~features/projects'; const MODULE_NAME = 'at.features'; @@ -24,6 +25,7 @@ angular.module(MODULE_NAME, [ atFeaturesOutput, atFeaturesTemplates, atFeaturesPortalMode, + atFeaturesProjects ]); export default MODULE_NAME; diff --git a/awx/ui/client/features/projects/index.controller.js b/awx/ui/client/features/projects/index.controller.js new file mode 100644 index 0000000000..4722753e97 --- /dev/null +++ b/awx/ui/client/features/projects/index.controller.js @@ -0,0 +1,19 @@ +function IndexProjectsController ($scope, strings, dataset) { + const vm = this; + vm.strings = strings; + vm.count = dataset.data.count; + + $scope.$on('updateCount', (e, count) => { + if (typeof count === 'number') { + vm.count = count; + } + }); +} + +IndexProjectsController.$inject = [ + '$scope', + 'ProjectsStrings', + 'Dataset', +]; + +export default IndexProjectsController; diff --git a/awx/ui/client/features/projects/index.js b/awx/ui/client/features/projects/index.js new file mode 100644 index 0000000000..c9e51f7fbc --- /dev/null +++ b/awx/ui/client/features/projects/index.js @@ -0,0 +1,9 @@ +import ProjectsStrings from './projects.strings'; + +const MODULE_NAME = 'at.features.projects'; + +angular + .module(MODULE_NAME, []) + .service('ProjectsStrings', ProjectsStrings); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/projects/index.view.html b/awx/ui/client/features/projects/index.view.html new file mode 100644 index 0000000000..13e18dfb28 --- /dev/null +++ b/awx/ui/client/features/projects/index.view.html @@ -0,0 +1,12 @@ +
+ +Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' + - 'Select button, located bottom right.
Create a new project by clicking the button.
', - index: false, - hover: true, - emptyListText: i18n._('No Projects Have Been Created'), + name: 'projects', + iterator: 'project', + basePath: 'projects', + selectTitle: i18n._('Add Project'), + editTitle: i18n._('PROJECTS'), + listTitle: i18n._('PROJECTS'), + selectInstructions: 'Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' + + 'Select button, located bottom right.
Create a new project by clicking the button.
', + index: false, + hover: true, + emptyListText: i18n._('No Projects Have Been Created'), - fields: { - status: { - label: '', - iconOnly: true, - ngClick: 'showSCMStatus(project.id)', - awToolTip: '{{ project.statusTip }}', - dataTipWatch: 'project.statusTip', - dataPlacement: 'right', - icon: "icon-job-{{ project.statusIcon }}", - columnClass: "List-staticColumn--smallStatus", - nosort: true, - excludeModal: true - }, - name: { - key: true, - label: i18n._('Name'), - columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent", - modalColumnClass: 'col-md-8', - awToolTip: '{{project.description | sanitize}}', - dataPlacement: 'top' - }, - scm_type: { - label: i18n._('Type'), - ngBind: 'project.type_label', - excludeModal: true, - columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs' - }, - scm_revision: { - label: i18n._('Revision'), - excludeModal: true, - columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs', - type: 'revision' - }, - last_updated: { - label: i18n._('Last Updated'), - filter: "longDate", - columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs", - excludeModal: true - } - }, + fields: { + status: { + label: '', + iconOnly: true, + ngClick: 'showSCMStatus(project.id)', + awToolTip: '{{ project.statusTip }}', + dataTipWatch: 'project.statusTip', + dataPlacement: 'right', + icon: "icon-job-{{ project.statusIcon }}", + columnClass: "List-staticColumn--smallStatus", + nosort: true, + excludeModal: true + }, + name: { + key: true, + label: i18n._('Name'), + columnClass: "col-lg-4 col-md-4 col-sm-4 col-xs-7 List-staticColumnAdjacent", + modalColumnClass: 'col-md-8', + awToolTip: '{{project.description | sanitize}}', + dataPlacement: 'top' + }, + scm_type: { + label: i18n._('Type'), + ngBind: 'project.type_label', + excludeModal: true, + columnClass: 'col-lg-2 col-md-2 col-sm-2 hidden-xs' + }, + scm_revision: { + label: i18n._('Revision'), + excludeModal: true, + columnClass: 'List-tableCell col-lg-2 col-md-2 hidden-sm hidden-xs', + type: 'revision' + }, + last_updated: { + label: i18n._('Last Updated'), + filter: "longDate", + columnClass: "col-lg-3 hidden-md hidden-sm hidden-xs", + excludeModal: true + } + }, - actions: { - refresh: { - mode: 'all', - awToolTip: i18n._("Refresh the page"), - ngClick: "refresh()", - ngShow: "socketStatus === 'error'", - actionClass: 'btn List-buttonDefault', - buttonContent: i18n._('REFRESH') - }, - add: { - mode: 'all', // One of: edit, select, all - ngClick: 'addProject()', - awToolTip: i18n._('Create a new project'), - actionClass: 'at-Button--add', - actionId: 'button-add', - ngShow: "canAdd" - } - }, + actions: { + refresh: { + mode: 'all', + awToolTip: i18n._("Refresh the page"), + ngClick: "refresh()", + ngShow: "socketStatus === 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: i18n._('REFRESH') + }, + add: { + mode: 'all', // One of: edit, select, all + ngClick: 'addProject()', + awToolTip: i18n._('Create a new project'), + actionClass: 'at-Button--add', + actionId: 'button-add', + ngShow: "canAdd" + } + }, - fieldActions: { + fieldActions: { - columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5', - edit: { - ngClick: "editProject(project.id)", - awToolTip: i18n._('Edit the project'), - dataPlacement: 'top', - ngShow: "project.summary_fields.user_capabilities.edit" - }, - scm_update: { - ngClick: 'SCMUpdate(project.id, $event)', - awToolTip: "{{ project.scm_update_tooltip }}", - dataTipWatch: "project.scm_update_tooltip", - ngClass: "project.scm_type_class", - dataPlacement: 'top', - ngShow: "project.summary_fields.user_capabilities.start" - }, - copy: { - label: i18n._('Copy'), - ngClick: 'copyProject(project)', - "class": 'btn-danger btn-xs', - awToolTip: i18n._('Copy project'), - dataPlacement: 'top', - ngShow: 'project.summary_fields.user_capabilities.copy' - }, - view: { - ngClick: "editProject(project.id)", - awToolTip: i18n._('View the project'), - dataPlacement: 'top', - ngShow: "!project.summary_fields.user_capabilities.edit", - icon: 'fa-eye', - }, - "delete": { - ngClick: "deleteProject(project.id, project.name)", - awToolTip: i18n._('Delete the project'), - ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete", - dataPlacement: 'top' - }, - cancel: { - ngClick: "cancelUpdate(project)", - awToolTip: i18n._('Cancel the SCM update'), - ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start", - dataPlacement: 'top', - ngDisabled: "project.pending_cancellation || project.status == 'canceled'" - } - } - };}]; + columnClass: 'col-lg-4 col-md-3 col-sm-4 col-xs-5', + edit: { + ngClick: "editProject(project.id)", + awToolTip: i18n._('Edit the project'), + dataPlacement: 'top', + ngShow: "project.summary_fields.user_capabilities.edit" + }, + scm_update: { + ngClick: 'SCMUpdate(project.id, $event)', + awToolTip: "{{ project.scm_update_tooltip }}", + dataTipWatch: "project.scm_update_tooltip", + ngClass: "project.scm_type_class", + dataPlacement: 'top', + ngShow: "project.summary_fields.user_capabilities.start" + }, + copy: { + label: i18n._('Copy'), + ngClick: 'copyProject(project)', + "class": 'btn-danger btn-xs', + awToolTip: i18n._('Copy project'), + dataPlacement: 'top', + ngShow: 'project.summary_fields.user_capabilities.copy' + }, + view: { + ngClick: "editProject(project.id)", + awToolTip: i18n._('View the project'), + dataPlacement: 'top', + ngShow: "!project.summary_fields.user_capabilities.edit", + icon: 'fa-eye', + }, + "delete": { + ngClick: "deleteProject(project.id, project.name)", + awToolTip: i18n._('Delete the project'), + ngShow: "(project.status !== 'updating' && project.status !== 'running' && project.status !== 'pending' && project.status !== 'waiting') && project.summary_fields.user_capabilities.delete", + dataPlacement: 'top' + }, + cancel: { + ngClick: "cancelUpdate(project)", + awToolTip: i18n._('Cancel the SCM update'), + ngShow: "(project.status == 'updating' || project.status == 'running' || project.status == 'pending' || project.status == 'waiting') && project.summary_fields.user_capabilities.start", + dataPlacement: 'top', + ngDisabled: "project.pending_cancellation || project.status == 'canceled'" + } + } + };}]; \ No newline at end of file diff --git a/awx/ui/client/src/projects/projects.strings.js b/awx/ui/client/src/projects/projects.strings.js deleted file mode 100644 index 37e4141a7b..0000000000 --- a/awx/ui/client/src/projects/projects.strings.js +++ /dev/null @@ -1,7 +0,0 @@ -function ProjectsStrings (BaseString) { - BaseString.call(this, 'projects'); -} - -ProjectsStrings.$inject = ['BaseStringService']; - -export default ProjectsStrings; diff --git a/awx/ui/test/e2e/objects/projects.js b/awx/ui/test/e2e/objects/projects.js index 85965c4019..345bf1664a 100644 --- a/awx/ui/test/e2e/objects/projects.js +++ b/awx/ui/test/e2e/objects/projects.js @@ -56,10 +56,10 @@ module.exports = { } }, list: { - selector: '.Panel', + selector: '.at-Panel', elements: { - badge: 'span[class~="badge"]', - title: 'div[class="List-titleText"]', + badge: '.at-Panel-headingTitleBadge', + title: '.at-Panel-headingTitle', add: '#button-add' }, sections: { diff --git a/awx/ui/test/e2e/objects/sections/search.js b/awx/ui/test/e2e/objects/sections/search.js index 9feed81554..64fd557da5 100644 --- a/awx/ui/test/e2e/objects/sections/search.js +++ b/awx/ui/test/e2e/objects/sections/search.js @@ -2,8 +2,8 @@ const search = { selector: 'smart-search', locateStrategy: 'css selector', elements: { - clearAll: 'a[class*="clear"]', - searchButton: 'i[class$="search"]', + clearAll: 'a[class*="clearAll"]', + searchButton: 'i[class*="fa-search"]', input: 'input', tags: '.SmartSearch-tagContainer' } diff --git a/awx/ui/test/e2e/tests/test-projects-list-actions.js b/awx/ui/test/e2e/tests/test-projects-list-actions.js index a252160b85..f01921dad5 100644 --- a/awx/ui/test/e2e/tests/test-projects-list-actions.js +++ b/awx/ui/test/e2e/tests/test-projects-list-actions.js @@ -31,7 +31,7 @@ module.exports = { projects.waitForElementNotVisible('div.spinny'); projects.section.list.expect.element('@badge').text.equal('1'); - projects.expect.element(`#projects_table tr[id="${data.project.id}"]`).visible; + projects.expect.element(`#row-${data.project.id}`).visible; projects.expect.element('i[class*="copy"]').visible; projects.expect.element('i[class*="copy"]').enabled; diff --git a/awx/ui/test/e2e/tests/test-xss.js b/awx/ui/test/e2e/tests/test-xss.js index 6383926709..d33ee0b754 100644 --- a/awx/ui/test/e2e/tests/test-xss.js +++ b/awx/ui/test/e2e/tests/test-xss.js @@ -508,36 +508,36 @@ module.exports = { client.expect.element('#project_form').visible; }, 'check project list for unsanitized content': client => { - const itemRow = `#projects_table tr[id="${data.project.id}"]`; - const itemName = `${itemRow} td[class*="name-"] a`; + const itemRow = `#row-${data.project.id}`; + const itemName = `${itemRow} .at-RowItem-header`; - client.expect.element('div[class^="Panel"] smart-search').visible; - client.expect.element('div[class^="Panel"] smart-search input').enabled; + client.expect.element('.at-Panel smart-search').visible; + client.expect.element('.at-Panel smart-search input').enabled; - client.sendKeys('div[class^="Panel"] smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); - client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER); + client.sendKeys('.at-Panel smart-search input', `id:>${data.project.id - 1} id:<${data.project.id + 1}`); + client.sendKeys('.at-Panel smart-search input', client.Keys.ENTER); - client.expect.element('div.spinny').visible; client.expect.element('div.spinny').not.visible; - client.expect.element('.List-titleBadge').text.equal('1'); + client.expect.element('.at-Panel-headingTitleBadge').text.equal('1'); client.expect.element(itemName).visible; - client.moveToElement(itemName, 0, 0, () => { - client.expect.element(itemName).attribute('aria-describedby'); - - client.getAttribute(itemName, 'aria-describedby', ({ value }) => { - const tooltip = `#${value}`; - - client.expect.element(tooltip).present; - client.expect.element(tooltip).visible; - - client.expect.element('#xss').not.present; - client.expect.element('[class=xss]').not.present; - client.expect.element(tooltip).attribute('innerHTML') - .contains('<div id="xss" class="xss">test</div>'); - }); - }); + // TODO: uncomment when tooltips are added + // client.moveToElement(itemName, 0, 0, () => { + // client.expect.element(itemName).attribute('aria-describedby'); + // + // client.getAttribute(itemName, 'aria-describedby', ({ value }) => { + // const tooltip = `#${value}`; + // + // client.expect.element(tooltip).present; + // client.expect.element(tooltip).visible; + // + // client.expect.element('#xss').not.present; + // client.expect.element('[class=xss]').not.present; + // client.expect.element(tooltip).attribute('innerHTML') + // .contains('<div id="xss" class="xss">test</div>'); + // }); + // }); client.click(`${itemRow} i[class*="trash"]`); diff --git a/requirements/README.md b/requirements/README.md index e93b1a6162..1fa9935206 100644 --- a/requirements/README.md +++ b/requirements/README.md @@ -21,5 +21,3 @@ pip-compile requirements/requirements_ansible.in > requirements/requirements_ans * As of `pip-tools` `1.8.1` `pip-compile` does not resolve packages specified using a git url. Thus, dependencies for things like `dm.xmlsec.binding` do not get resolved and output to `requirements.txt`. This means that: * can't use `pip install --no-deps` because other deps WILL be sucked in * all dependencies are NOT captured in our `.txt` files. This means you can't rely on the `.txt` when gathering licenses. - -* Package `docutils`, as an upstream of `boto3`, is commented out in both `requirements.txt` and `requirements_ansible.txt` because the official package has a bug that causes RPM build failure. [Here](https://sourceforge.net/p/docutils/bugs/321/) is the bug report. Please do not uncomment it before the bug fix lands. For now we are using [a monkey-patch version of `docutils`](https://github.com/ansible/docutils.git) that comes with the bug fix. It's included in `requirements_git.txt` and `requirements_ansible_git.txt`. diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3cfac66dea..6045d655fa 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -40,6 +40,7 @@ django-taggit==0.22.2 django==1.11.16 djangorestframework-yaml==1.0.3 djangorestframework==3.7.7 +docutils==0.14 # via botocore enum34==1.1.6 # via cryptography functools32==3.2.3.post2 # via jsonschema futures==3.2.0 # via requests-futures diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index 3af40155e8..fdb5c07001 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -1,36 +1,54 @@ +# GCE apache-libcloud==2.2.1 -# azure deps from https://github.com/ansible/ansible/blob/fe1153c0afa1ffd648147af97454e900560b3532/packaging/requirements/requirements-azure.txt -azure-mgmt-compute>=2.0.0,<3 # Taken from Ansible core module requirements -azure-mgmt-network>=1.3.0,<2 -azure-mgmt-storage>=1.5.0,<2 -azure-mgmt-resource>=1.1.0,<2 -azure-storage>=0.35.1,<0.36 -azure-cli-core>=2.0.12,<3 -msrest!=0.4.15 -msrestazure>=0.4.11,<0.5 -azure-mgmt-dns>=1.0.1,<2 -azure-mgmt-keyvault>=0.40.0,<0.41 -azure-mgmt-batch>=4.1.0,<5 -azure-mgmt-sql>=0.7.1,<0.8 -azure-mgmt-web>=0.32.0,<0.33 -azure-mgmt-containerservice>=2.0.0,<3.0.0 -azure-mgmt-containerregistry>=1.0.1 -azure-mgmt-rdbms>=0.2.0rc1,<0.3.0 -azure-mgmt-containerinstance>=0.3.1 -backports.ssl-match-hostname==3.5.0.1 +# Azure +# azure deps from https://github.com/ansible/ansible/blob/stable-2.7/packaging/requirements/requirements-azure.txt +packaging +azure-cli-core==2.0.35 +azure-cli-nspkg==3.0.2 +azure-common==1.1.11 +azure-mgmt-batch==4.1.0 +azure-mgmt-compute==2.1.0 +azure-mgmt-containerinstance==0.4.0 +azure-mgmt-containerregistry==2.0.0 +azure-mgmt-containerservice==3.0.1 +azure-mgmt-dns==1.2.0 +azure-mgmt-keyvault==0.40.0 +azure-mgmt-marketplaceordering==0.1.0 +azure-mgmt-monitor==0.5.2 +azure-mgmt-network==1.7.1 +azure-mgmt-nspkg==2.0.0 +azure-mgmt-rdbms==1.2.0 +azure-mgmt-resource==1.2.2 +azure-mgmt-sql==0.7.1 +azure-mgmt-storage==1.5.0 +azure-mgmt-trafficmanager==0.50.0 +azure-mgmt-web==0.32.0 +azure-nspkg==2.0.0 +azure-storage==0.35.1 +msrest==0.4.29 +msrestazure==0.4.31 +azure-keyvault==1.0.0a1 +azure-graphrbac==0.40.0 +# AWS boto==2.47.0 # last which does not break ec2 scripts boto3==1.6.2 +# netaddr filter netaddr +# oVirt/RHV ovirt-engine-sdk-python==4.2.4 # minimum set inside Ansible facts module requirements +# AWX usage pexpect==4.6.0 # same as AWX requirement python-memcached==1.59 # same as AWX requirement -psphere==0.5.2 psutil==5.4.3 # same as AWX requirement -pyvmomi==6.5 -pywinrm[kerberos]==0.3.0 -requests<2.16 # Older versions rely on certify -requests-credssp==0.1.0 # For windows authentication awx/issues/1144 -secretstorage==2.3.1 -shade==1.27.0 setuptools==36.0.1 pip==9.0.1 +# VMware +psphere==0.5.2 +pyvmomi==6.5 +# WinRM +backports.ssl-match-hostname==3.5.0.1 +pywinrm[kerberos]==0.3.0 +requests +requests-credssp==0.1.0 # For windows authentication awx/issues/1144 +# OpenStack +shade==1.27.0 diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 860bf817e6..4d7dbfcc26 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -4,60 +4,69 @@ # # pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in # -adal==0.5.0 # via azure-cli-core, msrestazure +adal==0.5.0 # via msrestazure apache-libcloud==2.2.1 appdirs==1.4.3 # via openstacksdk, os-client-config applicationinsights==0.11.1 # via azure-cli-core argcomplete==1.9.4 # via azure-cli-core, knack asn1crypto==0.24.0 # via cryptography -azure-cli-core==2.0.28 -azure-cli-nspkg==3.0.1 # via azure-cli-core -azure-common==1.1.8 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-web, azure-storage +azure-cli-core==2.0.35 +azure-cli-nspkg==3.0.2 +azure-common==1.1.11 +azure-graphrbac==0.40.0 +azure-keyvault==1.0.0a1 azure-mgmt-batch==4.1.0 azure-mgmt-compute==2.1.0 -azure-mgmt-containerinstance==0.3.1 -azure-mgmt-containerregistry==1.0.1 -azure-mgmt-containerservice==2.0.0 +azure-mgmt-containerinstance==0.4.0 +azure-mgmt-containerregistry==2.0.0 +azure-mgmt-containerservice==3.0.1 azure-mgmt-dns==1.2.0 azure-mgmt-keyvault==0.40.0 +azure-mgmt-marketplaceordering==0.1.0 +azure-mgmt-monitor==0.5.2 azure-mgmt-network==1.7.1 -azure-mgmt-nspkg==2.0.0 # via azure-mgmt-batch, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-dns, azure-mgmt-keyvault, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-resource, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-web -azure-mgmt-rdbms==0.2.0rc1 +azure-mgmt-nspkg==2.0.0 +azure-mgmt-rdbms==1.2.0 azure-mgmt-resource==1.2.2 azure-mgmt-sql==0.7.1 azure-mgmt-storage==1.5.0 +azure-mgmt-trafficmanager==0.50.0 azure-mgmt-web==0.32.0 -azure-nspkg==2.0.0 # via azure-cli-nspkg, azure-common, azure-mgmt-nspkg, azure-storage +azure-nspkg==2.0.0 azure-storage==0.35.1 backports.ssl-match-hostname==3.5.0.1 bcrypt==3.1.4 # via paramiko boto3==1.6.2 boto==2.47.0 botocore==1.9.3 # via boto3, s3transfer -certifi==2018.1.18 # via msrest +certifi==2018.1.18 # via msrest, requests cffi==1.11.5 # via bcrypt, cryptography, pynacl +chardet==3.0.4 # via requests colorama==0.3.9 # via azure-cli-core, knack -cryptography==2.1.4 # via adal, azure-storage, paramiko, pyopenssl, requests-kerberos, requests-ntlm, secretstorage +configparser==3.5.0 # via entrypoints +cryptography==2.1.4 # via adal, azure-keyvault, azure-storage, paramiko, pyopenssl, requests-kerberos, requests-ntlm, secretstorage decorator==4.2.1 # via openstacksdk deprecation==2.0 # via openstacksdk +docutils==0.14 # via botocore dogpile.cache==0.6.5 # via openstacksdk +entrypoints==0.2.3 # via keyring enum34==1.1.6 # via cryptography, knack, msrest, ovirt-engine-sdk-python futures==3.2.0 # via openstacksdk, s3transfer humanfriendly==4.8 # via azure-cli-core -idna==2.6 # via cryptography +idna==2.6 # via cryptography, requests ipaddress==1.0.19 # via cryptography, openstacksdk iso8601==0.1.12 # via keystoneauth1, openstacksdk isodate==0.6.0 # via msrest jmespath==0.9.3 # via azure-cli-core, boto3, botocore, knack, openstacksdk jsonpatch==1.21 # via openstacksdk jsonpointer==2.0 # via jsonpatch -keyring==11.0.0 # via msrestazure +keyring==15.1.0 # via msrestazure keystoneauth1==3.4.0 # via openstacksdk, os-client-config -knack==0.3.1 # via azure-cli-core +knack==0.3.3 # via azure-cli-core lxml==4.1.1 # via pyvmomi monotonic==1.4 # via humanfriendly -msrest==0.4.26 -msrestazure==0.4.22 +msrest==0.4.29 +msrestazure==0.4.31 munch==2.2.0 # via openstacksdk netaddr==0.7.19 netifaces==0.10.6 # via openstacksdk @@ -67,10 +76,10 @@ openstacksdk==0.12.0 # via shade os-client-config==1.29.0 # via shade os-service-types==1.2.0 # via openstacksdk ovirt-engine-sdk-python==4.2.4 -packaging==17.1 # via deprecation +packaging==17.1 paramiko==2.4.0 # via azure-cli-core -pexpect==4.6.0 pbr==3.1.1 # via keystoneauth1, openstacksdk, os-service-types, shade, stevedore +pexpect==4.6.0 psphere==0.5.2 psutil==5.4.3 ptyprocess==0.5.2 # via pexpect @@ -92,15 +101,16 @@ requests-credssp==0.1.0 requests-kerberos==0.12.0 # via pywinrm requests-ntlm==1.1.0 # via pywinrm requests-oauthlib==0.8.0 # via msrest -requests==2.15.1 +requests==2.20.0 requestsexceptions==1.4.0 # via openstacksdk, os-client-config s3transfer==0.1.13 # via boto3 -secretstorage==2.3.1 +secretstorage==2.3.1 # via keyring shade==1.27.0 six==1.11.0 # via azure-cli-core, bcrypt, cryptography, isodate, keystoneauth1, knack, munch, ntlm-auth, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, python-memcached, pyvmomi, pywinrm, stevedore stevedore==1.28.0 # via keystoneauth1 suds==0.4 # via psphere tabulate==0.7.7 # via azure-cli-core, knack +urllib3==1.24 # via requests wheel==0.30.0 # via azure-cli-core xmltodict==0.11.0 # via pywinrm diff --git a/requirements/requirements_ansible_git.txt b/requirements/requirements_ansible_git.txt index 425e2e171f..e69de29bb2 100644 --- a/requirements/requirements_ansible_git.txt +++ b/requirements/requirements_ansible_git.txt @@ -1 +0,0 @@ -git+https://github.com/ansible/docutils.git@master#egg=docutils diff --git a/requirements/requirements_ansible_uninstall.txt b/requirements/requirements_ansible_uninstall.txt index 30d8dedee3..28176c3daf 100644 --- a/requirements/requirements_ansible_uninstall.txt +++ b/requirements/requirements_ansible_uninstall.txt @@ -1,2 +1 @@ -certifi pycurl # requires system package version diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index 7ccc5a7227..3b65bbc4b3 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -2,4 +2,3 @@ git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield -git+https://github.com/ansible/docutils.git@master#egg=docutils