diff --git a/awx/api/filters.py b/awx/api/filters.py index 55155224c4..08a26735d2 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -14,7 +14,7 @@ from django.contrib.contenttypes.models import ContentType from django.utils.encoding import force_text # Django REST Framework -from rest_framework.exceptions import ParseError +from rest_framework.exceptions import ParseError, PermissionDenied from rest_framework.filters import BaseFilterBackend # Ansible Tower @@ -97,7 +97,10 @@ class FieldLookupBackend(BaseFilterBackend): new_parts.append(name) - if name == 'pk': + + if name in getattr(model, 'PASSWORD_FIELDS', ()): + raise PermissionDenied('Filtering on password fields is not allowed.') + elif name == 'pk': field = model._meta.pk else: field = model._meta.get_field_by_name(name)[0] diff --git a/awx/api/templates/api/job_template_label_list.md b/awx/api/templates/api/job_template_label_list.md index 76c520eab5..9d503e9c65 100644 --- a/awx/api/templates/api/job_template_label_list.md +++ b/awx/api/templates/api/job_template_label_list.md @@ -2,7 +2,7 @@ Labels not associated with any other resources are deleted. A label can become disassociated with a resource as a result of 3 events. -1. A label is explicitly diassociated with a related job template +1. A label is explicitly disassociated with a related job template 2. A job is deleted with labels 3. A cleanup job deletes a job with labels diff --git a/awx/main/access.py b/awx/main/access.py index 3fd0ee0f0e..39fba096ca 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -764,7 +764,6 @@ class CredentialAccess(BaseAccess): or (not organization_pk and obj.organization): return False - print(self.user in obj.admin_role) return self.user in obj.admin_role def can_delete(self, obj): @@ -1237,17 +1236,32 @@ class JobAccess(BaseAccess): if self.user.is_superuser: return True - # If a user can launch the job template then they can relaunch a job from that - # job template + inventory_access = obj.inventory and self.user in obj.inventory.use_role + credential_access = obj.credential and self.user in obj.credential.use_role + + # Check if JT execute access (and related prompts) is sufficient if obj.job_template is not None: - return self.user in obj.job_template.execute_role + prompts_access = True + job_fields = {} + for fd in obj.job_template._ask_for_vars_dict(): + job_fields[fd] = getattr(obj, fd) + accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields) + for fd in ignored_fields: + if fd != 'extra_vars' and job_fields[fd] != getattr(obj.job_template, fd): + # Job has field that is not promptable + prompts_access = False + if obj.credential != obj.job_template.credential and not credential_access: + prompts_access = False + if obj.inventory != obj.job_template.inventory and not inventory_access: + prompts_access = False + if prompts_access and self.user in obj.job_template.execute_role: + return True - inventory_access = self.user in obj.inventory.use_role - credential_access = self.user in obj.credential.use_role - org_access = self.user in obj.inventory.organization.admin_role + org_access = obj.inventory and self.user in obj.inventory.organization.admin_role project_access = obj.project is None or self.user in obj.project.admin_role + # job can be relaunched if user could make an equivalent JT return inventory_access and credential_access and (org_access or project_access) def can_cancel(self, obj): diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index 6381660948..5013e38aa4 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -51,7 +51,6 @@ class CallbackBrokerWorker(ConsumerMixin): logger.error('Callback Task Processor Raised Exception: %r', exc) message.ack() - class Command(NoArgsCommand): ''' Save Job Callback receiver (see awx.plugins.callbacks.job_event_callback) diff --git a/awx/main/migrations/0033_v303_v245_host_variable_fix.py b/awx/main/migrations/0033_v303_v245_host_variable_fix.py new file mode 100644 index 0000000000..fad3545b65 --- /dev/null +++ b/awx/main/migrations/0033_v303_v245_host_variable_fix.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations +from awx.main.migrations import _migration_utils as migration_utils + + +def update_dashed_host_variables(apps, schema_editor): + Host = apps.get_model('main', 'Host') + for host in Host.objects.filter(variables='---'): + host.variables = '' + host.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0032_v302_credential_permissions_update'), + ] + + operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), + migrations.RunPython(update_dashed_host_variables), + ] diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 35858577a7..b76bcb48a6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -551,7 +551,7 @@ class BaseTask(Task): output_replacements=output_replacements) job_start = time.time() while child.isalive(): - result_id = child.expect(expect_list, timeout=pexpect_timeout) + result_id = child.expect(expect_list, timeout=pexpect_timeout, searchwindowsize=100) if result_id in expect_passwords: child.sendline(expect_passwords[result_id]) if logfile_pos != logfile.tell(): diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index 18060126e1..c934973cf4 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -2,11 +2,7 @@ import pytest from awx.main.models.inventory import Inventory from awx.main.models.credential import Credential -from awx.main.models.jobs import JobTemplate - -@pytest.fixture -def machine_credential(): - return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word') +from awx.main.models.jobs import JobTemplate, Job @pytest.mark.django_db @pytest.mark.job_permissions @@ -45,3 +41,52 @@ def test_inventory_use_access(inventory, user): inventory.use_role.members.add(common_user) assert common_user.can_access(Inventory, 'use', inventory) + +@pytest.mark.django_db +class TestJobRelaunchAccess: + @pytest.fixture + def job_no_prompts(self, machine_credential, inventory): + jt = JobTemplate.objects.create(name='test-job_template', credential=machine_credential, inventory=inventory) + return jt.create_unified_job() + + @pytest.fixture + def job_with_prompts(self, machine_credential, inventory, organization): + jt = JobTemplate.objects.create( + name='test-job-template-prompts', credential=machine_credential, inventory=inventory, + ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True, + ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_inventory_on_launch=True, + ask_credential_on_launch=True) + new_cred = Credential.objects.create(name='new-cred', kind='ssh', username='test_user', password='pas4word') + new_inv = Inventory.objects.create(name='new-inv', organization=organization) + return jt.create_unified_job(credential=new_cred, inventory=new_inv) + + def test_normal_relaunch_via_job_template(self, job_no_prompts, rando): + "Has JT execute_role, job unchanged relative to JT" + job_no_prompts.job_template.execute_role.members.add(rando) + assert rando.can_access(Job, 'start', job_no_prompts) + + def test_no_relaunch_without_prompted_fields_access(self, job_with_prompts, rando): + "Has JT execute_role but no use_role on inventory & credential - deny relaunch" + job_with_prompts.job_template.execute_role.members.add(rando) + assert not rando.can_access(Job, 'start', job_with_prompts) + + def test_can_relaunch_with_prompted_fields_access(self, job_with_prompts, rando): + "Has use_role on the prompted inventory & credential - allow relaunch" + job_with_prompts.job_template.execute_role.members.add(rando) + job_with_prompts.credential.use_role.members.add(rando) + job_with_prompts.inventory.use_role.members.add(rando) + assert rando.can_access(Job, 'start', job_with_prompts) + + def test_no_relaunch_after_limit_change(self, job_no_prompts, rando): + "State of the job contradicts the JT state - deny relaunch" + job_no_prompts.job_template.execute_role.members.add(rando) + job_no_prompts.limit = 'webservers' + job_no_prompts.save() + assert not rando.can_access(Job, 'start', job_no_prompts) + + def test_can_relaunch_if_limit_was_prompt(self, job_with_prompts, rando): + "Job state differs from JT, but only on prompted fields - allow relaunch" + job_with_prompts.job_template.execute_role.members.add(rando) + job_with_prompts.limit = 'webservers' + job_with_prompts.save() + assert not rando.can_access(Job, 'start', job_with_prompts) diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 8f045db877..55ef257567 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -1,7 +1,8 @@ import pytest +from rest_framework.exceptions import PermissionDenied from awx.api.filters import FieldLookupBackend -from awx.main.models import JobTemplate +from awx.main.models import Credential, JobTemplate @pytest.mark.parametrize(u"empty_value", [u'', '']) def test_empty_in(empty_value): @@ -15,3 +16,21 @@ def test_valid_in(valid_value): field_lookup = FieldLookupBackend() value, new_lookup = field_lookup.value_to_python(JobTemplate, 'project__in', valid_value) assert 'foo' in value + +@pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in']) +@pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS) +def test_filter_on_password_field(password_field, lookup_suffix): + field_lookup = FieldLookupBackend() + lookup = '__'.join(filter(None, [password_field, lookup_suffix])) + with pytest.raises(PermissionDenied) as excinfo: + field, new_lookup = field_lookup.get_field_from_lookup(Credential, lookup) + assert 'not allowed' in str(excinfo.value) + +@pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in']) +@pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS) +def test_filter_on_related_password_field(password_field, lookup_suffix): + field_lookup = FieldLookupBackend() + lookup = '__'.join(filter(None, ['credential', password_field, lookup_suffix])) + with pytest.raises(PermissionDenied) as excinfo: + field, new_lookup = field_lookup.get_field_from_lookup(JobTemplate, lookup) + assert 'not allowed' in str(excinfo.value) diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index fa6c34b95e..4e6addcbef 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -9,6 +9,15 @@ from awx.main.access import ( check_superuser, JobTemplateAccess, WorkflowJobTemplateAccess, + SystemJobTemplateAccess, +) + +from awx.main.models import ( + Credential, + Inventory, + Project, + Role, + Organization, ) from awx.conf.license import LicenseForbids from awx.main.models import Credential, Inventory, Project, Role, Organization, Instance @@ -124,7 +133,6 @@ def test_jt_can_add_bad_data(user_unit): access = JobTemplateAccess(user_unit) assert not access.can_add({'asdf': 'asdf'}) - class TestWorkflowAccessMethods: @pytest.fixture def workflow(self, workflow_job_template_factory): @@ -172,3 +180,12 @@ def test_user_capabilities_method(): 'copy': 'foobar' } +def test_system_job_template_can_start(mocker): + user = mocker.MagicMock(spec=User, id=1, is_system_auditor=True, is_superuser=False) + assert user.is_system_auditor + access = SystemJobTemplateAccess(user) + assert not access.can_start(None) + + user.is_superuser = True + access = SystemJobTemplateAccess(user) + assert access.can_start(None) diff --git a/awx/main/tests/unit/test_settings.py b/awx/main/tests/unit/test_settings.py new file mode 100644 index 0000000000..2018771c63 --- /dev/null +++ b/awx/main/tests/unit/test_settings.py @@ -0,0 +1,11 @@ +from split_settings.tools import include + +def test_postprocess_auth_basic_enabled(): + locals().update({'__file__': __file__}) + + include('../../../settings/defaults.py', scope=locals()) + assert 'awx.api.authentication.LoggedBasicAuthentication' in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES'] + + locals().update({'AUTH_BASIC_ENABLED': False}) + include('../../../settings/postprocess.py', scope=locals()) + assert 'awx.api.authentication.LoggedBasicAuthentication' not in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES'] diff --git a/awx/plugins/inventory/azure_rm.ini.example b/awx/plugins/inventory/azure_rm.ini.example index 6ea2688efa..816da16532 100644 --- a/awx/plugins/inventory/azure_rm.ini.example +++ b/awx/plugins/inventory/azure_rm.ini.example @@ -1,5 +1,5 @@ # -# Configuration file for azure_rm_invetory.py +# Configuration file for azure_rm.py # [azure] # Control which resource groups are included. By default all resources groups are included. @@ -9,11 +9,14 @@ # Control which tags are included. Set tags to a comma separated list of keys or key:value pairs #tags= +# Control which locations are included. Set locations to a comma separated list (e.g. eastus,eastus2,westus) +#locations= + # Include powerstate. If you don't need powerstate information, turning it off improves runtime performance. include_powerstate=yes # Control grouping with the following boolean flags. Valid values: yes, no, true, false, True, False, 0, 1. group_by_resource_group=yes group_by_location=yes -group_by_security_group=no +group_by_security_group=yes group_by_tag=yes diff --git a/awx/plugins/inventory/azure_rm.py b/awx/plugins/inventory/azure_rm.py index 0554ecf879..f3c9e7c28d 100755 --- a/awx/plugins/inventory/azure_rm.py +++ b/awx/plugins/inventory/azure_rm.py @@ -76,7 +76,7 @@ required. For a specific host, this script returns the following variables: "version": "latest" }, "location": "westus", - "mac_address": "00-0D-3A-31-2C-EC", + "mac_address": "00-00-5E-00-53-FE", "name": "object-name", "network_interface": "interface-name", "network_interface_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkInterfaces/object-name1", @@ -115,7 +115,7 @@ When run in --list mode, instances are grouped by the following categories: - tag key - tag key_value -Control groups using azure_rm_inventory.ini or set environment variables: +Control groups using azure_rm.ini or set environment variables: AZURE_GROUP_BY_RESOURCE_GROUP=yes AZURE_GROUP_BY_LOCATION=yes @@ -130,6 +130,10 @@ Select hosts for specific tag key by assigning a comma separated list of tag key AZURE_TAGS=key1,key2,key3 +Select hosts for specific locations: + +AZURE_LOCATIONS=eastus,westus,eastus2 + Or, select hosts for specific tag key:value pairs by assigning a comma separated list key:value pairs to: AZURE_TAGS=key1:value1,key2:value2 @@ -137,12 +141,14 @@ AZURE_TAGS=key1:value1,key2:value2 If you don't need the powerstate, you can improve performance by turning off powerstate fetching: AZURE_INCLUDE_POWERSTATE=no -azure_rm_inventory.ini ----------------------- -As mentioned above you can control execution using environment variables or an .ini file. A sample -azure_rm_inventory.ini is included. The name of the .ini file is the basename of the inventory script (in this case -'azure_rm_inventory') with a .ini extension. This provides you with the flexibility of copying and customizing this -script and having matching .ini files. Go forth and customize your Azure inventory! +azure_rm.ini +------------ +As mentioned above, you can control execution using environment variables or a .ini file. A sample +azure_rm.ini is included. The name of the .ini file is the basename of the inventory script (in this case +'azure_rm') with a .ini extension. It also assumes the .ini file is alongside the script. To specify +a different path for the .ini file, define the AZURE_INI_PATH environment variable: + + export AZURE_INI_PATH=/path/to/custom.ini Powerstate: ----------- @@ -152,13 +158,13 @@ up. If the value is anything other than 'running', the machine is down, and will Examples: --------- Execute /bin/uname on all instances in the galaxy-qa resource group - $ ansible -i azure_rm_inventory.py galaxy-qa -m shell -a "/bin/uname -a" + $ ansible -i azure_rm.py galaxy-qa -m shell -a "/bin/uname -a" Use the inventory script to print instance specific information - $ contrib/inventory/azure_rm_inventory.py --host my_instance_host_name --pretty + $ contrib/inventory/azure_rm.py --host my_instance_host_name --pretty Use with a playbook - $ ansible-playbook -i contrib/inventory/azure_rm_inventory.py my_playbook.yml --limit galaxy-qa + $ ansible-playbook -i contrib/inventory/azure_rm.py my_playbook.yml --limit galaxy-qa Insecure Platform Warning @@ -180,11 +186,13 @@ Version: 1.0.0 import argparse import ConfigParser -import json +import json import os import re import sys +from distutils.version import LooseVersion + from os.path import expanduser HAS_AZURE = True @@ -195,12 +203,9 @@ try: from azure.mgmt.compute import __version__ as azure_compute_version from azure.common import AzureMissingResourceHttpError, AzureHttpError from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials - from azure.mgmt.network.network_management_client import NetworkManagementClient,\ - NetworkManagementClientConfiguration - from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient,\ - ResourceManagementClientConfiguration - from azure.mgmt.compute.compute_management_client import ComputeManagementClient,\ - ComputeManagementClientConfiguration + from azure.mgmt.network.network_management_client import NetworkManagementClient + from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient + from azure.mgmt.compute.compute_management_client import ComputeManagementClient except ImportError as exc: HAS_AZURE_EXC = exc HAS_AZURE = False @@ -219,6 +224,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict( AZURE_CONFIG_SETTINGS = dict( resource_groups='AZURE_RESOURCE_GROUPS', tags='AZURE_TAGS', + locations='AZURE_LOCATIONS', include_powerstate='AZURE_INCLUDE_POWERSTATE', group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP', group_by_location='AZURE_GROUP_BY_LOCATION', @@ -226,7 +232,7 @@ AZURE_CONFIG_SETTINGS = dict( group_by_tag='AZURE_GROUP_BY_TAG' ) -AZURE_MIN_VERSION = "2016-03-30" +AZURE_MIN_VERSION = "0.30.0rc5" def azure_id_to_dict(id): @@ -362,8 +368,7 @@ class AzureRM(object): def network_client(self): self.log('Getting network client') if not self._network_client: - self._network_client = NetworkManagementClient( - NetworkManagementClientConfiguration(self.azure_credentials, self.subscription_id)) + self._network_client = NetworkManagementClient(self.azure_credentials, self.subscription_id) self._register('Microsoft.Network') return self._network_client @@ -371,16 +376,14 @@ class AzureRM(object): def rm_client(self): self.log('Getting resource manager client') if not self._resource_client: - self._resource_client = ResourceManagementClient( - ResourceManagementClientConfiguration(self.azure_credentials, self.subscription_id)) + self._resource_client = ResourceManagementClient(self.azure_credentials, self.subscription_id) return self._resource_client @property def compute_client(self): self.log('Getting compute client') if not self._compute_client: - self._compute_client = ComputeManagementClient( - ComputeManagementClientConfiguration(self.azure_credentials, self.subscription_id)) + self._compute_client = ComputeManagementClient(self.azure_credentials, self.subscription_id) self._register('Microsoft.Compute') return self._compute_client @@ -403,6 +406,7 @@ class AzureInventory(object): self.resource_groups = [] self.tags = None + self.locations = None self.replace_dash_in_groups = False self.group_by_resource_group = True self.group_by_location = True @@ -425,6 +429,9 @@ class AzureInventory(object): if self._args.tags: self.tags = self._args.tags.split(',') + if self._args.locations: + self.locations = self._args.locations.split(',') + if self._args.no_powerstate: self.include_powerstate = False @@ -462,6 +469,8 @@ class AzureInventory(object): help='Return inventory for comma separated list of resource group names') parser.add_argument('--tags', action='store', help='Return inventory for comma separated list of tag key:value pairs') + parser.add_argument('--locations', action='store', + help='Return inventory for comma separated list of locations') parser.add_argument('--no-powerstate', action='store_true', default=False, help='Do not include the power state of each virtual host') return parser.parse_args() @@ -487,7 +496,7 @@ class AzureInventory(object): except Exception as exc: sys.exit("Error: fetching virtual machines - {0}".format(str(exc))) - if self._args.host or self.tags > 0: + if self._args.host or self.tags or self.locations: selected_machines = self._selected_machines(virtual_machines) self._load_machines(selected_machines) else: @@ -524,7 +533,7 @@ class AzureInventory(object): resource_group=resource_group, mac_address=None, plan=(machine.plan.name if machine.plan else None), - virtual_machine_size=machine.hardware_profile.vm_size.value, + virtual_machine_size=machine.hardware_profile.vm_size, computer_name=machine.os_profile.computer_name, provisioning_state=machine.provisioning_state, ) @@ -576,7 +585,7 @@ class AzureInventory(object): host_vars['mac_address'] = network_interface.mac_address for ip_config in network_interface.ip_configurations: host_vars['private_ip'] = ip_config.private_ip_address - host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method.value + host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method if ip_config.public_ip_address: public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id) public_ip_address = self._network_client.public_ip_addresses.get( @@ -585,7 +594,7 @@ class AzureInventory(object): host_vars['ansible_host'] = public_ip_address.ip_address host_vars['public_ip'] = public_ip_address.ip_address host_vars['public_ip_name'] = public_ip_address.name - host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method.value + host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method host_vars['public_ip_id'] = public_ip_address.id if public_ip_address.dns_settings: host_vars['fqdn'] = public_ip_address.dns_settings.fqdn @@ -599,6 +608,8 @@ class AzureInventory(object): selected_machines.append(machine) if self.tags and self._tags_match(machine.tags, self.tags): selected_machines.append(machine) + if self.locations and machine.location in self.locations: + selected_machines.append(machine) return selected_machines def _get_security_groups(self, resource_group): @@ -676,17 +687,17 @@ class AzureInventory(object): file_settings = self._load_settings() if file_settings: for key in AZURE_CONFIG_SETTINGS: - if key in ('resource_groups', 'tags') and file_settings.get(key, None) is not None: + if key in ('resource_groups', 'tags', 'locations') and file_settings.get(key): values = file_settings.get(key).split(',') if len(values) > 0: setattr(self, key, values) - elif file_settings.get(key, None) is not None: + elif file_settings.get(key): val = self._to_boolean(file_settings[key]) setattr(self, key, val) else: env_settings = self._get_env_settings() for key in AZURE_CONFIG_SETTINGS: - if key in('resource_groups', 'tags') and env_settings.get(key, None) is not None: + if key in('resource_groups', 'tags', 'locations') and env_settings.get(key): values = env_settings.get(key).split(',') if len(values) > 0: setattr(self, key, values) @@ -719,7 +730,8 @@ class AzureInventory(object): def _load_settings(self): basename = os.path.splitext(os.path.basename(__file__))[0] - path = basename + '.ini' + default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini')) + path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_INI_PATH', default_path))) config = None settings = None try: @@ -774,11 +786,11 @@ class AzureInventory(object): def main(): if not HAS_AZURE: - sys.exit("The Azure python sdk is not installed (try 'pip install azure') - {0}".format(HAS_AZURE_EXC)) + sys.exit("The Azure python sdk is not installed (try 'pip install azure==2.0.0rc5') - {0}".format(HAS_AZURE_EXC)) - if azure_compute_version < AZURE_MIN_VERSION: - sys.exit("Expecting azure.mgmt.compute.__version__ to be >= {0}. Found version {1} " - "Do you have Azure >= 2.0.0rc2 installed?".format(AZURE_MIN_VERSION, azure_compute_version)) + if LooseVersion(azure_compute_version) != LooseVersion(AZURE_MIN_VERSION): + sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} " + "Do you have Azure == 2.0.0rc5 installed?".format(AZURE_MIN_VERSION, azure_compute_version)) AzureInventory() diff --git a/awx/plugins/inventory/rax.py b/awx/plugins/inventory/rax.py index f29e0e8ba0..89ff425717 100755 --- a/awx/plugins/inventory/rax.py +++ b/awx/plugins/inventory/rax.py @@ -155,8 +155,6 @@ import ConfigParser from six import iteritems -from ansible.constants import get_config, mk_boolean - try: import json except ImportError: @@ -166,11 +164,12 @@ try: import pyrax from pyrax.utils import slugify except ImportError: - print('pyrax is required for this module') - sys.exit(1) + sys.exit('pyrax is required for this module') from time import time +from ansible.constants import get_config, mk_boolean + NON_CALLABLES = (basestring, bool, dict, int, list, type(None)) @@ -227,12 +226,21 @@ def _list_into_cache(regions): prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta') - networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', - 'public', islist=True) try: - ip_versions = map(int, get_config(p, 'rax', 'access_ip_version', - 'RAX_ACCESS_IP_VERSION', 4, - islist=True)) + # Ansible 2.3+ + networks = get_config(p, 'rax', 'access_network', + 'RAX_ACCESS_NETWORK', 'public', value_type='list') + except TypeError: + # Ansible 2.2.x and below + networks = get_config(p, 'rax', 'access_network', + 'RAX_ACCESS_NETWORK', 'public', islist=True) + try: + try: + ip_versions = map(int, get_config(p, 'rax', 'access_ip_version', + 'RAX_ACCESS_IP_VERSION', 4, value_type='list')) + except TypeError: + ip_versions = map(int, get_config(p, 'rax', 'access_ip_version', + 'RAX_ACCESS_IP_VERSION', 4, islist=True)) except: ip_versions = [4] else: @@ -406,10 +414,9 @@ def setup(): if os.path.isfile(default_creds_file): creds_file = default_creds_file elif not keyring_username: - sys.stderr.write('No value in environment variable %s and/or no ' - 'credentials file at %s\n' - % ('RAX_CREDS_FILE', default_creds_file)) - sys.exit(1) + sys.exit('No value in environment variable %s and/or no ' + 'credentials file at %s' + % ('RAX_CREDS_FILE', default_creds_file)) identity_type = pyrax.get_setting('identity_type') pyrax.set_setting('identity_type', identity_type or 'rackspace') @@ -422,23 +429,28 @@ def setup(): else: pyrax.set_credential_file(creds_file, region=region) except Exception as e: - sys.stderr.write("%s: %s\n" % (e, e.message)) - sys.exit(1) + sys.exit("%s: %s" % (e, e.message)) regions = [] if region: regions.append(region) else: - region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all', - islist=True) + try: + # Ansible 2.3+ + region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all', + value_type='list') + except TypeError: + # Ansible 2.2.x and below + region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all', + islist=True) + for region in region_list: region = region.strip().upper() if region == 'ALL': regions = pyrax.regions break elif region not in pyrax.regions: - sys.stderr.write('Unsupported region %s' % region) - sys.exit(1) + sys.exit('Unsupported region %s' % region) elif region not in regions: regions.append(region) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index bd5aa72b1a..9c6ed0950b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -494,6 +494,9 @@ AWX_TASK_ENV = {} # before it recycles JOB_EVENT_RECYCLE_THRESHOLD = 3000 +# Number of workers used to proecess job events in parallel +JOB_EVENT_WORKERS = 4 + # Maximum number of job events that can be waiting on a single worker queue before # it can be skipped as too busy JOB_EVENT_MAX_QUEUE_SIZE = 100 @@ -592,6 +595,7 @@ INV_ENV_VARIABLE_BLACKLIST = ("HOME", "USER", "_", "TERM") # http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region EC2_REGION_NAMES = { 'us-east-1': 'US East (Northern Virginia)', + 'us-east-2': 'US East (Ohio)', 'us-west-2': 'US West (Oregon)', 'us-west-1': 'US West (Northern California)', 'eu-central-1': 'EU (Frankfurt)', @@ -600,6 +604,7 @@ EC2_REGION_NAMES = { 'ap-southeast-2': 'Asia Pacific (Sydney)', 'ap-northeast-1': 'Asia Pacific (Tokyo)', 'ap-northeast-2': 'Asia Pacific (Seoul)', + 'ap-south-1': 'Asia Pacific (Mumbai)', 'sa-east-1': 'South America (Sao Paulo)', 'us-gov-west-1': 'US West (GovCloud)', 'cn-north-1': 'China (Beijing)', @@ -739,7 +744,7 @@ OPENSTACK_INSTANCE_ID_VAR = 'openstack.id' # ----- Foreman ----- # --------------------- SATELLITE6_ENABLED_VAR = 'foreman.enabled' -SATELLITE6_ENABLED_VALUE = 'true' +SATELLITE6_ENABLED_VALUE = 'True' SATELLITE6_GROUP_FILTER = r'^.+$' SATELLITE6_HOST_FILTER = r'^.+$' SATELLITE6_EXCLUDE_EMPTY_GROUPS = True diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index e26fcd5ecf..e020319223 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -438,6 +438,16 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, } }); + if ($scope.pathsReadyRemove) { + $scope.pathsReadyRemove(); + } + $scope.pathsReadyRemove = $scope.$on('pathsReady', function () { + CreateSelect2({ + element: '#local-path-select', + multiple: false + }); + }); + // After the project is loaded, retrieve each related set if ($scope.projectLoadedRemove) { $scope.projectLoadedRemove(); @@ -455,6 +465,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, $scope.project_local_paths = opts; $scope.local_path = $scope.project_local_paths[0]; $scope.base_dir = 'You do not have access to view this property'; + $scope.$emit('pathsReady'); } $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; @@ -524,11 +535,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, multiple: false }); - CreateSelect2({ - element: '#local-path-select', - multiple: false - }); - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; $scope.scm_update_tooltip = "Start an SCM update"; $scope.scm_type_class = ""; diff --git a/awx/ui/client/src/forms/Users.js b/awx/ui/client/src/forms/Users.js index 03a70e0c6f..d96dd5b11b 100644 --- a/awx/ui/client/src/forms/Users.js +++ b/awx/ui/client/src/forms/Users.js @@ -157,7 +157,7 @@ export default open: false, index: false, actions: {}, - + emptyListText: 'This user is not a member of any teams', fields: { name: { key: true, diff --git a/awx/ui/client/src/helpers/ProjectPath.js b/awx/ui/client/src/helpers/ProjectPath.js index e0868b7bc4..83753be443 100644 --- a/awx/ui/client/src/helpers/ProjectPath.js +++ b/awx/ui/client/src/helpers/ProjectPath.js @@ -80,6 +80,7 @@ export default // trigger display of alert block when scm_type == manual scope.showMissingPlaybooksAlert = true; } + scope.$emit('pathsReady'); }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index ed90b50a01..ede9a6ef26 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -5,14 +5,14 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'ParseTypeChange', 'GenerateForm', 'inventoryData', - 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', - function($state, $stateParams, $scope, GroupForm, CredentialList, ParseTypeChange, GenerateForm, inventoryData, - GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService) { - var form = GroupForm(); - + 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', 'ToJSON', + function($state, $stateParams, $scope, GroupForm, CredentialList, ParseTypeChange, GenerateForm, inventoryData, + GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, ToJSON) { + var generator = GenerateForm, + form = GroupForm(); init(); - function init() { + function init() {n // apply form definition's default field values GenerateForm.applyDefaults(form, $scope); @@ -46,9 +46,10 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList $scope.formSave = function() { var params, source; + json_data = ToJSON($scope.parseType, $scope.variables, true); // group fields var group = { - variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + variables: json_data, name: $scope.name, description: $scope.description, inventory: inventoryData.id diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index c815fac7c4..805713e909 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'ToggleNotification', 'ParseVariableString', - 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', - function($state, $stateParams, $scope, ToggleNotification, ParseVariableString, - ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData) { + 'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', 'ToJSON', + function($state, $stateParams, $scope, ToggleNotification, ParseVariableString, + ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData, ToJSON) { init(); @@ -58,9 +58,10 @@ export default ['$state', '$stateParams', '$scope', 'ToggleNotification', 'Parse }; $scope.formSave = function() { var params, source; + json_data = ToJSON($scope.parseType, $scope.variables, true); // group fields var group = { - variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + variables: json_data, name: $scope.name, description: $scope.description, inventory: $scope.inventory, diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js index ad7578061a..503baaf584 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-add.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', - 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, - GenerateForm, HostManageService, rbacUiControlService, GetBasePath) { + 'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', 'ToJSON', + function($state, $stateParams, $scope, HostForm, ParseTypeChange, + GenerateForm, HostManageService, rbacUiControlService, GetBasePath, ToJSON) { init(); @@ -36,9 +36,10 @@ export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange $scope.toggleHostEnabled = function() { $scope.host.enabled = !$scope.host.enabled; }; - $scope.formSave = function() { - var params = { - variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + $scope.formSave = function(){ + var json_data = ToJSON($scope.parseType, $scope.variables, true), + params = { + variables: json_data,// $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, name: $scope.name, description: $scope.description, enabled: $scope.host.enabled, diff --git a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js b/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js index a49ca2eddf..4914cf9312 100644 --- a/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/hosts/hosts-edit.controller.js @@ -5,8 +5,8 @@ *************************************************/ export default - ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'HostManageService', 'host', - function($state, $stateParams, $scope, HostForm, ParseTypeChange, HostManageService, host){ + ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'HostManageService', 'host', 'ToJSON', + function($state, $stateParams, $scope, HostForm, ParseTypeChange, HostManageService, host, ToJSON){ init(); @@ -34,9 +34,10 @@ $scope.host.enabled = !$scope.host.enabled; }; $scope.formSave = function(){ - var host = { + var json_data = ToJSON($scope.parseType, $scope.variables, true), + host = { id: $scope.host.id, - variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables, + variables: json_data, name: $scope.name, description: $scope.description, enabled: $scope.host.enabled diff --git a/awx/ui/client/src/job-detail/host-event/host-event.block.less b/awx/ui/client/src/job-detail/host-event/host-event.block.less index 178fae596b..fc84890856 100644 --- a/awx/ui/client/src/job-detail/host-event/host-event.block.less +++ b/awx/ui/client/src/job-detail/host-event/host-event.block.less @@ -125,6 +125,8 @@ .OnePlusTwo-left--detailsRow; } .HostEvent-field--content{ + word-wrap: break-word; + max-width: 13em; flex: 0 1 13em; } .HostEvent-details--left, .HostEvent-details--right{ @@ -138,6 +140,7 @@ flex: 0 1 25em; } .HostEvent-field--content{ + max-width: 15em; flex: 0 1 15em; align-self: flex-end; } diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index 3968eaf484..6c3d861afa 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -130,12 +130,20 @@ export default goToJobDetails('managementJobStdout'); } else if(_.has(data, 'project_update')) { - if($state.current.name !== 'projects') { + // If we are on the projects list or any child state of that list + // then we want to stay on that page. Otherwise go to the stdout + // view. + if(!$state.includes('projects')) { goToJobDetails('scmUpdateStdout'); } } else if(_.has(data, 'inventory_update')) { - goToJobDetails('inventorySyncStdout'); + // If we are on the inventory manage page or any child state of that + // page then we want to stay on that page. Otherwise go to the stdout + // view. + if(!$state.includes('inventoryManage')) { + goToJobDetails('inventorySyncStdout'); + } } } if(scope.clearDialog) { diff --git a/awx/ui/client/src/job-templates/copy/job-templates-copy.service.js b/awx/ui/client/src/job-templates/copy/job-templates-copy.service.js index 53e86a78bb..a6f937cd58 100644 --- a/awx/ui/client/src/job-templates/copy/job-templates-copy.service.js +++ b/awx/ui/client/src/job-templates/copy/job-templates-copy.service.js @@ -5,39 +5,56 @@ *************************************************/ export default - ['$rootScope', 'Rest', 'ProcessErrors', 'GetBasePath', 'moment', - function($rootScope, Rest, ProcessErrors, GetBasePath, moment){ - return { - get: function(id){ - var defaultUrl = GetBasePath('job_templates') + '?id=' + id; - Rest.setUrl(defaultUrl); - return Rest.get() - .success(function(res){ - return res; - }) - .error(function(res, status){ - ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', - msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); - }); - }, - set: function(data){ - var defaultUrl = GetBasePath('job_templates'); - Rest.setUrl(defaultUrl); - var name = this.buildName(data.results[0].name); - data.results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm - return Rest.post(data.results[0]) - .success(function(res){ - return res; - }) - .error(function(res, status){ - ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', - msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); - }); - }, - buildName: function(name){ - var result = name.split('@')[0]; - return result; - } - }; - } - ]; + ['$rootScope', 'Rest', 'ProcessErrors', 'GetBasePath', 'moment', + function($rootScope, Rest, ProcessErrors, GetBasePath, moment){ + return { + get: function(id){ + var defaultUrl = GetBasePath('job_templates') + '?id=' + id; + Rest.setUrl(defaultUrl); + return Rest.get() + .success(function(res){ + return res; + }) + .error(function(res, status){ + ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', + msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); + }); + }, + getSurvey: function(endpoint){ + Rest.setUrl(endpoint); + return Rest.get(); + }, + copySurvey: function(source, target){ + return this.getSurvey(source.related.survey_spec).success( (data) => { + Rest.setUrl(target.related.survey_spec); + return Rest.post(data); + }); + }, + set: function(data){ + var defaultUrl = GetBasePath('job_templates'); + var self = this; + Rest.setUrl(defaultUrl); + var name = this.buildName(data.results[0].name); + data.results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm + return Rest.post(data.results[0]) + .success(function(job_template_res){ + // also copy any associated survey_spec + if (data.results[0].related.survey_spec){ + return self.copySurvey(data.results[0], job_template_res).success( () => job_template_res); + } + else{ + return job_template_res; + } + }) + .error(function(res, status){ + ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', + msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); + }); + }, + buildName: function(name){ + var result = name.split('@')[0]; + return result; + } + }; + } + ]; diff --git a/awx/ui/client/src/login/authenticationServices/pendo.service.js b/awx/ui/client/src/login/authenticationServices/pendo.service.js index 5e034eaa87..10cdbd33d8 100644 --- a/awx/ui/client/src/login/authenticationServices/pendo.service.js +++ b/awx/ui/client/src/login/authenticationServices/pendo.service.js @@ -13,6 +13,7 @@ export default return { setPendoOptions: function (config) { var tower_version = config.version.split('-')[0], + trial = (config.trial) ? config.trial : false, options = { visitor: { id: null, @@ -24,7 +25,7 @@ export default planLevel: config.license_type, planPrice: config.instance_count, creationDate: config.license_date, - trial: config.trial, + trial: trial, tower_version: tower_version, ansible_version: config.ansible_version } @@ -92,49 +93,18 @@ export default return deferred.promise; }, - getConfig: function () { - var config = ConfigService.get(), - deferred = $q.defer(); - if(_.isEmpty(config)){ - var url = GetBasePath('config'); - Rest.setUrl(url); - var promise = Rest.get(); - promise.then(function (response) { - config = response.data.license_info; - config.analytics_status = response.data.analytics_status; - config.version = response.data.version; - config.ansible_version = response.data.ansible_version; - if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){ - $pendolytics.bootstrap(); - deferred.resolve(config); - } - else { - deferred.reject('Pendo is turned off.'); - } - }); - promise.catch(function (response) { - ProcessErrors($rootScope, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory name. GET returned status: ' + - response.status }); - deferred.reject('Could not resolve pendo config.'); - }); - } - else if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){ - $pendolytics.bootstrap(); - deferred.resolve(config); - } - else { - deferred.reject('Pendo is turned off.'); - } - return deferred.promise; - }, - issuePendoIdentity: function () { - var that = this; - this.getConfig().then(function(config){ - var options = that.setPendoOptions(config); - that.setRole(options).then(function(options){ + var config, + options, + c = ConfigService.get(), + config = c.license_info; + config.analytics_status = c.analytics_status; + config.version = c.version; + config.ansible_version = c.ansible_version; + if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){ + $pendolytics.bootstrap(); + options = this.setPendoOptions(config); + this.setRole(options).then(function(options){ $log.debug('Pendo status is '+ config.analytics_status + '. Object below:'); $log.debug(options); $pendolytics.identify(options); @@ -142,10 +112,10 @@ export default // reject function for setRole $log.debug(reason); }); - }, function(reason){ - // reject function for getConfig - $log.debug(reason); - }); + } + else { + $log.debug('Pendo is turned off.') + } } }; } diff --git a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js index 2269c91b81..7d984ca836 100644 --- a/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js +++ b/awx/ui/client/src/organizations/linkout/organizations-linkout.route.js @@ -44,7 +44,6 @@ export default [{ value: { order_by: 'username' } - }, add_user_search: { value: { order_by: 'username', @@ -54,17 +53,15 @@ export default [{ squash: true } }, + ncyBreadcrumb: { + parent: "organizations.edit", + label: "USERS" + }, + data: { activityStream: true, activityStreamTarget: 'organization' }, - ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, - label: "USERS" - }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -129,10 +126,7 @@ export default [{ activityStreamTarget: 'organization' }, ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, + parent: "organizations.edit", label: "TEAMS" }, resolve: { @@ -178,10 +172,7 @@ export default [{ activityStreamTarget: 'organization' }, ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, + parent: "organizations.edit", label: "INVENTORIES" }, resolve: { @@ -232,10 +223,7 @@ export default [{ }, }, ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, + parent: "organizations.edit", label: "PROJECTS" }, resolve: { @@ -293,10 +281,7 @@ export default [{ } }, ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, + parent: "organizations.edit", label: "JOB TEMPLATES" }, resolve: { @@ -367,10 +352,7 @@ export default [{ activityStreamTarget: 'organization' }, ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "organizations.edit"; - }, + parent: "organizations.edit", label: "ADMINS" }, resolve: { diff --git a/awx/ui/client/src/organizations/list/organizations-list.controller.js b/awx/ui/client/src/organizations/list/organizations-list.controller.js index 039b1750c0..7cc4cba6cf 100644 --- a/awx/ui/client/src/organizations/list/organizations-list.controller.js +++ b/awx/ui/client/src/organizations/list/organizations-list.controller.js @@ -12,7 +12,6 @@ export default ['$stateParams', '$scope', '$rootScope', '$location', $log, $compile, Rest, OrganizationList, Alert, Prompt, ClearScope, ProcessErrors, GetBasePath, Wait, $state, rbacUiControlService, $filter, Dataset) { - ClearScope(); var defaultUrl = GetBasePath('organizations'), @@ -85,6 +84,27 @@ export default ['$stateParams', '$scope', '$rootScope', '$location', }); return val; }); + }; + + $scope.$on("ReloadOrgListView", function() { + Rest.setUrl($scope.current_url); + Rest.get() + .success((data) => $scope.organizations = data.results) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status + }); + }); + }); + + + $scope.$watchCollection('organizations', function(value){ + $scope.orgCards = parseCardData(value); + }); + + if ($scope.removePostRefresh) { + $scope.removePostRefresh(); } $scope.$watchCollection(`${list.iterator}_dataset`, function(data) { @@ -129,5 +149,50 @@ export default ['$stateParams', '$scope', '$rootScope', '$location', actionText: 'DELETE' }); }; + var init = function(){ + // Pagination depends on html appended by list generator + view.inject(list, { + id: 'organizations-list', + scope: $scope, + mode: 'edit' + }); + // grab the pagination elements, move, destroy list generator elements + $('#organization-pagination').appendTo('#OrgCards'); + $('#organizations tag-search').appendTo('.OrgCards-search'); + $('#organizations-list').remove(); + + PaginateInit({ + scope: $scope, + list: list, + url: defaultUrl, + pageSize: pageSize, + }); + SearchInit({ + scope: $scope, + list: list, + url: defaultUrl, + set: 'organizations' + }); + + $scope.list = list; + $rootScope.flashMessage = null; + + $scope.search(list.iterator); + var getOrgCount = function() { + Rest.setUrl(defaultUrl); + Rest.get() + .success(function(data) { + $scope.orgCount = data.count; + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status + }); + }); + }; + getOrgCount(); + }; + init(); } ]; diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 06b90a5af2..5e4425c8df 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,11 +2,11 @@ git+https://github.com/chrismeyersfsu/ansiconv.git@tower_1.0.0#egg=ansiconv amqp==1.4.9 anyjson==0.3.3 appdirs==1.4.0 -azure==2.0.0rc2 +azure==2.0.0rc5 Babel==2.2.0 baron==0.6.2 billiard==3.3.0.16 -boto==2.40.0 +boto==2.43.0 celery==3.1.23 cliff==1.15.0 cmd2==0.6.8 @@ -116,7 +116,7 @@ rax-default-network-flags-python-novaclient-ext==0.3.2 rax-scheduled-images-python-novaclient-ext==0.3.1 redbaron==0.6.1 requests-oauthlib==0.5.0 -requests==2.9.1 +requests==2.11.0 requestsexceptions==1.1.1 rply==0.7.4 shade==1.4.0 diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 8d5cfcb4e2..e3d87315d7 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -3,7 +3,7 @@ apache-libcloud==0.20.1 appdirs==1.4.0 azure==2.0.0rc5 Babel==2.2.0 -boto==2.40.0 +boto==2.43.0 cliff==1.15.0 cmd2==0.6.8 cryptography==1.3.2