From c807d5dcf373cc84bbac34b21db09a1746b46ae7 Mon Sep 17 00:00:00 2001
From: AlanCoding
Date: Tue, 1 Mar 2016 09:33:17 -0500
Subject: [PATCH 01/13] Add keystone v3 support via new domain field on
credential
---
awx/api/serializers.py | 3 ++-
awx/main/migrations/0007_v300_changes.py | 19 +++++++++++++++++++
awx/main/models/credential.py | 7 +++++++
awx/main/tasks.py | 4 ++++
awx/main/tests/old/inventory.py | 20 ++++++++++++++++++++
5 files changed, 52 insertions(+), 1 deletion(-)
create mode 100644 awx/main/migrations/0007_v300_changes.py
diff --git a/awx/api/serializers.py b/awx/api/serializers.py
index 6ca73cf6a0..e48f12e6c4 100644
--- a/awx/api/serializers.py
+++ b/awx/api/serializers.py
@@ -1484,7 +1484,8 @@ class CredentialSerializer(BaseSerializer):
class Meta:
model = Credential
fields = ('*', 'user', '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')
diff --git a/awx/main/migrations/0007_v300_changes.py b/awx/main/migrations/0007_v300_changes.py
new file mode 100644
index 0000000000..f6d0ec1410
--- /dev/null
+++ b/awx/main/migrations/0007_v300_changes.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('main', '0006_v300_changes'),
+ ]
+
+ 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),
+ ),
+ ]
diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py
index 82e0f576e1..328c738cc0 100644
--- a/awx/main/models/credential.py
+++ b/awx/main/models/credential.py
@@ -114,6 +114,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
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='',
diff --git a/awx/main/tasks.py b/awx/main/tasks.py
index ec34886632..ba54e17e9b 100644
--- a/awx/main/tasks.py
+++ b/awx/main/tasks.py
@@ -712,6 +712,8 @@ class RunJob(BaseTask):
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': {
@@ -1151,6 +1153,8 @@ class RunInventoryUpdate(BaseTask):
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.
diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py
index 5c48f30bb6..ccfb7138c2 100644
--- a/awx/main/tests/old/inventory.py
+++ b/awx/main/tests/old/inventory.py
@@ -2068,6 +2068,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',
+ 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', 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', '')
From f4b1de766dd0670cd9243088ee1e9525d1fd415f Mon Sep 17 00:00:00 2001
From: AlanCoding
Date: Wed, 16 Mar 2016 15:32:05 -0400
Subject: [PATCH 02/13] Adding OpenStack v3 cred type
---
awx/main/constants.py | 2 +-
awx/main/models/base.py | 2 +-
awx/main/models/credential.py | 18 ++++++++++----
awx/main/models/inventory.py | 6 +++++
awx/main/tasks.py | 13 ++++++----
awx/main/tests/old/inventory.py | 28 ++++++++++++++++++++--
awx/ui/client/src/forms/Credentials.js | 23 +++++++++++++++---
awx/ui/client/src/forms/Source.js | 3 ++-
awx/ui/client/src/helpers/Credentials.js | 19 +++++++++++++++
awx/ui/client/src/helpers/Groups.js | 12 ++++++----
awx/ui/client/src/lists/HomeGroups.js | 5 +++-
awx/ui/client/src/lists/InventoryGroups.js | 5 +++-
12 files changed, 114 insertions(+), 22 deletions(-)
diff --git a/awx/main/constants.py b/awx/main/constants.py
index 64f6265569..a6bdafdf5a 100644
--- a/awx/main/constants.py
+++ b/awx/main/constants.py
@@ -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',)
diff --git a/awx/main/models/base.py b/awx/main/models/base.py
index c4edfbd8ba..b912a71572 100644
--- a/awx/main/models/base.py
+++ b/awx/main/models/base.py
@@ -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)'),
diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py
index 328c738cc0..0293d18e00 100644
--- a/awx/main/models/credential.py
+++ b/awx/main/models/credential.py
@@ -34,6 +34,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
('gce', _('Google Compute Engine')),
('azure', _('Microsoft Azure')),
('openstack', _('OpenStack')),
+ ('openstack_v3', _('OpenStack V3')),
]
BECOME_METHOD_CHOICES = [
@@ -210,10 +211,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
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':
@@ -223,7 +233,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
'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
@@ -235,13 +245,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
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
diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py
index c95c8488bd..e1dd36e64d 100644
--- a/awx/main/models/inventory.py
+++ b/awx/main/models/inventory.py
@@ -748,6 +748,7 @@ class InventorySourceOptions(BaseModel):
('azure', _('Microsoft Azure')),
('vmware', _('VMware vCenter')),
('openstack', _('OpenStack')),
+ ('openstack_v3', _('OpenStack V3')),
('custom', _('Custom Script')),
]
@@ -976,6 +977,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
diff --git a/awx/main/tasks.py b/awx/main/tasks.py
index ba54e17e9b..381ea31623 100644
--- a/awx/main/tasks.py
+++ b/awx/main/tasks.py
@@ -706,7 +706,7 @@ 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,
@@ -798,7 +798,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
@@ -1147,7 +1147,7 @@ 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,
@@ -1302,7 +1302,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.
@@ -1345,6 +1345,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',
diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py
index ccfb7138c2..3ac2310160 100644
--- a/awx/main/tests/old/inventory.py
+++ b/awx/main/tests/old/inventory.py
@@ -2078,13 +2078,13 @@ class InventoryUpdatesTest(BaseTransactionTest):
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',
+ 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', credential=credential)
+ 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())
@@ -2132,3 +2132,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)
diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js
index 221ab12b22..84aaf804e8 100644
--- a/awx/ui/client/src/forms/Credentials.js
+++ b/awx/ui/client/src/forms/Credentials.js
@@ -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',
diff --git a/awx/ui/client/src/forms/Source.js b/awx/ui/client/src/forms/Source.js
index 1eee07b344..86e6db5477 100644
--- a/awx/ui/client/src/forms/Source.js
+++ b/awx/ui/client/src/forms/Source.js
@@ -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',
diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js
index f986f06e4e..f1af37a011 100644
--- a/awx/ui/client/src/helpers/Credentials.js
+++ b/awx/ui/client/src/helpers/Credentials.js
@@ -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 = "The project value
";
+ scope.domainPopOver = "The domain name
";
scope.hostPopOver = "The host value
";
if (!Empty(scope.kind)) {
@@ -133,6 +136,22 @@ angular.module('CredentialsHelper', ['Utilities'])
" as the username.
";
scope.hostPopOver = "The host to authenticate with." +
"
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 = "
This is the tenant name " +
+ "or tenant id. This value is usually the same " +
+ " as the username.
";
+ scope.hostPopOver = "The host to authenticate with." +
+ "
For example, https://openstack.business.com/v3
";
+ scope.domainPopOver = "Domain used for Keystone v3 " +
+ "
identity service.
";
break;
}
}
diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js
index eeebb9d8bf..4e95d96857 100644
--- a/awx/ui/client/src/helpers/Groups.js
+++ b/awx/ui/client/src/helpers/Groups.js
@@ -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);
}
diff --git a/awx/ui/client/src/lists/HomeGroups.js b/awx/ui/client/src/lists/HomeGroups.js
index ad7180dff0..1c21fb6268 100644
--- a/awx/ui/client/src/lists/HomeGroups.js
+++ b/awx/ui/client/src/lists/HomeGroups.js
@@ -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'
diff --git a/awx/ui/client/src/lists/InventoryGroups.js b/awx/ui/client/src/lists/InventoryGroups.js
index 53881f3d7c..3b221e54e0 100644
--- a/awx/ui/client/src/lists/InventoryGroups.js
+++ b/awx/ui/client/src/lists/InventoryGroups.js
@@ -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'
From fcc02f7678dd47d6ece37ad6b86e3de872ab81c3 Mon Sep 17 00:00:00 2001
From: AlanCoding
Date: Fri, 18 Mar 2016 16:45:06 -0400
Subject: [PATCH 03/13] rebase and rename migrations corresponding to devel
change
---
...007_v300_changes.py => 0007_v300_credential_domain_field.py} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename awx/main/migrations/{0007_v300_changes.py => 0007_v300_credential_domain_field.py} (88%)
diff --git a/awx/main/migrations/0007_v300_changes.py b/awx/main/migrations/0007_v300_credential_domain_field.py
similarity index 88%
rename from awx/main/migrations/0007_v300_changes.py
rename to awx/main/migrations/0007_v300_credential_domain_field.py
index f6d0ec1410..8875f9071f 100644
--- a/awx/main/migrations/0007_v300_changes.py
+++ b/awx/main/migrations/0007_v300_credential_domain_field.py
@@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('main', '0006_v300_changes'),
+ ('main', '0006_v300_create_system_job_templates'),
]
operations = [
From 7c1efea037ca2af6d332ff59b0f3ffdd7d39e1b6 Mon Sep 17 00:00:00 2001
From: Leigh Johnson
Date: Sat, 19 Mar 2016 19:36:15 -0400
Subject: [PATCH 04/13] add onExit & onEnter hooks to $stateExtender, raze
HostEventViewer and replace with host-events module, resolves #1132
---
awx/ui/client/src/about/about.route.js | 5 +
awx/ui/client/src/app.js | 1 -
awx/ui/client/src/helpers.js | 2 -
awx/ui/client/src/helpers/HostEventsViewer.js | 287 ------------------
.../job-detail/host-event/host-event.route.js | 0
.../host-events/host-events.block.less | 82 +++++
.../host-events/host-events.controller.js | 184 +++++++++++
.../host-events/host-events.partial.html | 59 ++++
.../host-events/host-events.route.js | 27 ++
.../client/src/job-detail/host-events/main.js | 15 +
.../src/job-detail/job-detail.controller.js | 17 +-
.../src/job-detail/job-detail.partial.html | 47 +--
awx/ui/client/src/job-detail/main.js | 5 +-
.../src/shared/stateExtender.provider.js | 4 +-
14 files changed, 388 insertions(+), 347 deletions(-)
delete mode 100644 awx/ui/client/src/helpers/HostEventsViewer.js
delete mode 100644 awx/ui/client/src/job-detail/host-event/host-event.route.js
create mode 100644 awx/ui/client/src/job-detail/host-events/host-events.block.less
create mode 100644 awx/ui/client/src/job-detail/host-events/host-events.controller.js
create mode 100644 awx/ui/client/src/job-detail/host-events/host-events.partial.html
create mode 100644 awx/ui/client/src/job-detail/host-events/host-events.route.js
create mode 100644 awx/ui/client/src/job-detail/host-events/main.js
diff --git a/awx/ui/client/src/about/about.route.js b/awx/ui/client/src/about/about.route.js
index 5f8b5e9220..475cf1aea0 100644
--- a/awx/ui/client/src/about/about.route.js
+++ b/awx/ui/client/src/about/about.route.js
@@ -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')
};
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index cbf50b22b6..48d7d07019 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -177,7 +177,6 @@ var tower = angular.module('Tower', [
'StandardOutHelper',
'LogViewerOptionsDefinition',
'EventViewerHelper',
- 'HostEventsViewerHelper',
'JobDetailHelper',
'SocketIO',
'lrInfiniteScroll',
diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js
index b298a635ef..e8190ea50e 100644
--- a/awx/ui/client/src/helpers.js
+++ b/awx/ui/client/src/helpers.js
@@ -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,
diff --git a/awx/ui/client/src/helpers/HostEventsViewer.js b/awx/ui/client/src/helpers/HostEventsViewer.js
deleted file mode 100644
index e8fc5a940a..0000000000
--- a/awx/ui/client/src/helpers/HostEventsViewer.js
+++ /dev/null
@@ -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 += "\n";
- html += " " + res.status_text + " | \n";
- html += "" + res.host_name + " | \n";
- html += "" + res.play + " | \n";
- html += "" + res.task + " | \n";
- html += "
";
- return html;
- };
-
- buildTable = function(data) {
- var html = "\n";
- html += "\n";
- data.results.forEach(function(result) {
- var res = setStatus(result);
- html += buildRow(res);
- });
- html += "\n";
- html += "
\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 });
- });
- };
- }]);
diff --git a/awx/ui/client/src/job-detail/host-event/host-event.route.js b/awx/ui/client/src/job-detail/host-event/host-event.route.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.block.less b/awx/ui/client/src/job-detail/host-events/host-events.block.less
new file mode 100644
index 0000000000..bde3fe72fd
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.block.less
@@ -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: #848992;
+ background-color: #EBEBEB;
+ 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;
+}
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.controller.js b/awx/ui/client/src/job-detail/host-events/host-events.controller.js
new file mode 100644
index 0000000000..45a7268ed7
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.controller.js
@@ -0,0 +1,184 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+ export default
+ ['$stateParams', '$scope', '$rootScope', '$state', 'Wait',
+ 'JobDetailService', 'CreateSelect2', 'PaginateInit',
+ function($stateParams, $scope, $rootScope, $state, Wait,
+ JobDetailService, CreateSelect2, PaginateInit){
+
+
+ $scope.search = function(){
+ Wait('start');
+ if ($scope.searchStr == undefined){
+ return
+ }
+ // The API treats params as AND query
+ // We should discuss the possibility of an OR array
+
+ // search play description
+ /*
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ play: $scope.searchStr})
+ .success(function(res){
+ results.push(res.results);
+ });
+ */
+ // search host name
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $scope.searchStr})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('Stop')
+ });
+ // search task
+ /*
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ task: $scope.searchStr})
+ .success(function(res){
+ results.push(res.results);
+ });
+ */
+ };
+
+ $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})
+ .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'
+ // add param changed: false if 'ok' shouldn't display changed hosts
+ })
+ .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;
+ PaginateInit({ scope: $scope, list: defaultUrl });
+ Wait('stop');
+ $('#HostEvents').modal('show');
+
+
+ });;
+ }
+ else{
+ Wait('start');
+ JobDetailService.getRelatedJobEvents($stateParams.id, {host_name: $stateParams.hostName})
+ .success(function(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();
+
+ }];
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.partial.html b/awx/ui/client/src/job-detail/host-events/host-events.partial.html
new file mode 100644
index 0000000000..a0ee6956bb
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.partial.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{event.status}}
+ |
+ {{event.host_name}} |
+ {{event.play}} |
+ {{event.task}} |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.route.js b/awx/ui/client/src/job-detail/host-events/host-events.route.js
new file mode 100644
index 0000000000..5365fea95c
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.route.js
@@ -0,0 +1,27 @@
+/*************************************************
+ * Copyright (c) 2015 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import {templateUrl} from '../../shared/template-url/template-url.factory';
+
+export default {
+ name: 'jobDetail.hostEvents',
+ url: '/host-events/:hostName?:filter',
+ controller: 'HostEventsController',
+ 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();
+ }]
+ }
+};
diff --git a/awx/ui/client/src/job-detail/host-events/main.js b/awx/ui/client/src/job-detail/host-events/main.js
new file mode 100644
index 0000000000..8a9487aec4
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/main.js
@@ -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)
+ }]);
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js
index e36dbb13de..1383f04c35 100644
--- a/awx/ui/client/src/job-detail/job-detail.controller.js
+++ b/awx/ui/client/src/job-detail/job-detail.controller.js
@@ -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
) {
@@ -43,7 +43,7 @@ export default
scope.parseType = 'yaml';
scope.previousTaskFailed = false;
$scope.stdoutFullScreen = false;
-
+
scope.$watch('job_status', function(job_status) {
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
scope.previousTaskFailed = true;
@@ -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');
};
diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html
index 3ff7262d1c..aed0a5446e 100644
--- a/awx/ui/client/src/job-detail/job-detail.partial.html
+++ b/awx/ui/client/src/job-detail/job-detail.partial.html
@@ -1,9 +1,8 @@