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 @@ +
+
+ +
+ + +
+
+
diff --git a/awx/ui/client/features/projects/projects.strings.js b/awx/ui/client/features/projects/projects.strings.js new file mode 100644 index 0000000000..c09f3104d9 --- /dev/null +++ b/awx/ui/client/features/projects/projects.strings.js @@ -0,0 +1,53 @@ +function ProjectsStrings (BaseString) { + BaseString.call(this, 'projects'); + + const { t } = this; + const ns = this.projects; + + ns.list = { + PANEL_TITLE: t.s('PROJECTS'), + ROW_ITEM_LABEL_REVISION: t.s('REVISION'), + ROW_ITEM_LABEL_ORGANIZATION: t.s('ORGANIZATION'), + ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED'), + ROW_ITEM_LABEL_USED: t.s('LAST USED'), + }; + + ns.update = { + GET_LATEST: t.s('Get latest SCM revision'), + UPDATE_RUNNING: t.s('SCM update currently running'), + MANUAL_PROJECT_NO_UPDATE: t.s('Manual projects do not require an SCM update'), + CANCEL_UPDATE_REQUEST: t.s('Your request to cancel the update was submitted to the task manager.'), + NO_UPDATE_INFO: t.s('There is no SCM update information available for this project. An update has not yet been completed. If you have not already done so, start an update for this project.'), + NO_PROJ_SCM_CONFIG: t.s('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, and then run an update.'), + NO_ACCESS_OR_COMPLETED_UPDATE: t.s('Either you do not have access or the SCM update process completed'), + NO_RUNNING_UPDATE: t.s('An SCM update does not appear to be running for project: '), + }; + + ns.alert = { + NO_UPDATE: t.s('No Updates Available'), + UPDATE_CANCEL: t.s('SCM Update Cancel'), + CANCEL_NOT_ALLOWED: t.s('Cancel Not Allowed'), + NO_SCM_CONFIG: t.s('No SCM Configuration'), + UPDATE_NOT_FOUND: t.s('Update Not Found'), + }; + + ns.status = { + NOT_CONFIG: t.s('Not configured for SCM'), + NEVER_UPDATE: t.s('No SCM updates have run for this project'), + UPDATE_QUEUED: t.s('Update queued. Click for details'), + UPDATE_RUNNING: t.s('Update running. Click for details'), + UPDATE_SUCCESS: t.s('Update succeeded. Click for details'), + UPDATE_FAILED: t.s('Update failed. Click for details'), + UPDATE_MISSING: t.s('Update missing. Click for details'), + UPDATE_CANCELED: t.s('Update canceled. Click for details'), + }; + + ns.error = { + HEADER: this.error.HEADER, + CALL: this.error.CALL, + }; +} + +ProjectsStrings.$inject = ['BaseStringService']; + +export default ProjectsStrings; diff --git a/awx/ui/client/features/projects/projectsList.controller.js b/awx/ui/client/features/projects/projectsList.controller.js new file mode 100644 index 0000000000..fcfecba65f --- /dev/null +++ b/awx/ui/client/features/projects/projectsList.controller.js @@ -0,0 +1,442 @@ +/** *********************************************** + * Copyright (c) 2018 Ansible, Inc. + * + * All Rights Reserved + ************************************************ */ + +const mapChoices = choices => Object.assign(...choices.map(([k, v]) => ({ [k]: v.toUpperCase() }))); + +function projectsListController ( + $filter, $scope, $rootScope, $state, $log, Dataset, Alert, Rest, + ProcessErrors, resolvedModels, strings, Wait, ngToast, + Prompt, GetBasePath, qs, ProjectUpdate, +) { + const vm = this || {}; + const [ProjectModel] = resolvedModels; + $scope.canAdd = ProjectModel.options('actions.POST'); + + vm.strings = strings; + vm.scm_choices = ProjectModel.options('actions.GET.scm_type.choices'); + vm.projectTypes = mapChoices(vm.scm_choices); + + // smart-search + vm.list = { + iterator: 'project', + name: 'projects', + basePath: 'projects', + }; + vm.dataset = Dataset.data; + vm.projects = Dataset.data.results; + $scope.$watch('vm.dataset.count', () => { + $scope.$emit('updateCount', vm.dataset.count, 'projects'); + }); + // build tooltips + _.forEach(vm.projects, buildTooltips); + $rootScope.flashMessage = null; + + // when a project is added/deleted, rebuild tooltips + $scope.$watchCollection('vm.projects', () => { + _.forEach(vm.projects, buildTooltips); + }); + // show active item in the list + $scope.$watch('$state.params', () => { + const projectId = _.get($state.params, 'project_id'); + if ((projectId)) { + vm.activeId = parseInt($state.params.project_id, 10); + } else { + vm.activeId = ''; + } + }, true); + + $scope.$on('ws-jobs', (e, data) => { + $log.debug(data); + if (vm.projects) { + // Assuming we have a list of projects available + const project = vm.projects.find((p) => p.id === data.project_id); + if (project) { + // And we found the affected project + $log.debug(`Received event for project: ${project.name}`); + $log.debug(`Status changed to: ${data.status}`); + if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { + reloadList(); + } else { + project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); + } + project.status = data.status; + buildTooltips(project); + } + } + }); + + if ($scope.removeGoTojobResults) { + $scope.removeGoTojobResults(); + } + + $scope.removeGoTojobResults = $scope.$on('GoTojobResults', (e, data) => { + if (data.summary_fields.current_update || data.summary_fields.last_update) { + Wait('start'); + // Grab the id from summary_fields + const updateJobid = (data.summary_fields.current_update) ? + data.summary_fields.current_update.id : data.summary_fields.last_update.id; + + $state.go('output', { id: updateJobid, type: 'project' }, { reload: true }); + } else { + Alert(vm.strings.get('alert.NO_UPDATE'), vm.strings.get('update.NO_UPDATE_INFO'), 'alert-info'); + } + }); + + if ($scope.removeCancelUpdate) { + $scope.removeCancelUpdate(); + } + + $scope.removeCancelUpdate = $scope.$on('Cancel_Update', (e, url) => { + // Cancel the project update process + Rest.setUrl(url); + Rest.post() + .then(() => { + Alert(vm.strings.get('alert.UPDATE_CANCEL'), vm.strings.get('update.CANCEL_UPDATE_REQUEST'), 'alert-info'); + }) + .catch(createErrorHandler(url, 'POST')); + }); + + if ($scope.removeCheckCancel) { + $scope.removeCheckCancel(); + } + + $scope.removeCheckCancel = $scope.$on('Check_Cancel', (e, projectData) => { + // Check that we 'can' cancel the update + const url = projectData.related.cancel; + Rest.setUrl(url); + Rest.get() + .then(({ data }) => { + if (data.can_cancel) { + $scope.$emit('Cancel_Update', url); + } else { + Alert(vm.strings.get('alert.CANCEL_NOT_ALLOWED'), vm.strings.get('update.NO_ACCESS_OR_COMPLETED_UPDATE'), 'alert-info', null, null, null, null, true); + } + }) + .catch(createErrorHandler(url, 'GET')); + }); + + vm.showSCMStatus = (id) => { + // Refresh the project list + const project = vm.projects.find((p) => p.id === id); + + if ((!project.scm_type) || project.scm_type === 'Manual') { + Alert(vm.strings.get('alert.NO_SCM_CONFIG'), vm.strings.get('update.NO_PROJ_SCM_CONFIG'), 'alert-info'); + } else { + // Refresh what we have in memory + // to insure we're accessing the most recent status record + Rest.setUrl(project.url); + Rest.get() + .then(({ data }) => { + $scope.$emit('GoTojobResults', data); + }) + .catch(createErrorHandler(project.url, 'GET')); + } + }; + + vm.getLastModified = project => { + const modified = _.get(project, 'modified'); + + if (!modified) { + return undefined; + } + + const html = $filter('longDate')(modified); + + // NEED api to add field project.summary_fields.modified_by + + // const { username, id } = _.get(project, 'summary_fields.modified_by', {}); + + // if (username && id) { + // html += ` by ${$filter('sanitize')(username)}`; + // } + + return html; + }; + + vm.getLastUsed = project => { + const modified = _.get(project, 'last_job_run'); + + if (!modified) { + return undefined; + } + + const html = $filter('longDate')(modified); + + // NEED api to add last_job user information such as launch_by + + // const { id } = _.get(project, 'summary_fields.last_job', {}); + // if (id) { + // html += ` by + // ${$filter('sanitize')('placehoder')}`; + // } + return html; + }; + + vm.copyProject = project => { + Wait('start'); + ProjectModel + .create('get', project.id) + .then(model => model.copy()) + .then((copiedProj) => { + ngToast.success({ + content: ` +
+
+ +
+
+ ${vm.strings.get('SUCCESSFUL_CREATION', copiedProj.name)} +
+
`, + dismissButton: false, + dismissOnTimeout: true + }); + $state.go('.', null, { reload: true }); + }) + .catch(createErrorHandler('copy project', 'GET')) + .finally(() => Wait('stop')); + }; + + vm.deleteProject = (id, name) => { + const action = () => { + $('#prompt-modal').modal('hide'); + Wait('start'); + ProjectModel + .request('delete', id) + .then(() => { + let reloadListStateParams = null; + + if (vm.projects.length === 1 + && $state.params.project_search + && _.has($state, 'params.project_search.page') + && $state.params.project_search.page !== '1') { + reloadListStateParams = _.cloneDeep($state.params); + reloadListStateParams.project_search.page = + (parseInt(reloadListStateParams.project_search.page, 10) - 1).toString(); + } + + if (parseInt($state.params.project_id, 10) === id) { + $state.go('^', reloadListStateParams, { reload: true }); + } else { + $state.go('.', reloadListStateParams, { reload: true }); + } + }) + .catch(createErrorHandler(`${ProjectModel.path}${id}/`, 'DELETE')) + .finally(() => { + Wait('stop'); + }); + }; + + ProjectModel.getDependentResourceCounts(id) + .then((counts) => { + const invalidateRelatedLines = []; + let deleteModalBody = `
${vm.strings.get('deleteResource.CONFIRM', 'project')}
`; + + counts.forEach(countObj => { + if (countObj.count && countObj.count > 0) { + invalidateRelatedLines.push(`
${countObj.label}${countObj.count}
`); + } + }); + + if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { + deleteModalBody = `
${vm.strings.get('deleteResource.USED_BY', 'project')} ${vm.strings.get('deleteResource.CONFIRM', 'project')}
`; + invalidateRelatedLines.forEach(invalidateRelatedLine => { + deleteModalBody += invalidateRelatedLine; + }); + } + + Prompt({ + hdr: vm.strings.get('DELETE'), + resourceName: $filter('sanitize')(name), + body: deleteModalBody, + action, + actionText: vm.strings.get('DELETE'), + }); + }); + }; + + vm.cancelUpdate = (project) => { + project.pending_cancellation = true; + Rest.setUrl(GetBasePath('projects') + project.id); + Rest.get() + .then(({ data }) => { + if (data.related.current_update) { + cancelSCMUpdate(data); + } else { + Alert(vm.strings.get('update.UPDATE_NOT_FOUND'), vm.strings.get('update.NO_RUNNING_UPDATE') + project.name, 'alert-info', undefined, undefined, undefined, undefined, true); + } + }) + .catch(createErrorHandler('get project', 'GET')); + }; + + vm.SCMUpdate = (id, event) => { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + vm.projects.forEach((project) => { + if (project.id === id) { + if (project.scm_type === 'Manual' || (!project.scm_type)) { + // Do not respond. Button appears greyed out as if it is disabled. + // Not disabled though, because we need mouse over event + // to work. So user can click, but we just won't do anything. + // Alert('Missing SCM Setup', 'Before running an SCM update, + // edit the project and provide the SCM access information.', 'alert-info'); + } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { + // Alert('Update in Progress', 'The SCM update process is running. + // Use the Refresh button to monitor the status.', 'alert-info'); + } else { + ProjectUpdate({ scope: $scope, project_id: project.id }); + } + } + }); + }; + + function buildTooltips (project) { + project.statusIcon = getStatusIcon(project); + project.statusTip = getStatusTooltip(project); + project.scm_update_tooltip = vm.strings.get('update.GET_LATEST'); + project.scm_update_disabled = false; + + if (project.status === 'pending' || project.status === 'waiting') { + project.scm_update_disabled = true; + } + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + project.statusTip = vm.strings.get('status.UPDATE_CANCELED'); + project.scm_update_disabled = true; + } + + if (project.status === 'running' || project.status === 'updating') { + project.scm_update_tooltip = vm.strings.get('update.UPDATE_RUNNING'); + project.scm_update_disabled = true; + } + + if (project.scm_type === 'manual') { + project.statusIcon = 'none'; + project.statusTip = vm.strings.get('status.NOT_CONFIG'); + project.scm_update_tooltip = vm.strings.get('update.MANUAL_PROJECT_NO_UPDATE'); + project.scm_update_disabled = true; + } + } + + function cancelSCMUpdate (projectData) { + Rest.setUrl(projectData.related.current_update); + Rest.get() + .then(({ data }) => { + $scope.$emit('Check_Cancel', data); + }) + .catch(createErrorHandler(projectData.related.current_update, 'GET')); + } + + function reloadList () { + Wait('start'); + const path = GetBasePath(vm.list.basePath) || GetBasePath(vm.list.name); + qs.search(path, $state.params.project_search) + .then((searchResponse) => { + vm.dataset = searchResponse.data; + vm.projects = vm.dataset.results; + }) + .finally(() => Wait('stop')); + } + + function createErrorHandler (path, action) { + return ({ data, status }) => { + const hdr = strings.get('error.HEADER'); + const msg = strings.get('error.CALL', { path, action, status }); + ProcessErrors($scope, data, status, null, { hdr, msg }); + }; + } + + function getStatusIcon (project) { + let icon = 'none'; + switch (project.status) { + case 'n/a': + case 'ok': + case 'never updated': + icon = 'none'; + break; + case 'pending': + case 'waiting': + case 'new': + icon = 'none'; + break; + case 'updating': + case 'running': + icon = 'running'; + break; + case 'successful': + icon = 'success'; + break; + case 'failed': + case 'missing': + case 'canceled': + icon = 'error'; + break; + default: + break; + } + return icon; + } + + function getStatusTooltip (project) { + let tooltip = ''; + switch (project.status) { + case 'n/a': + case 'ok': + case 'never updated': + tooltip = vm.strings.get('status.NEVER_UPDATE'); + break; + case 'pending': + case 'waiting': + case 'new': + tooltip = vm.strings.get('status.UPDATE_QUEUED'); + break; + case 'updating': + case 'running': + tooltip = vm.strings.get('status.UPDATE_RUNNING'); + break; + case 'successful': + tooltip = vm.strings.get('status.UPDATE_SUCCESS'); + break; + case 'failed': + tooltip = vm.strings.get('status.UPDATE_FAILED'); + break; + case 'missing': + tooltip = vm.strings.get('status.UPDATE_MISSING'); + break; + case 'canceled': + tooltip = vm.strings.get('status.UPDATE_CANCELED'); + break; + default: + break; + } + return tooltip; + } +} + +projectsListController.$inject = [ + '$filter', + '$scope', + '$rootScope', + '$state', + '$log', + 'Dataset', + 'Alert', + 'Rest', + 'ProcessErrors', + 'resolvedModels', + 'ProjectsStrings', + 'Wait', + 'ngToast', + 'Prompt', + 'GetBasePath', + 'QuerySet', + 'ProjectUpdate', +]; + +export default projectsListController; diff --git a/awx/ui/client/features/projects/projectsList.view.html b/awx/ui/client/features/projects/projectsList.view.html new file mode 100644 index 0000000000..78cb78ddf2 --- /dev/null +++ b/awx/ui/client/features/projects/projectsList.view.html @@ -0,0 +1,92 @@ + +
+ + +
+ +
+
+ + +
+ + +
+
+ {{ :: vm.strings.get('list.ROW_ITEM_LABEL_REVISION') }} +
+ +
+ + + + + + +
+
+
+
+ +
+
+ + + + + + +
+
+
+ + +
\ No newline at end of file diff --git a/awx/ui/client/features/projects/routes/projectsList.route.js b/awx/ui/client/features/projects/routes/projectsList.route.js new file mode 100644 index 0000000000..d89b80c3f1 --- /dev/null +++ b/awx/ui/client/features/projects/routes/projectsList.route.js @@ -0,0 +1,90 @@ +import { N_ } from '../../../src/i18n'; +import projectsListController from '../projectsList.controller'; +import indexController from '../index.controller'; + +const indexTemplate = require('~features/projects/index.view.html'); +const projectsListTemplate = require('~features/projects/projectsList.view.html'); + +export default { + searchPrefix: 'project', + name: 'projects', + route: '/projects', + ncyBreadcrumb: { + label: N_('PROJECTS') + }, + data: { + activityStream: true, + activityStreamTarget: 'project', + socket: { + groups: { + jobs: ['status_changed'] + } + } + }, + params: { + project_search: { + dynamic: true, + } + }, + views: { + '@': { + templateUrl: indexTemplate, + controller: indexController, + controllerAs: 'vm' + }, + 'projectsList@projects': { + templateUrl: projectsListTemplate, + controller: projectsListController, + controllerAs: 'vm', + } + }, + resolve: { + CredentialTypes: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', + (Rest, $stateParams, GetBasePath, ProcessErrors) => { + const path = GetBasePath('credential_types'); + Rest.setUrl(path); + return Rest.get() + .then((data) => data.data.results) + .catch((response) => { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: `Failed to get credential types. GET returned status: ${response.status}`, + }); + }); + } + ], + ConfigData: ['ConfigService', 'ProcessErrors', + (ConfigService, ProcessErrors) => ConfigService + .getConfig() + .then(response => response) + .catch(({ data, status }) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: `Failed to get config. GET returned status: status: ${status}`, + }); + })], + Dataset: [ + '$stateParams', + 'Wait', + 'GetBasePath', + 'QuerySet', + ($stateParams, Wait, GetBasePath, qs) => { + const searchParam = $stateParams.project_search; + const searchPath = GetBasePath('projects'); + + Wait('start'); + return qs.search(searchPath, searchParam) + .finally(() => Wait('stop')); + } + ], + resolvedModels: [ + 'ProjectModel', + (Project) => { + const models = [ + new Project(['options']), + ]; + return Promise.all(models); + }, + ], + } +}; diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less index 4a804086a4..1f8d534034 100644 --- a/awx/ui/client/lib/components/list/_index.less +++ b/awx/ui/client/lib/components/list/_index.less @@ -139,6 +139,9 @@ .at-RowItem-status { margin-right: @at-margin-right-list-row-item-status; + & > a { + cursor: pointer; + } } .at-RowItem--isHeader { @@ -254,6 +257,12 @@ background-color: @at-color-list-row-action-hover-danger; } +.at-RowAction--disabled { + pointer-events: none; + opacity: 0.5; + cursor: not-allowed; +} + .at-Row .at-Row-checkbox { align-self: start; margin: 2px 20px 0 0; diff --git a/awx/ui/client/lib/components/list/row-item.directive.js b/awx/ui/client/lib/components/list/row-item.directive.js index 67e169835a..731aa837ec 100644 --- a/awx/ui/client/lib/components/list/row-item.directive.js +++ b/awx/ui/client/lib/components/list/row-item.directive.js @@ -15,6 +15,7 @@ function atRowItem () { headerTag: '@', status: '@', statusTip: '@', + statusClick: '&?', labelValue: '@', labelLink: '@', labelState: '@', diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html index 23de062d4f..eebeab39a9 100644 --- a/awx/ui/client/lib/components/list/row-item.partial.html +++ b/awx/ui/client/lib/components/list/row-item.partial.html @@ -1,12 +1,17 @@
- + + + -
diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index 0a3ddcf57d..5e444a0eed 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -19,6 +19,41 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', orgBase = GetBasePath('organizations'), projBase = GetBasePath('projects'); + + function updateStatus() { + if ($scope.projects) { + $scope.projects.forEach(function(project, i) { + $scope.projects[i].statusIcon = GetProjectIcon(project.status); + $scope.projects[i].statusTip = GetProjectToolTip(project.status); + $scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision"); + $scope.projects[i].scm_type_class = ""; + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + $scope.projects[i].statusTip = i18n._('Canceled. Click for details'); + } + + if (project.status === 'running' || project.status === 'updating') { + $scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running"); + $scope.projects[i].scm_type_class = "btn-disabled"; + } + + if ($scope.project_scm_type_options) { + $scope.project_scm_type_options.forEach(function(type) { + if (type.value === project.scm_type) { + $scope.projects[i].scm_type = type.label; + if (type.label === 'Manual') { + $scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); + $scope.projects[i].scm_type_class = 'btn-disabled'; + $scope.projects[i].statusTip = 'Not configured for SCM'; + $scope.projects[i].statusIcon = 'none'; + } + } + }); + } + }); + } + } + init(); function init() { @@ -31,35 +66,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', $scope.$on('choicesReadyProjectList', function() { Wait('stop'); - if ($scope.projects) { - $scope.projects.forEach(function(project, i) { - $scope.projects[i].statusIcon = GetProjectIcon(project.status); - $scope.projects[i].statusTip = GetProjectToolTip(project.status); - $scope.projects[i].scm_update_tooltip = i18n._("Get latest SCM revision"); - $scope.projects[i].scm_type_class = ""; - - if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { - $scope.projects[i].statusTip = i18n._('Canceled. Click for details'); - } - - if (project.status === 'running' || project.status === 'updating') { - $scope.projects[i].scm_update_tooltip = i18n._("SCM update currently running"); - $scope.projects[i].scm_type_class = "btn-disabled"; - } - - $scope.project_scm_type_options.forEach(function(type) { - if (type.value === project.scm_type) { - $scope.projects[i].scm_type = type.label; - if (type.label === 'Manual') { - $scope.projects[i].scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - $scope.projects[i].scm_type_class = 'btn-disabled'; - $scope.projects[i].statusTip = 'Not configured for SCM'; - $scope.projects[i].statusIcon = 'none'; - } - } - }); - }); - } + updateStatus(); }); } @@ -69,9 +76,9 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', }); $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); + optionsRequestDataProcessing(); + updateStatus(); + }); // iterate over the list and add fields like type label, after the // OPTIONS request returns, or the list is sorted/paginated/searched diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index aaa9fee909..4f2d9a0b5f 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -7,10 +7,10 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n', - 'CredentialTypes', 'ConfigData', + 'CredentialTypes', 'ConfigData', 'resolvedModels', function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state, - CreateSelect2, i18n, CredentialTypes, ConfigData) { + CreateSelect2, i18n, CredentialTypes, ConfigData, resolvedModels) { let form = ProjectsForm(), base = $location.path().replace(/^\//, '').split('/')[0], @@ -23,6 +23,9 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.canEditOrg = true; const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; + + const [ProjectModel] = resolvedModels; + $scope.canAdd = ProjectModel.options('actions.POST'); Rest.setUrl(GetBasePath('projects')); Rest.options() diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js deleted file mode 100644 index 11be31c54c..0000000000 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ /dev/null @@ -1,342 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', - 'ProjectList', 'Prompt', 'ProcessErrors', 'GetBasePath', 'ProjectUpdate', - 'Wait', 'Empty', 'Find', 'GetProjectIcon', 'GetProjectToolTip', '$filter', - '$state', 'rbacUiControlService', 'Dataset', 'i18n', 'QuerySet', 'ProjectModel', - 'ProjectsStrings', 'ngToast', - function($scope, $rootScope, $log, Rest, Alert, ProjectList, - Prompt, ProcessErrors, GetBasePath, ProjectUpdate, Wait, Empty, Find, - GetProjectIcon, GetProjectToolTip, $filter, $state, rbacUiControlService, - Dataset, i18n, qs, Project, ProjectsStrings, ngToast) { - - let project = new Project(); - - var list = ProjectList; - - init(); - - function init() { - $scope.canAdd = false; - - rbacUiControlService.canAdd('projects') - .then(function(params) { - $scope.canAdd = params.canAdd; - }); - - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - _.forEach($scope[list.name], buildTooltips); - $rootScope.flashMessage = null; - } - - $scope.$on(`${list.iterator}_options`, function(event, data){ - $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); - }); - - $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); - - // iterate over the list and add fields like type label, after the - // OPTIONS request returns, or the list is sorted/paginated/searched - function optionsRequestDataProcessing(){ - if ($scope[list.name] !== undefined) { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - - // Set the item type label - if (list.fields.scm_type && $scope.options && - $scope.options.hasOwnProperty('scm_type')) { - $scope.options.scm_type.choices.forEach(function(choice) { - if (choice[0] === item.scm_type) { - itm.type_label = choice[1]; - } - }); - } - - buildTooltips(itm); - - }); - } - } - - function buildTooltips(project) { - project.statusIcon = GetProjectIcon(project.status); - project.statusTip = GetProjectToolTip(project.status); - project.scm_update_tooltip = i18n._("Get latest SCM revision"); - project.scm_type_class = ""; - - if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { - project.statusTip = i18n._('Canceled. Click for details'); - project.scm_type_class = "btn-disabled"; - } - - if (project.status === 'running' || project.status === 'updating') { - project.scm_update_tooltip = i18n._("SCM update currently running"); - project.scm_type_class = "btn-disabled"; - } - if (project.scm_type === 'manual') { - project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - project.scm_type_class = 'btn-disabled'; - project.statusTip = i18n._('Not configured for SCM'); - project.statusIcon = 'none'; - } - } - - $scope.reloadList = function(){ - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(function(searchResponse) { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); - }; - - $scope.$on(`ws-jobs`, function(e, data) { - var project; - $log.debug(data); - if ($scope.projects) { - // Assuming we have a list of projects available - project = Find({ list: $scope.projects, key: 'id', val: data.project_id }); - if (project) { - // And we found the affected project - $log.debug('Received event for project: ' + project.name); - $log.debug('Status changed to: ' + data.status); - if (data.status === 'successful' || data.status === 'failed' || data.status === 'canceled') { - $scope.reloadList(); - } else { - project.scm_update_tooltip = i18n._("SCM update currently running"); - project.scm_type_class = "btn-disabled"; - } - project.status = data.status; - project.statusIcon = GetProjectIcon(data.status); - project.statusTip = GetProjectToolTip(data.status); - } - } - }); - - $scope.addProject = function() { - $state.go('projects.add'); - }; - - $scope.editProject = function(id) { - $state.go('projects.edit', { project_id: id }); - }; - - if ($scope.removeGoTojobResults) { - $scope.removeGoTojobResults(); - } - $scope.removeGoTojobResults = $scope.$on('GoTojobResults', function(e, data) { - if (data.summary_fields.current_update || data.summary_fields.last_update) { - - Wait('start'); - - // Grab the id from summary_fields - var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - - $state.go('output', { id: id, type: 'project'}, { reload: true }); - - } else { - Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + - ' completed. If you have not already done so, start an update for this project.'), 'alert-info'); - } - }); - - $scope.copyProject = project => { - Wait('start'); - new Project('get', project.id) - .then(model => model.copy()) - .then((copiedProj) => { - ngToast.success({ - content: ` -
-
- -
-
- ${ProjectsStrings.get('SUCCESSFUL_CREATION', copiedProj.name)} -
-
`, - dismissButton: false, - dismissOnTimeout: true - }); - $state.go('.', null, { reload: true }); - }) - .catch(({ data, status }) => { - const params = { hdr: 'Error!', msg: `Call to copy failed. Return status: ${status}` }; - ProcessErrors($scope, data, status, null, params); - }) - .finally(() => Wait('stop')); - }; - - $scope.showSCMStatus = function(id) { - // Refresh the project list - var project = Find({ list: $scope.projects, key: 'id', val: id }); - if (Empty(project.scm_type) || project.scm_type === 'Manual') { - Alert(i18n._('No SCM Configuration'), i18n._('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, ' + - 'and then run an update.'), 'alert-info'); - } else { - // Refresh what we have in memory to insure we're accessing the most recent status record - Rest.setUrl(project.url); - Rest.get() - .then(({data}) => { - $scope.$emit('GoTojobResults', data); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Project lookup failed. GET returned: ') + status }); - }); - } - }; - - $scope.deleteProject = function(id, name) { - var action = function() { - $('#prompt-modal').modal('hide'); - Wait('start'); - project.request('delete', id) - .then(() => { - - let reloadListStateParams = null; - - if($scope.projects.length === 1 && $state.params.project_search && _.has($state, 'params.project_search.page') && $state.params.project_search.page !== '1') { - reloadListStateParams = _.cloneDeep($state.params); - reloadListStateParams.project_search.page = (parseInt(reloadListStateParams.project_search.page)-1).toString(); - } - - if (parseInt($state.params.project_id) === id) { - $state.go("^", reloadListStateParams, { reload: true }); - } else { - $state.go('.', reloadListStateParams, {reload: true}); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), `${project.path}${id}/`) + status }); - }) - .finally(function() { - Wait('stop'); - }); - }; - - project.getDependentResourceCounts(id) - .then((counts) => { - const invalidateRelatedLines = []; - let deleteModalBody = `
${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}
`; - - counts.forEach(countObj => { - if(countObj.count && countObj.count > 0) { - invalidateRelatedLines.push(`
${countObj.label}${countObj.count}
`); - } - }); - - if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { - deleteModalBody = `
${ProjectsStrings.get('deleteResource.USED_BY', 'project')} ${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}
`; - invalidateRelatedLines.forEach(invalidateRelatedLine => { - deleteModalBody += invalidateRelatedLine; - }); - } - - Prompt({ - hdr: i18n._('Delete'), - resourceName: $filter('sanitize')(name), - body: deleteModalBody, - action: action, - actionText: i18n._('DELETE') - }); - }); - }; - - if ($scope.removeCancelUpdate) { - $scope.removeCancelUpdate(); - } - $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function(e, url) { - // Cancel the project update process - Rest.setUrl(url); - Rest.post() - .then(() => { - Alert(i18n._('SCM Update Cancel'), i18n._('Your request to cancel the update was submitted to the task manager.'), 'alert-info'); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST status: '), url) + status }); - }); - }); - - if ($scope.removeCheckCancel) { - $scope.removeCheckCancel(); - } - $scope.removeCheckCancel = $scope.$on('Check_Cancel', function(e, data) { - // Check that we 'can' cancel the update - var url = data.related.cancel; - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - if (data.can_cancel) { - $scope.$emit('Cancel_Update', url); - } else { - Alert(i18n._('Cancel Not Allowed'), '
' + i18n.sprintf(i18n._('Either you do not have access or the SCM update process completed. ' + - 'Click the %sRefresh%s button to view the latest status.'), '', '') + '
', 'alert-info', null, null, null, null, true); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), url) + status }); - }); - }); - - $scope.cancelUpdate = function(project) { - project.pending_cancellation = true; - Rest.setUrl(GetBasePath("projects") + project.id); - Rest.get() - .then(({data}) => { - if (data.related.current_update) { - Rest.setUrl(data.related.current_update); - Rest.get() - .then(({data}) => { - $scope.$emit('Check_Cancel', data); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), data.related.current_update) + status }); - }); - } else { - Alert(i18n._('Update Not Found'), '
' + i18n.sprintf(i18n._('An SCM update does not appear to be running for project: %s. Click the %sRefresh%s ' + - 'button to view the latest status.'), $filter('sanitize')(name), '', '') + '
', 'alert-info',undefined,undefined,undefined,undefined,true); - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Call to get project failed. GET status: ') + status }); - }); - }; - - $scope.SCMUpdate = function(project_id, event) { - try { - $(event.target).tooltip('hide'); - } catch (e) { - // ignore - } - $scope.projects.forEach(function(project) { - if (project.id === project_id) { - if (project.scm_type === "Manual" || Empty(project.scm_type)) { - // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event - // to work. So user can click, but we just won't do anything. - //Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); - } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { - // Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info'); - } else { - ProjectUpdate({ scope: $scope, project_id: project.id }); - } - } - }); - }; - } -]; diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js index 144fb99b91..ebb3aa7bb9 100644 --- a/awx/ui/client/src/projects/main.js +++ b/awx/ui/client/src/projects/main.js @@ -4,12 +4,10 @@ * All Rights Reserved *************************************************/ -import ProjectsList from './list/projects-list.controller'; import ProjectsAdd from './add/projects-add.controller'; import ProjectsEdit from './edit/projects-edit.controller'; -import ProjectList from './projects.list'; import ProjectsForm from './projects.form'; -import { N_ } from '../i18n'; +import ProjectList from './projects.list'; import GetProjectPath from './factories/get-project-path.factory'; import GetProjectIcon from './factories/get-project-icon.factory'; import GetProjectToolTip from './factories/get-project-tool-tip.factory'; @@ -20,93 +18,60 @@ import { } from '../scheduler/schedules.route'; import ProjectsTemplatesRoute from '~features/templates/routes/projectsTemplatesList.route'; -import ProjectsStrings from './projects.strings'; +import projectsListRoute from '~features/projects/routes/projectsList.route.js'; export default angular.module('Projects', []) - .controller('ProjectsList', ProjectsList) .controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsEdit', ProjectsEdit) .factory('GetProjectPath', GetProjectPath) .factory('GetProjectIcon', GetProjectIcon) .factory('GetProjectToolTip', GetProjectToolTip) - .factory('ProjectList', ProjectList) .factory('ProjectsForm', ProjectsForm) - .service('ProjectsStrings', ProjectsStrings) + .factory('ProjectList', ProjectList) .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', function($stateProvider, stateDefinitionsProvider,$stateExtenderProvider) { let stateDefinitions = stateDefinitionsProvider.$get(); let stateExtender = $stateExtenderProvider.$get(); - var projectResolve = { - CredentialTypes: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', - (Rest, $stateParams, GetBasePath, ProcessErrors) => { - var path = GetBasePath('credential_types'); - Rest.setUrl(path); - return Rest.get() - .then(function(data) { - return (data.data.results); - }).catch(function(response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get credential types. GET returned status: ' + - response.status - }); - }); - } - ], - ConfigData: ['ConfigService', 'ProcessErrors', (ConfigService, ProcessErrors) => { - return ConfigService.getConfig() - .then(response => response) - .catch(({data, status}) => { - ProcessErrors(null, data, status, null, { - hdr: 'Error!', - msg: 'Failed to get config. GET returned status: ' + - 'status: ' + status - }); - }); - }] - }; function generateStateTree() { - let projectTree = stateDefinitions.generateTree({ - parent: 'projects', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - generateSchedulerView: true, - list: 'ProjectList', + let projectAdd = stateDefinitions.generateTree({ + name: 'projects.add', + url: '/add', + modes: ['add'], form: 'ProjectsForm', controllers: { - list: ProjectsList, // DI strings or objects - add: ProjectsAdd, - edit: ProjectsEdit + add: 'ProjectsAdd', + }, + }); + + let projectEdit = stateDefinitions.generateTree({ + name: 'projects.edit', + url: '/:project_id', + modes: ['edit'], + form: 'ProjectsForm', + controllers: { + edit: 'ProjectsEdit', }, data: { activityStream: true, activityStreamTarget: 'project', - socket: { - "groups": { - "jobs": ["status_changed"] - } - } + activityStreamId: 'project_id' }, - ncyBreadcrumb: { - label: N_('PROJECTS') - }, - breadcrumbs: { + breadcrumbs: { edit: '{{breadcrumb.project_name}}' }, - resolve: { - add: projectResolve, - edit: projectResolve - } }); return Promise.all([ - projectTree + projectAdd, + projectEdit, ]).then((generated) => { return { states: _.reduce(generated, (result, definition) => { return result.concat(definition.states); }, [ + stateExtender.buildDefinition(projectsListRoute), stateExtender.buildDefinition(ProjectsTemplatesRoute), stateExtender.buildDefinition(projectsSchedulesListRoute), stateExtender.buildDefinition(projectsSchedulesAddRoute), diff --git a/awx/ui/client/src/projects/projects.list.js b/awx/ui/client/src/projects/projects.list.js index 64b0ddb4ab..436ee5c3b6 100644 --- a/awx/ui/client/src/projects/projects.list.js +++ b/awx/ui/client/src/projects/projects.list.js @@ -5,124 +5,124 @@ *************************************************/ export default ['i18n', function(i18n) { - return { + return { - 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'), + 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