diff --git a/Makefile b/Makefile index 4c99a5bd71..f79d2405a9 100644 --- a/Makefile +++ b/Makefile @@ -244,14 +244,22 @@ socketservice: factcacher: $(PYTHON) manage.py run_fact_cache_receiver -pep8: - pep8 -r awx/ +reports: + mkdir -p $@ -pyflakes: - pyflakes awx/ +pep8: reports + @(set -o pipefail && $@ | tee reports/$@.report) -check: - flake8 +flake8: reports + @$@ --output-file=reports/$@.report + +pyflakes: reports + @(set -o pipefail && $@ | tee reports/$@.report) + +pylint: reports + @(set -o pipefail && $@ | reports/$@.report) + +check: flake8 pep8 # pyflakes pylint # Run all API unit tests. test: diff --git a/awx/api/permissions.py b/awx/api/permissions.py index cb128bd0dc..df4d86bcdf 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -108,7 +108,7 @@ class ModelAccessPermission(permissions.BasePermission): raise PermissionDenied('your account is inactive') # Always allow superusers (as long as they are active). - if request.user.is_superuser: + if getattr(view, 'always_allow_superuser', True) and request.user.is_superuser: return True # Check if view supports the request method before checking permission diff --git a/awx/api/templates/api/job_template_callback.md b/awx/api/templates/api/job_template_callback.md index 2a62cf9d36..99ae79b42a 100644 --- a/awx/api/templates/api/job_template_callback.md +++ b/awx/api/templates/api/job_template_callback.md @@ -8,11 +8,15 @@ job template. For example, using curl: - curl --data-urlencode host_config_key=HOST_CONFIG_KEY http://server/api/v1/job_templates/N/callback/ + curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v1/job_templates/N/callback/ Or using wget: - wget -O /dev/null --post-data="host_config_key=HOST_CONFIG_KEY" http://server/api/v1/job_templates/N/callback/ + wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v1/job_templates/N/callback/ + +You may also pass `extra_vars` to the callback: + + curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v1/job_templates/N/callback/ The response will return status 202 if the request is valid, 403 for an invalid host config key, or 400 if the host cannot be determined from the diff --git a/awx/api/views.py b/awx/api/views.py index 04c06e31a9..7209f058b0 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1914,6 +1914,9 @@ class JobTemplateCallback(GenericAPIView): return Response(data) def post(self, request, *args, **kwargs): + extra_vars = None + if request.content_type == "application/json": + extra_vars = request.DATA.get("extra_vars", None) # Permission class should have already validated host_config_key. job_template = self.get_object() # Attempt to find matching hosts based on remote address. @@ -1968,8 +1971,10 @@ class JobTemplateCallback(GenericAPIView): job = job_template.create_job(limit=limit, launch_type='callback') # Send a signal to celery that the job should be started. - isau = inventory_sources_already_updated - result = job.signal_start(inventory_sources_already_updated=isau) + kv = {"inventory_sources_already_updated": inventory_sources_already_updated} + if extra_vars is not None: + kv['extra_vars'] = extra_vars + result = job.signal_start(**kv) if not result: data = dict(msg='Error starting job!') return Response(data, status=status.HTTP_400_BAD_REQUEST) @@ -2460,6 +2465,7 @@ class AdHocCommandList(ListCreateAPIView): model = AdHocCommand serializer_class = AdHocCommandListSerializer new_in_220 = True + always_allow_superuser = False @csrf_exempt @transaction.non_atomic_requests diff --git a/awx/main/access.py b/awx/main/access.py index 1cbc50bf09..5a609658ea 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2,6 +2,7 @@ # All Rights Reserved. # Python +import os import sys import logging @@ -147,7 +148,7 @@ class BaseAccess(object): def check_license(self, add_host=False): reader = TaskSerializer() validation_info = reader.from_file() - if 'test' in sys.argv or 'jenkins' in sys.argv: + if ('test' in sys.argv or 'jenkins' in sys.argv) and not os.environ.get('SKIP_LICENSE_FIXUP_FOR_TEST', ''): validation_info['free_instances'] = 99999999 validation_info['time_remaining'] = 99999999 validation_info['grace_period_remaining'] = 99999999 diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index ec8da4dc64..0b4e13414e 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -882,9 +882,8 @@ class Command(NoArgsCommand): continue mem_group = self.all_group.all_groups[group_name] group = self.inventory.groups.create(name=group_name, variables=json.dumps(mem_group.variables), description='imported') - # Access auto one-to-one attribute to create related object. - #group.inventory_source - InventorySource.objects.create(group=group, inventory=self.inventory, name=('%s (%s)' % (group_name, self.inventory.name))) + # Create related inventory source (name will be set by save() method on InventorySource). + InventorySource.objects.create(group=group, inventory=self.inventory) self.logger.info('Group "%s" added', group.name) if inv_src_group and group_name in root_group_names: self._batch_add_m2m(inv_src_group.children, group) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 31944e747b..be3903ccaa 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -192,6 +192,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): host = self.host or '' if not host and self.kind == 'vmware': raise ValidationError('Host required for VMware credential.') + if not host and self.kind == 'openstack': + raise ValidationError('Host required for OpenStack credential.') return host def clean_username(self): @@ -203,6 +205,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): 'credential.') if not username and self.kind == 'vmware': raise ValidationError('Username required for VMware credential.') + if not username and self.kind == 'openstack': + raise ValidationError('Username required for OpenStack credential.') return username def clean_password(self): @@ -213,6 +217,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): raise ValidationError('API key required for Rackspace credential.') if not password and self.kind == 'vmware': raise ValidationError('Password required for VMware credential.') + if not password and self.kind == 'openstack': + raise ValidationError('Password or API key required for OpenStack credential.') return password def clean_project(self): diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6769efc28d..bee15004a7 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1110,12 +1110,14 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): self.inventory = self.group.inventory if 'inventory' not in update_fields: update_fields.append('inventory') - # Set name automatically. + # Set name automatically. Include PK (or placeholder) to make sure the names are always unique. replace_text = '__replace_%s__' % now() old_name_re = re.compile(r'^inventory_source \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.*?$') if not self.name or old_name_re.match(self.name): - if self.inventory and self.group: - self.name = '%s (%s)' % (self.group.name, self.inventory.name) + if self.inventory and self.group and self.pk: + self.name = '%s (%s - %s)' % (self.group.name, self.inventory.name, self.pk) + elif self.inventory and self.group: + self.name = '%s (%s - %s)' % (self.group.name, self.inventory.name, replace_text) elif self.inventory and self.pk: self.name = '%s (%s)' % (self.inventory.name, self.pk) elif self.inventory: @@ -1247,6 +1249,19 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions): def task_impact(self): return 50 + # InventoryUpdate credential required + # Custom InventoryUpdate credential not required + @property + def can_start(self): + if not super(InventoryUpdate, self).can_start: + return False + + if (self.source != 'custom' + and not (self.credential and self.credential.active)): + return False + return True + + class CustomInventoryScript(CommonModelNameNotUnique): class Meta: diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 714ac794de..53dcdad818 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -491,6 +491,17 @@ class Job(UnifiedJob, JobOptions): presets[kw] = getattr(self, kw) return self.job_template.create_unified_job(**presets) + # Job Credential required + @property + def can_start(self): + if not super(Job, self).can_start: + return False + + if not (self.credential and self.credential.active): + return False + + return True + class JobHostSummary(CreatedModifiedModel): ''' Per-host statistics for each job. diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 0a34e18bca..f2ea8e4a9b 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -610,11 +610,15 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique """Return a file-like object containing the standard out of the job's result. """ + msg = { + 'pending': 'Waiting for results...', + 'missing': 'stdout capture is missing', + } if self.result_stdout_text: return StringIO(self.result_stdout_text) else: if not os.path.exists(self.result_stdout_file): - return StringIO("stdout capture is missing") + return StringIO(msg['missing' if self.finished else 'pending']) # There is a potential timing issue here, because another # process may be deleting the stdout file after it is written @@ -631,7 +635,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique self.result_stdout_text = type(self).objects.get(id=self.id).result_stdout_text return self.result_stdout_raw_handle(attempt=attempt + 1) else: - return StringIO("stdout capture is missing") + return StringIO(msg['missing' if self.finished else 'pending']) def _escape_ascii(self, content): ansi_escape = re.compile(r'\x1b[^m]*m') diff --git a/awx/main/tests/__init__.py b/awx/main/tests/__init__.py index 3b5e1fcf12..de61f95cf1 100644 --- a/awx/main/tests/__init__.py +++ b/awx/main/tests/__init__.py @@ -17,3 +17,4 @@ from awx.main.tests.redact import * # noqa from awx.main.tests.views import * # noqa from awx.main.tests.commands import * # noqa from awx.main.tests.fact import * # noqa +from awx.main.tests.unified_jobs import * # noqa diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index b5155ed155..45e0c095e7 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -6,6 +6,7 @@ import glob import os import subprocess import tempfile +import mock # Django from django.conf import settings @@ -15,36 +16,37 @@ from django.core.urlresolvers import reverse from crum import impersonate # AWX +from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.tests.base import BaseJobExecutionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK __all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest'] - class BaseAdHocCommandTest(BaseJobExecutionTest): ''' Common initialization for testing ad hoc commands. ''' def setUp(self): - super(BaseAdHocCommandTest, self).setUp() - self.setup_instances() - self.setup_users() - self.organization = self.make_organizations(self.super_django_user, 1)[0] - self.organization.admins.add(self.normal_django_user) - self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory') - self.host = self.inventory.hosts.create(name='host.example.com') - self.host2 = self.inventory.hosts.create(name='host2.example.com') - self.group = self.inventory.groups.create(name='test-group') - self.group2 = self.inventory.groups.create(name='test-group2') - self.group.hosts.add(self.host) - self.group2.hosts.add(self.host, self.host2) - self.inventory2 = self.organization.inventories.create(name='test-inventory2') - self.host3 = self.inventory2.hosts.create(name='host3.example.com') - self.credential = None - settings.INTERNAL_API_URL = self.live_server_url - settings.CALLBACK_CONSUMER_PORT = '' + with ignore_inventory_computed_fields(): + super(BaseAdHocCommandTest, self).setUp() + self.setup_instances() + self.setup_users() + self.organization = self.make_organizations(self.super_django_user, 1)[0] + self.organization.admins.add(self.normal_django_user) + self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory') + self.host = self.inventory.hosts.create(name='host.example.com') + self.host2 = self.inventory.hosts.create(name='host2.example.com') + self.group = self.inventory.groups.create(name='test-group') + self.group2 = self.inventory.groups.create(name='test-group2') + self.group.hosts.add(self.host) + self.group2.hosts.add(self.host, self.host2) + self.inventory2 = self.organization.inventories.create(name='test-inventory2') + self.host3 = self.inventory2.hosts.create(name='host3.example.com') + self.credential = None + settings.INTERNAL_API_URL = self.live_server_url + settings.CALLBACK_CONSUMER_PORT = '' def create_test_credential(self, **kwargs): self.credential = self.make_credential(**kwargs) @@ -124,7 +126,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.check_job_result(ad_hoc_command, 'failed') self.check_ad_hoc_command_events(ad_hoc_command, 'unreachable') - def test_cancel_ad_hoc_command(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('canceled', 0)) + def test_cancel_ad_hoc_command(self, ignore): ad_hoc_command = self.create_test_ad_hoc_command() self.assertEqual(ad_hoc_command.status, 'new') self.assertFalse(ad_hoc_command.cancel_flag) @@ -145,7 +148,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): # Unable to start ad hoc command again. self.assertFalse(ad_hoc_command.signal_start()) - def test_ad_hoc_command_options(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_ad_hoc_command_options(self, ignore): ad_hoc_command = self.create_test_ad_hoc_command(forks=2, verbosity=2) self.assertEqual(ad_hoc_command.status, 'new') self.assertFalse(ad_hoc_command.passwords_needed_to_start) @@ -191,7 +195,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.check_ad_hoc_command_events(ad_hoc_command3, 'ok', hosts=[]) self.assertEqual(ad_hoc_command3.ad_hoc_command_events.count(), 0) - def test_ssh_username_and_password(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_ssh_username_and_password(self, ignore): self.create_test_credential(username='sshuser', password='sshpass') ad_hoc_command = self.create_test_ad_hoc_command() self.assertEqual(ad_hoc_command.status, 'new') @@ -199,10 +204,11 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start()) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'successful') - self.assertTrue('"-u"' in ad_hoc_command.job_args) - self.assertTrue('"--ask-pass"' in ad_hoc_command.job_args) + self.assertIn('"-u"', ad_hoc_command.job_args) + self.assertIn('"--ask-pass"', ad_hoc_command.job_args) - def test_ssh_ask_password(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_ssh_ask_password(self, ignore): self.create_test_credential(password='ASK') ad_hoc_command = self.create_test_ad_hoc_command() self.assertEqual(ad_hoc_command.status, 'new') @@ -212,9 +218,10 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start(ssh_password='sshpass')) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'successful') - self.assertTrue('"--ask-pass"' in ad_hoc_command.job_args) + self.assertIn('"--ask-pass"', ad_hoc_command.job_args) - def test_sudo_username_and_password(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_sudo_username_and_password(self, ignore): self.create_test_credential(become_method="sudo", become_username='sudouser', become_password='sudopass') @@ -223,15 +230,14 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertFalse(ad_hoc_command.passwords_needed_to_start) self.assertTrue(ad_hoc_command.signal_start()) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) - # Job may fail if current user doesn't have password-less sudo - # privileges, but we're mainly checking the command line arguments. self.check_job_result(ad_hoc_command, ('successful', 'failed')) - self.assertTrue('"--become-method"' in ad_hoc_command.job_args) - self.assertTrue('"--become-user"' in ad_hoc_command.job_args) - self.assertTrue('"--ask-become-pass"' in ad_hoc_command.job_args) - self.assertFalse('"--become"' in ad_hoc_command.job_args) + self.assertIn('"--become-method"', ad_hoc_command.job_args) + self.assertIn('"--become-user"', ad_hoc_command.job_args) + self.assertIn('"--ask-become-pass"', ad_hoc_command.job_args) + self.assertNotIn('"--become"', ad_hoc_command.job_args) - def test_sudo_ask_password(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_sudo_ask_password(self, ignore): self.create_test_credential(become_password='ASK') ad_hoc_command = self.create_test_ad_hoc_command() self.assertEqual(ad_hoc_command.status, 'new') @@ -240,13 +246,13 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertFalse(ad_hoc_command.signal_start()) self.assertTrue(ad_hoc_command.signal_start(become_password='sudopass')) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) - # Job may fail, but we're mainly checking the command line arguments. self.check_job_result(ad_hoc_command, ('successful', 'failed')) - self.assertTrue('"--ask-become-pass"' in ad_hoc_command.job_args) - self.assertFalse('"--become-user"' in ad_hoc_command.job_args) - self.assertFalse('"--become"' in ad_hoc_command.job_args) + self.assertIn('"--ask-become-pass"', ad_hoc_command.job_args) + self.assertNotIn('"--become-user"', ad_hoc_command.job_args) + self.assertNotIn('"--become"', ad_hoc_command.job_args) - def test_unlocked_ssh_key(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('successful', 0)) + def test_unlocked_ssh_key(self, ignore): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA) ad_hoc_command = self.create_test_ad_hoc_command() self.assertEqual(ad_hoc_command.status, 'new') @@ -254,8 +260,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start()) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'successful') - self.assertFalse('"--private-key=' in ad_hoc_command.job_args) - self.assertTrue('ssh-agent' in ad_hoc_command.job_args) + self.assertNotIn('"--private-key=', ad_hoc_command.job_args) + self.assertIn('ssh-agent', ad_hoc_command.job_args) def test_locked_ssh_key_with_password(self): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED, @@ -266,8 +272,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start()) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'successful') - self.assertTrue('ssh-agent' in ad_hoc_command.job_args) - self.assertTrue('Bad passphrase' not in ad_hoc_command.result_stdout) + self.assertIn('ssh-agent', ad_hoc_command.job_args) + self.assertNotIn('Bad passphrase', ad_hoc_command.result_stdout) def test_locked_ssh_key_with_bad_password(self): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED, @@ -278,8 +284,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start()) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'failed') - self.assertTrue('ssh-agent' in ad_hoc_command.job_args) - self.assertTrue('Bad passphrase' in ad_hoc_command.result_stdout) + self.assertIn('ssh-agent', ad_hoc_command.job_args) + self.assertIn('Bad passphrase', ad_hoc_command.result_stdout) def test_locked_ssh_key_ask_password(self): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA_LOCKED, @@ -303,8 +309,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.assertTrue(ad_hoc_command.signal_start(ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK)) ad_hoc_command = AdHocCommand.objects.get(pk=ad_hoc_command.pk) self.check_job_result(ad_hoc_command, 'successful') - self.assertTrue('ssh-agent' in ad_hoc_command.job_args) - self.assertTrue('Bad passphrase' not in ad_hoc_command.result_stdout) + self.assertIn('ssh-agent', ad_hoc_command.job_args) + self.assertNotIn('Bad passphrase', ad_hoc_command.result_stdout) def test_run_with_proot(self): # Only run test if proot is installed @@ -348,7 +354,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.check_job_result(ad_hoc_command, 'successful') self.check_ad_hoc_command_events(ad_hoc_command, 'ok') - def test_run_with_proot_not_installed(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', return_value=('failed', 0)) + def test_run_with_proot_not_installed(self, ignore): # Enable proot for this test, specify invalid proot cmd. settings.AWX_PROOT_ENABLED = True settings.AWX_PROOT_CMD = 'PR00T' @@ -360,6 +367,9 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): self.check_job_result(ad_hoc_command, 'error', expect_traceback=True) +def run_pexpect_mock(self, *args, **kwargs): + return 'successful', 0 + class AdHocCommandApiTest(BaseAdHocCommandTest): ''' Test API list/detail views for ad hoc commands. @@ -385,7 +395,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): del data[k] return self.post(url, data, expect=expect) - def test_ad_hoc_command_list(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_ad_hoc_command_list(self, ignore): url = reverse('api:ad_hoc_command_list') # Retrieve the empty list of ad hoc commands. @@ -557,8 +568,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): with self.current_user('admin'): response = self.run_test_ad_hoc_command(become_enabled=True) self.assertEqual(response['become_enabled'], True) + + # Try to run with expired license. + self.create_expired_license_file() + with self.current_user('admin'): + self.run_test_ad_hoc_command(expect=403) + with self.current_user('normal'): + self.run_test_ad_hoc_command(expect=403) - def test_ad_hoc_command_detail(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_ad_hoc_command_detail(self, ignore): with self.current_user('admin'): response1 = self.run_test_ad_hoc_command() response2 = self.run_test_ad_hoc_command() @@ -622,7 +641,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.delete(url, expect=204) self.delete(url, expect=404) - def test_ad_hoc_command_cancel(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_ad_hoc_command_cancel(self, ignore): # Override setting so that ad hoc command isn't actually started. with self.settings(CELERY_UNIT_TEST=False): with self.current_user('admin'): @@ -674,7 +694,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.assertEqual(response['can_cancel'], False) self.post(url, {}, expect=405) - def test_ad_hoc_command_relaunch(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_ad_hoc_command_relaunch(self, ignore): with self.current_user('admin'): response = self.run_test_ad_hoc_command() @@ -734,7 +755,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.assertEqual(response['passwords_needed_to_start'], []) response = self.post(url, {}, expect=400) + # Try to relaunch with expired license. + with self.current_user('admin'): + response = self.run_test_ad_hoc_command(inventory=self.inventory2.pk) + self.create_expired_license_file() + with self.current_user('admin'): + self.post(response['related']['relaunch'], {}, expect=403) + def test_ad_hoc_command_events_list(self): + # TODO: Create test events instead of relying on playbooks execution + with self.current_user('admin'): response = self.run_test_ad_hoc_command() response = self.run_test_ad_hoc_command() @@ -823,6 +853,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.delete(url, expect=401) def test_ad_hoc_command_event_detail(self): + # TODO: Mock pexpect. Create test events instead of relying on playbooks execution + with self.current_user('admin'): response = self.run_test_ad_hoc_command() @@ -877,7 +909,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.patch(url, {}, expect=401) self.delete(url, expect=401) - def test_ad_hoc_command_activity_stream(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_ad_hoc_command_activity_stream(self, ignore): with self.current_user('admin'): response = self.run_test_ad_hoc_command() @@ -927,7 +960,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.patch(url, {}, expect=401) self.delete(url, expect=401) - def test_inventory_ad_hoc_commands_list(self): + @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) + def test_inventory_ad_hoc_commands_list(self, ignore): with self.current_user('admin'): response = self.run_test_ad_hoc_command() response = self.run_test_ad_hoc_command(inventory=self.inventory2.pk) @@ -1029,7 +1063,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): response = self.get(inventory_url, expect=200) self.assertTrue(response['can_run_ad_hoc_commands']) + # Try to run with expired license. + self.create_expired_license_file() + with self.current_user('admin'): + self.run_test_ad_hoc_command(url=url, expect=403) + with self.current_user('normal'): + self.run_test_ad_hoc_command(url=url, expect=403) + def test_host_ad_hoc_commands_list(self): + # TODO: Figure out why this test needs pexpect + with self.current_user('admin'): response = self.run_test_ad_hoc_command() response = self.run_test_ad_hoc_command(limit=self.host2.name) @@ -1078,7 +1121,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.patch(url, {}, expect=401) self.delete(url, expect=401) + # Try to run with expired license. + self.create_expired_license_file() + with self.current_user('admin'): + self.run_test_ad_hoc_command(url=url, expect=403) + with self.current_user('normal'): + self.run_test_ad_hoc_command(url=url, expect=403) + def test_group_ad_hoc_commands_list(self): + # TODO: Figure out why this test needs pexpect + with self.current_user('admin'): response = self.run_test_ad_hoc_command() # self.host + self.host2 response = self.run_test_ad_hoc_command(limit=self.group.name) # self.host @@ -1132,7 +1184,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.patch(url, {}, expect=401) self.delete(url, expect=401) + # Try to run with expired license. + self.create_expired_license_file() + with self.current_user('admin'): + self.run_test_ad_hoc_command(url=url, expect=403) + with self.current_user('normal'): + self.run_test_ad_hoc_command(url=url, expect=403) + def test_host_ad_hoc_command_events_list(self): + # TODO: Mock run_pexpect. Create test events instead of relying on playbooks execution + with self.current_user('admin'): response = self.run_test_ad_hoc_command() diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index bb38e21d79..10b350d743 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -14,6 +14,7 @@ import time from multiprocessing import Process from subprocess import Popen import re +import mock # PyYAML import yaml @@ -76,8 +77,14 @@ class QueueStartStopTestMixin(QueueTestMixin): super(QueueStartStopTestMixin, self).tearDown() self.terminate_queue() +class MockCommonlySlowTestMixin(object): + def __init__(self, *args, **kwargs): + from awx.api import generics + mock.patch.object(generics, 'get_view_description', return_value=None).start() + super(MockCommonlySlowTestMixin, self).__init__(*args, **kwargs) + ansible_version = get_ansible_version() -class BaseTestMixin(QueueTestMixin): +class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): ''' Mixin with shared code for use by all test cases. ''' @@ -179,6 +186,13 @@ class BaseTestMixin(QueueTestMixin): self._temp_paths.append(license_path) os.environ['AWX_LICENSE_FILE'] = license_path + def create_expired_license_file(self, instance_count=1000, grace_period=False): + license_date = time.time() - 1 + if not grace_period: + license_date -= 2592000 + self.create_test_license_file(instance_count, license_date) + os.environ['SKIP_LICENSE_FIXUP_FOR_TEST'] = '1' + def assertElapsedLessThan(self, seconds): elapsed = time.time() - self._start_time self.assertTrue(elapsed < seconds, 'elapsed time of %0.3fs is greater than %0.3fs' % (elapsed, seconds)) diff --git a/awx/main/tests/commands/commands_monolithic.py b/awx/main/tests/commands/commands_monolithic.py index 972ca8ac9b..1ee6c33bb1 100644 --- a/awx/main/tests/commands/commands_monolithic.py +++ b/awx/main/tests/commands/commands_monolithic.py @@ -341,7 +341,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest): shutil.rmtree(self.test_project_path, True) def create_test_credential(self, **kwargs): - self.credential = self.make_credential(kwargs) + self.credential = self.make_credential(**kwargs) return self.credential def create_test_project(self, playbook_content): @@ -409,6 +409,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(ad_hoc_commands_before, ad_hoc_commands_after) # Create and run job. + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 71a1b826b7..edb14a5850 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -20,7 +20,7 @@ from django.utils.timezone import now from awx.main.models import * # noqa from awx.main.tests.base import BaseTest, BaseTransactionTest -__all__ = ['InventoryTest', 'InventoryUpdatesTest'] +__all__ = ['InventoryTest', 'InventoryUpdatesTest', 'InventoryCredentialTest'] TEST_SIMPLE_INVENTORY_SCRIPT = "#!/usr/bin/env python\nimport json\nprint json.dumps({'hosts': ['ahost-01', 'ahost-02', 'ahost-03', 'ahost-04']})" TEST_SIMPLE_INVENTORY_SCRIPT_WITHOUT_HASHBANG = "import json\nprint json.dumps({'hosts': ['ahost-01', 'ahost-02', 'ahost-03', 'ahost-04']})" @@ -1762,6 +1762,22 @@ class InventoryUpdatesTest(BaseTransactionTest): self.assertTrue(self.group.children.get(name='images').children.filter(active=True).count()) self.assertTrue('instances' in child_names) self.assertTrue(self.group.children.get(name='instances').children.filter(active=True).count()) + # Sync again with overwrite set to False after renaming a group that + # was created by the sync. With overwrite false, the renamed group and + # the original group (created again by the sync) will both exist. + region_group = self.group.children.get(name='regions').children.all()[0] + region_group_original_name = region_group.name + region_group.name = region_group.name + '-renamed' + region_group.save(update_fields=['name']) + cache_path3 = tempfile.mkdtemp(prefix='awx_ec2_') + self._temp_paths.append(cache_path3) + inventory_source.source_vars = '---\n\ncache_path: %s\n' % cache_path3 + inventory_source.overwrite = False + inventory_source.save() + self.check_inventory_source(inventory_source, initial=False, instance_id_group_ok=True) + child_names = self.group.children.filter(active=True).values_list('name', flat=True) + self.assertTrue(region_group_original_name in self.group.children.get(name='regions').children.values_list('name', flat=True)) + self.assertTrue(region_group.name in self.group.children.get(name='regions').children.values_list('name', flat=True)) return # Print out group/host tree for debugging. print @@ -1978,3 +1994,35 @@ class InventoryUpdatesTest(BaseTransactionTest): project=api_project) inventory_source = self.update_inventory_source(self.group, source='openstack', credential=credential) self.check_inventory_source(inventory_source) + + +class InventoryCredentialTest(BaseTest): + def setUp(self): + super(InventoryCredentialTest, self).setUp() + #self.start_redis() + self.setup_instances() + self.setup_users() + + self.url = reverse('api:credential_list') + + def test_openstack_create_ok(self): + data = { + 'kind': 'openstack', + 'name': 'Best credential ever', + 'username': 'some_user', + 'password': 'some_password', + 'project': 'some_project', + 'host': 'some_host', + } + self.post(self.url, data=data, expect=201, auth=self.get_super_credentials()) + + def test_openstack_create_fail_required_fields(self): + data = { + 'kind': 'openstack', + '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) diff --git a/awx/main/tests/jobs/jobs_monolithic.py b/awx/main/tests/jobs/jobs_monolithic.py index 589ae3bb8a..6002e1ddd4 100644 --- a/awx/main/tests/jobs/jobs_monolithic.py +++ b/awx/main/tests/jobs/jobs_monolithic.py @@ -754,6 +754,13 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertEqual(job.hosts.count(), 1) self.assertEqual(job.hosts.all()[0], host) + # Run the callback job again with extra vars and verify their presence + data.update(dict(extra_vars=dict(key="value"))) + result = self.post(url, data, expect=202, remote_addr=host_ip) + jobs_qs = job_template.jobs.filter(launch_type='callback').order_by('-pk') + job = jobs_qs[0] + self.assertTrue("key" in job.extra_vars) + # GET as unauthenticated user will prompt for authentication. self.get(url, expect=401, remote_addr=host_ip) @@ -797,9 +804,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 1) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 2) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 3) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -822,9 +829,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 2) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 3) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 4) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -836,9 +843,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_qs = host_qs.filter(variables__icontains='ansible_ssh_host') host = host_qs[0] host_ip = host.variables_dict['ansible_ssh_host'] - self.assertEqual(jobs_qs.count(), 3) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 4) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 5) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -868,9 +875,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_ip = list(ips)[0] break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 4) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 5) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 6) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name])) diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index cfbb0bd401..0ef05567b3 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -18,6 +18,7 @@ from django.utils.timezone import now from crum import impersonate # AWX +from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.tests.base import BaseJobExecutionTest @@ -345,22 +346,23 @@ class RunJobTest(BaseJobExecutionTest): ''' def setUp(self): - super(RunJobTest, self).setUp() - self.test_project_path = None - self.setup_instances() - self.setup_users() - self.organization = self.make_organizations(self.super_django_user, 1)[0] - self.inventory = self.organization.inventories.create(name='test-inventory', - description='description for test-inventory') - self.host = self.inventory.hosts.create(name='host.example.com') - self.group = self.inventory.groups.create(name='test-group') - self.group2 = self.inventory.groups.create(name='test-group2') - self.group.hosts.add(self.host) - self.group2.hosts.add(self.host) - self.project = None - self.credential = None - self.cloud_credential = None - settings.INTERNAL_API_URL = self.live_server_url + with ignore_inventory_computed_fields(): + super(RunJobTest, self).setUp() + self.test_project_path = None + self.setup_instances() + self.setup_users() + self.organization = self.make_organizations(self.super_django_user, 1)[0] + self.inventory = self.organization.inventories.create(name='test-inventory', + description='description for test-inventory') + self.host = self.inventory.hosts.create(name='host.example.com') + self.group = self.inventory.groups.create(name='test-group') + self.group2 = self.inventory.groups.create(name='test-group2') + self.group.hosts.add(self.host) + self.group2.hosts.add(self.host) + self.project = None + self.credential = None + self.cloud_credential = None + settings.INTERNAL_API_URL = self.live_server_url def tearDown(self): super(RunJobTest, self).tearDown() @@ -562,6 +564,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertEqual(qs.count(), 0) def test_run_job(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -590,6 +593,7 @@ class RunJobTest(BaseJobExecutionTest): return job def test_check_job(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template, job_type='check') @@ -617,6 +621,7 @@ class RunJobTest(BaseJobExecutionTest): return job def test_run_job_that_fails(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK2) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -644,6 +649,7 @@ class RunJobTest(BaseJobExecutionTest): return job def test_run_job_with_ignore_errors(self): + self.create_test_credential() self.create_test_project(TEST_IGNORE_ERRORS_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -766,6 +772,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertEqual(self.host.last_job_host_summary, None) def test_check_job_where_task_would_fail(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK2) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template, job_type='check') @@ -799,6 +806,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertTrue(job.cancel()) # No change from calling again. def test_cancel_job(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template() # Pass save=False just for the sake of test coverage. @@ -824,6 +832,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertFalse(job.signal_start()) def test_extra_job_options(self): + self.create_test_credential() self.create_test_project(TEST_EXTRA_VARS_PLAYBOOK) # Test with extra_vars containing misc whitespace. job_template = self.create_test_job_template(force_handlers=True, @@ -856,6 +865,7 @@ class RunJobTest(BaseJobExecutionTest): self.check_job_result(job3, 'successful') def test_lots_of_extra_vars(self): + self.create_test_credential() self.create_test_project(TEST_EXTRA_VARS_PLAYBOOK) extra_vars = json.dumps(dict(('var_%d' % x, x) for x in xrange(200))) job_template = self.create_test_job_template(extra_vars=extra_vars) @@ -869,6 +879,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertTrue('"-e"' in job.job_args) def test_limit_option(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template(limit='bad.example.com') job = self.create_test_job(job_template=job_template) @@ -893,6 +904,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertTrue('ssh-agent' in job.job_args) def test_tag_and_task_options(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK_WITH_TAGS) job_template = self.create_test_job_template(job_tags='runme', skip_tags='skipme', @@ -972,6 +984,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertFalse('"--become-method"' in job.job_args) def test_job_template_become_enabled(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template(become_enabled=True) job = self.create_test_job(job_template=job_template) @@ -1127,6 +1140,7 @@ class RunJobTest(BaseJobExecutionTest): ssh_key_data=TEST_SSH_CERT_KEY) playbook = TEST_ENV_PLAYBOOK % {'env_var1': env_var1, 'env_var2': env_var2} + self.create_test_credential() self.create_test_project(playbook) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1154,6 +1168,7 @@ class RunJobTest(BaseJobExecutionTest): self._test_cloud_credential_environment_variables('vmware') def test_run_async_job(self): + self.create_test_credential() self.create_test_project(TEST_ASYNC_OK_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1184,6 +1199,7 @@ class RunJobTest(BaseJobExecutionTest): # FIXME: We are not sure why proot needs to be disabled on this test # Maybe they are simply susceptable to timing and proot adds time settings.AWX_PROOT_ENABLED = False + self.create_test_credential() self.create_test_project(TEST_ASYNC_FAIL_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1214,6 +1230,7 @@ class RunJobTest(BaseJobExecutionTest): # FIXME: We are not sure why proot needs to be disabled on this test # Maybe they are simply susceptable to timing and proot adds time settings.AWX_PROOT_ENABLED = False + self.create_test_credential() self.create_test_project(TEST_ASYNC_TIMEOUT_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1242,6 +1259,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertEqual(job.processed_hosts.count(), 1) def test_run_async_job_fire_and_forget(self): + self.create_test_credential() self.create_test_project(TEST_ASYNC_NOWAIT_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1269,6 +1287,7 @@ class RunJobTest(BaseJobExecutionTest): self.assertEqual(job.processed_hosts.count(), 1) def test_run_job_with_roles(self): + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK_WITH_ROLES, TEST_ROLE_PLAYBOOKS) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) @@ -1299,6 +1318,7 @@ class RunJobTest(BaseJobExecutionTest): settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] # Create another project alongside the one we're using to verify it # is hidden. + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) other_project_path = self.project.local_path # Create a temp directory that should not be visible to the playbook. @@ -1334,6 +1354,7 @@ class RunJobTest(BaseJobExecutionTest): # Enable proot for this test, specify invalid proot cmd. settings.AWX_PROOT_ENABLED = True settings.AWX_PROOT_CMD = 'PR00T' + self.create_test_credential() self.create_test_project(TEST_PLAYBOOK) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) diff --git a/awx/main/tests/unified_jobs.py b/awx/main/tests/unified_jobs.py new file mode 100644 index 0000000000..607feeec4e --- /dev/null +++ b/awx/main/tests/unified_jobs.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved + +# Python +import mock +from StringIO import StringIO +from django.utils.timezone import now + +# Django +from django.test import SimpleTestCase + +# AWX +from awx.main.models import * # noqa + +__all__ = ['UnifiedJobsUnitTest',] + +class UnifiedJobsUnitTest(SimpleTestCase): + + # stdout file present + @mock.patch('os.path.exists', return_value=True) + @mock.patch('codecs.open', return_value='my_file_handler') + def test_result_stdout_raw_handle_file__found(self, exists, open): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + + result = unified_job.result_stdout_raw_handle() + + self.assertEqual(result, 'my_file_handler') + + # stdout file missing, job finished + @mock.patch('os.path.exists', return_value=False) + def test_result_stdout_raw_handle__missing(self, exists): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + unified_job.finished = now() + + result = unified_job.result_stdout_raw_handle() + + self.assertIsInstance(result, StringIO) + self.assertEqual(result.read(), 'stdout capture is missing') + + # stdout file missing, job not finished + @mock.patch('os.path.exists', return_value=False) + def test_result_stdout_raw_handle__pending(self, exists): + unified_job = UnifiedJob() + unified_job.result_stdout_file = 'dummy' + unified_job.finished = None + + result = unified_job.result_stdout_raw_handle() + + self.assertIsInstance(result, StringIO) + self.assertEqual(result.read(), 'Waiting for results...') + diff --git a/awx/plugins/callback/job_event_callback.py b/awx/plugins/callback/job_event_callback.py index 3f91c55b19..81f4a00b92 100644 --- a/awx/plugins/callback/job_event_callback.py +++ b/awx/plugins/callback/job_event_callback.py @@ -36,11 +36,8 @@ import json import logging import os import pwd -import sys -import urllib import urlparse import time -from contextlib import closing # Requests import requests diff --git a/awx/plugins/fact_caching/tower.py b/awx/plugins/fact_caching/tower.py index 5d0516b92b..eaa404e744 100755 --- a/awx/plugins/fact_caching/tower.py +++ b/awx/plugins/fact_caching/tower.py @@ -32,7 +32,6 @@ import sys import time import datetime -import json from copy import deepcopy from ansible import constants as C from ansible.cache.base import BaseCacheModule diff --git a/awx/plugins/inventory/awxrest.py b/awx/plugins/inventory/awxrest.py index 347ecb0961..2ab30dedd4 100755 --- a/awx/plugins/inventory/awxrest.py +++ b/awx/plugins/inventory/awxrest.py @@ -4,10 +4,10 @@ # This file is a utility script that is not part of the AWX or Ansible # packages. It does not import any code from either package, nor does its # license apply to Ansible or AWX. -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # @@ -61,7 +61,7 @@ class TokenAuth(requests.auth.AuthBase): return request class InventoryScript(object): - + def __init__(self, **options): self.options = options @@ -95,11 +95,11 @@ class InventoryScript(object): def run(self): try: self.base_url = self.options.get('base_url', '') or \ - os.getenv('REST_API_URL', '') + os.getenv('REST_API_URL', '') if not self.base_url: raise ValueError('No REST API URL specified') self.auth_token = self.options.get('authtoken', '') or \ - os.getenv('REST_API_TOKEN', '') + os.getenv('REST_API_TOKEN', '') parts = urlparse.urlsplit(self.base_url) if not (parts.username and parts.password) and not self.auth_token: raise ValueError('No username/password specified in REST API ' @@ -107,7 +107,7 @@ class InventoryScript(object): try: # Command line argument takes precedence over environment # variable. - self.inventory_id = int(self.options.get('inventory_id', 0) or \ + self.inventory_id = int(self.options.get('inventory_id', 0) or os.getenv('INVENTORY_ID', 0)) except ValueError: raise ValueError('Inventory ID must be an integer') diff --git a/awx/plugins/library/scan_services.py b/awx/plugins/library/scan_services.py index 920daa860c..0db0c3d4c5 100644 --- a/awx/plugins/library/scan_services.py +++ b/awx/plugins/library/scan_services.py @@ -84,7 +84,7 @@ class ServiceScanService(BaseService): else: pid = None else: - pid = None + pid = None # NOQA payload = {"name": service_name, "state": service_state, "goal": service_goal, "source": "upstart"} services.append(payload) @@ -104,7 +104,7 @@ class ServiceScanService(BaseService): service_state = "dead" elif len(line_data) == 3: service_name = line_data[0] - service_pid = None + service_pid = None # NOQA service_state = "stopped" else: continue diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 18146a9042..7b7d4fa0e7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -3,9 +3,7 @@ import os import sys -import glob from datetime import timedelta -import tempfile MONGO_DB = 'system_tracking' @@ -119,13 +117,13 @@ ALLOWED_HOSTS = [] # reverse proxy. REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] -TEMPLATE_CONTEXT_PROCESSORS += ( +TEMPLATE_CONTEXT_PROCESSORS += ( # NOQA 'django.core.context_processors.request', 'awx.ui.context_processors.settings', 'awx.ui.context_processors.version', ) -MIDDLEWARE_CLASSES += ( +MIDDLEWARE_CLASSES += ( # NOQA 'awx.main.middleware.HAMiddleware', 'awx.main.middleware.ActivityStreamMiddleware', 'crum.CurrentRequestUserMiddleware', @@ -135,6 +133,13 @@ TEMPLATE_DIRS = ( os.path.join(BASE_DIR, 'templates'), ) +TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), +) + ROOT_URLCONF = 'awx.urls' WSGI_APPLICATION = 'awx.wsgi.application' @@ -247,7 +252,7 @@ EMAIL_USE_TLS = False # Use Django-Debug-Toolbar if installed. try: import debug_toolbar - INSTALLED_APPS += ('debug_toolbar',) + INSTALLED_APPS += (debug_toolbar.__name__,) except ImportError: pass @@ -259,7 +264,7 @@ DEBUG_TOOLBAR_CONFIG = { # Use Django-devserver if installed. try: import devserver - INSTALLED_APPS += ('devserver',) + INSTALLED_APPS += (devserver.__name__,) except ImportError: pass @@ -444,7 +449,7 @@ VMWARE_REGIONS_BLACKLIST = [] # Inventory variable name/values for determining whether a host is # active in vSphere. -VMWARE_ENABLED_VAR = 'vmware_powerState' +VMWARE_ENABLED_VAR = 'vmware_powerState' VMWARE_ENABLED_VALUE = 'poweredOn' # Inventory variable name containing the unique instance ID. @@ -609,7 +614,7 @@ LOGGING = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'tower_warnings.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', }, @@ -618,7 +623,7 @@ LOGGING = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'callback_receiver.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', }, @@ -627,7 +632,7 @@ LOGGING = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'socketio_service.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', }, @@ -636,7 +641,7 @@ LOGGING = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'task_system.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', }, diff --git a/awx/settings/development.py b/awx/settings/development.py index a5bbfa36ac..abc983af73 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -6,13 +6,12 @@ # Python import sys import traceback -import glob # Django Split Settings from split_settings.tools import optional, include # Load default settings. -from defaults import * +from defaults import * # NOQA MONGO_DB = 'system_tracking_dev' @@ -31,27 +30,24 @@ AWX_PROOT_ENABLED = True # Use Django-Jenkins if installed. Only run tests for awx.main app. try: import django_jenkins - INSTALLED_APPS += ('django_jenkins',) + INSTALLED_APPS += (django_jenkins.__name__,) PROJECT_APPS = ('awx.main.tests', 'awx.api.tests', 'awx.fact.tests',) except ImportError: pass if 'django_jenkins' in INSTALLED_APPS: JENKINS_TASKS = ( - 'django_jenkins.tasks.run_pylint', - 'django_jenkins.tasks.run_flake8', + # 'django_jenkins.tasks.run_pylint', + # 'django_jenkins.tasks.run_flake8', # The following are not needed when including run_flake8 # 'django_jenkins.tasks.run_pep8', # 'django_jenkins.tasks.run_pyflakes', # The following are handled by various grunt tasks and no longer required # 'django_jenkins.tasks.run_jshint', # 'django_jenkins.tasks.run_csslint', - ) + ) PEP8_RCFILE = "setup.cfg" PYLINT_RCFILE = ".pylintrc" - CSSLINT_CHECKED_FILES = glob.glob(os.path.join(BASE_DIR, 'ui/static/less/*.less')) - JSHINT_CHECKED_FILES = [os.path.join(BASE_DIR, 'ui/static/js'), - os.path.join(BASE_DIR, 'ui/static/lib/ansible'),] # Much faster than the default # https://docs.djangoproject.com/en/1.6/topics/auth/passwords/#how-django-stores-passwords diff --git a/awx/settings/development_quiet.py b/awx/settings/development_quiet.py index 282319b503..ef017f7340 100644 --- a/awx/settings/development_quiet.py +++ b/awx/settings/development_quiet.py @@ -4,10 +4,10 @@ # Development settings for AWX project, but with DEBUG disabled # Load development settings. -from defaults import * +from defaults import * # NOQA # Load development settings. -from development import * +from development import * # NOQA # Disable capturing DEBUG DEBUG = False diff --git a/awx/settings/production.py b/awx/settings/production.py index 2fa3c439fe..2552d91ce6 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -12,7 +12,7 @@ import traceback from split_settings.tools import optional, include # Load default settings. -from defaults import * +from defaults import * # NOQA DEBUG = False TEMPLATE_DEBUG = DEBUG @@ -49,7 +49,7 @@ LOGGING['handlers']['tower_warnings'] = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': '/var/log/tower/tower.log', - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', } @@ -60,7 +60,7 @@ LOGGING['handlers']['callback_receiver'] = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': '/var/log/tower/callback_receiver.log', - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', } @@ -70,7 +70,7 @@ LOGGING['handlers']['socketio_service'] = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': '/var/log/tower/socketio_service.log', - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', } @@ -80,7 +80,7 @@ LOGGING['handlers']['task_system'] = { 'class':'logging.handlers.RotatingFileHandler', 'filters': ['require_debug_false'], 'filename': '/var/log/tower/task_system.log', - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, 'formatter':'simple', } diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 2ee0160c33..7cebb19339 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -220,6 +220,18 @@ var tower = angular.module('Tower', [ resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); + }], + jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.event_socket) { + $rootScope.event_socket = Socket({ + scope: $rootScope, + endpoint: "job_events" + }); + $rootScope.event_socket.init(); + return true; + } else { + return true; + } }] } }). @@ -231,6 +243,18 @@ var tower = angular.module('Tower', [ resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); + }], + jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.event_socket) { + $rootScope.event_socket = Socket({ + scope: $rootScope, + endpoint: "job_events" + }); + $rootScope.event_socket.init(); + return true; + } else { + return true; + } }] } }). @@ -242,6 +266,18 @@ var tower = angular.module('Tower', [ resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); + }], + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.adhoc_event_socket) { + $rootScope.adhoc_event_socket = Socket({ + scope: $rootScope, + endpoint: "ad_hoc_command_events" + }); + $rootScope.adhoc_event_socket.init(); + return true; + } else { + return true; + } }] } }). @@ -942,16 +978,50 @@ var tower = angular.module('Tower', [ // Listen for job changes and issue callbacks to initiate // DOM updates function openSocket() { + var schedule_socket; + sock = Socket({ scope: $rootScope, endpoint: "jobs" }); sock.init(); sock.on("status_changed", function(data) { - $log.debug('Job ' + data.unified_job_id + ' status changed to ' + data.status); - $rootScope.$emit('JobStatusChange', data); + $log.debug('Job ' + data.unified_job_id + + ' status changed to ' + data.status + + ' send to ' + $location.$$url); + + // this acts as a router...it emits the proper + // value based on what URL the user is currently + // accessing. + if ($location.$$url === '/jobs') { + $rootScope.$emit('JobStatusChange-jobs', data); + } else if (/\/jobs\/(\d)+\/stdout/.test($location.$$url) || + /\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { + $log.debug("sending status to standard out"); + $rootScope.$emit('JobStatusChange-jobStdout', data); + } else if (/\/jobs\/(\d)+/.test($location.$$url)) { + $rootScope.$emit('JobStatusChange-jobDetails', data); + } else if ($location.$$url === '/home') { + $rootScope.$emit('JobStatusChange-home', data); + } else if ($location.$$url === '/portal') { + $rootScope.$emit('JobStatusChange-portal', data); + } else if ($location.$$url === '/projects') { + $rootScope.$emit('JobStatusChange-projects', data); + } else if (/\/inventory\/(\d)+\/manage/.test($location.$$url)) { + $rootScope.$emit('JobStatusChange-inventory', data); + } }); sock.on("summary_complete", function(data) { $log.debug('Job summary_complete ' + data.unified_job_id); $rootScope.$emit('JobSummaryComplete', data); }); + + schedule_socket = Socket({ + scope: $rootScope, + endpoint: "schedules" + }); + schedule_socket.init(); + schedule_socket.on("schedule_changed", function(data) { + $log.debug('Schedule ' + data.unified_job_id + ' status changed to ' + data.status); + $rootScope.$emit('ScheduleStatusChange', data); + }); } openSocket(); diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js index d130501828..00664fc466 100644 --- a/awx/ui/static/js/controllers/Home.js +++ b/awx/ui/static/js/controllers/Home.js @@ -115,9 +115,9 @@ Home.$inject = ['$scope', '$compile', '$routeParams', '$rootScope', '$location', * @description This controls the 'home/groups' page that is loaded from the dashboard * */ -export function HomeGroups($log, $scope, $filter, $compile, $location, $routeParams, LogViewer, HomeGroupList, GenerateList, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, +export function HomeGroups($rootScope, $log, $scope, $filter, $compile, $location, $routeParams, LogViewer, HomeGroupList, GenerateList, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GetBasePath, SearchInit, PaginateInit, FormatDate, GetHostsStatusMsg, GetSyncStatusMsg, ViewUpdateStatus, Stream, GroupsEdit, Wait, - Alert, Rest, Empty, InventoryUpdate, Find, GroupsCancelUpdate, Store, Socket) { + Alert, Rest, Empty, InventoryUpdate, Find, GroupsCancelUpdate, Store) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -127,8 +127,7 @@ export function HomeGroups($log, $scope, $filter, $compile, $location, $routePar defaultUrl = GetBasePath('groups'), scope = $scope, modal_scope = $scope.$new(), - opt, PreviousSearchParams, - io; + opt, PreviousSearchParams; generator.inject(list, { mode: 'edit', scope: scope, breadCrumbs: true }); @@ -296,10 +295,10 @@ export function HomeGroups($log, $scope, $filter, $compile, $location, $routePar LoadBreadCrumbs(); - io = Socket({ scope: $scope, endpoint: "jobs" }); - io.init(); - $log.debug('Watching for job updates: '); - io.on("status_changed", function(data) { + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-home', function(e, data) { var stat, group; if (data.group_id) { group = Find({ list: scope[list.name], key: 'id', val: data.group_id }); @@ -539,7 +538,7 @@ export function HomeGroups($log, $scope, $filter, $compile, $location, $routePar } -HomeGroups.$inject = ['$log', '$scope', '$filter', '$compile', '$location', '$routeParams', 'LogViewer', 'HomeGroupList', 'generateList', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', +HomeGroups.$inject = ['$rootScope', '$log', '$scope', '$filter', '$compile', '$location', '$routeParams', 'LogViewer', 'HomeGroupList', 'generateList', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GetBasePath', 'SearchInit', 'PaginateInit', 'FormatDate', 'GetHostsStatusMsg', 'GetSyncStatusMsg', 'ViewUpdateStatus', 'Stream', 'GroupsEdit', 'Wait', 'Alert', 'Rest', 'Empty', 'InventoryUpdate', 'Find', 'GroupsCancelUpdate', 'Store', 'Socket' ]; diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 8e2a42bde3..6586ac4951 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -424,44 +424,19 @@ export function InventoriesAdd($scope, $rootScope, $compile, $location, $log, $r data = {}; for (fld in form.fields) { - if (fld !== 'variables') { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = $scope[fld]; - } else { - data[fld] = $scope[fld]; - } + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; } } - if ($scope.removeUpdateInventoryVariables) { - $scope.removeUpdateInventoryVariables(); - } - $scope.removeUpdateInventoryVariables = $scope.$on('UpdateInventoryVariables', function(e, data) { - var inventory_id = data.id; - var vars_to_send = {"variables": json_data}; - Rest.setUrl(data.related.variable_data); - Rest.put(vars_to_send) - .success(function () { - Wait('stop'); - $location.path('/inventories/' + inventory_id + '/manage'); - }) - .error(function (data, status) { - ProcessErrors( $scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to update inventory varaibles. PUT returned status: ' + status - }); - }); - }); - Rest.setUrl(defaultUrl); Rest.post(data) .success(function (data) { var inventory_id = data.id; - if ($scope.variables) { - $scope.$emit('UpdateInventoryVariables', data); - } else { - Wait('stop'); - $location.path('/inventories/' + inventory_id + '/'); - } + Wait('stop'); + $location.path('/inventories/' + inventory_id + '/'); }) .error(function (data, status) { ProcessErrors( $scope, data, status, form, { hdr: 'Error!', @@ -597,40 +572,18 @@ export function InventoriesEdit($scope, $rootScope, $compile, $location, $log, $ data = {}; for (fld in form.fields) { - if (fld !== 'variables') { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = $scope[fld]; - } else { - data[fld] = $scope[fld]; - } + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; } } - if ($scope.removeUpdateInventoryVariables) { - $scope.removeUpdateInventoryVariables(); - } - $scope.removeUpdateInventoryVariables = $scope.$on('UpdateInventoryVariables', function(e, data) { - Rest.setUrl(data.related.variable_data); - Rest.put(json_data) - .success(function () { - Wait('stop'); - $location.path('/inventories/'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update inventory varaibles. PUT returned status: ' + status - }); - }); - }); - Rest.setUrl(defaultUrl + inventory_id + '/'); Rest.put(data) - .success(function (data) { - if ($scope.variables) { - $scope.$emit('UpdateInventoryVariables', data); - } else { - $location.path('/inventories/'); - } + .success(function () { + Wait('stop'); + $location.path('/inventories/'); }) .error(function (data, status) { ProcessErrors($scope, data, status, form, { hdr: 'Error!', @@ -849,12 +802,11 @@ export function InventoriesManage ($log, $scope, $rootScope, $location, ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete, EditInventoryProperties, ToggleHostEnabled, Stream, ShowJobSummary, InventoryGroupsHelp, HelpDialog, ViewJob, - GroupsCopy, HostsCopy, Socket) { + GroupsCopy, HostsCopy) { var PreviousSearchParams, url, - hostScope = $scope.$new(), - io; + hostScope = $scope.$new(); ClearScope(); @@ -1095,38 +1047,33 @@ export function InventoriesManage ($log, $scope, $rootScope, $location, if ($scope.removeWatchUpdateStatus) { $scope.removeWatchUpdateStatus(); } - $scope.removeWatchUpdateStatus = $scope.$on('WatchUpdateStatus', function() { - io = Socket({ scope: $scope, endpoint: "jobs" }); - io.init(); - $log.debug('Watching for job updates: '); - io.on("status_changed", function(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; + $scope.removeWatchUpdateStatus = $scope.$on('JobStatusChange-inventory', function(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 @@ -1453,5 +1400,5 @@ InventoriesManage.$inject = ['$log', '$scope', '$rootScope', '$location', 'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete', 'EditInventoryProperties', 'ToggleHostEnabled', 'Stream', 'ShowJobSummary', 'InventoryGroupsHelp', 'HelpDialog', 'ViewJob', 'GroupsCopy', - 'HostsCopy', 'Socket' + 'HostsCopy' ]; diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 9e1d435b9d..d2199b576b 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -18,7 +18,6 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r ClearScope(); var job_id = $routeParams.id, - event_socket, scope = $scope, api_complete = false, refresh_count = 0, @@ -99,12 +98,7 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r "

Unreachable

\n" + "

Failed

\n"; function openSocket() { - event_socket = Socket({ - scope: scope, - endpoint: "job_events" - }); - event_socket.init(); - event_socket.on("job_events-" + job_id, function(data) { + $rootScope.event_socket.on("job_events-" + job_id, function(data) { if (api_complete && data.id > lastEventId) { scope.waiting = false; data.event = data.event_name; @@ -117,12 +111,12 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) { + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { // if we receive a status change event for the current job indicating the job // is finished, stop event queue processing and reload if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) { if (data.status === 'failed' || data.status === 'canceled' || - data.status === 'error' || data.status === 'successful') { + data.status === 'error' || data.status === 'successful' || data.status === 'running') { $scope.liveEventProcessing = false; if ($rootScope.jobDetailInterval) { window.clearInterval($rootScope.jobDetailInterval); diff --git a/awx/ui/static/js/controllers/JobStdout.js b/awx/ui/static/js/controllers/JobStdout.js index 5f77b3a91d..cbd6818e52 100644 --- a/awx/ui/static/js/controllers/JobStdout.js +++ b/awx/ui/static/js/controllers/JobStdout.js @@ -11,7 +11,7 @@ */ -export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) { +export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors) { ClearScope(); @@ -19,8 +19,6 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi api_complete = false, stdout_url, current_range, - event_socket, - status_socket, loaded_sections = [], event_queue = 0, auto_scroll_down=true, // programmatic scroll to bottom @@ -35,44 +33,50 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi function openSockets() { - status_socket = Socket({ - scope: $scope, - endpoint: "jobs" - }); - status_socket.init(); - status_socket.on("status_changed", function(data) { - if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) { - $scope.job.status = data.status; - if (data.status === 'failed' || data.status === 'canceled' || - data.status === 'error' || data.status === 'successful') { - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); - } - if (live_event_processing) { - if (loaded_sections.length === 0) { - $scope.$emit('LoadStdout'); - } - else { - getNextSection(); - } - } - live_event_processing = false; + if (/\/jobs\/(\d)+\/stdout/.test($location.$$url)) { + $log.debug("socket watching on job_events-" + job_id); + $rootScope.event_socket.on("job_events-" + job_id, function() { + $log.debug("socket fired on job_events-" + job_id); + if (api_complete) { + event_queue++; } - } - }); - event_socket = Socket({ - scope: $scope, - endpoint: "job_events" - }); - event_socket.init(); - event_socket.on("job_events-" + job_id, function() { - if (api_complete) { - event_queue++; - } - }); + }); + } else if (/\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { + $log.debug("socket watching on ad_hoc_command_events-" + job_id); + $rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() { + $log.debug("socket fired on ad_hoc_command_events-" + job_id); + if (api_complete) { + event_queue++; + } + }); + } } openSockets(); + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) { + if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) { + $scope.job.status = data.status; + if (data.status === 'failed' || data.status === 'canceled' || + data.status === 'error' || data.status === 'successful') { + if ($rootScope.jobStdOutInterval) { + window.clearInterval($rootScope.jobStdOutInterval); + } + if (live_event_processing) { + if (loaded_sections.length === 0) { + $scope.$emit('LoadStdout'); + } + else { + getNextSection(); + } + } + live_event_processing = false; + } + } + }); + $rootScope.jobStdOutInterval = setInterval( function() { if (event_queue > 0) { // events happened since the last check @@ -260,7 +264,11 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi Rest.setUrl(url); Rest.get() .success( function(data) { - $('#pre-container-content').append(data.content); + if ($('#pre-container-content').html() === "stdout capture pending") { + $('#pre-container-content').html(data.content); + } else { + $('#pre-container-content').append(data.content); + } loaded_sections.push({ start: (data.range.start < 0) ? 0 : data.range.start, end: data.range.end @@ -283,5 +291,4 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi } -JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', - 'Socket' ]; +JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors']; diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index a0aec0953d..e6064444e0 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -16,7 +16,7 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $routeParams, ClearScope, Breadcrumbs, LoadBreadCrumbs, LoadSchedulesScope, - LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait, Socket) { + LoadJobsScope, AllJobsList, ScheduledJobsList, GetChoices, GetBasePath, Wait) { ClearScope(); @@ -24,35 +24,26 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $routePa choicesCount = 0, listCount = 0, api_complete = false, - schedule_socket, - job_socket, max_rows; - function openSockets() { - job_socket = Socket({ - scope: $scope, - endpoint: "jobs" - }); - job_socket.init(); - job_socket.on("status_changed", function() { - // if (api_complete) { - jobs_scope.refreshJobs(); - // } - }); - schedule_socket = Socket({ - scope: $scope, - endpoint: "schedules" - }); - schedule_socket.init(); - schedule_socket.on("schedule_changed", function() { - if (api_complete) { - scheduled_scope.search('schedule'); - } - }); - } - LoadBreadCrumbs(); + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobs', function() { + jobs_scope.refreshJobs(); + }); + + if ($rootScope.removeScheduleStatusChange) { + $rootScope.removeScheduleStatusChange(); + } + $rootScope.removeScheduleStatusChange = $rootScope.$on('ScheduleStatusChange', function() { + if (api_complete) { + scheduled_scope.search('schedule'); + } + }); + if ($scope.removeListLoaded) { $scope.removeListLoaded(); } @@ -60,7 +51,6 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $routePa listCount++; if (listCount === 2) { api_complete = true; - openSockets(); } }); @@ -193,4 +183,4 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $routePa JobsListController.$inject = ['$rootScope', '$log', '$scope', '$compile', '$routeParams', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'LoadSchedulesScope', 'LoadJobsScope', -'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', 'Socket']; +'AllJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait']; diff --git a/awx/ui/static/js/controllers/Portal.js b/awx/ui/static/js/controllers/Portal.js index ecf4e0f38a..7e222b973d 100644 --- a/awx/ui/static/js/controllers/Portal.js +++ b/awx/ui/static/js/controllers/Portal.js @@ -86,7 +86,7 @@ export function PortalController($scope, $compile, $routeParams, $rootScope, $lo if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function() { + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-portal', function() { jobs_scope.search('portal_job'); //processEvent(event); }); diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index 82a7c05fe3..b3241de5b7 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -86,7 +86,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) { + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-projects', function(e, data) { var project; $log.debug(data); if ($scope.projects) { @@ -722,30 +722,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, $rou callback: 'choicesReady' }); - // Handle project update status changes - if ($rootScope.removeJobStatusChange) { - $rootScope.removeJobStatusChange(); - } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) { - if ($scope.project_obj && data.project_id === $scope.project_obj.id) { - // This is the affected project - $log.debug('Received event for project: ' + $scope.project_obj.name); - $log.debug('Status changed to: ' + data.status); - // Set the status and re-evaluate the update button tooltip and class - $scope.project_obj.status = data.status; - $scope.scm_update_tooltip = "Start an SCM update"; - $scope.scm_type_class = ""; - if (data.status === 'running' || data.status === 'updating') { - $scope.scm_update_tooltip = "SCM update currently running"; - $scope.scm_type_class = "btn-disabled"; - } - if (Empty($scope.project_obj.scm_type)) { - $scope.scm_update_tooltip = 'Manual projects do not require an SCM update'; - $scope.scm_type_class = "btn-disabled"; - } - } - }); - // Save changes to the parent $scope.formSave = function () { var fld, i, params; diff --git a/awx/ui/static/js/controllers/Sockets.js b/awx/ui/static/js/controllers/Sockets.js index 4c38f6964c..d80dcae90f 100644 --- a/awx/ui/static/js/controllers/Sockets.js +++ b/awx/ui/static/js/controllers/Sockets.js @@ -80,11 +80,6 @@ export function SocketsController ($scope, $compile, ClearScope, Socket) { e.append(html); $compile(e)(job_events_scope); - schedules_socket.init(); - test_socket.init(); - jobs_socket.init(); - job_events_socket.init(); - schedules_scope.url = schedules_socket.getUrl(); test_scope.url = test_socket.getUrl(); jobs_scope.url = jobs_socket.getUrl(); diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js index 38eca49858..47239d4bff 100644 --- a/awx/ui/static/js/helpers/PaginationHelpers.js +++ b/awx/ui/static/js/helpers/PaginationHelpers.js @@ -28,7 +28,7 @@ export default scope[iterator + '_num_pages'] = Math.ceil((count / scope[iterator + '_page_size'])); scope[iterator + '_num_pages'] = (scope[iterator + '_num_pages'] <= 0) ? 1 : scope[iterator + '_num_pages']; scope[iterator + '_total_rows'] = count; - + $('#pagination-links li:eq(1)').removeAttr('class'); // Which page are we on? if (Empty(next) && previous) { // no next page, but there is a previous page @@ -36,6 +36,7 @@ export default } else if (next && Empty(previous)) { // next page available, but no previous page scope[iterator + '_page'] = 1; + $('#pagination-links li:eq(1)').attr('class', 'disabled'); } else if (next && previous) { // we're in between next and previous scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1; @@ -75,6 +76,9 @@ export default scope.getPage = function (page, set, iterator) { var new_url = scope[iterator + '_url'].replace(/.page\=\d+/, ''), connect = (/\/$/.test(new_url)) ? '?' : '&'; + if(scope[iterator + '_page'] === 1 && page === 0){ + return; + } new_url += connect + 'page=' + page; new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size'] : 'page_size=' + scope[iterator + 'PageSize']; @@ -136,6 +140,9 @@ export default scope.getPage = function (page, set, iterator) { var new_url = scope[iterator + '_url'].replace(/.page\=\d+/, ''), connect = (/\/$/.test(new_url)) ? '?' : '&'; + if(scope[iterator + '_page'] === 1 && page === 0){ + return; + } new_url += connect + 'page=' + page; new_url += (scope[iterator + 'SearchParams']) ? '&' + scope[iterator + 'SearchParams'] + '&page_size=' + scope[iterator + '_page_size'] : '&page_size=' + scope[iterator + 'PageSize']; diff --git a/awx/ui/static/js/services/job-status-graph-data.js b/awx/ui/static/js/services/job-status-graph-data.js index 492cda20fe..9c92eafd5d 100644 --- a/awx/ui/static/js/services/job-status-graph-data.js +++ b/awx/ui/static/js/services/job-status-graph-data.js @@ -38,7 +38,7 @@ function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q) { destroyWatcher: angular.noop, setupWatcher: function(period, jobType) { this.destroyWatcher = - $rootScope.$on('JobStatusChange', function() { + $rootScope.$on('JobStatusChange-home', function() { getData(period, jobType).then(function(result) { $rootScope. $broadcast('DataReceived:JobStatusGraph', diff --git a/awx/ui/static/js/shared/form-generator.js b/awx/ui/static/js/shared/form-generator.js index d7e97a7f63..5a6ff4ed94 100644 --- a/awx/ui/static/js/shared/form-generator.js +++ b/awx/ui/static/js/shared/form-generator.js @@ -1003,7 +1003,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } if ($AnsibleConfig.password_hasSymbol) { html += "
Your password must contain one of the following characters: `~!@#$%^&*()_-+=|}\]{\[;:\"\'?\/>.<,\n"; + ".$error.hasSymbol\">Your password must contain one of the following characters: `~!@#$%^&*()_-+=|}\]{\[;:\"\'?\/>.<,
\n"; } } diff --git a/awx/ui/static/js/shared/generator-helpers.js b/awx/ui/static/js/shared/generator-helpers.js index ac317d7608..37a9517dc1 100644 --- a/awx/ui/static/js/shared/generator-helpers.js +++ b/awx/ui/static/js/shared/generator-helpers.js @@ -799,7 +799,7 @@ angular.module('GeneratorHelpers', [systemStatus.name]) html += "
  • " + "
  • \n"; - html += "
  • " + "
  • \n"; diff --git a/awx/ui/static/js/widgets/DashboardJobs.js b/awx/ui/static/js/widgets/DashboardJobs.js index ca4affefd5..1eeb44d040 100644 --- a/awx/ui/static/js/widgets/DashboardJobs.js +++ b/awx/ui/static/js/widgets/DashboardJobs.js @@ -48,7 +48,7 @@ angular.module('DashboardJobsWidget', ['RestServices', 'Utilities']) e.html(html); $compile(e)(scope); - $rootScope.$on('JobStatusChange', function() { + $rootScope.$on('JobStatusChange-home', function() { jobs_scope.refreshJobs(); }); diff --git a/awx/ui/static/less/stdout.less b/awx/ui/static/less/stdout.less index fc5c850afd..6477c05be4 100644 --- a/awx/ui/static/less/stdout.less +++ b/awx/ui/static/less/stdout.less @@ -58,3 +58,9 @@ .ansi45 { background-color: #E850A8; } .ansi46 { background-color: @skipped; } .ansi47 { background-color: #F5F1DE; } + +#pre-container-content > span { + display: inline-block; + white-space: pre-wrap; + word-wrap: normal; +} diff --git a/awx/ui/tests/unit/services/job-status-graph-data-test.js b/awx/ui/tests/unit/services/job-status-graph-data-test.js index 9ec9e3f9be..42babc9ee8 100644 --- a/awx/ui/tests/unit/services/job-status-graph-data-test.js +++ b/awx/ui/tests/unit/services/job-status-graph-data-test.js @@ -58,7 +58,7 @@ describeModule('DashboardGraphs') $rootScope.$on('DataReceived:JobStatusGraph', function(e, data) { result.resolve(data); }); - $rootScope.$emit('JobStatusChange'); + $rootScope.$emit('JobStatusChange-home'); restStub.succeed({ data: expected }); restStub.flush(); }]); diff --git a/config/wsgi.py b/config/wsgi.py index d517e2385a..9999eb08c8 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -1 +1 @@ -from awx.wsgi import application +from awx.wsgi import application # NOQA diff --git a/setup.cfg b/setup.cfg index bd64ca1943..64d74af131 100755 --- a/setup.cfg +++ b/setup.cfg @@ -14,8 +14,8 @@ # W391 - Blank line at end of file # W293 - Blank line contains whitespace ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E302,E303,E501,W291,W391,W293 -exclude=awx/lib/site-packages,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data +exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data [flake8] ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E302,E303,E501,W291,W391,W293,E731 -exclude=awx/lib/site-packages,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/ +exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker diff --git a/tools/sosreport/tower.py b/tools/sosreport/tower.py index 53dff76195..154c628a0b 100644 --- a/tools/sosreport/tower.py +++ b/tools/sosreport/tower.py @@ -4,6 +4,33 @@ import sos from distutils.version import LooseVersion +SOSREPORT_TOWER_COMMANDS = [ + "ansible --version", # ansible core version + "tower-manage --version", # tower version + "supervisorctl status", # tower process status + "pip list" # pip package list + "tree -d /var/lib/awx", # show me the dirs + "ls -ll /var/lib/awx", # check permissions + "ls -ll /etc/tower", +] + +SOSREPORT_TOWER_DIRS = [ + "/etc/tower/", + "/var/log/tower", + "/var/log/httpd", + "/var/log/apache2", + "/var/log/redis", + "/var/log/supervisor", + "/var/log/syslog", + "/var/log/udev", + "/var/log/kern*", + "/var/log/dist-upgrade", + "/var/log/installer", + "/var/log/unattended-upgrades", + "/var/log/apport.log" +] + + if LooseVersion(sos.__version__) >= LooseVersion('3.0'): from sos.plugins import Plugin, RedHatPlugin, UbuntuPlugin @@ -13,36 +40,10 @@ if LooseVersion(sos.__version__) >= LooseVersion('3.0'): def setup(self): - commands = [ - "ansible --version", # ansible core version - "awx-manage --version", # tower version - "supervisorctl status", # tower process status - "pip list" # pip package list - "tree -d /var/lib/awx", # show me the dirs - "ls -ll /var/lib/awx", # check permissions - "ls -ll /etc/tower" - ] - - dirs = [ - "/etc/tower/", - "/var/log/tower", - "/var/log/httpd", - "/var/log/apache2", - "/var/log/redis", - "/var/log/supervisor", - "/var/log/syslog", - "/var/log/udev", - "/var/log/kern*", - "/var/log/dist-upgrade", - "/var/log/installer", - "/var/log/unattended-upgrades", - "/var/log/apport.log" - ] - - for path in dirs: + for path in SOSREPORT_TOWER_DIRS: self.add_copy_spec(path) - for command in commands: + for command in SOSREPORT_TOWER_COMMANDS: self.add_cmd_output(command) else: @@ -53,35 +54,9 @@ else: def setup(self): - commands = [ - "ansible --version", # ansible core version - "awx-manage --version", # tower version - "supervisorctl status", # tower process status - "pip list" # pip package list - "tree -d /var/lib/awx", # show me the dirs - "ls -ll /var/lib/awx", # check permissions - "ls -ll /etc/tower" - ] - - dirs = [ - "/etc/tower/", - "/var/log/tower", - "/var/log/httpd", - "/var/log/apache2", - "/var/log/redis", - "/var/log/supervisor", - "/var/log/syslog", - "/var/log/udev", - "/var/log/kern*", - "/var/log/dist-upgrade", - "/var/log/installer", - "/var/log/unattended-upgrades", - "/var/log/apport.log" - ] - - for path in dirs: + for path in SOSREPORT_TOWER_DIRS: self.addCopySpec(path) - for command in commands: + for command in SOSREPORT_TOWER_COMMANDS: self.collectExtOutput(command)