mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
Merge branch 'merge-devel' into rbac
This commit is contained in:
commit
e323f5a48b
2
Makefile
2
Makefile
@ -361,7 +361,7 @@ check: flake8 pep8 # pyflakes pylint
|
||||
|
||||
# Run all API unit tests.
|
||||
test:
|
||||
py.test awx/main/tests awx/api/tests awx/fact/tests
|
||||
py.test awx/main/tests awx/api/tests
|
||||
|
||||
test_unit:
|
||||
py.test awx/main/tests/unit
|
||||
|
@ -1554,7 +1554,8 @@ class CredentialSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'deprecated_user', 'deprecated_team', 'kind', 'cloud', 'host', 'username',
|
||||
'password', 'security_token', 'project', 'ssh_key_data', 'ssh_key_unlock',
|
||||
'password', 'security_token', 'project', 'domain',
|
||||
'ssh_key_data', 'ssh_key_unlock',
|
||||
'become_method', 'become_username', 'become_password',
|
||||
'vault_password')
|
||||
|
||||
@ -1665,16 +1666,15 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
notifiers_success = reverse('api:job_template_notifiers_success_list', args=(obj.pk,)),
|
||||
notifiers_error = reverse('api:job_template_notifiers_error_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
||||
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,))
|
||||
))
|
||||
if obj.host_config_key:
|
||||
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
|
||||
if obj.survey_enabled:
|
||||
res['survey_spec'] = reverse('api:job_template_survey_spec', args=(obj.pk,))
|
||||
return res
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
d = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
||||
if obj.survey_enabled and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
||||
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
||||
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
|
||||
request = self.context.get('request', None)
|
||||
if request is not None and request.user is not None and obj.inventory is not None and obj.project is not None:
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack')
|
||||
CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack', 'openstack_v3')
|
||||
SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom',)
|
||||
|
@ -137,7 +137,7 @@ class CallbackReceiver(object):
|
||||
'playbook_on_import_for_host',
|
||||
'playbook_on_not_import_for_host'):
|
||||
parent = job_parent_events.get('playbook_on_play_start', None)
|
||||
elif message['event'].startswith('runner_on_'):
|
||||
elif message['event'].startswith('runner_on_') or message['event'].startswith('runner_item_on_'):
|
||||
list_parents = []
|
||||
list_parents.append(job_parent_events.get('playbook_on_setup', None))
|
||||
list_parents.append(job_parent_events.get('playbook_on_task_start', None))
|
||||
|
19
awx/main/migrations/0010_v300_credential_domain_field.py
Normal file
19
awx/main/migrations/0010_v300_credential_domain_field.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0009_v300_create_system_job_templates'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='domain',
|
||||
field=models.CharField(default=b'', help_text='The identifier for the domain.', max_length=100, verbose_name='Domain', blank=True),
|
||||
),
|
||||
]
|
@ -56,7 +56,7 @@ PERMISSION_TYPE_CHOICES = [
|
||||
(PERM_JOBTEMPLATE_CREATE, _('Create a Job Template')),
|
||||
]
|
||||
|
||||
CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'custom']
|
||||
CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'openstack_v3', 'custom']
|
||||
|
||||
VERBOSITY_CHOICES = [
|
||||
(0, '0 (Normal)'),
|
||||
|
@ -40,6 +40,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
('gce', _('Google Compute Engine')),
|
||||
('azure', _('Microsoft Azure')),
|
||||
('openstack', _('OpenStack')),
|
||||
('openstack_v3', _('OpenStack V3')),
|
||||
]
|
||||
|
||||
BECOME_METHOD_CHOICES = [
|
||||
@ -119,6 +120,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
verbose_name=_('Project'),
|
||||
help_text=_('The identifier for the project.'),
|
||||
)
|
||||
domain = models.CharField(
|
||||
blank=True,
|
||||
default='',
|
||||
max_length=100,
|
||||
verbose_name=_('Domain'),
|
||||
help_text=_('The identifier for the domain.'),
|
||||
)
|
||||
ssh_key_data = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
@ -229,10 +237,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
host = self.host or ''
|
||||
if not host and self.kind == 'vmware':
|
||||
raise ValidationError('Host required for VMware credential.')
|
||||
if not host and self.kind == 'openstack':
|
||||
if not host and self.kind in ('openstack', 'openstack_v3'):
|
||||
raise ValidationError('Host required for OpenStack credential.')
|
||||
return host
|
||||
|
||||
def clean_domain(self):
|
||||
"""For case of Keystone v3 identity service that requires a
|
||||
`domain`, that a domain is provided.
|
||||
"""
|
||||
domain = self.domain or ''
|
||||
if not domain and self.kind == 'openstack_v3':
|
||||
raise ValidationError('Domain required for OpenStack with Keystone v3.')
|
||||
return domain
|
||||
|
||||
def clean_username(self):
|
||||
username = self.username or ''
|
||||
if not username and self.kind == 'aws':
|
||||
@ -242,7 +259,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
'credential.')
|
||||
if not username and self.kind == 'vmware':
|
||||
raise ValidationError('Username required for VMware credential.')
|
||||
if not username and self.kind == 'openstack':
|
||||
if not username and self.kind in ('openstack', 'openstack_v3'):
|
||||
raise ValidationError('Username required for OpenStack credential.')
|
||||
return username
|
||||
|
||||
@ -254,13 +271,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
raise ValidationError('API key required for Rackspace credential.')
|
||||
if not password and self.kind == 'vmware':
|
||||
raise ValidationError('Password required for VMware credential.')
|
||||
if not password and self.kind == 'openstack':
|
||||
if not password and self.kind in ('openstack', 'openstack_v3'):
|
||||
raise ValidationError('Password or API key required for OpenStack credential.')
|
||||
return password
|
||||
|
||||
def clean_project(self):
|
||||
project = self.project or ''
|
||||
if self.kind == 'openstack' and not project:
|
||||
if self.kind in ('openstack', 'openstack_v3') and not project:
|
||||
raise ValidationError('Project name required for OpenStack credential.')
|
||||
return project
|
||||
|
||||
|
@ -733,6 +733,7 @@ class InventorySourceOptions(BaseModel):
|
||||
('azure', _('Microsoft Azure')),
|
||||
('vmware', _('VMware vCenter')),
|
||||
('openstack', _('OpenStack')),
|
||||
('openstack_v3', _('OpenStack V3')),
|
||||
('custom', _('Custom Script')),
|
||||
]
|
||||
|
||||
@ -961,6 +962,11 @@ class InventorySourceOptions(BaseModel):
|
||||
"""I don't think openstack has regions"""
|
||||
return [('all', 'All')]
|
||||
|
||||
@classmethod
|
||||
def get_openstack_v3_region_choices(self):
|
||||
"""Defer to the behavior of openstack"""
|
||||
return self.get_openstack_region_choices()
|
||||
|
||||
def clean_credential(self):
|
||||
if not self.source:
|
||||
return None
|
||||
|
@ -20,6 +20,9 @@ class CustomEmailBackend(EmailBackend):
|
||||
sender_parameter = "sender"
|
||||
|
||||
def format_body(self, body):
|
||||
if "body" in body:
|
||||
body_actual = body['body']
|
||||
else:
|
||||
body_actual = smart_text("{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'],
|
||||
body['id'],
|
||||
body['status'],
|
||||
|
@ -695,12 +695,14 @@ class RunJob(BaseTask):
|
||||
if credential.ssh_key_data not in (None, ''):
|
||||
private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or ''
|
||||
|
||||
if job.cloud_credential and job.cloud_credential.kind == 'openstack':
|
||||
if job.cloud_credential and job.cloud_credential.kind in ('openstack', 'openstack_v3'):
|
||||
credential = job.cloud_credential
|
||||
openstack_auth = dict(auth_url=credential.host,
|
||||
username=credential.username,
|
||||
password=decrypt_field(credential, "password"),
|
||||
project_name=credential.project)
|
||||
if credential.domain not in (None, ''):
|
||||
openstack_auth['domain_name'] = credential.domain
|
||||
openstack_data = {
|
||||
'clouds': {
|
||||
'devstack': {
|
||||
@ -785,7 +787,7 @@ class RunJob(BaseTask):
|
||||
env['VMWARE_USER'] = cloud_cred.username
|
||||
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
|
||||
env['VMWARE_HOST'] = cloud_cred.host
|
||||
elif cloud_cred and cloud_cred.kind == 'openstack':
|
||||
elif cloud_cred and cloud_cred.kind in ('openstack', 'openstack_v3'):
|
||||
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
|
||||
|
||||
# Set environment variables related to scan jobs
|
||||
@ -1134,12 +1136,14 @@ class RunInventoryUpdate(BaseTask):
|
||||
credential = inventory_update.credential
|
||||
return dict(cloud_credential=decrypt_field(credential, 'ssh_key_data'))
|
||||
|
||||
if inventory_update.source == 'openstack':
|
||||
if inventory_update.source in ('openstack', 'openstack_v3'):
|
||||
credential = inventory_update.credential
|
||||
openstack_auth = dict(auth_url=credential.host,
|
||||
username=credential.username,
|
||||
password=decrypt_field(credential, "password"),
|
||||
project_name=credential.project)
|
||||
if credential.domain not in (None, ''):
|
||||
openstack_auth['domain_name'] = credential.domain
|
||||
private_state = str(inventory_update.source_vars_dict.get('private', 'true'))
|
||||
# Retrieve cache path from inventory update vars if available,
|
||||
# otherwise create a temporary cache path only for this update.
|
||||
@ -1287,7 +1291,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
env['GCE_PROJECT'] = passwords.get('source_project', '')
|
||||
env['GCE_PEM_FILE_PATH'] = cloud_credential
|
||||
env['GCE_ZONE'] = inventory_update.source_regions
|
||||
elif inventory_update.source == 'openstack':
|
||||
elif inventory_update.source in ('openstack', 'openstack_v3'):
|
||||
env['OS_CLIENT_CONFIG_FILE'] = cloud_credential
|
||||
elif inventory_update.source == 'file':
|
||||
# FIXME: Parse source_env to dict, update env.
|
||||
@ -1330,6 +1334,11 @@ class RunInventoryUpdate(BaseTask):
|
||||
# to a shorter variable. :)
|
||||
src = inventory_update.source
|
||||
|
||||
# OpenStack V3 has everything in common with OpenStack aside
|
||||
# from one extra parameter, so share these resources between them.
|
||||
if src == 'openstack_v3':
|
||||
src = 'openstack'
|
||||
|
||||
# Get the path to the inventory plugin, and append it to our
|
||||
# arguments.
|
||||
plugin_path = self.get_path_to('..', 'plugins', 'inventory',
|
||||
|
@ -1969,6 +1969,26 @@ class InventoryUpdatesTest(BaseTransactionTest):
|
||||
self.check_inventory_source(inventory_source)
|
||||
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
|
||||
|
||||
def test_update_from_openstack_v3(self):
|
||||
# Check that update works with Keystone v3 identity service
|
||||
api_url = getattr(settings, 'TEST_OPENSTACK_HOST_V3', '')
|
||||
api_user = getattr(settings, 'TEST_OPENSTACK_USER', '')
|
||||
api_password = getattr(settings, 'TEST_OPENSTACK_PASSWORD', '')
|
||||
api_project = getattr(settings, 'TEST_OPENSTACK_PROJECT', '')
|
||||
api_domain = getattr(settings, 'TEST_OPENSTACK_DOMAIN', '')
|
||||
if not all([api_url, api_user, api_password, api_project, api_domain]):
|
||||
self.skipTest("No test openstack v3 credentials defined")
|
||||
self.create_test_license_file()
|
||||
credential = Credential.objects.create(kind='openstack_v3',
|
||||
host=api_url,
|
||||
username=api_user,
|
||||
password=api_password,
|
||||
project=api_project,
|
||||
domain=api_domain)
|
||||
inventory_source = self.update_inventory_source(self.group, source='openstack_v3', credential=credential)
|
||||
self.check_inventory_source(inventory_source)
|
||||
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
|
||||
|
||||
def test_update_from_azure(self):
|
||||
source_username = getattr(settings, 'TEST_AZURE_USERNAME', '')
|
||||
source_key_data = getattr(settings, 'TEST_AZURE_KEY_DATA', '')
|
||||
@ -2013,3 +2033,27 @@ class InventoryCredentialTest(BaseTest):
|
||||
self.assertIn('password', response)
|
||||
self.assertIn('host', response)
|
||||
self.assertIn('project', response)
|
||||
|
||||
def test_openstack_v3_create_ok(self):
|
||||
data = {
|
||||
'kind': 'openstack_v3',
|
||||
'name': 'Best credential ever',
|
||||
'username': 'some_user',
|
||||
'password': 'some_password',
|
||||
'project': 'some_project',
|
||||
'host': 'some_host',
|
||||
'domain': 'some_domain',
|
||||
}
|
||||
self.post(self.url, data=data, expect=201, auth=self.get_super_credentials())
|
||||
|
||||
def test_openstack_v3_create_fail_required_fields(self):
|
||||
data = {
|
||||
'kind': 'openstack_v3',
|
||||
'name': 'Best credential ever',
|
||||
}
|
||||
response = self.post(self.url, data=data, expect=400, auth=self.get_super_credentials())
|
||||
self.assertIn('username', response)
|
||||
self.assertIn('password', response)
|
||||
self.assertIn('host', response)
|
||||
self.assertIn('project', response)
|
||||
self.assertIn('domain', response)
|
||||
|
@ -196,7 +196,7 @@ class BaseCallbackModule(object):
|
||||
self._init_connection()
|
||||
if self.context is None:
|
||||
self._start_connection()
|
||||
if 'res' in event_data \
|
||||
if 'res' in event_data and hasattr(event_data['res'], 'get') \
|
||||
and event_data['res'].get('_ansible_no_log', False):
|
||||
res = event_data['res']
|
||||
if 'stdout' in res and res['stdout']:
|
||||
@ -271,16 +271,19 @@ class BaseCallbackModule(object):
|
||||
ignore_errors=ignore_errors)
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
|
||||
self._log_event('runner_on_failed', host=result._host.name,
|
||||
res=result._result, task=result._task,
|
||||
ignore_errors=ignore_errors)
|
||||
ignore_errors=ignore_errors, event_loop=event_is_loop)
|
||||
|
||||
def runner_on_ok(self, host, res):
|
||||
self._log_event('runner_on_ok', host=host, res=res)
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
|
||||
self._log_event('runner_on_ok', host=result._host.name,
|
||||
task=result._task, res=result._result)
|
||||
task=result._task, res=result._result,
|
||||
event_loop=event_is_loop)
|
||||
|
||||
def runner_on_error(self, host, msg):
|
||||
self._log_event('runner_on_error', host=host, msg=msg)
|
||||
@ -292,8 +295,9 @@ class BaseCallbackModule(object):
|
||||
self._log_event('runner_on_skipped', host=host, item=item)
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
event_is_loop = result._task.loop if hasattr(result._task, 'loop') else None
|
||||
self._log_event('runner_on_skipped', host=result._host.name,
|
||||
task=result._task)
|
||||
task=result._task, event_loop=event_is_loop)
|
||||
|
||||
def runner_on_unreachable(self, host, res):
|
||||
self._log_event('runner_on_unreachable', host=host, res=res)
|
||||
@ -327,6 +331,18 @@ class BaseCallbackModule(object):
|
||||
self._log_event('runner_on_file_diff', host=result._host.name,
|
||||
task=result._task, diff=diff)
|
||||
|
||||
def v2_runner_item_on_ok(self, result):
|
||||
self._log_event('runner_item_on_ok', res=result._result, host=result._host.name,
|
||||
task=result._task)
|
||||
|
||||
def v2_runner_item_on_failed(self, result):
|
||||
self._log_event('runner_item_on_failed', res=result._result, host=result._host.name,
|
||||
task=result._task)
|
||||
|
||||
def v2_runner_item_on_skipped(self, result):
|
||||
self._log_event('runner_item_on_skipped', res=result._result, host=result._host.name,
|
||||
task=result._task)
|
||||
|
||||
@staticmethod
|
||||
@statsd.timer('terminate_ssh_control_masters')
|
||||
def terminate_ssh_control_masters():
|
||||
|
@ -8,5 +8,10 @@ export default {
|
||||
ncyBreadcrumb: {
|
||||
label: "ABOUT"
|
||||
},
|
||||
onExit: function(){
|
||||
// hacky way to handle user browsing away via URL bar
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
},
|
||||
templateUrl: templateUrl('about/about')
|
||||
};
|
||||
|
@ -183,7 +183,6 @@ var tower = angular.module('Tower', [
|
||||
'StandardOutHelper',
|
||||
'LogViewerOptionsDefinition',
|
||||
'EventViewerHelper',
|
||||
'HostEventsViewerHelper',
|
||||
'JobDetailHelper',
|
||||
'SocketIO',
|
||||
'lrInfiniteScroll',
|
||||
|
@ -169,7 +169,7 @@ export default
|
||||
"host": {
|
||||
labelBind: 'hostLabel',
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'vmware' || kind.value == 'openstack'",
|
||||
ngShow: "kind.value == 'vmware' || kind.value == 'openstack' || kind.value === 'openstack_v3'",
|
||||
awPopOverWatch: "hostPopOver",
|
||||
awPopOver: "set in helpers/credentials",
|
||||
dataTitle: 'Host',
|
||||
@ -243,7 +243,7 @@ export default
|
||||
"password": {
|
||||
labelBind: 'passwordLabel',
|
||||
type: 'sensitive',
|
||||
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'",
|
||||
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack' || kind.value == 'openstack_v3'",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
ask: false,
|
||||
@ -338,7 +338,7 @@ export default
|
||||
"project": {
|
||||
labelBind: 'projectLabel',
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'gce' || kind.value == 'openstack'",
|
||||
ngShow: "kind.value == 'gce' || kind.value == 'openstack' || kind.value == 'openstack_v3'",
|
||||
awPopOverWatch: "projectPopOver",
|
||||
awPopOver: "set in helpers/credentials",
|
||||
dataTitle: 'Project ID',
|
||||
@ -352,6 +352,23 @@ export default
|
||||
},
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"domain": {
|
||||
labelBind: 'domainLabel',
|
||||
type: 'text',
|
||||
ngShow: "kind.value == 'openstack_v3'",
|
||||
awPopOverWatch: "domainPopOver",
|
||||
awPopOver: "set in helpers/credentials",
|
||||
dataTitle: 'Domain Name',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
awRequiredWhen: {
|
||||
variable: 'domain_required',
|
||||
init: false
|
||||
},
|
||||
subForm: 'credentialSubForm'
|
||||
},
|
||||
"vault_password": {
|
||||
label: "Vault Password",
|
||||
type: 'sensitive',
|
||||
|
@ -169,7 +169,8 @@ export default
|
||||
label: 'Source Variables', //"{{vars_label}}" ,
|
||||
|
||||
ngShow: "source && (source.value == 'vmware' || " +
|
||||
"source.value == 'openstack')",
|
||||
"source.value == 'openstack' || " +
|
||||
"source.value == 'openstack_v3')",
|
||||
type: 'textarea',
|
||||
addRequired: false,
|
||||
class: 'Form-textAreaLabel',
|
||||
|
@ -12,7 +12,6 @@ import Credentials from "./helpers/Credentials";
|
||||
import EventViewer from "./helpers/EventViewer";
|
||||
import Events from "./helpers/Events";
|
||||
import Groups from "./helpers/Groups";
|
||||
import HostEventsViewer from "./helpers/HostEventsViewer";
|
||||
import Hosts from "./helpers/Hosts";
|
||||
import JobDetail from "./helpers/JobDetail";
|
||||
import JobSubmission from "./helpers/JobSubmission";
|
||||
@ -46,7 +45,6 @@ export
|
||||
EventViewer,
|
||||
Events,
|
||||
Groups,
|
||||
HostEventsViewer,
|
||||
Hosts,
|
||||
JobDetail,
|
||||
JobSubmission,
|
||||
|
@ -62,6 +62,7 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE)
|
||||
scope.key_required = false; // JT -- doing the same for key and project
|
||||
scope.project_required = false;
|
||||
scope.domain_required = false;
|
||||
scope.subscription_required = false;
|
||||
scope.key_description = "Paste the contents of the SSH private key file.";
|
||||
scope.key_hint= "drag and drop an SSH private key file on the field below";
|
||||
@ -69,9 +70,11 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
scope.password_required = false;
|
||||
scope.hostLabel = '';
|
||||
scope.projectLabel = '';
|
||||
scope.domainLabel = '';
|
||||
scope.project_required = false;
|
||||
scope.passwordLabel = 'Password (API Key)';
|
||||
scope.projectPopOver = "<p>The project value</p>";
|
||||
scope.domainPopOver = "<p>The domain name</p>";
|
||||
scope.hostPopOver = "<p>The host value</p>";
|
||||
|
||||
if (!Empty(scope.kind)) {
|
||||
@ -133,6 +136,22 @@ angular.module('CredentialsHelper', ['Utilities'])
|
||||
" as the username.</p>";
|
||||
scope.hostPopOver = "<p>The host to authenticate with." +
|
||||
"<br />For example, https://openstack.business.com/v2.0/";
|
||||
case 'openstack_v3':
|
||||
scope.hostLabel = "Host (Authentication URL)";
|
||||
scope.projectLabel = "Project (Tenet Name/ID)";
|
||||
scope.domainLabel = "Domain Name";
|
||||
scope.password_required = true;
|
||||
scope.project_required = true;
|
||||
scope.domain_required = true;
|
||||
scope.host_required = true;
|
||||
scope.username_required = true;
|
||||
scope.projectPopOver = "<p>This is the tenant name " +
|
||||
"or tenant id. This value is usually the same " +
|
||||
" as the username.</p>";
|
||||
scope.hostPopOver = "<p>The host to authenticate with." +
|
||||
"<br />For example, https://openstack.business.com/v3</p>";
|
||||
scope.domainPopOver = "<p>Domain used for Keystone v3 " +
|
||||
"<br />identity service.</p>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +305,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
field_id: 'source_extra_vars', onReady: callback });
|
||||
}
|
||||
if(scope.source.value==="vmware" ||
|
||||
scope.source.value==="openstack"){
|
||||
scope.source.value==="openstack" ||
|
||||
scope.source.value==="openstack_v3"){
|
||||
scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars;
|
||||
ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName,
|
||||
field_id: 'source_inventory_variables', onReady: callback });
|
||||
@ -315,7 +316,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
scope.source.value==='gce' ||
|
||||
scope.source.value === 'azure' ||
|
||||
scope.source.value === 'vmware' ||
|
||||
scope.source.value === 'openstack') {
|
||||
scope.source.value === 'openstack' ||
|
||||
scope.source.value === 'openstack_v3') {
|
||||
if (scope.source.value === 'ec2') {
|
||||
kind = 'aws';
|
||||
} else {
|
||||
@ -924,7 +926,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName,
|
||||
field_id: 'source_source_vars', onReady: waitStop });
|
||||
} else if (sources_scope.source && (sources_scope.source.value === 'vmware' ||
|
||||
sources_scope.source.value === 'openstack')) {
|
||||
sources_scope.source.value === 'openstack' ||
|
||||
sources_scope.source.value === 'openstack_v3')) {
|
||||
Wait('start');
|
||||
ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName,
|
||||
field_id: 'source_inventory_variables', onReady: waitStop });
|
||||
@ -1303,7 +1306,8 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
}
|
||||
|
||||
if (sources_scope.source && (sources_scope.source.value === 'vmware' ||
|
||||
sources_scope.source.value === 'openstack')) {
|
||||
sources_scope.source.value === 'openstack' ||
|
||||
sources_scope.source.value === 'openstack_v3')) {
|
||||
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true);
|
||||
}
|
||||
|
||||
|
@ -1,287 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name helpers.function:HostEventsViewer
|
||||
* @description view a list of events for a given job and host
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('HostEventsViewerHelper', ['ModalDialog', 'Utilities', 'EventViewerHelper'])
|
||||
|
||||
.factory('HostEventsViewer', ['$log', '$compile', 'CreateDialog', 'Wait', 'GetBasePath', 'Empty', 'GetEvents', 'EventViewer',
|
||||
function($log, $compile, CreateDialog, Wait, GetBasePath, Empty, GetEvents, EventViewer) {
|
||||
return function(params) {
|
||||
var parent_scope = params.scope,
|
||||
scope = parent_scope.$new(true),
|
||||
job_id = params.job_id,
|
||||
url = params.url,
|
||||
title = params.title, //optional
|
||||
fixHeight, buildTable,
|
||||
lastID, setStatus, buildRow, status;
|
||||
|
||||
// initialize the status dropdown
|
||||
scope.host_events_status_options = [
|
||||
{ value: "all", name: "All" },
|
||||
{ value: "changed", name: "Changed" },
|
||||
{ value: "failed", name: "Failed" },
|
||||
{ value: "ok", name: "OK" },
|
||||
{ value: "unreachable", name: "Unreachable" }
|
||||
];
|
||||
scope.host_events_search_name = params.name;
|
||||
status = (params.status) ? params.status : 'all';
|
||||
scope.host_events_status_options.every(function(opt, idx) {
|
||||
if (opt.value === status) {
|
||||
scope.host_events_search_status = scope.host_events_status_options[idx];
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!scope.host_events_search_status) {
|
||||
scope.host_events_search_status = scope.host_events_status_options[0];
|
||||
}
|
||||
|
||||
$log.debug('job_id: ' + job_id + ' url: ' + url + ' title: ' + title + ' name: ' + name + ' status: ' + status);
|
||||
|
||||
scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
|
||||
|
||||
if (scope.removeModalReady) {
|
||||
scope.removeModalReady();
|
||||
}
|
||||
scope.removeModalReady = scope.$on('ModalReady', function() {
|
||||
scope.hostViewSearching = false;
|
||||
$('#host-events-modal-dialog').dialog('open');
|
||||
});
|
||||
|
||||
if (scope.removeJobReady) {
|
||||
scope.removeJobReady();
|
||||
}
|
||||
scope.removeEventReady = scope.$on('EventsReady', function(e, data, maxID) {
|
||||
var elem, html;
|
||||
|
||||
lastID = maxID;
|
||||
html = buildTable(data);
|
||||
$('#host-events').html(html);
|
||||
elem = angular.element(document.getElementById('host-events-modal-dialog'));
|
||||
$compile(elem)(scope);
|
||||
|
||||
CreateDialog({
|
||||
scope: scope,
|
||||
width: 675,
|
||||
height: 600,
|
||||
minWidth: 450,
|
||||
callback: 'ModalReady',
|
||||
id: 'host-events-modal-dialog',
|
||||
onResizeStop: fixHeight,
|
||||
title: ( (title) ? title : 'Host Events' ),
|
||||
onClose: function() {
|
||||
try {
|
||||
scope.$destroy();
|
||||
}
|
||||
catch(e) {
|
||||
//ignore
|
||||
}
|
||||
},
|
||||
onOpen: function() {
|
||||
fixHeight();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (scope.removeRefreshHTML) {
|
||||
scope.removeRefreshHTML();
|
||||
}
|
||||
scope.removeRefreshHTML = scope.$on('RefreshHTML', function(e, data) {
|
||||
var elem, html = buildTable(data);
|
||||
$('#host-events').html(html);
|
||||
scope.hostViewSearching = false;
|
||||
elem = angular.element(document.getElementById('host-events'));
|
||||
$compile(elem)(scope);
|
||||
});
|
||||
|
||||
setStatus = function(result) {
|
||||
var msg = '', status = 'ok', status_text = 'OK';
|
||||
if (!result.task && result.event_data && result.event_data.res && result.event_data.res.ansible_facts) {
|
||||
result.task = "Gathering Facts";
|
||||
}
|
||||
if (result.event === "runner_on_no_hosts") {
|
||||
msg = "No hosts remaining";
|
||||
}
|
||||
if (result.event === 'runner_on_unreachable') {
|
||||
status = 'unreachable';
|
||||
status_text = 'Unreachable';
|
||||
}
|
||||
else if (result.failed) {
|
||||
status = 'failed';
|
||||
status_text = 'Failed';
|
||||
}
|
||||
else if (result.changed) {
|
||||
status = 'changed';
|
||||
status_text = 'Changed';
|
||||
}
|
||||
if (result.event_data.res && result.event_data.res.msg) {
|
||||
msg = result.event_data.res.msg;
|
||||
}
|
||||
result.msg = msg;
|
||||
result.status = status;
|
||||
result.status_text = status_text;
|
||||
return result;
|
||||
};
|
||||
|
||||
buildRow = function(res) {
|
||||
var html = '';
|
||||
html += "<tr>\n";
|
||||
html += "<td class=\"col-md-3\"><a href=\"\" ng-click=\"showDetails(" + res.id + ")\" aw-tool-tip=\"Click to view details\" data-placement=\"top\"><i class=\"fa icon-job-" + res.status + "\"></i> " + res.status_text + "</a></td>\n";
|
||||
html += "<td class=\"col-md=3\" ng-non-bindable>" + res.host_name + "</td>\n";
|
||||
html += "<td class=\"col-md-3\" ng-non-bindable>" + res.play + "</td>\n";
|
||||
html += "<td class=\"col-md-3\" ng-non-bindable>" + res.task + "</td>\n";
|
||||
html += "</tr>";
|
||||
return html;
|
||||
};
|
||||
|
||||
buildTable = function(data) {
|
||||
var html = "<table class=\"table\">\n";
|
||||
html += "<tbody>\n";
|
||||
data.results.forEach(function(result) {
|
||||
var res = setStatus(result);
|
||||
html += buildRow(res);
|
||||
});
|
||||
html += "</tbody>\n";
|
||||
html += "</table>\n";
|
||||
return html;
|
||||
};
|
||||
|
||||
fixHeight = function() {
|
||||
var available_height = $('#host-events-modal-dialog').height() - $('#host-events-modal-dialog #search-form').height() - $('#host-events-modal-dialog #fixed-table-header').height();
|
||||
$('#host-events').height(available_height);
|
||||
$log.debug('set height to: ' + available_height);
|
||||
// Check width and reset search fields
|
||||
if ($('#host-events-modal-dialog').width() <= 450) {
|
||||
$('#host-events-modal-dialog #status-field').css({'margin-left': '7px'});
|
||||
}
|
||||
else {
|
||||
$('#host-events-modal-dialog #status-field').css({'margin-left': '15px'});
|
||||
}
|
||||
};
|
||||
|
||||
GetEvents({
|
||||
url: url,
|
||||
scope: scope,
|
||||
callback: 'EventsReady'
|
||||
});
|
||||
|
||||
scope.modalOK = function() {
|
||||
$('#host-events-modal-dialog').dialog('close');
|
||||
scope.$destroy();
|
||||
};
|
||||
|
||||
scope.searchEvents = function() {
|
||||
scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
|
||||
GetEvents({
|
||||
scope: scope,
|
||||
url: url,
|
||||
callback: 'RefreshHTML'
|
||||
});
|
||||
};
|
||||
|
||||
scope.searchEventKeyPress = function(e) {
|
||||
if (e.keyCode === 13) {
|
||||
scope.searchEvents();
|
||||
}
|
||||
};
|
||||
|
||||
scope.showDetails = function(id) {
|
||||
EventViewer({
|
||||
scope: parent_scope,
|
||||
url: GetBasePath('jobs') + job_id + '/job_events/?id=' + id,
|
||||
});
|
||||
};
|
||||
|
||||
if (scope.removeEventsScrollDownBuild) {
|
||||
scope.removeEventsScrollDownBuild();
|
||||
}
|
||||
scope.removeEventsScrollDownBuild = scope.$on('EventScrollDownBuild', function(e, data, maxID) {
|
||||
var elem, html = '';
|
||||
lastID = maxID;
|
||||
data.results.forEach(function(result) {
|
||||
var res = setStatus(result);
|
||||
html += buildRow(res);
|
||||
});
|
||||
if (html) {
|
||||
$('#host-events table tbody').append(html);
|
||||
elem = angular.element(document.getElementById('host-events'));
|
||||
$compile(elem)(scope);
|
||||
}
|
||||
});
|
||||
|
||||
scope.hostEventsScrollDown = function() {
|
||||
GetEvents({
|
||||
scope: scope,
|
||||
url: url,
|
||||
gt: lastID,
|
||||
callback: 'EventScrollDownBuild'
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('GetEvents', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
|
||||
return function(params) {
|
||||
var url = params.url,
|
||||
scope = params.scope,
|
||||
gt = params.gt,
|
||||
callback = params.callback;
|
||||
|
||||
if (scope.host_events_search_name) {
|
||||
url += '?host_name=' + scope.host_events_search_name;
|
||||
}
|
||||
else {
|
||||
url += '?host_name__isnull=false';
|
||||
}
|
||||
|
||||
if (scope.host_events_search_status.value === 'changed') {
|
||||
url += '&event__icontains=runner&changed=true';
|
||||
}
|
||||
else if (scope.host_events_search_status.value === 'failed') {
|
||||
url += '&event__icontains=runner&failed=true';
|
||||
}
|
||||
else if (scope.host_events_search_status.value === 'ok') {
|
||||
url += '&event=runner_on_ok&changed=false';
|
||||
}
|
||||
else if (scope.host_events_search_status.value === 'unreachable') {
|
||||
url += '&event=runner_on_unreachable';
|
||||
}
|
||||
else if (scope.host_events_search_status.value === 'all') {
|
||||
url += '&event__icontains=runner¬__event=runner_on_skipped';
|
||||
}
|
||||
|
||||
if (gt) {
|
||||
// used for endless scroll
|
||||
url += '&id__gt=' + gt;
|
||||
}
|
||||
|
||||
url += '&page_size=50&order=id';
|
||||
|
||||
scope.hostViewSearching = true;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
var lastID;
|
||||
scope.hostViewSearching = false;
|
||||
if (data.results.length > 0) {
|
||||
lastID = data.results[data.results.length - 1].id;
|
||||
}
|
||||
scope.$emit(callback, data, lastID);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
scope.hostViewSearching = false;
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to get events ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
};
|
||||
}]);
|
@ -0,0 +1,82 @@
|
||||
@import "awx/ui/client/src/shared/branding/colors.less";
|
||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||
|
||||
.HostEvents .modal-footer{
|
||||
border: 0;
|
||||
margin-top: 0px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.HostEvents-status--ok{
|
||||
color: @green;
|
||||
}
|
||||
.HostEvents-status--unreachable{
|
||||
color: @unreachable;
|
||||
}
|
||||
.HostEvents-status--changed{
|
||||
color: @changed;
|
||||
}
|
||||
.HostEvents-status--failed{
|
||||
color: @warning;
|
||||
}
|
||||
.HostEvents-status--skipped{
|
||||
color: @skipped;
|
||||
}
|
||||
.HostEvents-search--form{
|
||||
max-width: 420px;
|
||||
display: inline-block;
|
||||
}
|
||||
.HostEvents-close{
|
||||
width: 70px;
|
||||
}
|
||||
.HostEvents-filter--form{
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
float: right;
|
||||
display: inline-block;
|
||||
}
|
||||
.HostEvents .modal-body{
|
||||
padding: 20px;
|
||||
}
|
||||
.HostEvents .select2-container{
|
||||
text-transform: capitalize;
|
||||
max-width: 220px;
|
||||
float: right;
|
||||
}
|
||||
.HostEvents-form--container{
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.HostEvents-title{
|
||||
color: @default-interface-txt;
|
||||
font-weight: 600;
|
||||
}
|
||||
.HostEvents-status i {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.HostEvents-table--header {
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
background-color: @default-list-header-bg;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
.HostEvents-table--header:first-of-type{
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
.HostEvents-table--header:last-of-type{
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
.HostEvents-table--row{
|
||||
color: @default-data-txt;
|
||||
border: 0 !important;
|
||||
}
|
||||
.HostEvents-table--row:nth-child(odd){
|
||||
background: @default-tertiary-bg;
|
||||
}
|
||||
.HostEvents-table--cell{
|
||||
border: 0 !important;
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
['$stateParams', '$scope', '$rootScope', '$state', 'Wait',
|
||||
'JobDetailService', 'CreateSelect2',
|
||||
function($stateParams, $scope, $rootScope, $state, Wait,
|
||||
JobDetailService, CreateSelect2){
|
||||
|
||||
// pagination not implemented yet, but it'll depend on this
|
||||
$scope.page_size = $stateParams.page_size;
|
||||
|
||||
$scope.activeFilter = $stateParams.filter || null;
|
||||
|
||||
$scope.search = function(){
|
||||
Wait('start');
|
||||
if ($scope.searchStr == undefined){
|
||||
return
|
||||
}
|
||||
//http://docs.ansible.com/ansible-tower/latest/html/towerapi/intro.html#filtering
|
||||
// SELECT WHERE host_name LIKE str OR WHERE play LIKE str OR WHERE task LIKE str AND host_name NOT ""
|
||||
// selecting non-empty host_name fields prevents us from displaying non-runner events, like playbook_on_task_start
|
||||
JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
or__host_name__icontains: $scope.searchStr,
|
||||
or__play__icontains: $scope.searchStr,
|
||||
or__task__icontains: $scope.searchStr,
|
||||
not__host_name: "" ,
|
||||
page_size: $scope.pageSize})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filters = ['all', 'changed', 'failed', 'ok', 'unreachable', 'skipped'];
|
||||
|
||||
var filter = function(filter){
|
||||
Wait('start');
|
||||
if (filter == 'all'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
page_size: $scope.pageSize})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
// handle runner cases
|
||||
if (filter == 'skipped'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event: 'runner_on_skipped'})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter == 'unreachable'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event: 'runner_on_unreachable'})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter == 'ok'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
event: 'runner_on_ok',
|
||||
changed: false
|
||||
})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
// handle convience properties .changed .failed
|
||||
if (filter == 'changed'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
changed: true})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
if (filter == 'failed'){
|
||||
return JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
failed: true})
|
||||
.success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// watch select2 for changes
|
||||
$('.HostEvents-select').on("select2:select", function (e) {
|
||||
filter($('.HostEvents-select').val());
|
||||
});
|
||||
|
||||
$scope.processStatus = function(event, $index){
|
||||
// the stack for which status to display is
|
||||
// unreachable > failed > changed > ok
|
||||
// uses the API's runner events and convenience properties .failed .changed to determine status.
|
||||
// see: job_event_callback.py
|
||||
if (event.event == 'runner_on_unreachable'){
|
||||
$scope.results[$index].status = 'Unreachable';
|
||||
return 'HostEvents-status--unreachable'
|
||||
}
|
||||
// equiv to 'runner_on_error' && 'runner on failed'
|
||||
if (event.failed){
|
||||
$scope.results[$index].status = 'Failed';
|
||||
return 'HostEvents-status--failed'
|
||||
}
|
||||
// catch the changed case before ok, because both can be true
|
||||
if (event.changed){
|
||||
$scope.results[$index].status = 'Changed';
|
||||
return 'HostEvents-status--changed'
|
||||
}
|
||||
if (event.event == 'runner_on_ok'){
|
||||
$scope.results[$index].status = 'OK';
|
||||
return 'HostEvents-status--ok'
|
||||
}
|
||||
if (event.event == 'runner_on_skipped'){
|
||||
$scope.results[$index].status = 'Skipped';
|
||||
return 'HostEvents-status--skipped'
|
||||
}
|
||||
else{
|
||||
// study a case where none of these apply
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var init = function(){
|
||||
// create filter dropdown
|
||||
CreateSelect2({
|
||||
element: '.HostEvents-select',
|
||||
multiple: false
|
||||
});
|
||||
// process the filter if one was passed
|
||||
if ($stateParams.filter){
|
||||
filter($stateParams.filter).success(function(res){
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
$('#HostEvents').modal('show');
|
||||
});;
|
||||
}
|
||||
else{
|
||||
Wait('start');
|
||||
JobDetailService.getRelatedJobEvents($stateParams.id, {
|
||||
host_name: $stateParams.hostName,
|
||||
page_size: $stateParams.page_size})
|
||||
.success(function(res){
|
||||
$scope.pagination = res;
|
||||
$scope.results = res.results;
|
||||
Wait('stop');
|
||||
$('#HostEvents').modal('show');
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.goBack = function(){
|
||||
// go back to the job details state
|
||||
// we're leaning on $stateProvider's onExit to close the modal
|
||||
$state.go('jobDetail');
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
}];
|
@ -0,0 +1,64 @@
|
||||
<div id="HostEvents" class="HostEvents modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="HostEvents-header">
|
||||
<span class="HostEvents-title">HOST EVENTS</span>
|
||||
<!-- Close -->
|
||||
<button ng-click="goBack()" type="button" class="close">
|
||||
<i class="fa fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="HostEvents-form--container">
|
||||
<form ng-submit="search()" class="form-inline HostEvents-search--form">
|
||||
<!-- Search -->
|
||||
<div class="form-group" >
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="searchStr" class="form-control" placeholder="SEARCH">
|
||||
<span ng-click="search()" type="submit" class="input-group-addon btn btn-default"><i class="fa fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<select class="HostEvents-select">
|
||||
<option ng-selected="filter == activeFilter" class="HostEvents-select--option" value="{{filter}}" ng-repeat="filter in filters">{{filter}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- event results table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<!-- column labels -->
|
||||
<th ng-hide="results.length == 0" class="HostEvents-table--header">STATUS</th>
|
||||
<th ng-hide="results.length == 0" class="HostEvents-table--header">HOST</th>
|
||||
<th ng-hide="results.length == 0" class="HostEvents-table--header">PLAY</th>
|
||||
<th ng-hide="results.length == 0" class="HostEvents-table--header">TASK</th>
|
||||
<!-- result rows -->
|
||||
<tr class="HostEvents-table--row" ng-repeat="event in results track by $index" modal-paginate="event in results | page_size: page_size">
|
||||
<td class=HostEvents-table--cell>
|
||||
<!-- status circles -->
|
||||
<a class="HostEvents-status">
|
||||
<i class="fa fa-circle" ng-class="processStatus(event, $index)"></i>
|
||||
</a>
|
||||
{{event.status}}
|
||||
</td>
|
||||
<td class=HostEvents-table--cell>{{event.host_name}}</td>
|
||||
<td class=HostEvents-table--cell>{{event.play}}</td>
|
||||
<td class=HostEvents-table--cell>{{event.task}}</td>
|
||||
</tr>
|
||||
<tr ng-show="results.length == 0" class="HostEvents-table--row">
|
||||
<td class=HostEvents-table--cell>
|
||||
No results were found.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<!-- pagination -->
|
||||
<!-- close -->
|
||||
<button ng-click="goBack()" class="btn btn-default pull-right HostEvents-close">OK</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,30 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'jobDetail.host-events',
|
||||
url: '/host-events/:hostName?:filter',
|
||||
controller: 'HostEventsController',
|
||||
params: {
|
||||
page_size: 10
|
||||
},
|
||||
templateUrl: templateUrl('job-detail/host-events/host-events'),
|
||||
onExit: function(){
|
||||
// close the modal
|
||||
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
|
||||
$('#HostEvents').modal('hide');
|
||||
// hacky way to handle user browsing away via URL bar
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
};
|
15
awx/ui/client/src/job-detail/host-events/main.js
Normal file
15
awx/ui/client/src/job-detail/host-events/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import route from './host-events.route';
|
||||
import controller from './host-events.controller';
|
||||
|
||||
export default
|
||||
angular.module('jobDetail.hostEvents', [])
|
||||
.controller('HostEventsController', controller)
|
||||
.run(['$stateExtender', function($stateExtender){
|
||||
$stateExtender.addState(route)
|
||||
}]);
|
@ -15,7 +15,7 @@ export default
|
||||
'$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait',
|
||||
'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed',
|
||||
'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
|
||||
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer',
|
||||
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
|
||||
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
|
||||
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
|
||||
'EditSchedule', 'ParseTypeChange', 'JobDetailService', 'EventViewer',
|
||||
@ -25,7 +25,7 @@ export default
|
||||
SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph,
|
||||
LoadHostSummary, ReloadHostSummaryList, JobIsFinished,
|
||||
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
|
||||
PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts,
|
||||
PlaybookRun, LoadPlays, LoadTasks, LoadHosts,
|
||||
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
|
||||
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService, EventViewer
|
||||
) {
|
||||
@ -1400,17 +1400,6 @@ export default
|
||||
}
|
||||
};
|
||||
|
||||
scope.hostEventsViewer = function(id, name, status) {
|
||||
HostEventsViewer({
|
||||
scope: scope,
|
||||
id: id,
|
||||
name: name,
|
||||
url: scope.job.related.job_events,
|
||||
job_id: scope.job.id,
|
||||
status: status
|
||||
});
|
||||
};
|
||||
|
||||
scope.refresh = function(){
|
||||
$scope.$emit('LoadJob');
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
<div class="tab-pane" id="jobs-detail">
|
||||
<div ng-cloak id="htmlTemplate" class="JobDetail">
|
||||
|
||||
<div ui-view></div>
|
||||
<!--beginning of job-detail-container (left side) -->
|
||||
<div id="job-detail-container" class="JobDetail-leftSide" ng-class="{'JobDetail-stdoutActionButton--active': stdoutFullScreen}">
|
||||
|
||||
<!--beginning of results-->
|
||||
<div id="job-results-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
|
||||
<div class="JobDetail-panelHeader">
|
||||
@ -423,13 +422,13 @@
|
||||
<tbody>
|
||||
<tr class="List-tableRow" ng-repeat="host in summaryList = (hosts) track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
|
||||
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
|
||||
<a href="" ng-click="hostEventsViewer(host.id, host.name)" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a>
|
||||
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id})" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a>
|
||||
</td>
|
||||
<td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column">
|
||||
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'ok')" aw-tool-tip="{{ host.okTip }}" data-tip-watch="host.okTip" data-placement="top" ng-hide="host.ok == 0"><span class="badge successful-hosts">{{ host.ok }}</span></a>
|
||||
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'changed')" aw-tool-tip="{{ host.changedTip }}" data-tip-watch="host.changedTip" data-placement="top" ng-hide="host.changed == 0"><span class="badge changed-hosts">{{ host.changed }}</span></a>
|
||||
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'unreachable')" aw-tool-tip="{{ host.unreachableTip }}" data-tip-watch="host.unreachableTip" data-placement="top" ng-hide="host.unreachable == 0"><span class="badge unreachable-hosts">{{ host.unreachable }}</span></a>
|
||||
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'failed')" aw-tool-tip="{{ host.failedTip }}" data-tip-watch="host.failedTip" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failed }}</span></a>
|
||||
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'ok'})" aw-tool-tip="{{ host.okTip }}" data-tip-watch="host.okTip" data-placement="top" ng-hide="host.ok == 0"><span class="badge successful-hosts">{{ host.ok }}</span></a>
|
||||
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'changed'})" aw-tool-tip="{{ host.changedTip }}" data-tip-watch="host.changedTip" data-placement="top" ng-hide="host.changed == 0"><span class="badge changed-hosts">{{ host.changed }}</span></a>
|
||||
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'unreachable'})" aw-tool-tip="{{ host.unreachableTip }}" data-tip-watch="host.unreachableTip" data-placement="top" ng-hide="host.unreachable == 0"><span class="badge unreachable-hosts">{{ host.unreachable }}</span></a>
|
||||
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'failed'})" aw-tool-tip="{{ host.failedTip }}" data-tip-watch="host.failedTip" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failed }}</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="summaryList.length === 0 && waiting">
|
||||
@ -483,40 +482,6 @@
|
||||
|
||||
<div ng-include="'/static/partials/eventviewer.html'"></div>
|
||||
|
||||
<div id="host-events-modal-dialog" style="display:none;">
|
||||
<div id="search-form" class="form-inline">
|
||||
<div class="form-group" style="position:relative;">
|
||||
<label>Search</label>
|
||||
<div class="search-name" style="display:inline-block; position:relative;">
|
||||
<input type="text" class="form-control input-sm" id="host-events-search-name" ng-model="host_events_search_name" placeholder="Host name" ng-keypress="searchEventKeyPress($event)" >
|
||||
<div id="search-all-input-icons">
|
||||
<a class="search-icon" ng-show="!eventsSearchActive" ng-click="searchEvents()"><i class="fa fa-search"></i></a>
|
||||
<a class="search-icon" ng-show="eventsSearchActive" ng-click="host_events_search_name=''; searchEvents()"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="status-field">
|
||||
<label>Status</label>
|
||||
<select id="host-events-search-status" class="form-control input-sm" ng-model="host_events_search_status" name="host-events-search-name" ng-change="searchEvents()"
|
||||
ng-options="opt.name for opt in host_events_status_options track by opt.value"></select>
|
||||
</div>
|
||||
<div class="form-group" id="search-indicator" ng-show="hostViewSearching"><i class="fa fa-lg fa-spin fa-cog"></i></div>
|
||||
</div>
|
||||
<!-- lr-infinite-scroll="hostEventsTable" scroll-threshold="10" time-threshold="500" -->
|
||||
<div id="host-events-table">
|
||||
<table id="fixed-table-header" class="table">
|
||||
<thead>
|
||||
<tr><th class="col-md-3">Status</th>
|
||||
<th class="col-md-3">Host</th>
|
||||
<th class="col-md-3">Play</th>
|
||||
<th class="col-md-3">Task</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div id="host-events" lr-infinite-scroll="hostEventsScrollDown" scroll-threshold="10" time-threshold="500"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="host-modal-dialog" style="display: none;" class="dialog-content"></div>
|
||||
|
||||
<div ng-include="'/static/partials/schedule_dialog.html'"></div>
|
||||
|
@ -7,9 +7,12 @@
|
||||
import route from './job-detail.route';
|
||||
import controller from './job-detail.controller';
|
||||
import service from './job-detail.service';
|
||||
import hostEvents from './host-events/main';
|
||||
|
||||
export default
|
||||
angular.module('jobDetail', [])
|
||||
angular.module('jobDetail', [
|
||||
hostEvents.name
|
||||
])
|
||||
.controller('JobDetailController', controller)
|
||||
.service('JobDetailService', service)
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
|
@ -76,6 +76,9 @@ export default
|
||||
},{
|
||||
name: "OpenStack",
|
||||
value: "openstack"
|
||||
},{
|
||||
name: "OpenStack V3",
|
||||
value: "openstack_v3"
|
||||
}],
|
||||
sourceModel: 'inventory_source',
|
||||
sourceField: 'source',
|
||||
@ -84,7 +87,7 @@ export default
|
||||
has_external_source: {
|
||||
label: 'Has external source?',
|
||||
searchType: 'in',
|
||||
searchValue: 'ec2,rax,vmware,azure,gce,openstack',
|
||||
searchValue: 'ec2,rax,vmware,azure,gce,openstack,openstack_v3',
|
||||
searchOnly: true,
|
||||
sourceModel: 'inventory_source',
|
||||
sourceField: 'source'
|
||||
|
@ -51,6 +51,9 @@ export default
|
||||
},{
|
||||
name: "OpenStack",
|
||||
value: "openstack"
|
||||
},{
|
||||
name: "OpenStack V3",
|
||||
value: "openstack_v3"
|
||||
}],
|
||||
sourceModel: 'inventory_source',
|
||||
sourceField: 'source',
|
||||
@ -59,7 +62,7 @@ export default
|
||||
has_external_source: {
|
||||
label: 'Has external source?',
|
||||
searchType: 'in',
|
||||
searchValue: 'ec2,rax,vmware,azure,gce,openstack',
|
||||
searchValue: 'ec2,rax,vmware,azure,gce,openstack,openstack_v3',
|
||||
searchOnly: true,
|
||||
sourceModel: 'inventory_source',
|
||||
sourceField: 'source'
|
||||
|
@ -11,7 +11,9 @@ export default function($stateProvider){
|
||||
resolve: state.resolve,
|
||||
params: state.params,
|
||||
data: state.data,
|
||||
ncyBreadcrumb: state.ncyBreadcrumb
|
||||
ncyBreadcrumb: state.ncyBreadcrumb,
|
||||
onEnter: state.onEnter,
|
||||
onExit: state.onExit
|
||||
});
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user