Merge branch 'devel' into workflow-visualizer-search

This commit is contained in:
Daniel Sami 2018-10-19 10:26:12 -04:00 committed by GitHub
commit 0bbf1d7014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1149 additions and 671 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
<div ui-view="scheduler"></div>
<div ui-view="form"></div>
<at-panel ng-cloak id="htmlTemplate">
<div ng-if="$state.includes('projects')">
<at-panel-heading
title="{{:: vm.strings.get('list.PANEL_TITLE') }}"
hide-dismiss="true"
badge="{{ vm.count }}">
</at-panel-heading>
</div>
<div ui-view="projectsList"></div>
</at-panel>

View File

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

View File

@ -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 <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
// }
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 <a href="/#/jobs/project/${id}">
// ${$filter('sanitize')('placehoder')}</a>`;
// }
return html;
};
vm.copyProject = project => {
Wait('start');
ProjectModel
.create('get', project.id)
.then(model => model.copy())
.then((copiedProj) => {
ngToast.success({
content: `
<div class="Toast-wrapper">
<div class="Toast-icon">
<i class="fa fa-check-circle Toast-successIcon"></i>
</div>
<div>
${vm.strings.get('SUCCESSFUL_CREATION', copiedProj.name)}
</div>
</div>`,
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 = `<div class="Prompt-bodyQuery">${vm.strings.get('deleteResource.CONFIRM', 'project')}</div>`;
counts.forEach(countObj => {
if (countObj.count && countObj.count > 0) {
invalidateRelatedLines.push(`<div><span class="Prompt-warningResourceTitle">${countObj.label}</span><span class="badge List-titleBadge">${countObj.count}</span></div>`);
}
});
if (invalidateRelatedLines && invalidateRelatedLines.length > 0) {
deleteModalBody = `<div class="Prompt-bodyQuery">${vm.strings.get('deleteResource.USED_BY', 'project')} ${vm.strings.get('deleteResource.CONFIRM', 'project')}</div>`;
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;

View File

@ -0,0 +1,92 @@
<at-panel-body>
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="projects"
base-path="projects"
iterator="project"
list="vm.list"
collection="vm.projects"
dataset="vm.dataset"
search-tags="searchTags">
</smart-search>
<div class="at-List-toolbarAction" ng-show="canAdd">
<button
type="button"
class="at-Button--add"
id="button-add"
ui-sref="projects.add">
</button>
</div>
</div>
<at-list results="vm.projects">
<at-row ng-repeat="project in vm.projects"
ng-class="{'at-Row--active': (project.id === vm.activeId)}"
id="row-{{ project.id }}">
<div class="at-Row-items">
<at-row-item
status="{{ project.statusIcon }}"
status-tip="{{ project.statusTip }}"
status-click="vm.showSCMStatus(project.id)"
header-value="{{ project.name }}"
header-link="/#/projects/{{ project.id }}"
header-tag="{{ vm.projectTypes[project.scm_type] }}">
</at-row-item>
<div class="at-RowItem" ng-if="project.scm_revision">
<div class="at-RowItem-label">
{{ :: vm.strings.get('list.ROW_ITEM_LABEL_REVISION') }}
</div>
<at-truncate string="{{ project.scm_revision }}" maxLength="7"></at-truncate>
</div>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_ORGANIZATION')}}"
value="{{ project.summary_fields.organization.name }}"
value-link="/#/organizations/{{ project.organization }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_MODIFIED') }}"
value-bind-html="{{ vm.getLastModified(project) }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_USED') }}"
value-bind-html="{{ vm.getLastUsed(project) }}">
</at-row-item>
</div>
<div class="at-Row-actions">
<div aw-tool-tip="{{ project.scm_update_tooltip }}"
data-tip-watch="project.scm_update_tooltip"
data-placement="top">
<div class="at-RowAction"
ng-class="{'at-RowAction--disabled': project.scm_update_disabled }"
ng-click="vm.SCMUpdate(project.id, $event)"
ng-show="project.summary_fields.user_capabilities.start">
<i class="fa fa-refresh"></i>
</div>
</div>
<at-row-action icon="fa-copy" ng-click="vm.copyProject(project)"
ng-show="project.summary_fields.user_capabilities.copy">
</at-row-action>
<at-row-action icon="fa-trash" ng-click="vm.deleteProject(project.id, project.name)"
ng-show="(project.status !== 'updating'
&& project.status !== 'running'
&& project.status !== 'pending'
&& project.status !== 'waiting')
&& project.summary_fields.user_capabilities.delete">
</at-row-action>
<at-row-action icon="fa-minus-circle" ng-click="vm.cancelUpdate(project)"
ng-show="(project.status == 'updating'
|| project.status == 'running'
|| project.status == 'pending'
|| project.status == 'waiting')
&& project.summary_fields.user_capabilities.start">
</at-row-action>
</div>
</at-row>
</at-list>
<paginate
collection="vm.projects"
dataset="vm.dataset"
iterator="project"
base-path="projects">
</paginate>
</at-panel-body>

View File

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

View File

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

View File

@ -15,6 +15,7 @@ function atRowItem () {
headerTag: '@',
status: '@',
statusTip: '@',
statusClick: '&?',
labelValue: '@',
labelLink: '@',
labelState: '@',

View File

@ -1,12 +1,17 @@
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue, 'at-RowItem--inline': inline}"
ng-show="status || headerValue || value || valueBindHtml || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
<div class="at-RowItem-status" ng-if="status">
<a ng-if="headerLink || headerState" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
<a ng-if="statusClick" ng-click="statusClick()"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>
</a>
<a ng-if="(headerLink || headerState) && !statusClick" ng-href="{{ headerLink }}" ui-sref="{{ headerState }}"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>
</a>
<div ng-if="!headerLink && !headerState"
<div ng-if="!headerLink && !headerState && !statusClick"
aw-tool-tip="{{ statusTip }}" data-tip-watch="statusTip"
data-placement="top">
<i class="fa icon-job-{{ status }}"></i>

View File

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

View File

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

View File

@ -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: `
<div class="Toast-wrapper">
<div class="Toast-icon">
<i class="fa fa-check-circle Toast-successIcon"></i>
</div>
<div>
${ProjectsStrings.get('SUCCESSFUL_CREATION', copiedProj.name)}
</div>
</div>`,
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 = `<div class="Prompt-bodyQuery">${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}</div>`;
counts.forEach(countObj => {
if(countObj.count && countObj.count > 0) {
invalidateRelatedLines.push(`<div><span class="Prompt-warningResourceTitle">${countObj.label}</span><span class="badge List-titleBadge">${countObj.count}</span></div>`);
}
});
if (invalidateRelatedLines && invalidateRelatedLines.length > 0) {
deleteModalBody = `<div class="Prompt-bodyQuery">${ProjectsStrings.get('deleteResource.USED_BY', 'project')} ${ProjectsStrings.get('deleteResource.CONFIRM', 'project')}</div>`;
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'), '<div>' + 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.'), '<em>', '</em>') + '</div>', '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'), '<div>' + 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), '<em>', '</em>') + '</div>', '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 });
}
}
});
};
}
];

View File

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

View File

@ -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: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
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: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
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'"
}
}
};}];

View File

@ -1,7 +0,0 @@
function ProjectsStrings (BaseString) {
BaseString.call(this, 'projects');
}
ProjectsStrings.$inject = ['BaseStringService'];
export default ProjectsStrings;

View File

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

View File

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

View File

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

View File

@ -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('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
});
});
// 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('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
// });
// });
client.click(`${itemRow} i[class*="trash"]`);

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
git+https://github.com/ansible/docutils.git@master#egg=docutils

View File

@ -1,2 +1 @@
certifi
pycurl # requires system package version

View File

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