Merge branch 'rbac' of github.com:ansible/ansible-tower into rbac

This commit is contained in:
Wayne Witzel III
2016-03-31 12:41:47 -04:00
80 changed files with 4314 additions and 2302 deletions

View File

@@ -6,7 +6,7 @@ from collections import OrderedDict
# Django # Django
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.encoding import force_text from django.utils.encoding import force_text, smart_text
# Django REST Framework # Django REST Framework
from rest_framework import exceptions from rest_framework import exceptions
@@ -37,6 +37,25 @@ class Metadata(metadata.SimpleMetadata):
if value is not None and value != '': if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True) field_info[attr] = force_text(value, strings_only=True)
# Update help text for common fields.
serializer = getattr(field, 'parent', None)
if serializer:
field_help_text = {
'id': 'Database ID for this {}.',
'name': 'Name of this {}.',
'description': 'Optional description of this {}.',
'type': 'Data type for this {}.',
'url': 'URL for this {}.',
'related': 'Data structure with URLs of related resources.',
'summary_fields': 'Data structure with name/description for related resources.',
'created': 'Timestamp when this {} was created.',
'modified': 'Timestamp when this {} was last modified.',
}
if field.field_name in field_help_text:
opts = serializer.Meta.model._meta.concrete_model._meta
verbose_name = smart_text(opts.verbose_name)
field_info['help_text'] = field_help_text[field.field_name].format(verbose_name)
# Indicate if a field has a default value. # Indicate if a field has a default value.
# FIXME: Still isn't showing all default values? # FIXME: Still isn't showing all default values?
try: try:
@@ -77,7 +96,7 @@ class Metadata(metadata.SimpleMetadata):
# Update type of fields returned... # Update type of fields returned...
if field.field_name == 'type': if field.field_name == 'type':
field_info['type'] = 'multiple choice' field_info['type'] = 'choice'
elif field.field_name == 'url': elif field.field_name == 'url':
field_info['type'] = 'string' field_info['type'] = 'string'
elif field.field_name in ('related', 'summary_fields'): elif field.field_name in ('related', 'summary_fields'):

View File

@@ -3,7 +3,7 @@
# Django REST Framework # Django REST Framework
from rest_framework import pagination from rest_framework import pagination
from rest_framework.utils.urls import remove_query_param, replace_query_param from rest_framework.utils.urls import replace_query_param
class Pagination(pagination.PageNumberPagination): class Pagination(pagination.PageNumberPagination):
@@ -22,6 +22,4 @@ class Pagination(pagination.PageNumberPagination):
return None return None
url = self.request and self.request.get_full_path() or '' url = self.request and self.request.get_full_path() or ''
page_number = self.page.previous_page_number() page_number = self.page.previous_page_number()
if page_number == 1:
return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number) return replace_query_param(url, self.page_query_param, page_number)

View File

@@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
from django.db import models from django.db import models
# from django.utils.translation import ugettext_lazy as _ # from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text, smart_text from django.utils.encoding import force_text
from django.utils.text import capfirst from django.utils.text import capfirst
# Django REST Framework # Django REST Framework
@@ -351,7 +351,6 @@ class BaseSerializer(serializers.ModelSerializer):
return obj.modified return obj.modified
def build_standard_field(self, field_name, model_field): def build_standard_field(self, field_name, model_field):
# DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits # DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
# when a Model's editable field is set to False. The short circuit skips choice rendering. # when a Model's editable field is set to False. The short circuit skips choice rendering.
# #
@@ -368,27 +367,6 @@ class BaseSerializer(serializers.ModelSerializer):
if was_editable is False: if was_editable is False:
field_kwargs['read_only'] = True field_kwargs['read_only'] = True
# Update help text for common fields.
opts = self.Meta.model._meta.concrete_model._meta
if field_name == 'id':
field_kwargs.setdefault('help_text', 'Database ID for this %s.' % smart_text(opts.verbose_name))
elif field_name == 'name':
field_kwargs['help_text'] = 'Name of this %s.' % smart_text(opts.verbose_name)
elif field_name == 'description':
field_kwargs['help_text'] = 'Optional description of this %s.' % smart_text(opts.verbose_name)
elif field_name == 'type':
field_kwargs['help_text'] = 'Data type for this %s.' % smart_text(opts.verbose_name)
elif field_name == 'url':
field_kwargs['help_text'] = 'URL for this %s.' % smart_text(opts.verbose_name)
elif field_name == 'related':
field_kwargs['help_text'] = 'Data structure with URLs of related resources.'
elif field_name == 'summary_fields':
field_kwargs['help_text'] = 'Data structure with name/description for related resources.'
elif field_name == 'created':
field_kwargs['help_text'] = 'Timestamp when this %s was created.' % smart_text(opts.verbose_name)
elif field_name == 'modified':
field_kwargs['help_text'] = 'Timestamp when this %s was last modified.' % smart_text(opts.verbose_name)
# Pass model field default onto the serializer field if field is not read-only. # Pass model field default onto the serializer field if field is not read-only.
if model_field.has_default() and not field_kwargs.get('read_only', False): if model_field.has_default() and not field_kwargs.get('read_only', False):
field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default() field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default()
@@ -414,6 +392,7 @@ class BaseSerializer(serializers.ModelSerializer):
# Update the message used for the unique validator to use capitalized # Update the message used for the unique validator to use capitalized
# verbose name; keeps unique message the same as with DRF 2.x. # verbose name; keeps unique message the same as with DRF 2.x.
opts = self.Meta.model._meta.concrete_model._meta
for validator in field_kwargs.get('validators', []): for validator in field_kwargs.get('validators', []):
if isinstance(validator, validators.UniqueValidator): if isinstance(validator, validators.UniqueValidator):
unique_error_message = model_field.error_messages.get('unique', None) unique_error_message = model_field.error_messages.get('unique', None)
@@ -1662,6 +1641,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
d['can_copy'] = False d['can_copy'] = False
d['can_edit'] = False d['can_edit'] = False
d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.order_by('-created')[:10]] d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.order_by('-created')[:10]]
d['labels'] = [{'id': x.id, 'name': x.name} for x in obj.labels.all().order_by('-name')[:10]]
return d return d
def validate(self, attrs): def validate(self, attrs):
@@ -1703,6 +1683,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
res['relaunch'] = reverse('api:job_relaunch', args=(obj.pk,)) res['relaunch'] = reverse('api:job_relaunch', args=(obj.pk,))
return res return res
def get_summary_fields(self, obj):
d = super(JobSerializer, self).get_summary_fields(obj)
d['labels'] = [{'id': x.id, 'name': x.name} for x in obj.labels.all().order_by('-name')[:10]]
return d
def to_internal_value(self, data): def to_internal_value(self, data):
# When creating a new job and a job template is specified, populate any # When creating a new job and a job template is specified, populate any
# fields not provided in data from the job template. # fields not provided in data from the job template.

View File

@@ -1,6 +1,6 @@
{% for fn, fm in serializer_fields.items %}{% spaceless %} {% for fn, fm in serializer_fields.items %}{% spaceless %}
{% if not write_only or not fm.read_only %} {% if not write_only or not fm.read_only %}
* `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if write_only and fm.required %}, required{% endif %}{% if write_only and fm.read_only %}, read-only{% endif %}{% if write_only and not fm.choices and not fm.required %}, default=`{% if fm.type == "string" or fm.type == "email" %}"{% firstof fm.default "" %}"{% else %}{{ fm.default }}{% endif %}`{% endif %}){% if fm.choices %}{% for c in fm.choices %} * `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if write_only and fm.required %}, required{% endif %}{% if write_only and fm.read_only %}, read-only{% endif %}{% if write_only and not fm.choices and not fm.required %}, default=`{% if fm.type == "string" or fm.type == "email" %}"{% firstof fm.default "" %}"{% else %}{% if fm.type == "field" and not fm.default %}None{% else %}{{ fm.default }}{% endif %}{% endif %}`{% endif %}){% if fm.choices %}{% for c in fm.choices %}
- `{% if c.0 == "" %}""{% else %}{{ c.0 }}{% endif %}`{% if c.1 != c.0 %}: {{ c.1 }}{% endif %}{% if write_only and c.0 == fm.default %} (default){% endif %}{% endfor %}{% endif %}{% endif %} - `{% if c.0 == "" %}""{% else %}{{ c.0 }}{% endif %}`{% if c.1 != c.0 %}: {{ c.1 }}{% endif %}{% if write_only and c.0 == fm.default %} (default){% endif %}{% endfor %}{% endif %}{% endif %}
{% endspaceless %} {% endspaceless %}
{% endfor %} {% endfor %}

View File

@@ -134,6 +134,7 @@ class ApiV1RootView(APIView):
data['roles'] = reverse('api:role_list') data['roles'] = reverse('api:role_list')
data['notifiers'] = reverse('api:notifier_list') data['notifiers'] = reverse('api:notifier_list')
data['notifications'] = reverse('api:notification_list') data['notifications'] = reverse('api:notification_list')
data['labels'] = reverse('api:label_list')
data['unified_job_templates'] = reverse('api:unified_job_template_list') data['unified_job_templates'] = reverse('api:unified_job_template_list')
data['unified_jobs'] = reverse('api:unified_job_list') data['unified_jobs'] = reverse('api:unified_job_list')
data['activity_stream'] = reverse('api:activity_stream_list') data['activity_stream'] = reverse('api:activity_stream_list')

View File

@@ -1054,12 +1054,16 @@ class UnifiedJobTemplateAccess(BaseAccess):
'last_job', 'last_job',
'current_job', 'current_job',
) )
qs = qs.prefetch_related(
#'project', # WISH - sure would be nice if the following worked, but it does not.
'inventory', # In the future, as django and polymorphic libs are upgraded, try again.
'credential',
'cloud_credential', #qs = qs.prefetch_related(
) # 'project',
# 'inventory',
# 'credential',
# 'cloud_credential',
#)
return qs.all() return qs.all()
@@ -1089,20 +1093,26 @@ class UnifiedJobAccess(BaseAccess):
) )
qs = qs.prefetch_related( qs = qs.prefetch_related(
'unified_job_template', 'unified_job_template',
'project',
'inventory',
'credential',
'job_template',
'inventory_source',
'cloud_credential',
'project___credential',
'inventory_source___credential',
'inventory_source___inventory',
'job_template__inventory',
'job_template__project',
'job_template__credential',
'job_template__cloud_credential',
) )
# WISH - sure would be nice if the following worked, but it does not.
# In the future, as django and polymorphic libs are upgraded, try again.
#qs = qs.prefetch_related(
# 'project',
# 'inventory',
# 'credential',
# 'job_template',
# 'inventory_source',
# 'cloud_credential',
# 'project___credential',
# 'inventory_source___credential',
# 'inventory_source___inventory',
# 'job_template__inventory',
# 'job_template__project',
# 'job_template__credential',
# 'job_template__cloud_credential',
#)
return qs.all() return qs.all()
class ScheduleAccess(BaseAccess): class ScheduleAccess(BaseAccess):

View File

@@ -1,5 +1,5 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack', 'openstack_v3') CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack')
SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom',) SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom',)

View File

@@ -381,7 +381,7 @@ class Migration(migrations.Migration):
name='AdHocCommand', name='AdHocCommand',
fields=[ fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')), ('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check'), (b'scan', 'Scan')])), ('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check')])),
('limit', models.CharField(default=b'', max_length=1024, blank=True)), ('limit', models.CharField(default=b'', max_length=1024, blank=True)),
('module_name', models.CharField(default=b'', max_length=1024, blank=True)), ('module_name', models.CharField(default=b'', max_length=1024, blank=True)),
('module_args', models.TextField(default=b'', blank=True)), ('module_args', models.TextField(default=b'', blank=True)),

View File

@@ -36,7 +36,7 @@ class AdHocCommand(UnifiedJob):
job_type = models.CharField( job_type = models.CharField(
max_length=64, max_length=64,
choices=JOB_TYPE_CHOICES, choices=AD_HOC_JOB_TYPE_CHOICES,
default='run', default='run',
) )
inventory = models.ForeignKey( inventory = models.ForeignKey(

View File

@@ -29,7 +29,7 @@ __all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN',
'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES', 'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES',
'PERMISSION_TYPE_CHOICES', 'CLOUD_INVENTORY_SOURCES', 'AD_HOC_JOB_TYPE_CHOICES', 'PERMISSION_TYPE_CHOICES', 'CLOUD_INVENTORY_SOURCES',
'VERBOSITY_CHOICES'] 'VERBOSITY_CHOICES']
PERM_INVENTORY_ADMIN = 'admin' PERM_INVENTORY_ADMIN = 'admin'
@@ -46,6 +46,11 @@ JOB_TYPE_CHOICES = [
(PERM_INVENTORY_SCAN, _('Scan')), (PERM_INVENTORY_SCAN, _('Scan')),
] ]
AD_HOC_JOB_TYPE_CHOICES = [
(PERM_INVENTORY_DEPLOY, _('Run')),
(PERM_INVENTORY_CHECK, _('Check')),
]
PERMISSION_TYPE_CHOICES = [ PERMISSION_TYPE_CHOICES = [
(PERM_INVENTORY_READ, _('Read Inventory')), (PERM_INVENTORY_READ, _('Read Inventory')),
(PERM_INVENTORY_WRITE, _('Edit Inventory')), (PERM_INVENTORY_WRITE, _('Edit Inventory')),
@@ -56,7 +61,7 @@ PERMISSION_TYPE_CHOICES = [
(PERM_JOBTEMPLATE_CREATE, _('Create a Job Template')), (PERM_JOBTEMPLATE_CREATE, _('Create a Job Template')),
] ]
CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'openstack_v3', 'custom'] CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'custom']
VERBOSITY_CHOICES = [ VERBOSITY_CHOICES = [
(0, '0 (Normal)'), (0, '0 (Normal)'),

View File

@@ -40,7 +40,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
('gce', _('Google Compute Engine')), ('gce', _('Google Compute Engine')),
('azure', _('Microsoft Azure')), ('azure', _('Microsoft Azure')),
('openstack', _('OpenStack')), ('openstack', _('OpenStack')),
('openstack_v3', _('OpenStack V3')),
] ]
BECOME_METHOD_CHOICES = [ BECOME_METHOD_CHOICES = [
@@ -237,18 +236,12 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
host = self.host or '' host = self.host or ''
if not host and self.kind == 'vmware': if not host and self.kind == 'vmware':
raise ValidationError('Host required for VMware credential.') raise ValidationError('Host required for VMware credential.')
if not host and self.kind in ('openstack', 'openstack_v3'): if not host and self.kind == 'openstack':
raise ValidationError('Host required for OpenStack credential.') raise ValidationError('Host required for OpenStack credential.')
return host return host
def clean_domain(self): def clean_domain(self):
"""For case of Keystone v3 identity service that requires a return self.domain or ''
`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): def clean_username(self):
username = self.username or '' username = self.username or ''
@@ -259,7 +252,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
'credential.') 'credential.')
if not username and self.kind == 'vmware': if not username and self.kind == 'vmware':
raise ValidationError('Username required for VMware credential.') raise ValidationError('Username required for VMware credential.')
if not username and self.kind in ('openstack', 'openstack_v3'): if not username and self.kind == 'openstack':
raise ValidationError('Username required for OpenStack credential.') raise ValidationError('Username required for OpenStack credential.')
return username return username
@@ -271,13 +264,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
raise ValidationError('API key required for Rackspace credential.') raise ValidationError('API key required for Rackspace credential.')
if not password and self.kind == 'vmware': if not password and self.kind == 'vmware':
raise ValidationError('Password required for VMware credential.') raise ValidationError('Password required for VMware credential.')
if not password and self.kind in ('openstack', 'openstack_v3'): if not password and self.kind == 'openstack':
raise ValidationError('Password or API key required for OpenStack credential.') raise ValidationError('Password or API key required for OpenStack credential.')
return password return password
def clean_project(self): def clean_project(self):
project = self.project or '' project = self.project or ''
if self.kind in ('openstack', 'openstack_v3') and not project: if self.kind == 'openstack' and not project:
raise ValidationError('Project name required for OpenStack credential.') raise ValidationError('Project name required for OpenStack credential.')
return project return project

View File

@@ -733,7 +733,6 @@ class InventorySourceOptions(BaseModel):
('azure', _('Microsoft Azure')), ('azure', _('Microsoft Azure')),
('vmware', _('VMware vCenter')), ('vmware', _('VMware vCenter')),
('openstack', _('OpenStack')), ('openstack', _('OpenStack')),
('openstack_v3', _('OpenStack V3')),
('custom', _('Custom Script')), ('custom', _('Custom Script')),
] ]
@@ -962,11 +961,6 @@ class InventorySourceOptions(BaseModel):
"""I don't think openstack has regions""" """I don't think openstack has regions"""
return [('all', 'All')] 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): def clean_credential(self):
if not self.source: if not self.source:
return None return None

View File

@@ -378,6 +378,10 @@ class BaseTask(Task):
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported: if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
raise RuntimeError(OPENSSH_KEY_ERROR) raise RuntimeError(OPENSSH_KEY_ERROR)
for name, data in private_data.iteritems(): for name, data in private_data.iteritems():
# OpenSSH formatted keys must have a trailing newline to be
# accepted by ssh-add.
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
data += '\n'
# For credentials used with ssh-add, write to a named pipe which # For credentials used with ssh-add, write to a named pipe which
# will be read then closed, instead of leaving the SSH key on disk. # will be read then closed, instead of leaving the SSH key on disk.
if name in ('credential', 'scm_credential', 'ad_hoc_credential') and not ssh_too_old: if name in ('credential', 'scm_credential', 'ad_hoc_credential') and not ssh_too_old:
@@ -695,7 +699,7 @@ class RunJob(BaseTask):
if credential.ssh_key_data not in (None, ''): if credential.ssh_key_data not in (None, ''):
private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or '' private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or ''
if job.cloud_credential and job.cloud_credential.kind in ('openstack', 'openstack_v3'): if job.cloud_credential and job.cloud_credential.kind == 'openstack':
credential = job.cloud_credential credential = job.cloud_credential
openstack_auth = dict(auth_url=credential.host, openstack_auth = dict(auth_url=credential.host,
username=credential.username, username=credential.username,
@@ -787,7 +791,7 @@ class RunJob(BaseTask):
env['VMWARE_USER'] = cloud_cred.username env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_HOST'] = cloud_cred.host env['VMWARE_HOST'] = cloud_cred.host
elif cloud_cred and cloud_cred.kind in ('openstack', 'openstack_v3'): elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '') env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
# Set environment variables related to scan jobs # Set environment variables related to scan jobs
@@ -1136,7 +1140,7 @@ class RunInventoryUpdate(BaseTask):
credential = inventory_update.credential credential = inventory_update.credential
return dict(cloud_credential=decrypt_field(credential, 'ssh_key_data')) return dict(cloud_credential=decrypt_field(credential, 'ssh_key_data'))
if inventory_update.source in ('openstack', 'openstack_v3'): if inventory_update.source == 'openstack':
credential = inventory_update.credential credential = inventory_update.credential
openstack_auth = dict(auth_url=credential.host, openstack_auth = dict(auth_url=credential.host,
username=credential.username, username=credential.username,
@@ -1291,7 +1295,7 @@ class RunInventoryUpdate(BaseTask):
env['GCE_PROJECT'] = passwords.get('source_project', '') env['GCE_PROJECT'] = passwords.get('source_project', '')
env['GCE_PEM_FILE_PATH'] = cloud_credential env['GCE_PEM_FILE_PATH'] = cloud_credential
env['GCE_ZONE'] = inventory_update.source_regions env['GCE_ZONE'] = inventory_update.source_regions
elif inventory_update.source in ('openstack', 'openstack_v3'): elif inventory_update.source == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = cloud_credential env['OS_CLIENT_CONFIG_FILE'] = cloud_credential
elif inventory_update.source == 'file': elif inventory_update.source == 'file':
# FIXME: Parse source_env to dict, update env. # FIXME: Parse source_env to dict, update env.
@@ -1334,11 +1338,6 @@ class RunInventoryUpdate(BaseTask):
# to a shorter variable. :) # to a shorter variable. :)
src = inventory_update.source 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 # Get the path to the inventory plugin, and append it to our
# arguments. # arguments.
plugin_path = self.get_path_to('..', 'plugins', 'inventory', plugin_path = self.get_path_to('..', 'plugins', 'inventory',

View File

@@ -84,8 +84,7 @@ HPUhg3adAmIJ9z9u/VmTErbVklcKWlyZuTUkxeQ/BJmSIRUQAAAIEA3oKAzdDURjy8zxLX
gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd
hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY
h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH
-----END OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----'''
'''
TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY----- TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc
@@ -114,8 +113,7 @@ C6Oxl1Wsp3gPkK2yiuy8qcrvoEoJ25TeEhUGEAPWx2OuQJO/Lpq9aF/JJoqGwnBaXdCsi+
5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM 5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM
YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR
Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc= Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc=
-----END OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----'''
'''
TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE----- TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93 MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93

View File

@@ -128,6 +128,7 @@ def test_two_organizations(resourced_organization, organizations, user, get):
'teams': 0 'teams': 0
} }
@pytest.mark.skip(reason="resolution planned for after RBAC merge")
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.skipif("True") # XXX: This needs to be implemented @pytest.mark.skipif("True") # XXX: This needs to be implemented
def test_JT_associated_with_project(organizations, project, user, get): def test_JT_associated_with_project(organizations, project, user, get):

View File

@@ -1970,7 +1970,7 @@ class InventoryUpdatesTest(BaseTransactionTest):
self.check_inventory_source(inventory_source) self.check_inventory_source(inventory_source)
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists()) self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
def test_update_from_openstack_v3(self): def test_update_from_openstack_with_domain(self):
# Check that update works with Keystone v3 identity service # Check that update works with Keystone v3 identity service
api_url = getattr(settings, 'TEST_OPENSTACK_HOST_V3', '') api_url = getattr(settings, 'TEST_OPENSTACK_HOST_V3', '')
api_user = getattr(settings, 'TEST_OPENSTACK_USER', '') api_user = getattr(settings, 'TEST_OPENSTACK_USER', '')
@@ -1978,15 +1978,15 @@ class InventoryUpdatesTest(BaseTransactionTest):
api_project = getattr(settings, 'TEST_OPENSTACK_PROJECT', '') api_project = getattr(settings, 'TEST_OPENSTACK_PROJECT', '')
api_domain = getattr(settings, 'TEST_OPENSTACK_DOMAIN', '') api_domain = getattr(settings, 'TEST_OPENSTACK_DOMAIN', '')
if not all([api_url, api_user, api_password, api_project, api_domain]): if not all([api_url, api_user, api_password, api_project, api_domain]):
self.skipTest("No test openstack v3 credentials defined") self.skipTest("No test openstack credentials defined with a domain")
self.create_test_license_file() self.create_test_license_file()
credential = Credential.objects.create(kind='openstack_v3', credential = Credential.objects.create(kind='openstack',
host=api_url, host=api_url,
username=api_user, username=api_user,
password=api_password, password=api_password,
project=api_project, project=api_project,
domain=api_domain) domain=api_domain)
inventory_source = self.update_inventory_source(self.group, source='openstack_v3', credential=credential) inventory_source = self.update_inventory_source(self.group, source='openstack', credential=credential)
self.check_inventory_source(inventory_source) self.check_inventory_source(inventory_source)
self.assertFalse(self.group.all_hosts.filter(instance_id='').exists()) self.assertFalse(self.group.all_hosts.filter(instance_id='').exists())
@@ -2034,27 +2034,3 @@ class InventoryCredentialTest(BaseTest):
self.assertIn('password', response) self.assertIn('password', response)
self.assertIn('host', response) self.assertIn('host', response)
self.assertIn('project', 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)

View File

@@ -43,11 +43,13 @@ $(function() {
$('.description').addClass('prettyprint').parent().css('float', 'none'); $('.description').addClass('prettyprint').parent().css('float', 'none');
$('.hidden a.hide-description').prependTo('.description'); $('.hidden a.hide-description').prependTo('.description');
$('a.hide-description').click(function() { $('a.hide-description').click(function() {
$(this).tooltip('hide');
$('.description').slideUp('fast'); $('.description').slideUp('fast');
return false; return false;
}); });
$('.hidden a.toggle-description').appendTo('.page-header h1'); $('.hidden a.toggle-description').appendTo('.page-header h1');
$('a.toggle-description').click(function() { $('a.toggle-description').click(function() {
$(this).tooltip('hide');
$('.description').slideToggle('fast'); $('.description').slideToggle('fast');
return false; return false;
}); });
@@ -68,6 +70,7 @@ $(function() {
}); });
$('a.resize').click(function() { $('a.resize').click(function() {
$(this).tooltip('hide');
if ($(this).find('span.glyphicon-resize-full').size()) { if ($(this).find('span.glyphicon-resize-full').size()) {
$(this).find('span.glyphicon').addClass('glyphicon-resize-small').removeClass('glyphicon-resize-full'); $(this).find('span.glyphicon').addClass('glyphicon-resize-small').removeClass('glyphicon-resize-full');
$('.container').addClass('container-fluid').removeClass('container'); $('.container').addClass('container-fluid').removeClass('container');

View File

@@ -26,6 +26,7 @@ import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Cr
import {JobsListController} from './controllers/Jobs'; import {JobsListController} from './controllers/Jobs';
import {PortalController} from './controllers/Portal'; import {PortalController} from './controllers/Portal';
import systemTracking from './system-tracking/main'; import systemTracking from './system-tracking/main';
import inventories from './inventories/main';
import inventoryScripts from './inventory-scripts/main'; import inventoryScripts from './inventory-scripts/main';
import organizations from './organizations/main'; import organizations from './organizations/main';
import permissions from './permissions/main'; import permissions from './permissions/main';
@@ -55,7 +56,7 @@ import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import OrganizationsList from './organizations/list/organizations-list.controller'; import OrganizationsList from './organizations/list/organizations-list.controller';
import OrganizationsAdd from './organizations/add/organizations-add.controller'; import OrganizationsAdd from './organizations/add/organizations-add.controller';
import OrganizationsEdit from './organizations/edit/organizations-edit.controller'; import OrganizationsEdit from './organizations/edit/organizations-edit.controller';
import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from './controllers/Inventories'; import {InventoriesAdd, InventoriesEdit, InventoriesList, InventoriesManage} from './inventories/main';
import {AdminsList} from './controllers/Admins'; import {AdminsList} from './controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; import {UsersList, UsersAdd, UsersEdit} from './controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams'; import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams';
@@ -88,6 +89,7 @@ var tower = angular.module('Tower', [
RestServices.name, RestServices.name,
browserData.name, browserData.name,
systemTracking.name, systemTracking.name,
inventories.name,
inventoryScripts.name, inventoryScripts.name,
organizations.name, organizations.name,
permissions.name, permissions.name,
@@ -182,7 +184,6 @@ var tower = angular.module('Tower', [
'LogViewerStatusDefinition', 'LogViewerStatusDefinition',
'StandardOutHelper', 'StandardOutHelper',
'LogViewerOptionsDefinition', 'LogViewerOptionsDefinition',
'EventViewerHelper',
'JobDetailHelper', 'JobDetailHelper',
'SocketIO', 'SocketIO',
'lrInfiniteScroll', 'lrInfiniteScroll',
@@ -213,6 +214,8 @@ var tower = angular.module('Tower', [
templateUrl: urlPrefix + 'partials/breadcrumb.html' templateUrl: urlPrefix + 'partials/breadcrumb.html'
}); });
// route to the details pane of /job/:id/host-event/:eventId if no other child specified
$urlRouterProvider.when('/jobs/*/host-event/*', '/jobs/*/host-event/*/details')
// $urlRouterProvider.otherwise("/home"); // $urlRouterProvider.otherwise("/home");
$urlRouterProvider.otherwise(function($injector){ $urlRouterProvider.otherwise(function($injector){
var $state = $injector.get("$state"); var $state = $injector.get("$state");
@@ -370,69 +373,6 @@ var tower = angular.module('Tower', [
} }
}). }).
state('inventories', {
url: '/inventories',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesList,
data: {
activityStream: true,
activityStreamTarget: 'inventory'
},
ncyBreadcrumb: {
label: "INVENTORIES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventories.add', {
url: '/add',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesAdd,
ncyBreadcrumb: {
parent: "inventories",
label: "CREATE INVENTORY"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventories.edit', {
url: '/:inventory_id',
templateUrl: urlPrefix + 'partials/inventories.html',
controller: InventoriesEdit,
data: {
activityStreamId: 'inventory_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('inventoryManage', {
url: '/inventories/:inventory_id/manage?groups',
templateUrl: urlPrefix + 'partials/inventory-manage.html',
controller: InventoriesManage,
data: {
activityStream: true,
activityStreamTarget: 'inventory',
activityStreamId: 'inventory_id'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('organizationAdmins', { state('organizationAdmins', {
url: '/organizations/:organization_id/admins', url: '/organizations/:organization_id/admins',
templateUrl: urlPrefix + 'partials/organizations.html', templateUrl: urlPrefix + 'partials/organizations.html',

File diff suppressed because it is too large Load Diff

View File

@@ -169,7 +169,7 @@ export default
"host": { "host": {
labelBind: 'hostLabel', labelBind: 'hostLabel',
type: 'text', type: 'text',
ngShow: "kind.value == 'vmware' || kind.value == 'openstack' || kind.value === 'openstack_v3'", ngShow: "kind.value == 'vmware' || kind.value == 'openstack'",
awPopOverWatch: "hostPopOver", awPopOverWatch: "hostPopOver",
awPopOver: "set in helpers/credentials", awPopOver: "set in helpers/credentials",
dataTitle: 'Host', dataTitle: 'Host',
@@ -243,7 +243,7 @@ export default
"password": { "password": {
labelBind: 'passwordLabel', labelBind: 'passwordLabel',
type: 'sensitive', type: 'sensitive',
ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack' || kind.value == 'openstack_v3'", ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'",
addRequired: false, addRequired: false,
editRequired: false, editRequired: false,
ask: false, ask: false,
@@ -338,10 +338,10 @@ export default
"project": { "project": {
labelBind: 'projectLabel', labelBind: 'projectLabel',
type: 'text', type: 'text',
ngShow: "kind.value == 'gce' || kind.value == 'openstack' || kind.value == 'openstack_v3'", ngShow: "kind.value == 'gce' || kind.value == 'openstack'",
awPopOverWatch: "projectPopOver", awPopOverWatch: "projectPopOver",
awPopOver: "set in helpers/credentials", awPopOver: "set in helpers/credentials",
dataTitle: 'Project ID', dataTitle: 'Project Name',
dataPlacement: 'right', dataPlacement: 'right',
dataContainer: "body", dataContainer: "body",
addRequired: false, addRequired: false,
@@ -355,18 +355,17 @@ export default
"domain": { "domain": {
labelBind: 'domainLabel', labelBind: 'domainLabel',
type: 'text', type: 'text',
ngShow: "kind.value == 'openstack_v3'", ngShow: "kind.value == 'openstack'",
awPopOverWatch: "domainPopOver", awPopOver: "<p>OpenStack domains define administrative " +
awPopOver: "set in helpers/credentials", "boundaries. It is only needed for Keystone v3 authentication URLs. " +
"Common scenarios include:<ul><li><b>v2 URLs</b> - leave blank</li>" +
"<li><b>v3 default</b> - set to 'default'</br></li>" +
"<li><b>v3 multi-domain</b> - your domain name</p></li></ul></p>",
dataTitle: 'Domain Name', dataTitle: 'Domain Name',
dataPlacement: 'right', dataPlacement: 'right',
dataContainer: "body", dataContainer: "body",
addRequired: false, addRequired: false,
editRequired: false, editRequired: false,
awRequiredWhen: {
variable: 'domain_required',
init: false
},
subForm: 'credentialSubForm' subForm: 'credentialSubForm'
}, },
"vault_password": { "vault_password": {

View File

@@ -169,8 +169,7 @@ export default
label: 'Source Variables', //"{{vars_label}}" , label: 'Source Variables', //"{{vars_label}}" ,
ngShow: "source && (source.value == 'vmware' || " + ngShow: "source && (source.value == 'vmware' || " +
"source.value == 'openstack' || " + "source.value == 'openstack')",
"source.value == 'openstack_v3')",
type: 'textarea', type: 'textarea',
addRequired: false, addRequired: false,
class: 'Form-textAreaLabel', class: 'Form-textAreaLabel',

View File

@@ -9,7 +9,6 @@ import './lists';
import Children from "./helpers/Children"; import Children from "./helpers/Children";
import Credentials from "./helpers/Credentials"; import Credentials from "./helpers/Credentials";
import EventViewer from "./helpers/EventViewer";
import Events from "./helpers/Events"; import Events from "./helpers/Events";
import Groups from "./helpers/Groups"; import Groups from "./helpers/Groups";
import Hosts from "./helpers/Hosts"; import Hosts from "./helpers/Hosts";
@@ -42,7 +41,6 @@ import ActivityStreamHelper from "./helpers/ActivityStream";
export export
{ Children, { Children,
Credentials, Credentials,
EventViewer,
Events, Events,
Groups, Groups,
Hosts, Hosts,

View File

@@ -74,7 +74,6 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.project_required = false; scope.project_required = false;
scope.passwordLabel = 'Password (API Key)'; scope.passwordLabel = 'Password (API Key)';
scope.projectPopOver = "<p>The project value</p>"; scope.projectPopOver = "<p>The project value</p>";
scope.domainPopOver = "<p>The domain name</p>";
scope.hostPopOver = "<p>The host value</p>"; scope.hostPopOver = "<p>The host value</p>";
if (!Empty(scope.kind)) { if (!Empty(scope.kind)) {
@@ -126,32 +125,17 @@ angular.module('CredentialsHelper', ['Utilities'])
break; break;
case 'openstack': case 'openstack':
scope.hostLabel = "Host (Authentication URL)"; scope.hostLabel = "Host (Authentication URL)";
scope.projectLabel = "Project (Tenet Name/ID)"; scope.projectLabel = "Project (Tenant Name)";
scope.password_required = true;
scope.project_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/v2.0/";
case 'openstack_v3':
scope.hostLabel = "Host (Authentication URL)";
scope.projectLabel = "Project (Tenet Name/ID)";
scope.domainLabel = "Domain Name"; scope.domainLabel = "Domain Name";
scope.password_required = true; scope.password_required = true;
scope.project_required = true; scope.project_required = true;
scope.domain_required = true;
scope.host_required = true; scope.host_required = true;
scope.username_required = true; scope.username_required = true;
scope.projectPopOver = "<p>This is the tenant name " + scope.projectPopOver = "<p>This is the tenant name. " +
"or tenant id. This value is usually the same " + " This value is usually the same " +
" as the username.</p>"; " as the username.</p>";
scope.hostPopOver = "<p>The host to authenticate with." + scope.hostPopOver = "<p>The host to authenticate with." +
"<br />For example, https://openstack.business.com/v3</p>"; "<br />For example, https://openstack.business.com/v2.0/";
scope.domainPopOver = "<p>Domain used for Keystone v3 " +
"<br />identity service.</p>";
break; break;
} }
} }

View File

@@ -1,568 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:EventViewer
* @description eventviewerhelper
*/
export default
angular.module('EventViewerHelper', ['ModalDialog', 'Utilities', 'EventsViewerFormDefinition', 'HostsHelper'])
.factory('EventViewer', ['$compile', 'CreateDialog', 'GetEvent', 'Wait', 'EventAddTable', 'GetBasePath', 'Empty', 'EventAddPreFormattedText',
function($compile, CreateDialog, GetEvent, Wait, EventAddTable, GetBasePath, Empty, EventAddPreFormattedText) {
return function(params) {
var parent_scope = params.scope,
url = params.url,
event_id = params.event_id,
parent_id = params.parent_id,
title = params.title, //optional
scope = parent_scope.$new(true),
index = params.index,
page,
current_event;
if (scope.removeShowNextEvent) {
scope.removeShowNextEvent();
}
scope.removeShowNextEvent = scope.$on('ShowNextEvent', function(e, data, show_event) {
scope.events = data;
$('#event-next-spinner').slideUp(200);
if (show_event === 'prev') {
showEvent(scope.events.length - 1);
}
else if (show_event === 'next') {
showEvent(0);
}
});
// show scope.events[idx]
function showEvent(idx) {
var show_tabs = false, elem, data;
if (idx > scope.events.length - 1) {
GetEvent({
scope: scope,
url: scope.next_event_set,
show_event: 'next'
});
return;
}
if (idx < 0) {
GetEvent({
scope: scope,
url: scope.prev_event_set,
show_event: 'prev'
});
return;
}
data = scope.events[idx];
current_event = idx;
$('#status-form-container').empty();
$('#results-form-container').empty();
$('#timing-form-container').empty();
$('#stdout-form-container').empty();
$('#stderr-form-container').empty();
$('#traceback-form-container').empty();
$('#json-form-container').empty();
$('#eventview-tabs li:eq(1)').hide();
$('#eventview-tabs li:eq(2)').hide();
$('#eventview-tabs li:eq(3)').hide();
$('#eventview-tabs li:eq(4)').hide();
$('#eventview-tabs li:eq(5)').hide();
$('#eventview-tabs li:eq(6)').hide();
EventAddTable({ scope: scope, id: 'status-form-container', event: data, section: 'Event' });
if (EventAddTable({ scope: scope, id: 'results-form-container', event: data, section: 'Results'})) {
show_tabs = true;
$('#eventview-tabs li:eq(1)').show();
}
if (EventAddTable({ scope: scope, id: 'timing-form-container', event: data, section: 'Timing' })) {
show_tabs = true;
$('#eventview-tabs li:eq(2)').show();
}
if (data.stdout) {
show_tabs = true;
$('#eventview-tabs li:eq(3)').show();
EventAddPreFormattedText({
id: 'stdout-form-container',
val: data.stdout
});
}
if (data.stderr) {
show_tabs = true;
$('#eventview-tabs li:eq(4)').show();
EventAddPreFormattedText({
id: 'stderr-form-container',
val: data.stderr
});
}
if (data.traceback) {
show_tabs = true;
$('#eventview-tabs li:eq(5)').show();
EventAddPreFormattedText({
id: 'traceback-form-container',
val: data.traceback
});
}
show_tabs = true;
$('#eventview-tabs li:eq(6)').show();
EventAddPreFormattedText({
id: 'json-form-container',
val: JSON.stringify(data, null, 2)
});
if (!show_tabs) {
$('#eventview-tabs').hide();
}
elem = angular.element(document.getElementById('eventviewer-modal-dialog'));
$compile(elem)(scope);
}
function setButtonMargin() {
var width = ($('.ui-dialog[aria-describedby="eventviewer-modal-dialog"] .ui-dialog-buttonpane').innerWidth() / 2) - $('#events-next-button').outerWidth() - 73;
$('#events-next-button').css({'margin-right': width + 'px'});
}
function addSpinner() {
var position;
if ($('#event-next-spinner').length > 0) {
$('#event-next-spinner').remove();
}
position = $('#events-next-button').position();
$('#events-next-button').after('<i class="fa fa-cog fa-spin" id="event-next-spinner" style="display:none; position:absolute; top:' + (position.top + 15) + 'px; left:' + (position.left + 75) + 'px;"></i>');
}
if (scope.removeModalReady) {
scope.removeModalReady();
}
scope.removeModalReady = scope.$on('ModalReady', function() {
Wait('stop');
$('#eventviewer-modal-dialog').dialog('open');
});
if (scope.removeJobReady) {
scope.removeJobReady();
}
scope.removeEventReady = scope.$on('EventReady', function(e, data) {
var btns;
scope.events = data;
if (event_id) {
// find and show the selected event
data.every(function(row, idx) {
if (parseInt(row.id,10) === parseInt(event_id,10)) {
current_event = idx;
return false;
}
return true;
});
}
else {
current_event = 0;
}
showEvent(current_event);
btns = [];
if (scope.events.length > 1) {
btns.push({
label: "Prev",
onClick: function () {
if (current_event - 1 === 0 && !scope.prev_event_set) {
$('#events-prev-button').prop('disabled', true);
}
if (current_event - 1 < scope.events.length - 1) {
$('#events-next-button').prop('disabled', false);
}
showEvent(current_event - 1);
},
icon: "fa-chevron-left",
"class": "btn btn-primary",
id: "events-prev-button"
});
btns.push({
label: "Next",
onClick: function() {
if (current_event + 1 > 0) {
$('#events-prev-button').prop('disabled', false);
}
if (current_event + 1 >= scope.events.length - 1 && !scope.next_event_set) {
$('#events-next-button').prop('disabled', true);
}
showEvent(current_event + 1);
},
icon: "fa-chevron-right",
"class": "btn btn-primary",
id: "events-next-button"
});
}
btns.push({
label: "OK",
onClick: function() {
scope.modalOK();
},
icon: "",
"class": "btn btn-primary",
id: "dialog-ok-button"
});
CreateDialog({
scope: scope,
width: 675,
height: 600,
minWidth: 450,
callback: 'ModalReady',
id: 'eventviewer-modal-dialog',
// onResizeStop: resizeText,
title: ( (title) ? title : 'Host Event' ),
buttons: btns,
closeOnEscape: true,
onResizeStop: function() {
setButtonMargin();
addSpinner();
},
onClose: function() {
try {
scope.$destroy();
}
catch(e) {
//ignore
}
},
onOpen: function() {
$('#eventview-tabs a:first').tab('show');
$('#dialog-ok-button').focus();
if (scope.events.length > 1 && current_event === 0 && !scope.prev_event_set) {
$('#events-prev-button').prop('disabled', true);
}
if ((current_event === scope.events.length - 1) && !scope.next_event_set) {
$('#events-next-button').prop('disabled', true);
}
if (scope.events.length > 1) {
setButtonMargin();
addSpinner();
}
}
});
});
page = (index) ? Math.ceil((index+1)/50) : 1;
url += (/\/$/.test(url)) ? '?' : '&';
url += (parent_id) ? 'page='+page +'&parent=' + parent_id + '&page_size=50&order=host_name,counter' : 'page_size=50&order=host_name,counter';
GetEvent({
url: url,
scope: scope
});
scope.modalOK = function() {
$('#eventviewer-modal-dialog').dialog('close');
scope.$destroy();
};
};
}])
.factory('GetEvent', ['Wait', 'Rest', 'ProcessErrors',
function(Wait, Rest, ProcessErrors) {
return function(params) {
var url = params.url,
scope = params.scope,
show_event = params.show_event,
results= [];
if (show_event) {
$('#event-next-spinner').show();
}
else {
Wait('start');
}
function getStatus(e) {
return (e.event === "runner_on_unreachable") ? "unreachable" : (e.event === "runner_on_skipped") ? 'skipped' : (e.failed) ? 'failed' :
(e.changed) ? 'changed' : 'ok';
}
Rest.setUrl(url);
Rest.get()
.success( function(data) {
if(jQuery.isEmptyObject(data)) {
Wait('stop');
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get event ' + url + '. ' });
}
else {
scope.next_event_set = data.next;
scope.prev_event_set = data.previous;
data.results.forEach(function(event) {
var msg, key, event_data = {};
if (event.event_data.res) {
if (typeof event.event_data.res !== 'object') {
// turn event_data.res into an object
msg = event.event_data.res;
event.event_data.res = {};
event.event_data.res.msg = msg;
}
for (key in event.event_data) {
if (key !== "res") {
event.event_data.res[key] = event.event_data[key];
}
}
if (event.event_data.res.ansible_facts) {
// don't show fact gathering results
event.event_data.res.task = "Gathering Facts";
delete event.event_data.res.ansible_facts;
}
event.event_data.res.status = getStatus(event);
event_data = event.event_data.res;
}
else {
event.event_data.status = getStatus(event);
event_data = event.event_data;
}
// convert results to stdout
if (event_data.results && typeof event_data.results === "object" && Array.isArray(event_data.results)) {
event_data.stdout = "";
event_data.results.forEach(function(row) {
event_data.stdout += row + "\n";
});
delete event_data.results;
}
if (event_data.invocation) {
for (key in event_data.invocation) {
event_data[key] = event_data.invocation[key];
}
delete event_data.invocation;
}
event_data.play = event.play;
if (event.task) {
event_data.task = event.task;
}
event_data.created = event.created;
event_data.role = event.role;
event_data.host_id = event.host;
event_data.host_name = event.host_name;
if (event_data.host) {
delete event_data.host;
}
event_data.id = event.id;
event_data.parent = event.parent;
event_data.event = (event.event_display) ? event.event_display : event.event;
results.push(event_data);
});
if (show_event) {
scope.$emit('ShowNextEvent', results, show_event);
}
else {
scope.$emit('EventReady', results);
}
} //else statement
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get event ' + url + '. GET returned: ' + status });
});
};
}])
.factory('EventAddTable', ['$compile', '$filter', 'Empty', 'EventsViewerForm', function($compile, $filter, Empty, EventsViewerForm) {
return function(params) {
var scope = params.scope,
id = params.id,
event = params.event,
section = params.section,
html = '', e;
function parseObject(obj) {
// parse nested JSON objects. a mini version of parseJSON without references to the event form object.
var i, key, html = '';
for (key in obj) {
if (typeof obj[key] === "boolean" || typeof obj[key] === "number" || typeof obj[key] === "string") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value\">" + obj[key] + "</td></tr>";
}
else if (typeof obj[key] === "object" && Array.isArray(obj[key])) {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value\">[";
for (i = 0; i < obj[key].length; i++) {
html += obj[key][i] + ",";
}
html = html.replace(/,$/,'');
html += "]</td></tr>\n";
}
else if (typeof obj[key] === "object") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"nested-table\"><table>\n<tbody>\n" + parseObject(obj[key]) + "</tbody>\n</table>\n</td></tr>\n";
}
}
return html;
}
function parseItem(itm, key, label) {
var i, html = '';
if (Empty(itm)) {
// exclude empty items
}
else if (typeof itm === "boolean" || typeof itm === "number" || typeof itm === "string") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">";
if (key === "status") {
html += "<i class=\"fa icon-job-" + itm + "\"></i> " + itm;
}
else if (key === "start" || key === "end" || key === "created") {
if (!/Z$/.test(itm)) {
itm = itm.replace(/\ /,'T') + 'Z';
html += $filter('longDate')(itm);
}
else {
html += $filter('longDate')(itm);
}
}
else if (key === "host_name" && event.host_id) {
html += "<a href=\"/#/home/hosts/?id=" + event.host_id + "\" target=\"_blank\" " +
"aw-tool-tip=\"View host. Opens in new tab or window.\" data-placement=\"top\" " +
">" + itm + "</a>";
}
else {
if( typeof itm === "string"){
if(itm.indexOf('<') > -1 || itm.indexOf('>') > -1){
itm = $filter('sanitize')(itm);
}
}
html += "<span ng-non-bindable>" + itm + "</span>";
}
html += "</td></tr>\n";
}
else if (typeof itm === "object" && Array.isArray(itm)) {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">[";
for (i = 0; i < itm.length; i++) {
html += itm[i] + ",";
}
html = html.replace(/,$/,'');
html += "]</td></tr>\n";
}
else if (typeof itm === "object") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"nested-table\"><table>\n<tbody>\n" + parseObject(itm) + "</tbody>\n</table>\n</td></tr>\n";
}
return html;
}
function parseJSON(obj) {
var h, html = '', key, keys, found = false, string_warnings = "", string_cmd = "";
if (typeof obj === "object") {
html += "<table class=\"table eventviewer-status\">\n";
html += "<tbody>\n";
keys = [];
for (key in EventsViewerForm.fields) {
if (EventsViewerForm.fields[key].section === section) {
keys.push(key);
}
}
keys.forEach(function(key) {
var h, label;
label = EventsViewerForm.fields[key].label;
h = parseItem(obj[key], key, label);
if (h) {
html += h;
found = true;
}
});
if (section === 'Results') {
// Add to result fields that might not be found in the form object.
for (key in obj) {
h = '';
if (key !== 'host_id' && key !== 'parent' && key !== 'event' && key !== 'src' && key !== 'md5sum' &&
key !== 'stdout' && key !== 'traceback' && key !== 'stderr' && key !== 'cmd' && key !=='changed' && key !== "verbose_override" &&
key !== 'feature_result' && key !== 'warnings') {
if (!EventsViewerForm.fields[key]) {
h = parseItem(obj[key], key, key);
if (h) {
html += h;
found = true;
}
}
} else if (key === 'cmd') {
// only show cmd if it's a cmd that was run
if (!EventsViewerForm.fields[key] && obj[key].length > 0) {
// include the label head Shell Command instead of CMD in the modal
if(typeof(obj[key]) === 'string'){
obj[key] = [obj[key]];
}
string_cmd += obj[key].join(" ");
h = parseItem(string_cmd, key, "Shell Command");
if (h) {
html += h;
found = true;
}
}
} else if (key === 'warnings') {
if (!EventsViewerForm.fields[key] && obj[key].length > 0) {
if(typeof(obj[key]) === 'string'){
obj[key] = [obj[key]];
}
string_warnings += obj[key].join(" ");
h = parseItem(string_warnings, key, "Warnings");
if (h) {
html += h;
found = true;
}
}
}
}
}
html += "</tbody>\n";
html += "</table>\n";
}
return (found) ? html : '';
}
html = parseJSON(event);
e = angular.element(document.getElementById(id));
e.empty();
if (html) {
e.html(html);
$compile(e)(scope);
}
return (html) ? true : false;
};
}])
.factory('EventAddTextarea', [ function() {
return function(params) {
var container_id = params.container_id,
val = params.val,
fld_id = params.fld_id,
html;
html = "<div class=\"form-group\">\n" +
"<textarea ng-non-bindable id=\"" + fld_id + "\" class=\"form-control mono-space\" rows=\"12\" readonly>" + val + "</textarea>" +
"</div>\n";
$('#' + container_id).empty().html(html);
};
}])
.factory('EventAddPreFormattedText', ['$filter', function($filter) {
return function(params) {
var id = params.id,
val = params.val,
html;
if( typeof val === "string"){
if(val.indexOf('<') > -1 || val.indexOf('>') > -1){
val = $filter('sanitize')(val);
}
}
html = "<pre ng-non-bindable>" + val + "</pre>\n";
$('#' + id).empty().html(html);
};
}]);

View File

@@ -305,8 +305,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
field_id: 'source_extra_vars', onReady: callback }); field_id: 'source_extra_vars', onReady: callback });
} }
if(scope.source.value==="vmware" || 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; scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars;
ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName, ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName,
field_id: 'source_inventory_variables', onReady: callback }); field_id: 'source_inventory_variables', onReady: callback });
@@ -316,8 +315,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
scope.source.value==='gce' || scope.source.value==='gce' ||
scope.source.value === 'azure' || scope.source.value === 'azure' ||
scope.source.value === 'vmware' || scope.source.value === 'vmware' ||
scope.source.value === 'openstack' || scope.source.value === 'openstack') {
scope.source.value === 'openstack_v3') {
if (scope.source.value === 'ec2') { if (scope.source.value === 'ec2') {
kind = 'aws'; kind = 'aws';
} else { } else {
@@ -926,8 +924,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName, ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: SourceForm.fields.source_vars.parseTypeName,
field_id: 'source_source_vars', onReady: waitStop }); field_id: 'source_source_vars', onReady: waitStop });
} else if (sources_scope.source && (sources_scope.source.value === 'vmware' || } 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'); Wait('start');
ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName, ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName,
field_id: 'source_inventory_variables', onReady: waitStop }); field_id: 'source_inventory_variables', onReady: waitStop });
@@ -1306,8 +1303,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
} }
if (sources_scope.source && (sources_scope.source.value === 'vmware' || 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); data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true);
} }

View File

@@ -437,10 +437,10 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
.factory('HostsEdit', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', .factory('HostsEdit', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm',
'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange', 'Wait', 'Find', 'SetStatus', 'ApplyEllipsis',
'ToJSON', 'ParseVariableString', 'CreateDialog', 'TextareaResize', 'ToJSON', 'ParseVariableString', 'CreateDialog', 'TextareaResize', 'ParamPass',
function($rootScope, $location, $log, $stateParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors, function($rootScope, $location, $log, $stateParams, Rest, Alert, HostForm, GenerateForm, Prompt, ProcessErrors,
GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, ToJSON, GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, ToJSON,
ParseVariableString, CreateDialog, TextareaResize) { ParseVariableString, CreateDialog, TextareaResize, ParamPass) {
return function(params) { return function(params) {
var parent_scope = params.host_scope, var parent_scope = params.host_scope,

View File

@@ -0,0 +1,95 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
$stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors,
ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
$state) {
ClearScope();
// Inject dynamic view
var defaultUrl = GetBasePath('inventory'),
form = InventoryForm(),
generator = GenerateForm;
form.formLabelSize = null;
form.formFieldSize = null;
generator.inject(form, { mode: 'add', related: false, scope: $scope });
generator.reset();
$scope.parseType = 'yaml';
ParseTypeChange({
scope: $scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'inventory_variables'
});
LookUpInit({
scope: $scope,
form: form,
current_item: ($stateParams.organization_id) ? $stateParams.organization_id : null,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
// Save
$scope.formSave = function () {
generator.clearApiErrors();
Wait('start');
try {
var fld, json_data, data;
json_data = ToJSON($scope.parseType, $scope.variables, true);
data = {};
for (fld in form.fields) {
if (form.fields[fld].realName) {
data[form.fields[fld].realName] = $scope[fld];
} else {
data[fld] = $scope[fld];
}
}
Rest.setUrl(defaultUrl);
Rest.post(data)
.success(function (data) {
var inventory_id = data.id;
Wait('stop');
$location.path('/inventories/' + inventory_id + '/manage');
})
.error(function (data, status) {
ProcessErrors( $scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to add new inventory. Post returned status: ' + status });
});
} catch (err) {
Wait('stop');
Alert("Error", "Error parsing inventory variables. Parser returned: " + err);
}
};
$scope.formCancel = function () {
$state.transitionTo('inventories');
};
}
export default['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state', InventoriesAdd]

View File

@@ -0,0 +1,24 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesAdd from './inventory-add.controller';
export default {
name: 'inventories.add',
route: '/add',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesAdd,
ncyBreadcrumb: {
parent: "inventories",
label: "CREATE INVENTORY"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};

View File

@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-add.route';
import controller from './inventory-add.controller';
export default
angular.module('inventoryAdd', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@@ -0,0 +1,329 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesEdit($scope, $rootScope, $compile, $location,
$log, $stateParams, InventoryForm, GenerateForm, Rest, Alert, ProcessErrors,
ReturnToCaller, ClearScope, generateList, OrganizationList, SearchInit,
PaginateInit, LookUpInit, GetBasePath, ParseTypeChange, Wait, ToJSON,
ParseVariableString, RelatedSearchInit, RelatedPaginateInit,
Prompt, PlaybookRun, CreateDialog, deleteJobTemplate, $state) {
ClearScope();
// Inject dynamic view
var defaultUrl = GetBasePath('inventory'),
form = InventoryForm(),
generator = GenerateForm,
inventory_id = $stateParams.inventory_id,
master = {},
fld, json_data, data,
relatedSets = {};
form.formLabelSize = null;
form.formFieldSize = null;
$scope.inventory_id = inventory_id;
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
// After the project is loaded, retrieve each related set
if ($scope.inventoryLoadedRemove) {
$scope.inventoryLoadedRemove();
}
$scope.projectLoadedRemove = $scope.$on('inventoryLoaded', function () {
var set;
for (set in relatedSets) {
$scope.search(relatedSets[set].iterator);
}
});
Wait('start');
Rest.setUrl(GetBasePath('inventory') + inventory_id + '/');
Rest.get()
.success(function (data) {
var fld;
for (fld in form.fields) {
if (fld === 'variables') {
$scope.variables = ParseVariableString(data.variables);
master.variables = $scope.variables;
} else if (fld === 'inventory_name') {
$scope[fld] = data.name;
master[fld] = $scope[fld];
} else if (fld === 'inventory_description') {
$scope[fld] = data.description;
master[fld] = $scope[fld];
} else if (data[fld]) {
$scope[fld] = data[fld];
master[fld] = $scope[fld];
}
if (form.fields[fld].sourceModel && data.summary_fields &&
data.summary_fields[form.fields[fld].sourceModel]) {
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
relatedSets = form.relatedSets(data.related);
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
RelatedSearchInit({
scope: $scope,
form: form,
relatedSets: relatedSets
});
RelatedPaginateInit({
scope: $scope,
relatedSets: relatedSets
});
Wait('stop');
$scope.parseType = 'yaml';
ParseTypeChange({
scope: $scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'inventory_variables'
});
LookUpInit({
scope: $scope,
form: form,
current_item: $scope.organization,
list: OrganizationList,
field: 'organization',
input_type: 'radio'
});
$scope.$emit('inventoryLoaded');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get inventory: ' + inventory_id + '. GET returned: ' + status });
});
// Save
$scope.formSave = function () {
Wait('start');
// Make sure we have valid variable data
json_data = ToJSON($scope.parseType, $scope.variables);
data = {};
for (fld in form.fields) {
if (form.fields[fld].realName) {
data[form.fields[fld].realName] = $scope[fld];
} else {
data[fld] = $scope[fld];
}
}
Rest.setUrl(defaultUrl + inventory_id + '/');
Rest.put(data)
.success(function () {
Wait('stop');
$location.path('/inventories/');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to update inventory. PUT returned status: ' + status });
});
};
$scope.manageInventory = function(){
$location.path($location.path() + '/manage');
};
$scope.formCancel = function () {
$state.transitionTo('inventories');
};
$scope.addScanJob = function(){
$location.path($location.path()+'/job_templates/add');
};
$scope.launchScanJob = function(){
PlaybookRun({ scope: $scope, id: this.scan_job_template.id });
};
$scope.scheduleScanJob = function(){
$location.path('/job_templates/'+this.scan_job_template.id+'/schedules');
};
$scope.editScanJob = function(){
$location.path($location.path()+'/job_templates/'+this.scan_job_template.id);
};
$scope.copyScanJobTemplate = function(){
var id = this.scan_job_template.id,
name = this.scan_job_template.name,
element,
buttons = [{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "copy-close-button"
},{
"label": "Copy",
"onClick": function() {
copyAction();
},
"icon": "fa-copy",
"class": "btn btn-primary",
"id": "job-copy-button"
}],
copyAction = function () {
// retrieve the copy of the job template object from the api, then overwrite the name and throw away the id
Wait('start');
var url = GetBasePath('job_templates')+id;
Rest.setUrl(url);
Rest.get()
.success(function (data) {
data.name = $scope.new_copy_name;
delete data.id;
$scope.$emit('GoToCopy', data);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
CreateDialog({
id: 'copy-job-modal' ,
title: "Copy",
scope: $scope,
buttons: buttons,
width: 500,
height: 300,
minWidth: 200,
callback: 'CopyDialogReady'
});
$('#job_name').text(name);
$('#copy-job-modal').show();
if ($scope.removeCopyDialogReady) {
$scope.removeCopyDialogReady();
}
$scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() {
//clear any old remaining text
$scope.new_copy_name = "" ;
$scope.copy_form.$setPristine();
$('#copy-job-modal').dialog('open');
$('#job-copy-button').attr('ng-disabled', "!copy_form.$valid");
element = angular.element(document.getElementById('job-copy-button'));
$compile(element)($scope);
});
if ($scope.removeGoToCopy) {
$scope.removeGoToCopy();
}
$scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) {
var url = GetBasePath('job_templates'),
old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ;
Rest.setUrl(url);
Rest.post(data)
.success(function (data) {
if(data.survey_enabled===true){
$scope.$emit("CopySurvey", data, old_survey_url);
}
else {
$('#copy-job-modal').dialog('close');
Wait('stop');
$location.path($location.path() + '/job_templates/' + data.id);
}
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
});
});
if ($scope.removeCopySurvey) {
$scope.removeCopySurvey();
}
$scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) {
// var url = data.related.survey_spec;
Rest.setUrl(old_url);
Rest.get()
.success(function (survey_data) {
Rest.setUrl(new_data.related.survey_spec);
Rest.post(survey_data)
.success(function () {
$('#copy-job-modal').dialog('close');
Wait('stop');
$location.path($location.path() + '/job_templates/' + new_data.id);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status });
});
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status });
});
});
};
$scope.deleteScanJob = function () {
var id = this.scan_job_template.id ,
action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
deleteJobTemplate(id)
.success(function () {
$('#prompt-modal').modal('hide');
$scope.search(form.related.scan_job_templates.iterator);
})
.error(function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the job template below?</div><div class="Prompt-bodyTarget">' + this.scan_job_template.name + '</div>',
action: action,
actionText: 'DELETE'
});
};
}
export default ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList',
'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit',
'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', 'ParseVariableString',
'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt',
'PlaybookRun', 'CreateDialog', 'deleteJobTemplate', '$state',
InventoriesEdit,
];

View File

@@ -0,0 +1,26 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesEdit from './inventory-edit.controller';
export default {
name: 'inventories.edit',
route: '/:inventory_id',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesEdit,
data: {
activityStreamId: 'inventory_id'
},
ncyBreadcrumb: {
label: "INVENTORY EDIT"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};

View File

@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-edit.route';
import controller from './inventory-edit.controller';
export default
angular.module('inventoryEdit', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@@ -0,0 +1,364 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesList($scope, $rootScope, $location, $log,
$stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList,
generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, Wait,
Find, Empty, $state) {
var list = InventoryList,
defaultUrl = GetBasePath('inventory'),
view = generateList,
paths = $location.path().replace(/^\//, '').split('/'),
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
function ellipsis(a) {
if (a.length > 20) {
return a.substr(0,20) + '...';
}
return a;
}
function attachElem(event, html, title) {
var elem = $(event.target).parent();
try {
elem.tooltip('hide');
elem.popover('destroy');
}
catch(err) {
//ignore
}
$('.popover').each(function() {
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
$(this).remove();
});
$('.tooltip').each( function() {
// close any lingering tool tipss
$(this).hide();
});
elem.attr({
"aw-pop-over": html,
"data-popover-title": title,
"data-placement": "right" });
$compile(elem)($scope);
elem.on('shown.bs.popover', function() {
$('.popover').each(function() {
$compile($(this))($scope); //make nested directives work!
});
$('.popover-content, .popover-title').click(function() {
elem.popover('hide');
});
});
elem.popover('show');
}
view.inject(InventoryList, { mode: mode, scope: $scope });
$rootScope.flashMessage = null;
SearchInit({
scope: $scope,
set: 'inventories',
list: list,
url: defaultUrl
});
PaginateInit({
scope: $scope,
list: list,
url: defaultUrl
});
if ($stateParams.name) {
$scope[InventoryList.iterator + 'InputDisable'] = false;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.name;
$scope[InventoryList.iterator + 'SearchField'] = 'name';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.name.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = null;
}
if ($stateParams.has_active_failures) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_active_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'has_active_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_active_failures.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_active_failures === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.has_inventory_sources) {
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.has_inventory_sources;
$scope[InventoryList.iterator + 'SearchField'] = 'has_inventory_sources';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.has_inventory_sources.label;
$scope[InventoryList.iterator + 'SearchSelectValue'] = ($stateParams.has_inventory_sources === 'true') ? {
value: 1
} : {
value: 0
};
}
if ($stateParams.inventory_sources_with_failures) {
// pass a value of true, however this field actually contains an integer value
$scope[InventoryList.iterator + 'InputDisable'] = true;
$scope[InventoryList.iterator + 'SearchValue'] = $stateParams.inventory_sources_with_failures;
$scope[InventoryList.iterator + 'SearchField'] = 'inventory_sources_with_failures';
$scope[InventoryList.iterator + 'SearchFieldLabel'] = InventoryList.fields.inventory_sources_with_failures.label;
$scope[InventoryList.iterator + 'SearchType'] = 'gtzero';
}
$scope.search(list.iterator);
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function () {
//If we got here by deleting an inventory, stop the spinner and cleanup events
Wait('stop');
try {
$('#prompt-modal').modal('hide');
}
catch(e) {
// ignore
}
$scope.inventories.forEach(function(inventory, idx) {
$scope.inventories[idx].launch_class = "";
if (inventory.has_inventory_sources) {
if (inventory.inventory_sources_with_failures > 0) {
$scope.inventories[idx].syncStatus = 'error';
$scope.inventories[idx].syncTip = inventory.inventory_sources_with_failures + ' groups with sync failures. Click for details';
}
else {
$scope.inventories[idx].syncStatus = 'successful';
$scope.inventories[idx].syncTip = 'No inventory sync failures. Click for details.';
}
}
else {
$scope.inventories[idx].syncStatus = 'na';
$scope.inventories[idx].syncTip = 'Not configured for inventory sync.';
$scope.inventories[idx].launch_class = "btn-disabled";
}
if (inventory.has_active_failures) {
$scope.inventories[idx].hostsStatus = 'error';
$scope.inventories[idx].hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.';
}
else if (inventory.total_hosts) {
$scope.inventories[idx].hostsStatus = 'successful';
$scope.inventories[idx].hostsTip = 'No hosts with failures. Click for details.';
}
else {
$scope.inventories[idx].hostsStatus = 'none';
$scope.inventories[idx].hostsTip = 'Inventory contains 0 hosts.';
}
});
});
if ($scope.removeRefreshInventories) {
$scope.removeRefreshInventories();
}
$scope.removeRefreshInventories = $scope.$on('RefreshInventories', function () {
// Reflect changes after inventory properties edit completes
$scope.search(list.iterator);
});
if ($scope.removeHostSummaryReady) {
$scope.removeHostSummaryReady();
}
$scope.removeHostSummaryReady = $scope.$on('HostSummaryReady', function(e, event, data) {
var html, title = "Recent Jobs";
Wait('stop');
if (data.count > 0) {
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Finished</th>";
html += "<th>Name</th>";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
data.results.forEach(function(row) {
html += "<tr>\n";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>\n";
html += "<td>" + ($filter('longDate')(row.finished)).replace(/ /,'<br />') + "</td>";
html += "<td><a href=\"#/jobs/" + row.id + "\" " + "aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) +
". Click for details\" aw-tip-placement=\"top\">" + ellipsis(row.name) + "</a></td>";
html += "</tr>\n";
});
html += "</tbody>\n";
html += "</table>\n";
}
else {
html = "<p>No recent job data available for this inventory.</p>\n";
}
attachElem(event, html, title);
});
if ($scope.removeGroupSummaryReady) {
$scope.removeGroupSummaryReady();
}
$scope.removeGroupSummaryReady = $scope.$on('GroupSummaryReady', function(e, event, inventory, data) {
var html, title;
Wait('stop');
// Build the html for our popover
html = "<table class=\"table table-condensed flyout\" style=\"width: 100%\">\n";
html += "<thead>\n";
html += "<tr>";
html += "<th>Status</th>";
html += "<th>Last Sync</th>";
html += "<th>Group</th>";
html += "</tr>";
html += "</thead>\n";
html += "<tbody>\n";
data.results.forEach( function(row) {
if (row.related.last_update) {
html += "<tr>";
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\" aw-tool-tip=\"" + row.status.charAt(0).toUpperCase() + row.status.slice(1) + ". Click for details\" aw-tip-placement=\"top\"><i class=\"fa icon-job-" + row.status + "\"></i></a></td>";
html += "<td>" + ($filter('longDate')(row.last_updated)).replace(/ /,'<br />') + "</td>";
html += "<td><a href=\"\" ng-click=\"viewJob('" + row.related.last_update + "')\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
html += "</tr>\n";
}
else {
html += "<tr>";
html += "<td><a href=\"\" aw-tool-tip=\"No sync data\" aw-tip-placement=\"top\"><i class=\"fa icon-job-none\"></i></a></td>";
html += "<td>NA</td>";
html += "<td><a href=\"\">" + ellipsis(row.summary_fields.group.name) + "</a></td>";
html += "</tr>\n";
}
});
html += "</tbody>\n";
html += "</table>\n";
title = "Sync Status";
attachElem(event, html, title);
});
$scope.showGroupSummary = function(event, id) {
var inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.syncStatus !== 'na') {
Wait('start');
Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5');
Rest.get()
.success(function(data) {
$scope.$emit('GroupSummaryReady', event, inventory, data);
})
.error(function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + inventory.related.inventory_sources + ' failed. GET returned status: ' + status
});
});
}
}
};
$scope.showHostSummary = function(event, id) {
var url, inventory;
if (!Empty(id)) {
inventory = Find({ list: $scope.inventories, key: 'id', val: id });
if (inventory.total_hosts > 0) {
Wait('start');
url = GetBasePath('jobs') + "?type=job&inventory=" + id + "&failed=";
url += (inventory.has_active_failures) ? 'true' : "false";
url += "&order_by=-finished&page_size=5";
Rest.setUrl(url);
Rest.get()
.success( function(data) {
$scope.$emit('HostSummaryReady', event, data);
})
.error( function(data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. GET returned: ' + status
});
});
}
}
};
$scope.viewJob = function(url) {
// Pull the id out of the URL
var id = url.replace(/^\//, '').split('/')[3];
$state.go('inventorySyncStdout', {id: id});
};
$scope.addInventory = function () {
$state.go('inventories.add');
};
$scope.editInventory = function (id) {
$state.go('inventories.edit', {inventory_id: id});
};
$scope.manageInventory = function(id){
$location.path($location.path() + '/' + id + '/manage');
};
$scope.deleteInventory = function (id, name) {
var action = function () {
var url = defaultUrl + id + '/';
Wait('start');
$('#prompt-modal').modal('hide');
Rest.setUrl(url);
Rest.destroy()
.success(function () {
$scope.search(list.iterator);
})
.error(function (data, status) {
ProcessErrors( $scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status
});
});
};
Prompt({
hdr: 'Delete',
body: '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(name) + '</div>',
action: action,
actionText: 'DELETE'
});
};
$scope.lookupOrganization = function (organization_id) {
Rest.setUrl(GetBasePath('organizations') + organization_id + '/');
Rest.get()
.success(function (data) {
return data.name;
});
};
// Failed jobs link. Go to the jobs tabs, find all jobs for the inventory and sort by status
$scope.viewJobs = function (id) {
$location.url('/jobs/?inventory__int=' + id);
};
$scope.viewFailedJobs = function (id) {
$location.url('/jobs/?inventory__int=' + id + '&status=failed');
};
}
export default ['$scope', '$rootScope', '$location', '$log',
'$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList',
'generateList', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller',
'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', InventoriesList];

View File

@@ -0,0 +1,27 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesList from './inventory-list.controller';
export default {
name: 'inventories',
route: '/inventories',
templateUrl: templateUrl('inventories/inventories'),
controller: InventoriesList,
data: {
activityStream: true,
activityStreamTarget: 'inventory'
},
ncyBreadcrumb: {
label: "INVENTORIES"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};

View File

@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-list.route';
import controller from './inventory-list.controller';
export default
angular.module('inventoryList', [])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@@ -0,0 +1,18 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import inventoryAdd from './add/main';
import inventoryEdit from './edit/main';
import inventoryList from './list/main';
import inventoryManage from './manage/main';
export default
angular.module('inventory', [
inventoryAdd.name,
inventoryEdit.name,
inventoryList.name,
inventoryManage.name,
]);

View File

@@ -0,0 +1,525 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Inventories
* @description This controller's for the Inventory page
*/
function InventoriesManage($log, $scope, $rootScope, $location,
$state, $compile, generateList, ClearScope, Empty, Wait, Rest, Alert,
GetBasePath, ProcessErrors, InventoryGroups,
InjectHosts, Find, HostsReload, SearchInit, PaginateInit, GetSyncStatusMsg,
GetHostsStatusMsg, GroupsEdit, InventoryUpdate, GroupsCancelUpdate,
ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete,
EditInventoryProperties, ToggleHostEnabled, ShowJobSummary,
InventoryGroupsHelp, HelpDialog,
GroupsCopy, HostsCopy, $stateParams, ParamPass) {
var PreviousSearchParams,
url,
hostScope = $scope.$new();
ClearScope();
// TODO: only display adhoc button if the user has permission to use it.
// TODO: figure out how to get the action-list partial to update so that
// the tooltip can be changed based off things being selected or not.
$scope.adhocButtonTipContents = "Launch adhoc command for the inventory";
// watcher for the group list checkbox changes
$scope.$on('multiSelectList.selectionChanged', function(e, selection) {
if (selection.length > 0) {
$scope.groupsSelected = true;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "selected groups and hosts.";
} else {
$scope.groupsSelected = false;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "inventory.";
}
$scope.groupsSelectedItems = selection.selectedItems;
});
// watcher for the host list checkbox changes
hostScope.$on('multiSelectList.selectionChanged', function(e, selection) {
// you need this so that the event doesn't bubble to the watcher above
// for the host list
e.stopPropagation();
if (selection.length === 0) {
$scope.hostsSelected = false;
} else if (selection.length === 1) {
$scope.systemTrackingTooltip = "Compare host over time";
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = false;
} else if (selection.length === 2) {
$scope.systemTrackingTooltip = "Compare hosts against each other";
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = false;
} else {
$scope.hostsSelected = true;
$scope.systemTrackingDisabled = true;
}
$scope.hostsSelectedItems = selection.selectedItems;
});
$scope.systemTracking = function() {
var hostIds = _.map($scope.hostsSelectedItems, function(x){
return x.id;
});
$state.transitionTo('systemTracking',
{ inventory: $scope.inventory,
inventoryId: $scope.inventory.id,
hosts: $scope.hostsSelectedItems,
hostIds: hostIds
});
};
// populates host patterns based on selected hosts/groups
$scope.populateAdhocForm = function() {
var host_patterns = "all";
if ($scope.hostsSelected || $scope.groupsSelected) {
var allSelectedItems = [];
if ($scope.groupsSelectedItems) {
allSelectedItems = allSelectedItems.concat($scope.groupsSelectedItems);
}
if ($scope.hostsSelectedItems) {
allSelectedItems = allSelectedItems.concat($scope.hostsSelectedItems);
}
if (allSelectedItems) {
host_patterns = _.pluck(allSelectedItems, "name").join(":");
}
}
$rootScope.hostPatterns = host_patterns;
$state.go('inventoryManage.adhoc');
};
$scope.refreshHostsOnGroupRefresh = false;
$scope.selected_group_id = null;
Wait('start');
if ($scope.removeHostReloadComplete) {
$scope.removeHostReloadComplete();
}
$scope.removeHostReloadComplete = $scope.$on('HostReloadComplete', function() {
if ($scope.initial_height) {
var host_height = $('#hosts-container .well').height(),
group_height = $('#group-list-container .well').height(),
new_height;
if (host_height > group_height) {
new_height = host_height - (host_height - group_height);
}
else if (host_height < group_height) {
new_height = host_height + (group_height - host_height);
}
if (new_height) {
$('#hosts-container .well').height(new_height);
}
$scope.initial_height = null;
}
});
if ($scope.removeRowCountReady) {
$scope.removeRowCountReady();
}
$scope.removeRowCountReady = $scope.$on('RowCountReady', function(e, rows) {
// Add hosts view
$scope.show_failures = false;
InjectHosts({
group_scope: $scope,
host_scope: hostScope,
inventory_id: $scope.inventory.id,
tree_id: null,
group_id: null,
pageSize: rows
});
SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: $scope.inventory.related.root_groups });
PaginateInit({ scope: $scope, list: InventoryGroups , url: $scope.inventory.related.root_groups, pageSize: rows });
$scope.search(InventoryGroups.iterator, null, true);
});
if ($scope.removeInventoryLoaded) {
$scope.removeInventoryLoaded();
}
$scope.removeInventoryLoaded = $scope.$on('InventoryLoaded', function() {
var rows;
// Add groups view
generateList.inject(InventoryGroups, {
mode: 'edit',
id: 'group-list-container',
searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12',
scope: $scope
});
rows = 20;
hostScope.host_page_size = rows;
$scope.group_page_size = rows;
$scope.show_failures = false;
InjectHosts({
group_scope: $scope,
host_scope: hostScope,
inventory_id: $scope.inventory.id,
tree_id: null,
group_id: null,
pageSize: rows
});
// Load data
SearchInit({
scope: $scope,
set: 'groups',
list: InventoryGroups,
url: $scope.inventory.related.root_groups
});
PaginateInit({
scope: $scope,
list: InventoryGroups ,
url: $scope.inventory.related.root_groups,
pageSize: rows
});
$scope.search(InventoryGroups.iterator, null, true);
$scope.$emit('WatchUpdateStatus'); // init socket io conneciton and start watching for status updates
});
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.removePostRefresh = $scope.$on('PostRefresh', function(e, set) {
if (set === 'groups') {
$scope.groups.forEach( function(group, idx) {
var stat, hosts_status;
stat = GetSyncStatusMsg({
status: group.summary_fields.inventory_source.status,
has_inventory_sources: group.has_inventory_sources,
source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null )
}); // from helpers/Groups.js
$scope.groups[idx].status_class = stat['class'];
$scope.groups[idx].status_tooltip = stat.tooltip;
$scope.groups[idx].launch_tooltip = stat.launch_tip;
$scope.groups[idx].launch_class = stat.launch_class;
hosts_status = GetHostsStatusMsg({
active_failures: group.hosts_with_active_failures,
total_hosts: group.total_hosts,
inventory_id: $scope.inventory.id,
group_id: group.id
}); // from helpers/Groups.js
$scope.groups[idx].hosts_status_tip = hosts_status.tooltip;
$scope.groups[idx].show_failures = hosts_status.failures;
$scope.groups[idx].hosts_status_class = hosts_status['class'];
$scope.groups[idx].source = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null;
$scope.groups[idx].status = (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.status : null;
});
if ($scope.refreshHostsOnGroupRefresh) {
$scope.refreshHostsOnGroupRefresh = false;
HostsReload({
scope: hostScope,
group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id,
pageSize: hostScope.host_page_size
});
}
else {
Wait('stop');
}
}
});
// Load Inventory
url = GetBasePath('inventory') + $stateParams.inventory_id + '/';
Rest.setUrl(url);
Rest.get()
.success(function (data) {
$scope.inventory = data;
$scope.$emit('InventoryLoaded');
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory: ' + $stateParams.inventory_id +
' GET returned status: ' + status });
});
// start watching for real-time updates
if ($rootScope.removeWatchUpdateStatus) {
$rootScope.removeWatchUpdateStatus();
}
$rootScope.removeWatchUpdateStatus = $rootScope.$on('JobStatusChange-inventory', function(e, data) {
var stat, group;
if (data.group_id) {
group = Find({ list: $scope.groups, key: 'id', val: data.group_id });
if (data.status === "failed" || data.status === "successful") {
if (data.group_id === $scope.selected_group_id || group) {
// job completed, fefresh all groups
$log.debug('Update completed. Refreshing the tree.');
$scope.refreshGroups();
}
}
else if (group) {
// incremental update, just update
$log.debug('Status of group: ' + data.group_id + ' changed to: ' + data.status);
stat = GetSyncStatusMsg({
status: data.status,
has_inventory_sources: group.has_inventory_sources,
source: group.source
});
$log.debug('changing tooltip to: ' + stat.tooltip);
group.status = data.status;
group.status_class = stat['class'];
group.status_tooltip = stat.tooltip;
group.launch_tooltip = stat.launch_tip;
group.launch_class = stat.launch_class;
}
}
});
// Load group on selection
function loadGroups(url) {
SearchInit({ scope: $scope, set: 'groups', list: InventoryGroups, url: url });
PaginateInit({ scope: $scope, list: InventoryGroups , url: url, pageSize: $scope.group_page_size });
$scope.search(InventoryGroups.iterator, null, true, false, true);
}
$scope.refreshHosts = function() {
HostsReload({
scope: hostScope,
group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id,
pageSize: hostScope.host_page_size
});
};
$scope.refreshGroups = function() {
$scope.refreshHostsOnGroupRefresh = true;
$scope.search(InventoryGroups.iterator, null, true, false, true);
};
$scope.restoreSearch = function() {
// Restore search params and related stuff, plus refresh
// groups and hosts lists
SearchInit({
scope: $scope,
set: PreviousSearchParams.set,
list: PreviousSearchParams.list,
url: PreviousSearchParams.defaultUrl,
iterator: PreviousSearchParams.iterator,
sort_order: PreviousSearchParams.sort_order,
setWidgets: false
});
$scope.refreshHostsOnGroupRefresh = true;
$scope.search(InventoryGroups.iterator, null, true, false, true);
};
$scope.groupSelect = function(id) {
var groups = [], group = Find({ list: $scope.groups, key: 'id', val: id });
if($state.params.groups){
groups.push($state.params.groups);
}
groups.push(group.id);
groups = groups.join();
$state.transitionTo('inventoryManage', {inventory_id: $state.params.inventory_id, groups: groups}, { notify: false });
loadGroups(group.related.children, group.id);
};
$scope.createGroup = function () {
PreviousSearchParams = Store('group_current_search_params');
var params = {
scope: $scope,
inventory_id: $scope.inventory.id,
group_id: $scope.selected_group_id,
mode: 'add'
}
ParamPass.set(params);
$state.go('inventoryManage.addGroup');
};
$scope.editGroup = function (id) {
PreviousSearchParams = Store('group_current_search_params');
var params = {
scope: $scope,
inventory_id: $scope.inventory.id,
group_id: id,
mode: 'edit'
}
ParamPass.set(params);
$state.go('inventoryManage.editGroup', {group_id: id});
};
// Launch inventory sync
$scope.updateGroup = function (id) {
var group = Find({ list: $scope.groups, key: 'id', val: id });
if (group) {
if (Empty(group.source)) {
// if no source, do nothing.
} else if (group.status === 'updating') {
Alert('Update in Progress', 'The inventory update process is currently running for group <em>' +
group.name + '</em> Click the <i class="fa fa-refresh"></i> button to monitor the status.', 'alert-info', null, null, null, null, true);
} else {
Wait('start');
Rest.setUrl(group.related.inventory_source);
Rest.get()
.success(function (data) {
InventoryUpdate({
scope: $scope,
url: data.related.update,
group_name: data.summary_fields.group.name,
group_source: data.source,
group_id: group.id,
});
})
.error(function (data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' +
group.related.inventory_source + ' GET returned status: ' + status });
});
}
}
};
$scope.cancelUpdate = function (id) {
GroupsCancelUpdate({ scope: $scope, id: id });
};
$scope.viewUpdateStatus = function (id) {
ViewUpdateStatus({
scope: $scope,
group_id: id
});
};
$scope.copyGroup = function(id) {
PreviousSearchParams = Store('group_current_search_params');
GroupsCopy({
scope: $scope,
group_id: id
});
};
$scope.deleteGroup = function (id) {
GroupsDelete({
scope: $scope,
group_id: id,
inventory_id: $scope.inventory.id
});
};
$scope.editInventoryProperties = function () {
// EditInventoryProperties({ scope: $scope, inventory_id: $scope.inventory.id });
$location.path('/inventories/' + $scope.inventory.id + '/');
};
hostScope.createHost = function () {
var params = {
host_scope: hostScope,
group_scope: $scope,
mode: 'add',
host_id: null,
selected_group_id: $scope.selected_group_id,
inventory_id: $scope.inventory.id
}
ParamPass.set(params);
$state.go('inventoryManage.addHost');
};
hostScope.editHost = function (host_id) {
var params = {
host_scope: hostScope,
group_scope: $scope,
mode: 'edit',
host_id: host_id,
inventory_id: $scope.inventory.id
}
ParamPass.set(params);
$state.go('inventoryManage.editHost', {host_id: host_id});
};
hostScope.deleteHost = function (host_id, host_name) {
HostsDelete({
parent_scope: $scope,
host_scope: hostScope,
host_id: host_id,
host_name: host_name
});
};
hostScope.copyHost = function(id) {
PreviousSearchParams = Store('group_current_search_params');
HostsCopy({
group_scope: $scope,
host_scope: hostScope,
host_id: id
});
};
hostScope.toggleHostEnabled = function (host_id, external_source) {
ToggleHostEnabled({
parent_scope: $scope,
host_scope: hostScope,
host_id: host_id,
external_source: external_source
});
};
hostScope.showJobSummary = function (job_id) {
ShowJobSummary({
job_id: job_id
});
};
$scope.showGroupHelp = function (params) {
var opts = {
defn: InventoryGroupsHelp
};
if (params) {
opts.autoShow = params.autoShow || false;
}
HelpDialog(opts);
}
;
$scope.showHosts = function (group_id, show_failures) {
// Clicked on group
if (group_id !== null) {
Wait('start');
hostScope.show_failures = show_failures;
$scope.groupSelect(group_id);
hostScope.hosts = [];
$scope.show_failures = show_failures; // turn on failed hosts
// filter in hosts view
} else {
Wait('stop');
}
};
if ($scope.removeGroupDeleteCompleted) {
$scope.removeGroupDeleteCompleted();
}
$scope.removeGroupDeleteCompleted = $scope.$on('GroupDeleteCompleted',
function() {
$scope.refreshGroups();
}
);
}
export default [
'$log', '$scope', '$rootScope', '$location',
'$state', '$compile', 'generateList', 'ClearScope', 'Empty', 'Wait',
'Rest', 'Alert', 'GetBasePath', 'ProcessErrors',
'InventoryGroups', 'InjectHosts', 'Find', 'HostsReload',
'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg',
'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus',
'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete',
'EditInventoryProperties', 'ToggleHostEnabled', 'ShowJobSummary',
'InventoryGroupsHelp', 'HelpDialog', 'GroupsCopy',
'HostsCopy', '$stateParams', 'ParamPass', InventoriesManage,
];

View File

@@ -10,9 +10,6 @@
<div id="host-list-container" class="Panel"></div> <div id="host-list-container" class="Panel"></div>
</div> </div>
</div> </div>
<div id="inventory-modal-container"></div>
<div id="group-copy-dialog" style="display: none;"> <div id="group-copy-dialog" style="display: none;">
<div id="copy-group-radio-container" class="well"> <div id="copy-group-radio-container" class="well">
<div class="title"><span class="highlight">1.</span> Copy or move <span ng-bind="name"></span>?</div> <div class="title"><span class="highlight">1.</span> Copy or move <span ng-bind="name"></span>?</div>

View File

@@ -0,0 +1,28 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import InventoriesManage from './inventory-manage.controller';
export default {
name: 'inventoryManage',
url: '/inventories/:inventory_id/manage?groups',
templateUrl: templateUrl('inventories/manage/inventory-manage'),
controller: InventoriesManage,
data: {
activityStream: true,
activityStreamTarget: 'inventory',
activityStreamId: 'inventory_id'
},
ncyBreadcrumb: {
label: "INVENTORY MANAGE"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
};

View File

@@ -0,0 +1,19 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './inventory-manage.route';
import manageHosts from './manage-hosts/main';
import manageGroups from './manage-groups/main';
export default
angular.module('inventoryManage', [
manageHosts.name,
manageGroups.name
])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@@ -0,0 +1,550 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function manageGroupsDirectiveController($filter, $rootScope, $location, $log, $stateParams, $compile, $state, $scope, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors,
GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, LookUpInit, Empty, Wait,
GetChoices, UpdateGroup, SourceChange, Find, ParseVariableString, ToJSON, GroupsScheduleListInit,
SourceForm, SetSchedulesInnerDialogSize, CreateSelect2, ParamPass) {
var vm = this;
var params = ParamPass.get();
if(params === undefined) {
params = {};
params.scope = $scope.$new();
}
var parent_scope = params.scope,
group_id = $stateParams.group_id,
mode = $state.current.data.mode, // 'add' or 'edit'
inventory_id = $stateParams.inventory_id,
generator = GenerateForm,
group_created = false,
defaultUrl,
master = {},
choicesReady,
modal_scope = parent_scope.$new(),
properties_scope = parent_scope.$new(),
sources_scope = parent_scope.$new(),
elem, group,
schedules_url = '';
if (mode === 'edit') {
defaultUrl = GetBasePath('groups') + group_id + '/';
} else {
defaultUrl = (group_id !== undefined) ? GetBasePath('groups') + group_id + '/children/' :
GetBasePath('inventory') + inventory_id + '/groups/';
}
Rest.setUrl(defaultUrl);
Rest.get()
.success(function(data) {
group = data;
for (var fld in GroupForm.fields) {
if (data[fld]) {
properties_scope[fld] = data[fld];
master[fld] = properties_scope[fld];
}
}
if(mode === 'edit') {
schedules_url = data.related.inventory_source + 'schedules/';
properties_scope.variable_url = data.related.variable_data;
sources_scope.source_url = data.related.inventory_source;
modal_scope.$emit('LoadSourceData');
}
})
.error(function(data, status) {
ProcessErrors(modal_scope, data, status, {
hdr: 'Error!',
msg: 'Failed to retrieve group: ' + defaultUrl + '. GET status: ' + status
});
});
$('#properties-tab').empty();
$('#sources-tab').empty();
elem = document.getElementById('group-manage-panel');
$compile(elem)(modal_scope);
$scope.parseType = 'yaml';
var form_scope =
generator.inject(GroupForm, {
mode: mode,
id: 'properties-tab',
related: false,
scope: properties_scope,
cancelButton: false,
});
var source_form_scope =
generator.inject(SourceForm, {
mode: mode,
id: 'sources-tab',
related: false,
scope: sources_scope,
cancelButton: false
});
generator.reset();
GetSourceTypeOptions({
scope: sources_scope,
variable: 'source_type_options'
});
sources_scope.source = SourceForm.fields.source['default'];
sources_scope.sourcePathRequired = false;
sources_scope[SourceForm.fields.source_vars.parseTypeName] = 'yaml';
sources_scope.update_cache_timeout = 0;
properties_scope.parseType = 'yaml';
function waitStop() {
Wait('stop');
}
function initSourceChange() {
parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value !== "manual") ? true : false;
SourceChange({
scope: sources_scope,
form: SourceForm
});
}
// JT -- this gets called after the properties & properties variables are loaded, and is emitted from (groupLoaded)
if (modal_scope.removeLoadSourceData) {
modal_scope.removeLoadSourceData();
}
modal_scope.removeLoadSourceData = modal_scope.$on('LoadSourceData', function() {
ParseTypeChange({
scope: form_scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'group_variables'
});
if (sources_scope.source_url) {
// get source data
Rest.setUrl(sources_scope.source_url);
Rest.get()
.success(function(data) {
var fld, i, j, flag, found, set, opts, list, form;
form = SourceForm;
for (fld in form.fields) {
if (fld === 'checkbox_group') {
for (i = 0; i < form.fields[fld].fields.length; i++) {
flag = form.fields[fld].fields[i];
if (data[flag.name] !== undefined) {
sources_scope[flag.name] = data[flag.name];
master[flag.name] = sources_scope[flag.name];
}
}
}
if (fld === 'source') {
found = false;
data.source = (data.source === "") ? "manual" : data.source;
for (i = 0; i < sources_scope.source_type_options.length; i++) {
if (sources_scope.source_type_options[i].value === data.source) {
sources_scope.source = sources_scope.source_type_options[i];
found = true;
}
}
if (!found || sources_scope.source.value === "manual") {
sources_scope.groupUpdateHide = true;
} else {
sources_scope.groupUpdateHide = false;
}
master.source = sources_scope.source;
} else if (fld === 'source_vars') {
// Parse source_vars, converting to YAML.
sources_scope.source_vars = ParseVariableString(data.source_vars);
master.source_vars = sources_scope.variables;
} else if (fld === "inventory_script") {
// the API stores it as 'source_script', we call it inventory_script
data.summary_fields['inventory_script'] = data.summary_fields.source_script;
sources_scope.inventory_script = data.source_script;
master.inventory_script = sources_scope.inventory_script;
} else if (fld === "source_regions") {
if (data[fld] === "") {
sources_scope[fld] = data[fld];
master[fld] = sources_scope[fld];
} else {
sources_scope[fld] = data[fld].split(",");
master[fld] = sources_scope[fld];
}
} else if (data[fld] !== undefined) {
sources_scope[fld] = data[fld];
master[fld] = sources_scope[fld];
}
if (form.fields[fld].sourceModel && data.summary_fields &&
data.summary_fields[form.fields[fld].sourceModel]) {
sources_scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
initSourceChange();
if (data.source_regions) {
if (data.source === 'ec2' ||
data.source === 'rax' ||
data.source === 'gce' ||
data.source === 'azure') {
if (data.source === 'ec2') {
set = sources_scope.ec2_regions;
} else if (data.source === 'rax') {
set = sources_scope.rax_regions;
} else if (data.source === 'gce') {
set = sources_scope.gce_regions;
} else if (data.source === 'azure') {
set = sources_scope.azure_regions;
}
opts = [];
list = data.source_regions.split(',');
for (i = 0; i < list.length; i++) {
for (j = 0; j < set.length; j++) {
if (list[i] === set[j].value) {
opts.push({
id: set [j].value,
text: set [j].label
});
}
}
}
master.source_regions = opts;
CreateSelect2({
element: "#source_source_regions",
opts: opts
});
}
} else {
// If empty, default to all
master.source_regions = [{
id: 'all',
text: 'All'
}];
}
if (data.group_by && data.source === 'ec2') {
set = sources_scope.ec2_group_by;
opts = [];
list = data.group_by.split(',');
for (i = 0; i < list.length; i++) {
for (j = 0; j < set.length; j++) {
if (list[i] === set[j].value) {
opts.push({
id: set [j].value,
text: set [j].label
});
}
}
}
master.group_by = opts;
CreateSelect2({
element: "#source_group_by",
opts: opts
});
}
sources_scope.group_update_url = data.related.update;
})
.error(function(data, status) {
sources_scope.source = "";
ProcessErrors(modal_scope, data, status, null, {
hdr: 'Error!',
msg: 'Failed to retrieve inventory source. GET status: ' + status
});
});
}
});
if (sources_scope.removeScopeSourceTypeOptionsReady) {
sources_scope.removeScopeSourceTypeOptionsReady();
}
sources_scope.removeScopeSourceTypeOptionsReady = sources_scope.$on('sourceTypeOptionsReady', function() {
if (mode === 'add') {
sources_scope.source = Find({
list: sources_scope.source_type_options,
key: 'value',
val: ''
});
modal_scope.showSchedulesTab = false;
}
});
choicesReady = 0;
if (sources_scope.removeChoicesReady) {
sources_scope.removeChoicesReady();
}
sources_scope.removeChoicesReady = sources_scope.$on('choicesReadyGroup', function() {
CreateSelect2({
element: '#source_source',
multiple: false
});
modal_scope.$emit('LoadSourceData');
choicesReady++;
if (choicesReady === 5) {
if (mode !== 'edit') {
properties_scope.variables = "---";
master.variables = properties_scope.variables;
}
}
});
// Load options for source regions
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'rax_regions',
choice_name: 'rax_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'ec2_regions',
choice_name: 'ec2_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'gce_regions',
choice_name: 'gce_region_choices',
callback: 'choicesReadyGroup'
});
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'source_regions',
variable: 'azure_regions',
choice_name: 'azure_region_choices',
callback: 'choicesReadyGroup'
});
// Load options for group_by
GetChoices({
scope: sources_scope,
url: GetBasePath('inventory_sources'),
field: 'group_by',
variable: 'ec2_group_by',
choice_name: 'ec2_group_by_choices',
callback: 'choicesReadyGroup'
});
//Wait('start');
if (parent_scope.removeAddTreeRefreshed) {
parent_scope.removeAddTreeRefreshed();
}
parent_scope.removeAddTreeRefreshed = parent_scope.$on('GroupTreeRefreshed', function() {
// Clean up
Wait('stop');
if (modal_scope.searchCleanUp) {
modal_scope.searchCleanup();
}
try {
//$('#group-modal-dialog').dialog('close');
} catch (e) {
// ignore
}
});
if (modal_scope.removeSaveComplete) {
modal_scope.removeSaveComplete();
}
modal_scope.removeSaveComplete = modal_scope.$on('SaveComplete', function(e, error) {
if (!error) {
modal_scope.cancelPanel();
}
});
if (modal_scope.removeFormSaveSuccess) {
modal_scope.removeFormSaveSuccess();
}
modal_scope.removeFormSaveSuccess = modal_scope.$on('formSaveSuccess', function() {
// Source data gets stored separately from the group. Validate and store Source
// related fields, then call SaveComplete to wrap things up.
var parseError = false,
regions, r, i,
group_by,
data = {
group: group_id,
source: ((sources_scope.source && sources_scope.source.value !== 'manual') ? sources_scope.source.value : ''),
source_path: sources_scope.source_path,
credential: sources_scope.credential,
overwrite: sources_scope.overwrite,
overwrite_vars: sources_scope.overwrite_vars,
source_script: sources_scope.inventory_script,
update_on_launch: sources_scope.update_on_launch,
update_cache_timeout: (sources_scope.update_cache_timeout || 0)
};
// Create a string out of selected list of regions
if (sources_scope.source_regions) {
regions = $('#source_source_regions').select2("data");
r = [];
for (i = 0; i < regions.length; i++) {
r.push(regions[i].id);
}
data.source_regions = r.join();
}
if (sources_scope.source && (sources_scope.source.value === 'ec2')) {
data.instance_filters = sources_scope.instance_filters;
// Create a string out of selected list of regions
group_by = $('#source_group_by').select2("data");
r = [];
for (i = 0; i < group_by.length; i++) {
r.push(group_by[i].id);
}
data.group_by = r.join();
}
if (sources_scope.source && (sources_scope.source.value === 'ec2')) {
// for ec2, validate variable data
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true);
}
if (sources_scope.source && (sources_scope.source.value === 'custom')) {
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true);
}
if (sources_scope.source && (sources_scope.source.value === 'vmware' ||
sources_scope.source.value === 'openstack')) {
data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true);
}
// the API doesn't expect the credential to be passed with a custom inv script
if (sources_scope.source && sources_scope.source.value === 'custom') {
delete(data.credential);
}
if (!parseError) {
Rest.setUrl(sources_scope.source_url);
Rest.put(data)
.success(function() {
modal_scope.$emit('SaveComplete', false);
})
.error(function(data, status) {
$('#group_tabs a:eq(1)').tab('show');
ProcessErrors(sources_scope, data, status, SourceForm, {
hdr: 'Error!',
msg: 'Failed to update group inventory source. PUT status: ' + status
});
});
}
});
// Cancel
modal_scope.cancelPanel = function() {
Wait('stop');
$state.go('inventoryManage', {}, {reload: true})
};
// Save
modal_scope.saveGroup = function() {
Wait('start');
var fld, data, json_data;
try {
json_data = ToJSON(properties_scope.parseType, properties_scope.variables, true);
data = {};
for (fld in GroupForm.fields) {
data[fld] = properties_scope[fld];
}
data.inventory = inventory_id;
Rest.setUrl(defaultUrl);
if (mode === 'edit' || (mode === 'add' && group_created)) {
Rest.put(data)
.success(function() {
modal_scope.$emit('formSaveSuccess');
})
.error(function(data, status) {
$('#group_tabs a:eq(0)').tab('show');
ProcessErrors(properties_scope, data, status, GroupForm, {
hdr: 'Error!',
msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status
});
});
} else {
Rest.post(data)
.success(function(data) {
group_created = true;
group_id = data.id;
sources_scope.source_url = data.related.inventory_source;
modal_scope.$emit('formSaveSuccess');
})
.error(function(data, status) {
$('#group_tabs a:eq(0)').tab('show');
ProcessErrors(properties_scope, data, status, GroupForm, {
hdr: 'Error!',
msg: 'Failed to create group: ' + group_id + '. POST status: ' + status
});
});
}
} catch (e) {
// ignore. ToJSON will have already alerted the user
}
};
// Start the update process
modal_scope.updateGroup = function() {
if (sources_scope.source === "manual" || sources_scope.source === null) {
Alert('Missing Configuration', 'The selected group is not configured for updates. You must first edit the group, provide Source settings, ' +
'and then run an update.', 'alert-info');
} else if (sources_scope.status === 'updating') {
Alert('Update in Progress', 'The inventory update process is currently running for group <em>' +
$filter('sanitize')(sources_scope.summary_fields.group.name) + '</em>. Use the Refresh button to monitor the status.', 'alert-info', null, null, null, null, true);
} else {
InventoryUpdate({
scope: parent_scope,
group_id: group_id,
url: properties_scope.group_update_url,
group_name: properties_scope.name,
group_source: sources_scope.source.value
});
}
};
// Change the lookup and regions when the source changes
sources_scope.sourceChange = function() {
sources_scope.credential_name = "";
sources_scope.credential = "";
if (sources_scope.credential_name_api_error) {
delete sources_scope.credential_name_api_error;
}
initSourceChange();
};
angular.extend(vm, {
cancelPanel : modal_scope.cancelPanel,
saveGroup: modal_scope.saveGroup
})
}
export default ['$filter', '$rootScope', '$location', '$log', '$stateParams', '$compile', '$state', '$scope', 'Rest', 'Alert', 'GroupForm', 'GenerateForm',
'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate',
'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find',
'ParseVariableString', 'ToJSON', 'GroupsScheduleListInit', 'SourceForm', 'SetSchedulesInnerDialogSize', 'CreateSelect2', 'ParamPass',
manageGroupsDirectiveController
];

View File

@@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
import manageGroupsDirectiveController from './manage-groups.directive.controller';
export default ['templateUrl', 'ParamPass',
function(templateUrl, ParamPass) {
return {
restrict: 'EA',
scope: true,
replace: true,
templateUrl: templateUrl('inventories/manage/manage-groups/directive/manage-groups.directive'),
link: function(scope, element, attrs) {
},
controller: manageGroupsDirectiveController,
controllerAs: 'vm',
bindToController: true
};
}
];

View File

@@ -0,0 +1,20 @@
<div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div id="group-manage-panel">
<div id="properties-tab"></div>
<div id="sources-tab"></div>
</div>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<div class="ui-dialog-buttonset">
<button type="button" class="btn btn-primary Form-saveButton" id="Inventory-groupManage--okButton" ng-click="vm.saveGroup()">
Save</button>
<button type="button" class="btn btn-default Form-cancelButton" id="Inventory-groupManage--cancelButton" ng-click="vm.cancelPanel()">
Cancel</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './manage-groups.route';
import manageGroupsDirective from './directive/manage-groups.directive';
export default
angular.module('manage-groups', [])
.directive('manageGroups', manageGroupsDirective)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route.edit);
$stateExtender.addState(route.add);
}]);

View File

@@ -0,0 +1,5 @@
<div class="tab-pane" id="Inventory-groupManage">
<div ng-cloak id="Inventory-groupManage--panel" class="Panel">
<manage-groups></manage-groups>
</div>
</div>

View File

@@ -0,0 +1,46 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {
templateUrl
} from '../../../shared/template-url/template-url.factory';
export default {
edit: {
name: 'inventoryManage.editGroup',
route: '/:group_id/editGroup',
templateUrl: templateUrl('inventories/manage/manage-groups/manage-groups'),
data: {
group_id: 'group_id',
mode: 'edit'
},
ncyBreadcrumb: {
label: "INVENTORY EDIT GROUPS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
add: {
name: 'inventoryManage.addGroup',
route: '/addGroup',
templateUrl: templateUrl('inventories/manage/manage-groups/manage-groups'),
ncyBreadcrumb: {
label: "INVENTORY ADD GROUP"
},
data: {
mode: 'add'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
};

View File

@@ -0,0 +1,192 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function manageHostsDirectiveController($rootScope, $location, $log, $stateParams, $state, $scope, Rest, Alert, HostForm,
GenerateForm, Prompt, ProcessErrors, GetBasePath, HostsReload, ParseTypeChange, Wait,
Find, SetStatus, ApplyEllipsis, ToJSON, ParseVariableString, CreateDialog, TextareaResize, ParamPass) {
var vm = this;
var params = ParamPass.get();
if(params === undefined) {
params = {};
params.host_scope = $scope.$new();
params.group_scope = $scope.$new();
}
var parent_scope = params.host_scope,
group_scope = params.group_scope,
inventory_id = $stateParams.inventory_id,
mode = $state.current.data.mode, // 'add' or 'edit'
selected_group_id = params.selected_group_id,
generator = GenerateForm,
form = HostForm,
defaultUrl,
scope = parent_scope.$new(),
master = {},
relatedSets = {},
url, form_scope;
var host_id = $stateParams.host_id || undefined;
form_scope =
generator.inject(HostForm, {
mode: 'edit',
id: 'host-panel-form',
related: false,
scope: scope,
cancelButton: false
});
generator.reset();
console.info(angular.element(document.getElementById('host_variables')));
$scope.parseType = 'yaml';
ParseTypeChange({
scope: form_scope,
variable: 'variables',
parse_variable: 'parseType',
field_id: 'host_variables'
});
// Retrieve detail record and prepopulate the form
if (mode === 'edit') {
defaultUrl = GetBasePath('hosts') + host_id + '/';
Rest.setUrl(defaultUrl);
Rest.get()
.success(function(data) {
var set, fld, related;
for (fld in form.fields) {
if (data[fld]) {
scope[fld] = data[fld];
master[fld] = scope[fld];
}
}
related = data.related;
for (set in form.related) {
if (related[set]) {
relatedSets[set] = {
url: related[set],
iterator: form.related[set].iterator
};
}
}
scope.variable_url = data.related.variable_data;
scope.has_inventory_sources = data.has_inventory_sources;
})
.error(function(data, status) {
ProcessErrors(parent_scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to retrieve host: ' + host_id + '. GET returned status: ' + status
});
});
} else {
if (selected_group_id) {
// adding hosts to a group
url = GetBasePath('groups') + selected_group_id + '/';
} else {
// adding hosts to the top-level (inventory)
url = GetBasePath('inventory') + inventory_id + '/';
}
// Add mode
Rest.setUrl(url);
Rest.get()
.success(function(data) {
scope.has_inventory_sources = data.has_inventory_sources;
scope.enabled = true;
scope.variables = '---';
defaultUrl = data.related.hosts;
//scope.$emit('hostVariablesLoaded');
})
.error(function(data, status) {
ProcessErrors(parent_scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to retrieve group: ' + selected_group_id + '. GET returned status: ' + status
});
});
}
if (scope.removeSaveCompleted) {
scope.removeSaveCompleted();
}
scope.removeSaveCompleted = scope.$on('saveCompleted', function() {
Wait('stop');
try {
$('#host-modal-dialog').dialog('close');
} catch (err) {
// ignore
}
if (group_scope && group_scope.refreshHosts) {
group_scope.refreshHosts();
}
if (parent_scope.refreshHosts) {
parent_scope.refreshHosts();
}
scope.$destroy();
$state.go('inventoryManage', {}, {
reload: true
});
});
// Save changes to the parent
var saveHost = function() {
Wait('start');
var fld, data = {};
try {
data.variables = ToJSON(scope.parseType, scope.variables, true);
for (fld in form.fields) {
data[fld] = scope[fld];
}
data.inventory = inventory_id;
Rest.setUrl(defaultUrl);
if (mode === 'edit') {
Rest.put(data)
.success(function() {
scope.$emit('saveCompleted');
})
.error(function(data, status) {
ProcessErrors(scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to update host: ' + host_id + '. PUT returned status: ' + status
});
});
} else {
Rest.post(data)
.success(function() {
scope.$emit('saveCompleted');
})
.error(function(data, status) {
ProcessErrors(scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to create host. POST returned status: ' + status
});
});
}
} catch (e) {
// ignore. ToJSON will have already alerted the user
}
};
var cancelPanel = function() {
scope.$destroy();
if (scope.codeMirror) {
scope.codeMirror.destroy();
}
$state.go('inventoryManage');
};
angular.extend(vm, {
cancelPanel: cancelPanel,
saveHost: saveHost,
mode: mode
});
}
export default ['$rootScope', '$location', '$log', '$stateParams', '$state', '$scope', 'Rest', 'Alert', 'HostForm',
'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'HostsReload', 'ParseTypeChange',
'Wait', 'Find', 'SetStatus', 'ApplyEllipsis', 'ToJSON', 'ParseVariableString',
'CreateDialog', 'TextareaResize', 'ParamPass', manageHostsDirectiveController
];

View File

@@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
import manageHostsDirectiveController from './manage-hosts.directive.controller';
export default ['templateUrl', 'ParamPass',
function(templateUrl, ParamPass) {
return {
restrict: 'EA',
scope: true,
replace: true,
templateUrl: templateUrl('inventories/manage/manage-hosts/directive/manage-hosts.directive'),
link: function(scope, element, attrs) {
},
controller: manageHostsDirectiveController,
controllerAs: 'vm',
bindToController: true
};
}
];

View File

@@ -0,0 +1,25 @@
<div>
<div class="Form-header" ng-if="vm.mode === 'add'">
<div class="Form-title ng-binding" >Create Host</div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="Form-exitHolder" ng-if="vm.mode === 'edit'">
<button class="Form-exit" ng-click="vm.cancelPanel()">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div id="host-panel-form"></div>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<div class="ui-dialog-buttonset">
<button type="button" class="btn btn-primary Form-saveButton" id="Inventory-hostManage--okButton" ng-click="vm.saveHost()">
Save</button>
<button type="button" class="btn btn-default Form-cancelButton" id="Inventory-hostManage--cancelButton" ng-click="vm.cancelPanel()">
Cancel</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './manage-hosts.route';
import manageHostsDirective from './directive/manage-hosts.directive';
export default
angular.module('manage-hosts', [])
.directive('manageHosts', manageHostsDirective)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route.edit);
$stateExtender.addState(route.add);
}]);

View File

@@ -0,0 +1,5 @@
<div class="tab-pane" id="Inventory-hostManage">
<div ng-cloak id="Inventory-hostManage--panel" class="Panel">
<manage-hosts></manage-hosts>
</div>
</div>

View File

@@ -0,0 +1,46 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {
templateUrl
} from '../../../shared/template-url/template-url.factory';
export default {
edit: {
name: 'inventoryManage.editHost',
route: '/:host_id/editHost',
templateUrl: templateUrl('inventories/manage/manage-hosts/manage-hosts'),
data: {
host_id: 'host_id',
mode: 'edit'
},
ncyBreadcrumb: {
label: "INVENTORY EDIT HOSTS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
add: {
name: 'inventoryManage.addHost',
route: '/addHost',
templateUrl: templateUrl('inventories/manage/manage-hosts/manage-hosts'),
data: {
mode: 'add'
},
ncyBreadcrumb: {
label: "INVENTORY ADD HOST"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
},
};

View File

@@ -0,0 +1,18 @@
#Inventory-groupManage--panel,
#Inventory-hostManage--panel {
.ui-dialog-buttonpane.ui-widget-content {
border: none;
text-align: right;
}
#host-panel-form,
#properties-tab {
.Form-header {
margin-top: -20px;
}
}
.Form-textArea {
width: 100%;
}
}

View File

@@ -0,0 +1,49 @@
<div class="HostEvent-details--left">
<div class="HostEvent-field">
<div class="HostEvent-title">EVENT</div>
<span class="HostEvent-field--content"></span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">HOST</span>
<span class="HostEvent-field--content">
<a ui-sref="jobDetail.host-events({hostName: event.host_name})">{{event.host_name || "No result found"}}</a></span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">STATUS</span>
<span class="HostEvent-field--content">
<a class="HostEvents-status">
<i class="fa fa-circle" ng-class="processEventStatus"></i>
</a>
{{event.status || "No result found"}}
</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">ID</span>
<span class="HostEvent-field--content">{{event.id || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">CREATED</span>
<span class="HostEvent-field--content">{{event.created || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">PLAY</span>
<span class="HostEvent-field--content">{{event.play || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">TASK</span>
<span class="HostEvent-field--content">{{event.task || "No result found"}}</span>
</div>
<div class="HostEvent-field">
<span class="HostEvent-field--label">MODULE</span>
<span class="HostEvent-field--content">{{event.event_data.res.invocation.module_name || "No result found"}}</span>
</div>
</div>
<div class="HostEvent-details--right" ng-show="event.event_data.res">
<div class="HostEvent-title">RESULTS</div>
<!-- discard any objects in the ansible response until we decide to flatten them -->
<div class="HostEvent-field" ng-repeat="(key, value) in results = event.event_data.res track by $index" ng-if="processResults(value)">
<span class="HostEvent-field--label">{{key}}</span>
<span class="HostEvent-field--content">{{value}}</span>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<textarea id="HostEvent-json" class="HostEvent-json">
</textarea>

View File

@@ -0,0 +1,36 @@
<div id="HostEvent" class="HostEvent modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<!-- modal body -->
<div class="modal-body">
<div class="HostEvent-header">
<span class="HostEvent-title">HOST EVENT</span>
<!-- close -->
<button ui-sref="jobDetail" type="button" class="close">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div class="HostEvent-nav">
<!-- view navigation buttons -->
<button ui-sref="jobDetail.host-event.details" type="button" class="btn btn-sm btn-default" >Details</button>
<button ui-sref="jobDetail.host-event.json" type="button" class="btn btn-sm btn-default ">JSON</button>
<button ng-show="event.stdout" ui-sref="jobDetail.host-event.stdout" type="button" class="btn btn-sm btn-default ">Standard Out</button>
<button ng-show="event.timing" ui-sref="jobDetail.host-event.timing" type="button" class="btn btn-sm btn-default ">Timing</button>
</div>
<div class="HostEvent-body">
<!-- views -->
<div ui-view></div>
</div>
<!-- controls -->
<div class="HostEvent-controls">
<button ng-show="showPrev()" ng-click="goPrev()"
class="btn btn-sm btn-default">Prev</button>
<button ng-show="showNext()"ng-click="goNext()" class="btn btn-sm btn-default">Next</button>
<button ui-sref="jobDetail" class="btn btn-sm btn-default" ng-show="true" >Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="EventHost-stdoutPanel Panel">
<div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<a href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
<i class="fa fa-download"></i>
</button>
</a>
</div>
</div>
<standard-out-log stdout-endpoint="event._stdout"></standard-out-log>
</div>

View File

@@ -0,0 +1 @@
<div>timing</div>

View File

@@ -0,0 +1,69 @@
@import "awx/ui/client/src/shared/branding/colors.less";
@import "awx/ui/client/src/shared/branding/colors.default.less";
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
.HostEvent .modal-footer{
border: 0;
margin-top: 0px;
padding-top: 5px;
}
.HostEvent-controls{
float: right;
button {
margin-left: 10px;
}
}
.HostEvent-status--ok{
color: @green;
}
.HostEvent-status--unreachable{
color: @unreachable;
}
.HostEvent-status--changed{
color: @changed;
}
.HostEvent-status--failed{
color: @default-err;
}
.HostEvent-status--skipped{
color: @skipped;
}
.HostEvent-title{
color: @default-interface-txt;
font-weight: 600;
}
.HostEvent .modal-body{
max-height: 500px;
overflow-y: auto;
padding: 20px;
}
.HostEvent-nav{
padding-top: 12px;
padding-bottom: 12px;
}
.HostEvent-field{
margin-bottom: 8px;
}
.HostEvent-field--label{
.OnePlusTwo-left--detailsLabel;
width: 80px;
margin-right: 20px;
font-size: 12px;
}
.HostEvent-field{
.OnePlusTwo-left--detailsRow;
}
.HostEvent-field--content{
.OnePlusTwo-left--detailsContent;
}
.HostEvent-details--left, .HostEvent-details--right{
vertical-align:top;
width:270px;
display: inline-block;
}
.HostEvent-details--right{
.HostEvent-field--label{
width: 170px;
}
}

View File

@@ -0,0 +1,71 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'moment', 'event',
function($stateParams, $scope, $state, Wait, JobDetailService, moment, event){
// Avoid rendering objects in the details fieldset
// ng-if="processResults(value)" via host-event-details.partial.html
$scope.processResults = function(value){
if (typeof value == 'object'){return false}
else {return true}
};
var codeMirror = function(){
var el = $('#HostEvent-json')[0];
var editor = CodeMirror.fromTextArea(el, {
lineNumbers: true,
mode: {name: "javascript", json: true}
});
editor.getDoc().setValue(JSON.stringify($scope.json, null, 4));
};
$scope.getActiveHostIndex = function(){
var result = $scope.hostResults.filter(function( obj ) {
return obj.id == $scope.event.id;
});
return $scope.hostResults.indexOf(result[0])
};
$scope.showPrev = function(){
return $scope.getActiveHostIndex() != 0
};
$scope.showNext = function(){
return $scope.getActiveHostIndex() < $scope.hostResults.indexOf($scope.hostResults[$scope.hostResults.length - 1])
};
$scope.goNext = function(){
var index = $scope.getActiveHostIndex() + 1;
var id = $scope.hostResults[index].id;
$state.go('jobDetail.host-event.details', {eventId: id})
};
$scope.goPrev = function(){
var index = $scope.getActiveHostIndex() - 1;
var id = $scope.hostResults[index].id;
$state.go('jobDetail.host-event.details', {eventId: id})
};
var init = function(){
$scope.event = event.data.results[0];
$scope.event.created = moment($scope.event.created).format();
$scope.processEventStatus = JobDetailService.processEventStatus($scope.event);
$scope.hostResults = $stateParams.hostResults;
$scope.json = JobDetailService.processJson($scope.event);
if ($state.current.name == 'jobDetail.host-event.json'){
codeMirror();
}
try {
$scope.stdout = $scope.event.event_data.res.stdout
}
catch(err){
$scope.sdout = null;
}
$('#HostEvent').modal('show');
};
init();
}];

View File

@@ -0,0 +1,86 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
var hostEventModal = {
name: 'jobDetail.host-event',
url: '/host-event/:eventId',
controller: 'HostEventController',
params:{
hostResults: {
value: null,
squash: false,
}
},
templateUrl: templateUrl('job-detail/host-event/host-event-modal'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}],
event: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) {
return JobDetailService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId
}).success(function(res){ return res.results[0]})
}]
},
onExit: function($state){
// close the modal
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
$('#HostEvent').modal('hide');
// hacky way to handle user browsing away via URL bar
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
}
var hostEventDetails = {
name: 'jobDetail.host-event.details',
url: '/details',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-details'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}
var hostEventJson = {
name: 'jobDetail.host-event.json',
url: '/json',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-json'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
};
var hostEventTiming = {
name: 'jobDetail.host-event.timing',
url: '/timing',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-timing'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
};
var hostEventStdout = {
name: 'jobDetail.host-event.stdout',
url: '/stdout',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-stdout'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
};
export {hostEventDetails, hostEventJson, hostEventTiming, hostEventStdout, hostEventModal}

View File

@@ -0,0 +1,21 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {hostEventModal, hostEventDetails, hostEventTiming,
hostEventJson, hostEventStdout} from './host-event.route';
import controller from './host-event.controller';
export default
angular.module('jobDetail.hostEvent', [])
.controller('HostEventController', controller)
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(hostEventModal);
$stateExtender.addState(hostEventDetails);
$stateExtender.addState(hostEventTiming);
$stateExtender.addState(hostEventJson);
$stateExtender.addState(hostEventStdout);
}]);

View File

@@ -16,7 +16,7 @@
color: @changed; color: @changed;
} }
.HostEvents-status--failed{ .HostEvents-status--failed{
color: @warning; color: @default-err;
} }
.HostEvents-status--skipped{ .HostEvents-status--skipped{
color: @skipped; color: @skipped;
@@ -47,6 +47,7 @@
padding-bottom: 15px; padding-bottom: 15px;
} }
.HostEvents-title{ .HostEvents-title{
text-transform: uppercase;
color: @default-interface-txt; color: @default-interface-txt;
font-weight: 600; font-weight: 600;
} }

View File

@@ -6,13 +6,14 @@
export default export default
['$stateParams', '$scope', '$rootScope', '$state', 'Wait', ['$stateParams', '$scope', '$rootScope', '$state', 'Wait',
'JobDetailService', 'CreateSelect2', 'JobDetailService', 'CreateSelect2', 'hosts',
function($stateParams, $scope, $rootScope, $state, Wait, function($stateParams, $scope, $rootScope, $state, Wait,
JobDetailService, CreateSelect2){ JobDetailService, CreateSelect2, hosts){
// pagination not implemented yet, but it'll depend on this // pagination not implemented yet, but it'll depend on this
$scope.page_size = $stateParams.page_size; $scope.page_size = $stateParams.page_size;
$scope.processEventStatus = JobDetailService.processEventStatus;
$scope.activeFilter = $stateParams.filter || null; $scope.activeFilter = $stateParams.filter || null;
$scope.search = function(){ $scope.search = function(){
@@ -39,6 +40,7 @@
var filter = function(filter){ var filter = function(filter){
Wait('start'); Wait('start');
if (filter == 'all'){ if (filter == 'all'){
return JobDetailService.getRelatedJobEvents($stateParams.id, { return JobDetailService.getRelatedJobEvents($stateParams.id, {
host_name: $stateParams.hostName, host_name: $stateParams.hostName,
@@ -104,47 +106,17 @@
filter($('.HostEvents-select').val()); 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(){ var init = function(){
$scope.hostName = $stateParams.hostName;
// create filter dropdown // create filter dropdown
console.log($stateParams)
CreateSelect2({ CreateSelect2({
element: '.HostEvents-select', element: '.HostEvents-select',
multiple: false multiple: false
}); });
// process the filter if one was passed // process the filter if one was passed
if ($stateParams.filter){ if ($stateParams.filter){
Wait('start');
filter($stateParams.filter).success(function(res){ filter($stateParams.filter).success(function(res){
$scope.results = res.results; $scope.results = res.results;
Wait('stop'); Wait('stop');
@@ -152,25 +124,11 @@
});; });;
} }
else{ else{
Wait('start'); $scope.results = hosts.data.results;
JobDetailService.getRelatedJobEvents($stateParams.id, { $('#HostEvents').modal('show');
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(); init();

View File

@@ -3,9 +3,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
<div class="HostEvents-header"> <div class="HostEvents-header">
<span class="HostEvents-title">HOST EVENTS</span> <span class="HostEvents-title">HOST EVENTS | {{hostName}}</span>
<!-- Close --> <!-- Close -->
<button ng-click="goBack()" type="button" class="close"> <button ui-sref="jobDetail" type="button" class="close">
<i class="fa fa fa-times-circle"></i> <i class="fa fa fa-times-circle"></i>
</button> </button>
</div> </div>
@@ -29,7 +29,6 @@
<table class="table"> <table class="table">
<!-- column labels --> <!-- column labels -->
<th ng-hide="results.length == 0" class="HostEvents-table--header">STATUS</th> <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">PLAY</th>
<th ng-hide="results.length == 0" class="HostEvents-table--header">TASK</th> <th ng-hide="results.length == 0" class="HostEvents-table--header">TASK</th>
<!-- result rows --> <!-- result rows -->
@@ -37,11 +36,10 @@
<td class=HostEvents-table--cell> <td class=HostEvents-table--cell>
<!-- status circles --> <!-- status circles -->
<a class="HostEvents-status"> <a class="HostEvents-status">
<i class="fa fa-circle" ng-class="processStatus(event, $index)"></i> <i class="fa fa-circle" ng-class="processEventStatus(event)"></i>
</a> </a>
{{event.status}} {{event.status}}
</td> </td>
<td class=HostEvents-table--cell>{{event.host_name}}</td>
<td class=HostEvents-table--cell>{{event.play}}</td> <td class=HostEvents-table--cell>{{event.play}}</td>
<td class=HostEvents-table--cell>{{event.task}}</td> <td class=HostEvents-table--cell>{{event.task}}</td>
</tr> </tr>
@@ -56,7 +54,7 @@
<div class="modal-footer"> <div class="modal-footer">
<!-- pagination --> <!-- pagination -->
<!-- close --> <!-- close -->
<button ng-click="goBack()" class="btn btn-default pull-right HostEvents-close">OK</button> <button ui-sref="jobDetail" class="btn btn-default pull-right HostEvents-close">OK</button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
/************************************************* /*************************************************
* Copyright (c) 2015 Ansible, Inc. * Copyright (c) 2016 Ansible, Inc.
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
@@ -25,6 +25,11 @@ export default {
resolve: { resolve: {
features: ['FeaturesService', function(FeaturesService) { features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get(); return FeaturesService.get();
}] }],
hosts: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) {
return JobDetailService.getRelatedJobEvents($stateParams.id, {
host_name: $stateParams.hostName
}).success(function(res){ return res.results[0]})
}]
} }
}; };

View File

@@ -18,7 +18,7 @@ export default
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit', 'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule', 'ParseTypeChange', 'JobDetailService', 'EventViewer', 'EditSchedule', 'ParseTypeChange', 'JobDetailService',
function( function(
$location, $rootScope, $filter, $scope, $compile, $stateParams, $location, $rootScope, $filter, $scope, $compile, $stateParams,
$log, ClearScope, GetBasePath, Wait, ProcessErrors, $log, ClearScope, GetBasePath, Wait, ProcessErrors,
@@ -27,7 +27,7 @@ export default
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
PlaybookRun, LoadPlays, LoadTasks, LoadHosts, PlaybookRun, LoadPlays, LoadTasks, LoadHosts,
HostsEdit, ParseVariableString, GetChoices, fieldChoices, HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService, EventViewer fieldLabels, EditSchedule, ParseTypeChange, JobDetailService
) { ) {
ClearScope(); ClearScope();
@@ -1119,17 +1119,6 @@ export default
} }
}; };
scope.viewHostResults = function(id) {
EventViewer({
scope: scope,
url: scope.job.related.job_events,
parent_id: scope.selectedTask,
event_id: id,
index: this.$index,
title: 'Host Event'
});
};
if (scope.removeDeleteFinished) { if (scope.removeDeleteFinished) {
scope.removeDeleteFinished(); scope.removeDeleteFinished();
} }

View File

@@ -343,7 +343,9 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index"> <tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column"><a ng-click="viewHostResults(result.id)" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a></td> <td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
<a ui-sref="jobDetail.host-event.details({eventId: result.id, hostResults: hostResults})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a>
</td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>
</tr> </tr>
@@ -422,7 +424,7 @@
<tbody> <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'"> <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"> <td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id})" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a> <a ui-sref="jobDetail.host-events({hostName: host.name})" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a>
</td> </td>
<td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column"> <td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column">
<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: '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>
@@ -480,8 +482,6 @@
</div> </div>
</div> </div>
<div ng-include="'/static/partials/eventviewer.html'"></div>
<div id="host-modal-dialog" style="display: none;" class="dialog-content"></div> <div id="host-modal-dialog" style="display: none;" class="dialog-content"></div>
<div ng-include="'/static/partials/schedule_dialog.html'"></div> <div ng-include="'/static/partials/schedule_dialog.html'"></div>

View File

@@ -2,13 +2,85 @@ export default
['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){ ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){
return { return {
/* /*
For ES6 For ES6
it might be useful to set some default params here, e.g. it might be useful to set some default params here, e.g.
getJobHostSummaries: function(id, page_size=200, order='host_name'){} getJobHostSummaries: function(id, page_size=200, order='host_name'){}
without ES6, we'd have to supply defaults like this: without ES6, we'd have to supply defaults like this:
this.page_size = params.page_size ? params.page_size : 200; this.page_size = params.page_size ? params.page_size : 200;
*/ */
// the the API passes through Ansible's event_data response
// we need to massage away the verbose and redundant properties
processJson: function(data){
// a deep copy
var result = $.extend(true, {}, data);
// configure fields to ignore
var ignored = [
'event_data',
'related',
'summary_fields',
'url',
'ansible_facts',
];
// remove ignored properties
Object.keys(result).forEach(function(key, index){
if (ignored.indexOf(key) > -1) {
delete result[key]
}
});
// flatten Ansible's passed-through response
try{
result.event_data = {};
Object.keys(data.event_data.res).forEach(function(key, index){
if (ignored.indexOf(key) > -1) {
return
}
else{
result.event_data[key] = data.event_data.res[key];
}
});
}
catch(err){result.event_data = null;}
return result
},
processEventStatus: function(event){
// Generate a helper class for job_event statuses
// 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'){
event.status = 'Unreachable';
return 'HostEvents-status--unreachable'
}
// equiv to 'runner_on_error' && 'runner on failed'
if (event.failed){
event.status = 'Failed';
return 'HostEvents-status--failed'
}
// catch the changed case before ok, because both can be true
if (event.changed){
event.status = 'Changed';
return 'HostEvents-status--changed'
}
if (event.event == 'runner_on_ok'){
event.status = 'OK';
return 'HostEvents-status--ok'
}
if (event.event == 'runner_on_skipped'){
event.status = 'Skipped';
return 'HostEvents-status--skipped'
}
else{
// study a case where none of these apply
}
},
// GET events related to a job run // GET events related to a job run
// e.g. // e.g.

View File

@@ -8,10 +8,12 @@ import route from './job-detail.route';
import controller from './job-detail.controller'; import controller from './job-detail.controller';
import service from './job-detail.service'; import service from './job-detail.service';
import hostEvents from './host-events/main'; import hostEvents from './host-events/main';
import hostEvent from './host-event/main';
export default export default
angular.module('jobDetail', [ angular.module('jobDetail', [
hostEvents.name hostEvents.name,
hostEvent.name
]) ])
.controller('JobDetailController', controller) .controller('JobDetailController', controller)
.service('JobDetailService', service) .service('JobDetailService', service)

View File

@@ -23,7 +23,8 @@
set: function(data){ set: function(data){
var defaultUrl = GetBasePath('job_templates'); var defaultUrl = GetBasePath('job_templates');
Rest.setUrl(defaultUrl); Rest.setUrl(defaultUrl);
data.results[0].name = data.results[0].name + ' ' + moment().format('h:mm:ss a'); // 2:49:11 pm var name = this.buildName(data.results[0].name)
data.results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm
return Rest.post(data.results[0]) return Rest.post(data.results[0])
.success(function(res){ .success(function(res){
return res return res
@@ -32,6 +33,10 @@
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
}); });
},
buildName: function(name){
var result = name.split('@')[0];
return result
} }
} }
} }

View File

@@ -76,9 +76,6 @@ export default
},{ },{
name: "OpenStack", name: "OpenStack",
value: "openstack" value: "openstack"
},{
name: "OpenStack V3",
value: "openstack_v3"
}], }],
sourceModel: 'inventory_source', sourceModel: 'inventory_source',
sourceField: 'source', sourceField: 'source',
@@ -87,7 +84,7 @@ export default
has_external_source: { has_external_source: {
label: 'Has external source?', label: 'Has external source?',
searchType: 'in', searchType: 'in',
searchValue: 'ec2,rax,vmware,azure,gce,openstack,openstack_v3', searchValue: 'ec2,rax,vmware,azure,gce,openstack',
searchOnly: true, searchOnly: true,
sourceModel: 'inventory_source', sourceModel: 'inventory_source',
sourceField: 'source' sourceField: 'source'

View File

@@ -51,9 +51,6 @@ export default
},{ },{
name: "OpenStack", name: "OpenStack",
value: "openstack" value: "openstack"
},{
name: "OpenStack V3",
value: "openstack_v3"
}], }],
sourceModel: 'inventory_source', sourceModel: 'inventory_source',
sourceField: 'source', sourceField: 'source',
@@ -62,7 +59,7 @@ export default
has_external_source: { has_external_source: {
label: 'Has external source?', label: 'Has external source?',
searchType: 'in', searchType: 'in',
searchValue: 'ec2,rax,vmware,azure,gce,openstack,openstack_v3', searchValue: 'ec2,rax,vmware,azure,gce,openstack',
searchOnly: true, searchOnly: true,
sourceModel: 'inventory_source', sourceModel: 'inventory_source',
sourceField: 'source' sourceField: 'source'

View File

@@ -1,34 +0,0 @@
<div id="eventviewer-modal-dialog" style="display: none;">
<ul id="eventview-tabs" class="nav nav-tabs">
<li class="active"><a href="#status" id="status-link" data-toggle="tab" ng-click="toggleTab($event, 'status-link', 'eventview-tabs')">Event</a></li>
<li><a href="#results" id="results-link" data-toggle="tab" ng-click="toggleTab($event, 'results-link', 'eventview-tabs')">Results</a></li>
<li><a href="#timing" id="timing-link" data-toggle="tab" ng-click="toggleTab($event, 'timing-link', 'eventview-tabs')">Timing</a></li>
<li><a href="#stdout" id="stdout-link" data-toggle="tab" ng-click="toggleTab($event, 'stdout-link', 'eventview-tabs')">Standard Out</a></li>
<li><a href="#stderr" id="stderr-link" data-toggle="tab" ng-click="toggleTab($event, 'stderr-link', 'eventview-tabs')">Standard Error</a></li>
<li><a href="#traceback" id="traceback-link" data-toggle="tab" ng-click="toggleTab($event, 'traceback-link', 'eventview-tabs')">Traceback</a></li>
<li><a href="#json" id="json-link" data-toggle="tab" ng-click="toggleTab($event, 'json-link', 'eventview-tabs')">JSON</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="status">
<div id="status-form-container"></div>
</div>
<div class="tab-pane" id="results">
<div id="results-form-container"></div>
</div>
<div class="tab-pane" id="timing">
<div id="timing-form-container"></div>
</div>
<div class="tab-pane" id="stdout">
<div id="stdout-form-container"></div>
</div>
<div class="tab-pane" id="stderr">
<div id="stderr-form-container"></div>
</div>
<div class="tab-pane" id="traceback">
<div id="traceback-form-container"></div>
</div>
<div class="tab-pane" id="json">
<div id="json-form-container"></div>
</div>
</div>
</div>

View File

@@ -873,4 +873,20 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
}; };
} }
]); ])
.factory('ParamPass', function() {
var savedData = undefined;
function set(data) {
savedData = data;
}
function get() {
return savedData;
}
return {
set: set,
get: get
}
});

View File

@@ -1394,13 +1394,18 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
"ng-if=is_superuser>Admin</span>"; "ng-if=is_superuser>Admin</span>";
} }
html += "</div>\n"; html += "</div>\n";
if(options.cancelButton !== undefined && options.cancelButton === false) {
html += "<div class=\"Form-exitHolder\">";
html += "</div>";
} else {
html += "<div class=\"Form-exitHolder\">";
html += "<button class=\"Form-exit\" ng-click=\"formCancel()\">";
html += "<i class=\"fa fa-times-circle\"></i>";
html += "</button></div>\n";
}
html += "</div>\n"; //end of Form-header
html += "<div class=\"Form-exitHolder\">";
html += "<button class=\"Form-exit\" ng-click=\"formCancel()\">";
html += "<i class=\"fa fa-times-circle\"></i>";
html += "</button></div>\n";
html += "</div>\n"; //end of Form-header
if (this.form.tabs) { if (this.form.tabs) {
var collection; var collection;

View File

@@ -61,7 +61,8 @@
} }
.OnePlusTwo-left--detailsLabel { .OnePlusTwo-left--detailsLabel {
width: 140px; word-wrap: break-word;
width: 170px;
display: inline-block; display: inline-block;
color: @default-interface-txt; color: @default-interface-txt;
text-transform: uppercase; text-transform: uppercase;

1252
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,29 +13,8 @@ fi
service_action() { service_action() {
SERVICES=$TOWER_SERVICES SERVICES=$TOWER_SERVICES
# When determining whether mongod is required, postgres is required. The
# following ensures mongod is started after postgres, and stopped before
# postgres.
case ${1} in
start|status)
SERVICES="$SERVICES mongod"
;;
stop)
SERVICES="mongod $SERVICES"
;;
esac
for svc in ${SERVICES}; do for svc in ${SERVICES}; do
# Determine whether mongod is needed
if [[ ${svc} == mongod ]]; then
tower-manage uses_mongo --local 2> /dev/null >/dev/null
# if mongod is not required, break
if [ $? -ne 0 ]; then
continue
fi
fi
service ${svc} $1 service ${svc} $1
this_return=$? this_return=$?
if [ $this_return -gt $worst_return ]; then if [ $this_return -gt $worst_return ]; then
@@ -43,7 +22,7 @@ service_action() {
fi fi
# Allow supervisor time to cleanup child pids (ubuntu only) # Allow supervisor time to cleanup child pids (ubuntu only)
if [[ ${svc} == supervisor* && ${1} == stop && -e /etc/debian_version ]]; then if [[ ${svc} == supervisor* && ${1} == stop && -e /etc/debian_version ]]; then
S_PID=$(pidof -x supervisord) S_PID=$(pidof -x supervisord)
echo "Waiting to allow supervisor time to cleanup ... pid ${S_PID}" echo "Waiting to allow supervisor time to cleanup ... pid ${S_PID}"
if [ "${S_PID}" ]; then if [ "${S_PID}" ]; then
i=0 i=0
@@ -76,22 +55,22 @@ case "$1" in
usage usage
;; ;;
start) start)
echo "Starting Tower" echo "Starting Tower"
service_action start service_action start
;; ;;
stop) stop)
echo "Stopping Tower" echo "Stopping Tower"
service_action stop service_action stop
;; ;;
restart) restart)
echo "Restarting Tower" echo "Restarting Tower"
service_action stop service_action stop
service_action start service_action start
;; ;;
status) status)
echo "Showing Tower Status" echo "Showing Tower Status"
service_action status service_action status
;; ;;
*) *)
usage usage
worst_return=1 worst_return=1