diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 6fccdb887d..f5c72fed97 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -6,7 +6,7 @@ from collections import OrderedDict # Django from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.utils.encoding import force_text +from django.utils.encoding import force_text, smart_text # Django REST Framework from rest_framework import exceptions @@ -37,6 +37,25 @@ class Metadata(metadata.SimpleMetadata): if value is not None and value != '': 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. # FIXME: Still isn't showing all default values? try: @@ -77,7 +96,7 @@ class Metadata(metadata.SimpleMetadata): # Update type of fields returned... if field.field_name == 'type': - field_info['type'] = 'multiple choice' + field_info['type'] = 'choice' elif field.field_name == 'url': field_info['type'] = 'string' elif field.field_name in ('related', 'summary_fields'): diff --git a/awx/api/pagination.py b/awx/api/pagination.py index 822e6065ee..ee17aee0e1 100644 --- a/awx/api/pagination.py +++ b/awx/api/pagination.py @@ -3,7 +3,7 @@ # Django REST Framework 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): @@ -22,6 +22,4 @@ class Pagination(pagination.PageNumberPagination): return None url = self.request and self.request.get_full_path() or '' 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) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 46e27a2ccc..e9d34c64d7 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError from django.db import models # 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 # Django REST Framework @@ -351,7 +351,6 @@ class BaseSerializer(serializers.ModelSerializer): return obj.modified 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 # 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: 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. if model_field.has_default() and not field_kwargs.get('read_only', False): 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 # 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', []): if isinstance(validator, validators.UniqueValidator): unique_error_message = model_field.error_messages.get('unique', None) @@ -1662,6 +1641,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): d['can_copy'] = 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['labels'] = [{'id': x.id, 'name': x.name} for x in obj.labels.all().order_by('-name')[:10]] return d def validate(self, attrs): @@ -1703,6 +1683,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): res['relaunch'] = reverse('api:job_relaunch', args=(obj.pk,)) 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): # When creating a new job and a job template is specified, populate any # fields not provided in data from the job template. diff --git a/awx/api/templates/api/_result_fields_common.md b/awx/api/templates/api/_result_fields_common.md index 35fc3b55d1..43abefc534 100644 --- a/awx/api/templates/api/_result_fields_common.md +++ b/awx/api/templates/api/_result_fields_common.md @@ -1,6 +1,6 @@ {% for fn, fm in serializer_fields.items %}{% spaceless %} {% 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 %} {% endspaceless %} {% endfor %} diff --git a/awx/api/views.py b/awx/api/views.py index 986a8bc23b..0c406dc610 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -134,6 +134,7 @@ class ApiV1RootView(APIView): data['roles'] = reverse('api:role_list') data['notifiers'] = reverse('api:notifier_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_jobs'] = reverse('api:unified_job_list') data['activity_stream'] = reverse('api:activity_stream_list') diff --git a/awx/main/access.py b/awx/main/access.py index 999962bd99..15237a0ea3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1054,12 +1054,16 @@ class UnifiedJobTemplateAccess(BaseAccess): 'last_job', 'current_job', ) - qs = qs.prefetch_related( - #'project', - 'inventory', - 'credential', - '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', + # 'cloud_credential', + #) return qs.all() @@ -1089,20 +1093,26 @@ class UnifiedJobAccess(BaseAccess): ) qs = qs.prefetch_related( '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() class ScheduleAccess(BaseAccess): diff --git a/awx/main/constants.py b/awx/main/constants.py index a6bdafdf5a..64f6265569 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -1,5 +1,5 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack', 'openstack_v3') +CLOUD_PROVIDERS = ('azure', 'ec2', 'gce', 'rax', 'vmware', 'openstack') SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom',) diff --git a/awx/main/migrations/0001_initial.py b/awx/main/migrations/0001_initial.py index 6d2c78e454..bdc98cace2 100644 --- a/awx/main/migrations/0001_initial.py +++ b/awx/main/migrations/0001_initial.py @@ -381,7 +381,7 @@ class Migration(migrations.Migration): name='AdHocCommand', fields=[ ('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)), ('module_name', models.CharField(default=b'', max_length=1024, blank=True)), ('module_args', models.TextField(default=b'', blank=True)), diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index c97c484c5e..6a780da38a 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -36,7 +36,7 @@ class AdHocCommand(UnifiedJob): job_type = models.CharField( max_length=64, - choices=JOB_TYPE_CHOICES, + choices=AD_HOC_JOB_TYPE_CHOICES, default='run', ) inventory = models.ForeignKey( diff --git a/awx/main/models/base.py b/awx/main/models/base.py index e0f4b72c1a..b97edae8ee 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -29,7 +29,7 @@ __all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN', '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'] PERM_INVENTORY_ADMIN = 'admin' @@ -46,6 +46,11 @@ JOB_TYPE_CHOICES = [ (PERM_INVENTORY_SCAN, _('Scan')), ] +AD_HOC_JOB_TYPE_CHOICES = [ + (PERM_INVENTORY_DEPLOY, _('Run')), + (PERM_INVENTORY_CHECK, _('Check')), +] + PERMISSION_TYPE_CHOICES = [ (PERM_INVENTORY_READ, _('Read Inventory')), (PERM_INVENTORY_WRITE, _('Edit Inventory')), @@ -56,7 +61,7 @@ PERMISSION_TYPE_CHOICES = [ (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 = [ (0, '0 (Normal)'), diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 298560e97e..a4f31c7071 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -40,7 +40,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): ('gce', _('Google Compute Engine')), ('azure', _('Microsoft Azure')), ('openstack', _('OpenStack')), - ('openstack_v3', _('OpenStack V3')), ] BECOME_METHOD_CHOICES = [ @@ -237,18 +236,12 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): host = self.host or '' if not host and self.kind == 'vmware': raise ValidationError('Host required for VMware credential.') - if not host and self.kind in ('openstack', 'openstack_v3'): + if not host and self.kind == 'openstack': raise ValidationError('Host required for OpenStack credential.') return host def clean_domain(self): - """For case of Keystone v3 identity service that requires a - `domain`, that a domain is provided. - """ - domain = self.domain or '' - if not domain and self.kind == 'openstack_v3': - raise ValidationError('Domain required for OpenStack with Keystone v3.') - return domain + return self.domain or '' def clean_username(self): username = self.username or '' @@ -259,7 +252,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): 'credential.') if not username and self.kind == 'vmware': raise ValidationError('Username required for VMware credential.') - if not username and self.kind in ('openstack', 'openstack_v3'): + if not username and self.kind == 'openstack': raise ValidationError('Username required for OpenStack credential.') return username @@ -271,13 +264,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): raise ValidationError('API key required for Rackspace credential.') if not password and self.kind == 'vmware': raise ValidationError('Password required for VMware credential.') - if not password and self.kind in ('openstack', 'openstack_v3'): + if not password and self.kind == 'openstack': raise ValidationError('Password or API key required for OpenStack credential.') return password def clean_project(self): 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.') return project diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 1174c33fd6..0283a5c70c 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -733,7 +733,6 @@ class InventorySourceOptions(BaseModel): ('azure', _('Microsoft Azure')), ('vmware', _('VMware vCenter')), ('openstack', _('OpenStack')), - ('openstack_v3', _('OpenStack V3')), ('custom', _('Custom Script')), ] @@ -962,11 +961,6 @@ class InventorySourceOptions(BaseModel): """I don't think openstack has regions""" return [('all', 'All')] - @classmethod - def get_openstack_v3_region_choices(self): - """Defer to the behavior of openstack""" - return self.get_openstack_region_choices() - def clean_credential(self): if not self.source: return None diff --git a/awx/main/tasks.py b/awx/main/tasks.py index de912b0a05..0fbd322199 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -378,6 +378,10 @@ class BaseTask(Task): if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported: raise RuntimeError(OPENSSH_KEY_ERROR) 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 # 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: @@ -695,7 +699,7 @@ class RunJob(BaseTask): if credential.ssh_key_data not in (None, ''): private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or '' - if job.cloud_credential and job.cloud_credential.kind in ('openstack', 'openstack_v3'): + if job.cloud_credential and job.cloud_credential.kind == 'openstack': credential = job.cloud_credential openstack_auth = dict(auth_url=credential.host, username=credential.username, @@ -787,7 +791,7 @@ class RunJob(BaseTask): env['VMWARE_USER'] = cloud_cred.username env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') env['VMWARE_HOST'] = cloud_cred.host - elif cloud_cred and cloud_cred.kind 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', '') # Set environment variables related to scan jobs @@ -1136,7 +1140,7 @@ class RunInventoryUpdate(BaseTask): credential = inventory_update.credential 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 openstack_auth = dict(auth_url=credential.host, username=credential.username, @@ -1291,7 +1295,7 @@ class RunInventoryUpdate(BaseTask): env['GCE_PROJECT'] = passwords.get('source_project', '') env['GCE_PEM_FILE_PATH'] = cloud_credential env['GCE_ZONE'] = inventory_update.source_regions - elif inventory_update.source in ('openstack', 'openstack_v3'): + elif inventory_update.source == 'openstack': env['OS_CLIENT_CONFIG_FILE'] = cloud_credential elif inventory_update.source == 'file': # FIXME: Parse source_env to dict, update env. @@ -1334,11 +1338,6 @@ class RunInventoryUpdate(BaseTask): # to a shorter variable. :) src = inventory_update.source - # OpenStack V3 has everything in common with OpenStack aside - # from one extra parameter, so share these resources between them. - if src == 'openstack_v3': - src = 'openstack' - # Get the path to the inventory plugin, and append it to our # arguments. plugin_path = self.get_path_to('..', 'plugins', 'inventory', diff --git a/awx/main/tests/data/ssh.py b/awx/main/tests/data/ssh.py index ff5592358e..c2a9a29223 100644 --- a/awx/main/tests/data/ssh.py +++ b/awx/main/tests/data/ssh.py @@ -84,8 +84,7 @@ HPUhg3adAmIJ9z9u/VmTErbVklcKWlyZuTUkxeQ/BJmSIRUQAAAIEA3oKAzdDURjy8zxLX gBLCPdi8AxCiqQJBCsGxXCgKtZewset1XJHIN9ryfb4QSZFkSOlm/LgdeGtS8Or0GNPRYd hgnUCF0LkEsDQ7HzPZYujLrAwjumvGQH6ORp5vRh0tQb93o4e1/A2vpdSKeH7gCe/jfUSY h7dFGNoAI4cF7/0AAAAUcm9vdEBwaWxsb3cuaXhtbS5uZXQBAgMEBQYH ------END OPENSSH PRIVATE KEY----- -''' +-----END OPENSSH PRIVATE KEY-----''' TEST_OPENSSH_KEY_DATA_LOCKED = '''-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABALaWMfjc @@ -114,8 +113,7 @@ C6Oxl1Wsp3gPkK2yiuy8qcrvoEoJ25TeEhUGEAPWx2OuQJO/Lpq9aF/JJoqGwnBaXdCsi+ 5ig+ZMq5GKQtyydzyXImjlNEUH1w2prRDiGVEufANA5LSLCtqOLgDzXS62WUBjJBrQJVAM YpWz1tiZQoyv1RT3Y0O0Vwe2Z5AK3fVM0I5jWdiLrIErtcR4ULa6T56QtA52DufhKzINTR Vg9TtUBqfKIpRQikPSjm7vpY/Xnbc= ------END OPENSSH PRIVATE KEY----- -''' +-----END OPENSSH PRIVATE KEY-----''' TEST_SSH_CERT_KEY = """-----BEGIN CERTIFICATE----- MIIDNTCCAh2gAwIBAgIBATALBgkqhkiG9w0BAQswSTEWMBQGA1UEAwwNV2luZG93 diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index e3787aa1a1..550068da63 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -128,6 +128,7 @@ def test_two_organizations(resourced_organization, organizations, user, get): 'teams': 0 } +@pytest.mark.skip(reason="resolution planned for after RBAC merge") @pytest.mark.django_db @pytest.mark.skipif("True") # XXX: This needs to be implemented def test_JT_associated_with_project(organizations, project, user, get): diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py index fbe765f659..73e1bd5eb5 100644 --- a/awx/main/tests/old/inventory.py +++ b/awx/main/tests/old/inventory.py @@ -1970,7 +1970,7 @@ class InventoryUpdatesTest(BaseTransactionTest): self.check_inventory_source(inventory_source) self.assertFalse(self.group.all_hosts.filter(instance_id='').exists()) - def test_update_from_openstack_v3(self): + def test_update_from_openstack_with_domain(self): # Check that update works with Keystone v3 identity service api_url = getattr(settings, 'TEST_OPENSTACK_HOST_V3', '') api_user = getattr(settings, 'TEST_OPENSTACK_USER', '') @@ -1978,15 +1978,15 @@ class InventoryUpdatesTest(BaseTransactionTest): api_project = getattr(settings, 'TEST_OPENSTACK_PROJECT', '') api_domain = getattr(settings, 'TEST_OPENSTACK_DOMAIN', '') if not all([api_url, api_user, api_password, api_project, api_domain]): - self.skipTest("No test openstack v3 credentials defined") + self.skipTest("No test openstack credentials defined with a domain") self.create_test_license_file() - credential = Credential.objects.create(kind='openstack_v3', + credential = Credential.objects.create(kind='openstack', host=api_url, username=api_user, password=api_password, project=api_project, domain=api_domain) - inventory_source = self.update_inventory_source(self.group, source='openstack_v3', credential=credential) + inventory_source = self.update_inventory_source(self.group, source='openstack', credential=credential) self.check_inventory_source(inventory_source) self.assertFalse(self.group.all_hosts.filter(instance_id='').exists()) @@ -2034,27 +2034,3 @@ class InventoryCredentialTest(BaseTest): self.assertIn('password', response) self.assertIn('host', response) self.assertIn('project', response) - - def test_openstack_v3_create_ok(self): - data = { - 'kind': 'openstack_v3', - 'name': 'Best credential ever', - 'username': 'some_user', - 'password': 'some_password', - 'project': 'some_project', - 'host': 'some_host', - 'domain': 'some_domain', - } - self.post(self.url, data=data, expect=201, auth=self.get_super_credentials()) - - def test_openstack_v3_create_fail_required_fields(self): - data = { - 'kind': 'openstack_v3', - 'name': 'Best credential ever', - } - response = self.post(self.url, data=data, expect=400, auth=self.get_super_credentials()) - self.assertIn('username', response) - self.assertIn('password', response) - self.assertIn('host', response) - self.assertIn('project', response) - self.assertIn('domain', response) diff --git a/awx/static/api/api.js b/awx/static/api/api.js index 177770fb8f..67053ae2f6 100644 --- a/awx/static/api/api.js +++ b/awx/static/api/api.js @@ -43,11 +43,13 @@ $(function() { $('.description').addClass('prettyprint').parent().css('float', 'none'); $('.hidden a.hide-description').prependTo('.description'); $('a.hide-description').click(function() { + $(this).tooltip('hide'); $('.description').slideUp('fast'); return false; }); $('.hidden a.toggle-description').appendTo('.page-header h1'); $('a.toggle-description').click(function() { + $(this).tooltip('hide'); $('.description').slideToggle('fast'); return false; }); @@ -68,6 +70,7 @@ $(function() { }); $('a.resize').click(function() { + $(this).tooltip('hide'); if ($(this).find('span.glyphicon-resize-full').size()) { $(this).find('span.glyphicon').addClass('glyphicon-resize-small').removeClass('glyphicon-resize-full'); $('.container').addClass('container-fluid').removeClass('container'); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 1fe48ad12c..87d3b6b06b 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -26,6 +26,7 @@ import {CredentialsAdd, CredentialsEdit, CredentialsList} from './controllers/Cr import {JobsListController} from './controllers/Jobs'; import {PortalController} from './controllers/Portal'; import systemTracking from './system-tracking/main'; +import inventories from './inventories/main'; import inventoryScripts from './inventory-scripts/main'; import organizations from './organizations/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 OrganizationsAdd from './organizations/add/organizations-add.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 {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; import {TeamsList, TeamsAdd, TeamsEdit} from './controllers/Teams'; @@ -88,6 +89,7 @@ var tower = angular.module('Tower', [ RestServices.name, browserData.name, systemTracking.name, + inventories.name, inventoryScripts.name, organizations.name, permissions.name, @@ -182,7 +184,6 @@ var tower = angular.module('Tower', [ 'LogViewerStatusDefinition', 'StandardOutHelper', 'LogViewerOptionsDefinition', - 'EventViewerHelper', 'JobDetailHelper', 'SocketIO', 'lrInfiniteScroll', @@ -213,6 +214,8 @@ var tower = angular.module('Tower', [ 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(function($injector){ 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', { url: '/organizations/:organization_id/admins', templateUrl: urlPrefix + 'partials/organizations.html', diff --git a/awx/ui/client/src/controllers/Inventories.js b/awx/ui/client/src/controllers/Inventories.js deleted file mode 100644 index 62dfb5b03b..0000000000 --- a/awx/ui/client/src/controllers/Inventories.js +++ /dev/null @@ -1,1296 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Inventories - * @description This controller's for the Inventory page -*/ - -import '../job-templates/main'; - -export function InventoriesList($scope, $rootScope, $location, $log, - $stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList, - generateList, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, Wait, - EditInventoryProperties, 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
. 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 = "\n"; - html += "\n"; - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - html += "\n"; - html += "\n"; - - data.results.forEach(function(row) { - html += "\n"; - html += "\n"; - html += ""; - html += ""; - html += "\n"; - }); - html += "\n"; - html += "
StatusFinishedName
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + ellipsis(row.name) + "
\n"; - } - else { - html = "

No recent job data available for this inventory.

\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 = "\n"; - html += "\n"; - html += ""; - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - html += "\n"; - data.results.forEach( function(row) { - if (row.related.last_update) { - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - } - else { - html += ""; - html += ""; - html += ""; - html += ""; - html += "\n"; - } - }); - html += "\n"; - html += "
StatusLast SyncGroup
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + ellipsis(row.summary_fields.group.name) + "
NA" + ellipsis(row.summary_fields.group.name) + "
\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.editInventoryProperties = function (inventory_id) { - EditInventoryProperties({ scope: $scope, inventory_id: inventory_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: '
Are you sure you want to delete the inventory below?
' + $filter('sanitize')(name) + '
', - 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'); - }; -} - -InventoriesList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', 'generateList', - 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'Wait', 'EditInventoryProperties', 'Find', 'Empty', '$state' -]; - - -export 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.well = true; - 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'); - }; -} - -InventoriesAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'InventoryForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'generateList', - 'OrganizationList', 'SearchInit', 'PaginateInit', 'LookUpInit', - 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', '$state' -]; - -export 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.well = true; - 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: '
Are you sure you want to delete the job template below?
' + this.scan_job_template.name + '
', - action: action, - actionText: 'DELETE' - }); - - }; - -} - -InventoriesEdit.$inject = ['$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' -]; - - - -export 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) { - - 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'); - GroupsEdit({ - scope: $scope, - inventory_id: $scope.inventory.id, - group_id: $scope.selected_group_id, - mode: 'add' - }); - }; - - $scope.editGroup = function (id) { - PreviousSearchParams = Store('group_current_search_params'); - GroupsEdit({ - scope: $scope, - inventory_id: $scope.inventory.id, - group_id: id, - mode: 'edit' - }); - }; - - // 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 ' + - group.name + ' Click the 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 () { - HostsEdit({ - host_scope: hostScope, - group_scope: $scope, - mode: 'add', - host_id: null, - selected_group_id: $scope.selected_group_id, - inventory_id: $scope.inventory.id - }); - }; - - hostScope.editHost = function (host_id) { - HostsEdit({ - host_scope: hostScope, - group_scope: $scope, - mode: 'edit', - host_id: host_id, - inventory_id: $scope.inventory.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.restoreSearch = function() { - SearchInit({ - scope: hostScope, - set: PreviousSearchParams.set, - list: PreviousSearchParams.list, - url: PreviousSearchParams.defaultUrl, - iterator: PreviousSearchParams.iterator, - sort_order: PreviousSearchParams.sort_order, - setWidgets: false - }); - hostScope.search('host'); - };*/ - - 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(); - } - ); -} - - -InventoriesManage.$inject = ['$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' -]; diff --git a/awx/ui/client/src/forms/Credentials.js b/awx/ui/client/src/forms/Credentials.js index 84aaf804e8..4b40179f54 100644 --- a/awx/ui/client/src/forms/Credentials.js +++ b/awx/ui/client/src/forms/Credentials.js @@ -169,7 +169,7 @@ export default "host": { labelBind: 'hostLabel', type: 'text', - ngShow: "kind.value == 'vmware' || kind.value == 'openstack' || kind.value === 'openstack_v3'", + ngShow: "kind.value == 'vmware' || kind.value == 'openstack'", awPopOverWatch: "hostPopOver", awPopOver: "set in helpers/credentials", dataTitle: 'Host', @@ -243,7 +243,7 @@ export default "password": { labelBind: 'passwordLabel', type: 'sensitive', - ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack' || kind.value == 'openstack_v3'", + ngShow: "kind.value == 'scm' || kind.value == 'vmware' || kind.value == 'openstack'", addRequired: false, editRequired: false, ask: false, @@ -338,10 +338,10 @@ export default "project": { labelBind: 'projectLabel', type: 'text', - ngShow: "kind.value == 'gce' || kind.value == 'openstack' || kind.value == 'openstack_v3'", + ngShow: "kind.value == 'gce' || kind.value == 'openstack'", awPopOverWatch: "projectPopOver", awPopOver: "set in helpers/credentials", - dataTitle: 'Project ID', + dataTitle: 'Project Name', dataPlacement: 'right', dataContainer: "body", addRequired: false, @@ -355,18 +355,17 @@ export default "domain": { labelBind: 'domainLabel', type: 'text', - ngShow: "kind.value == 'openstack_v3'", - awPopOverWatch: "domainPopOver", - awPopOver: "set in helpers/credentials", + ngShow: "kind.value == 'openstack'", + awPopOver: "

OpenStack domains define administrative " + + "boundaries. It is only needed for Keystone v3 authentication URLs. " + + "Common scenarios include:

", dataTitle: 'Domain Name', dataPlacement: 'right', dataContainer: "body", addRequired: false, editRequired: false, - awRequiredWhen: { - variable: 'domain_required', - init: false - }, subForm: 'credentialSubForm' }, "vault_password": { diff --git a/awx/ui/client/src/forms/Source.js b/awx/ui/client/src/forms/Source.js index 86e6db5477..1eee07b344 100644 --- a/awx/ui/client/src/forms/Source.js +++ b/awx/ui/client/src/forms/Source.js @@ -169,8 +169,7 @@ export default label: 'Source Variables', //"{{vars_label}}" , ngShow: "source && (source.value == 'vmware' || " + - "source.value == 'openstack' || " + - "source.value == 'openstack_v3')", + "source.value == 'openstack')", type: 'textarea', addRequired: false, class: 'Form-textAreaLabel', diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js index e8190ea50e..aae8a17225 100644 --- a/awx/ui/client/src/helpers.js +++ b/awx/ui/client/src/helpers.js @@ -9,7 +9,6 @@ import './lists'; import Children from "./helpers/Children"; import Credentials from "./helpers/Credentials"; -import EventViewer from "./helpers/EventViewer"; import Events from "./helpers/Events"; import Groups from "./helpers/Groups"; import Hosts from "./helpers/Hosts"; @@ -42,7 +41,6 @@ import ActivityStreamHelper from "./helpers/ActivityStream"; export { Children, Credentials, - EventViewer, Events, Groups, Hosts, diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js index f1af37a011..653ad6b4bf 100644 --- a/awx/ui/client/src/helpers/Credentials.js +++ b/awx/ui/client/src/helpers/Credentials.js @@ -74,7 +74,6 @@ angular.module('CredentialsHelper', ['Utilities']) scope.project_required = false; scope.passwordLabel = 'Password (API Key)'; scope.projectPopOver = "

The project value

"; - scope.domainPopOver = "

The domain name

"; scope.hostPopOver = "

The host value

"; if (!Empty(scope.kind)) { @@ -126,32 +125,17 @@ angular.module('CredentialsHelper', ['Utilities']) break; case 'openstack': scope.hostLabel = "Host (Authentication URL)"; - scope.projectLabel = "Project (Tenet Name/ID)"; - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "

This is the tenant name " + - "or tenant id. This value is usually the same " + - " as the username.

"; - scope.hostPopOver = "

The host to authenticate with." + - "
For example, https://openstack.business.com/v2.0/"; - case 'openstack_v3': - scope.hostLabel = "Host (Authentication URL)"; - scope.projectLabel = "Project (Tenet Name/ID)"; + scope.projectLabel = "Project (Tenant Name)"; scope.domainLabel = "Domain Name"; scope.password_required = true; scope.project_required = true; - scope.domain_required = true; scope.host_required = true; scope.username_required = true; - scope.projectPopOver = "

This is the tenant name " + - "or tenant id. This value is usually the same " + + scope.projectPopOver = "

This is the tenant name. " + + " This value is usually the same " + " as the username.

"; scope.hostPopOver = "

The host to authenticate with." + - "
For example, https://openstack.business.com/v3

"; - scope.domainPopOver = "

Domain used for Keystone v3 " + - "
identity service.

"; + "
For example, https://openstack.business.com/v2.0/"; break; } } diff --git a/awx/ui/client/src/helpers/EventViewer.js b/awx/ui/client/src/helpers/EventViewer.js deleted file mode 100644 index cb075fa5e9..0000000000 --- a/awx/ui/client/src/helpers/EventViewer.js +++ /dev/null @@ -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(''); - } - - 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 += "" + key + ":" + obj[key] + ""; - } - else if (typeof obj[key] === "object" && Array.isArray(obj[key])) { - html += "" + key + ":["; - for (i = 0; i < obj[key].length; i++) { - html += obj[key][i] + ","; - } - html = html.replace(/,$/,''); - html += "]\n"; - } - else if (typeof obj[key] === "object") { - html += "" + key + ":\n\n" + parseObject(obj[key]) + "\n
\n\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 += "" + label + ":"; - if (key === "status") { - html += " " + 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 += "" + itm + ""; - } - else { - if( typeof itm === "string"){ - if(itm.indexOf('<') > -1 || itm.indexOf('>') > -1){ - itm = $filter('sanitize')(itm); - } - } - html += "" + itm + ""; - } - - html += "\n"; - } - else if (typeof itm === "object" && Array.isArray(itm)) { - html += "" + label + ":["; - for (i = 0; i < itm.length; i++) { - html += itm[i] + ","; - } - html = html.replace(/,$/,''); - html += "]\n"; - } - else if (typeof itm === "object") { - html += "" + label + ":\n\n" + parseObject(itm) + "\n
\n\n"; - } - return html; - } - - function parseJSON(obj) { - var h, html = '', key, keys, found = false, string_warnings = "", string_cmd = ""; - if (typeof obj === "object") { - html += "\n"; - html += "\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 += "\n"; - html += "
\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 = "
\n" + - "" + - "
\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 = "
" + val + "
\n"; - $('#' + id).empty().html(html); - }; - }]); diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js index 4e95d96857..eeebb9d8bf 100644 --- a/awx/ui/client/src/helpers/Groups.js +++ b/awx/ui/client/src/helpers/Groups.js @@ -305,8 +305,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name field_id: 'source_extra_vars', onReady: callback }); } if(scope.source.value==="vmware" || - scope.source.value==="openstack" || - scope.source.value==="openstack_v3"){ + scope.source.value==="openstack"){ scope.inventory_variables = (Empty(scope.source_vars)) ? "---" : scope.source_vars; ParseTypeChange({ scope: scope, variable: 'inventory_variables', parse_variable: form.fields.inventory_variables.parseTypeName, field_id: 'source_inventory_variables', onReady: callback }); @@ -316,8 +315,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name scope.source.value==='gce' || scope.source.value === 'azure' || scope.source.value === 'vmware' || - scope.source.value === 'openstack' || - scope.source.value === 'openstack_v3') { + scope.source.value === 'openstack') { if (scope.source.value === 'ec2') { kind = 'aws'; } 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, field_id: 'source_source_vars', onReady: waitStop }); } else if (sources_scope.source && (sources_scope.source.value === 'vmware' || - sources_scope.source.value === 'openstack' || - sources_scope.source.value === 'openstack_v3')) { + sources_scope.source.value === 'openstack')) { Wait('start'); ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: SourceForm.fields.inventory_variables.parseTypeName, 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' || - sources_scope.source.value === 'openstack' || - sources_scope.source.value === 'openstack_v3')) { + sources_scope.source.value === 'openstack')) { data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true); } diff --git a/awx/ui/client/src/helpers/Hosts.js b/awx/ui/client/src/helpers/Hosts.js index f55b1199d2..6a7b864e02 100644 --- a/awx/ui/client/src/helpers/Hosts.js +++ b/awx/ui/client/src/helpers/Hosts.js @@ -437,10 +437,10 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, .factory('HostsEdit', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'HostForm', 'GenerateForm', '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, GetBasePath, HostsReload, ParseTypeChange, Wait, Find, SetStatus, ApplyEllipsis, ToJSON, - ParseVariableString, CreateDialog, TextareaResize) { + ParseVariableString, CreateDialog, TextareaResize, ParamPass) { return function(params) { var parent_scope = params.host_scope, diff --git a/awx/ui/client/src/inventories/add/inventory-add.controller.js b/awx/ui/client/src/inventories/add/inventory-add.controller.js new file mode 100644 index 0000000000..bd3cde3041 --- /dev/null +++ b/awx/ui/client/src/inventories/add/inventory-add.controller.js @@ -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] diff --git a/awx/ui/client/src/inventories/add/inventory-add.route.js b/awx/ui/client/src/inventories/add/inventory-add.route.js new file mode 100644 index 0000000000..50ba5b26a6 --- /dev/null +++ b/awx/ui/client/src/inventories/add/inventory-add.route.js @@ -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(); + }] + } +}; diff --git a/awx/ui/client/src/inventories/add/main.js b/awx/ui/client/src/inventories/add/main.js new file mode 100644 index 0000000000..e12ff940ac --- /dev/null +++ b/awx/ui/client/src/inventories/add/main.js @@ -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); + }]); diff --git a/awx/ui/client/src/inventories/edit/inventory-edit.controller.js b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js new file mode 100644 index 0000000000..f7cb6f2601 --- /dev/null +++ b/awx/ui/client/src/inventories/edit/inventory-edit.controller.js @@ -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: '
Are you sure you want to delete the job template below?
' + this.scan_job_template.name + '
', + 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, +]; diff --git a/awx/ui/client/src/inventories/edit/inventory-edit.route.js b/awx/ui/client/src/inventories/edit/inventory-edit.route.js new file mode 100644 index 0000000000..d721ba92a4 --- /dev/null +++ b/awx/ui/client/src/inventories/edit/inventory-edit.route.js @@ -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(); + }] + } +}; diff --git a/awx/ui/client/src/inventories/edit/main.js b/awx/ui/client/src/inventories/edit/main.js new file mode 100644 index 0000000000..28c99819b7 --- /dev/null +++ b/awx/ui/client/src/inventories/edit/main.js @@ -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); + }]); diff --git a/awx/ui/client/src/partials/inventories.html b/awx/ui/client/src/inventories/inventories.partial.html similarity index 100% rename from awx/ui/client/src/partials/inventories.html rename to awx/ui/client/src/inventories/inventories.partial.html diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js new file mode 100644 index 0000000000..947b1c0341 --- /dev/null +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -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
. 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 = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + html += "\n"; + + data.results.forEach(function(row) { + html += "\n"; + html += "\n"; + html += ""; + html += ""; + html += "\n"; + }); + html += "\n"; + html += "
StatusFinishedName
" + ($filter('longDate')(row.finished)).replace(/ /,'
') + "
" + ellipsis(row.name) + "
\n"; + } + else { + html = "

No recent job data available for this inventory.

\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 = "\n"; + html += "\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + html += "\n"; + data.results.forEach( function(row) { + if (row.related.last_update) { + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + } + else { + html += ""; + html += ""; + html += ""; + html += ""; + html += "\n"; + } + }); + html += "\n"; + html += "
StatusLast SyncGroup
" + ($filter('longDate')(row.last_updated)).replace(/ /,'
') + "
" + ellipsis(row.summary_fields.group.name) + "
NA" + ellipsis(row.summary_fields.group.name) + "
\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: '
Are you sure you want to delete the inventory below?
' + $filter('sanitize')(name) + '
', + 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]; diff --git a/awx/ui/client/src/inventories/list/inventory-list.route.js b/awx/ui/client/src/inventories/list/inventory-list.route.js new file mode 100644 index 0000000000..2804370249 --- /dev/null +++ b/awx/ui/client/src/inventories/list/inventory-list.route.js @@ -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(); + }] + } +}; diff --git a/awx/ui/client/src/inventories/list/main.js b/awx/ui/client/src/inventories/list/main.js new file mode 100644 index 0000000000..4d67816cd7 --- /dev/null +++ b/awx/ui/client/src/inventories/list/main.js @@ -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); + }]); diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js new file mode 100644 index 0000000000..52f9986ef3 --- /dev/null +++ b/awx/ui/client/src/inventories/main.js @@ -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, +]); diff --git a/awx/ui/client/src/inventories/manage/inventory-manage.controller.js b/awx/ui/client/src/inventories/manage/inventory-manage.controller.js new file mode 100644 index 0000000000..508a74d4c2 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/inventory-manage.controller.js @@ -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 ' + + group.name + ' Click the 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, +]; diff --git a/awx/ui/client/src/partials/inventory-manage.html b/awx/ui/client/src/inventories/manage/inventory-manage.partial.html similarity index 99% rename from awx/ui/client/src/partials/inventory-manage.html rename to awx/ui/client/src/inventories/manage/inventory-manage.partial.html index ecae801c20..f465ef47c0 100644 --- a/awx/ui/client/src/partials/inventory-manage.html +++ b/awx/ui/client/src/inventories/manage/inventory-manage.partial.html @@ -10,9 +10,6 @@
- -
-