From 7ce9f3f38d475ab8f699b2f69c12d645aebe0067 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Sun, 17 May 2015 16:24:08 -0400 Subject: [PATCH 01/26] cache templates --- awx/settings/defaults.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 18146a9042..1c0e07cd8b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -135,6 +135,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' From e6b7f75ad7e9b47ccf2b2a41d2fc544afc0e29eb Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Sun, 17 May 2015 16:53:17 -0400 Subject: [PATCH 02/26] mock commonly slow but rarely used paths --- awx/main/tests/base.py | 76 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index bb38e21d79..a66119bdee 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -14,6 +14,8 @@ import time from multiprocessing import Process from subprocess import Popen import re +from copy import copy +import mock # PyYAML import yaml @@ -76,8 +78,15 @@ class QueueStartStopTestMixin(QueueTestMixin): super(QueueStartStopTestMixin, self).tearDown() self.terminate_queue() +class MockCommonlySlowTestMixin(object): + def __init__(self, *args, **kwargs): + #def setUp(self): + 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. ''' @@ -687,6 +696,71 @@ class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest): Base class for celery task tests. ''' +#class SetupTeardownOnceTestMixin(django.test.TestCase): +flag_setup_ran = False +self_backup = {} +class SetupTeardownOnceTestMixin(BaseTest): + + def __init__(self, *args, **kwargs): + global flag_setup_ran + self.setup_ran = flag_setup_ran + super(SetupTeardownOnceTestMixin, self).__init__(*args, **kwargs) + + @classmethod + def setUpClass(cls): + super(SetupTeardownOnceTestMixin, cls).setUpClass() + + ''' + def setUp(self): + global flag_setup_ran + super(SetupTeardownOnceTestMixin, self).setUp() + self.setup_ran = flag_setup_ran + ''' + + ''' + def _pre_setup(self): + if not self.setup_ran: + super(SetupTeardownOnceTestMixin, self)._pre_setup() + ''' + def _fixture_setup(self): + return + def _real_fixture_setup(self): + super(SetupTeardownOnceTestMixin, self)._fixture_setup() + + ''' + def _fixture_teardown(self): + return + def _post_teardown(self): + return + ''' + + def setup(self): + global flag_setup_ran + global self_backup + + if not self.setup_ran: + self._real_fixture_setup() + + if not flag_setup_ran: + self_backup = copy(self) + flag_setup_ran = True + else: + for k, v in self_backup.__dict__.iteritems(): + if k not in self_backup._keys_before: + setattr(self, k, v) + + + def should_setup(self): + self._keys_before = None + self._keys_before = self.__dict__.keys() + global flag_setup_ran + return not flag_setup_ran + + @classmethod + def tearDownClass(cls): + #super(SetupTeardownOnceTestMixin, cls._self)._fixture_teardown() + super(SetupTeardownOnceTestMixin, cls).tearDownClass() + # Helps with test cases. # Save all components of a uri (i.e. scheme, username, password, etc.) so that # when we construct a uri string and decompose it, we can verify the decomposition From dfc3ee0487e796827e4f0af39d17f3f45c2ccb53 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Sun, 17 May 2015 22:15:17 -0400 Subject: [PATCH 03/26] mock run_pexpect for adhoc --- awx/main/tests/ad_hoc.py | 33 +++++++++++++++----- awx/main/tests/base.py | 67 ---------------------------------------- 2 files changed, 26 insertions(+), 74 deletions(-) diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index b5155ed155..76c89c79f6 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 @@ -21,7 +22,6 @@ from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TE __all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest'] - class BaseAdHocCommandTest(BaseJobExecutionTest): ''' Common initialization for testing ad hoc commands. @@ -360,6 +360,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 +388,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. @@ -558,7 +562,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): response = self.run_test_ad_hoc_command(become_enabled=True) self.assertEqual(response['become_enabled'], True) - 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 +627,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 +680,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() @@ -735,6 +742,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): response = self.post(url, {}, expect=400) 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 +832,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 +888,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 +939,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) @@ -1030,6 +1043,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.assertTrue(response['can_run_ad_hoc_commands']) 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) @@ -1079,6 +1094,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.delete(url, expect=401) 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 @@ -1133,6 +1150,8 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.delete(url, expect=401) 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 a66119bdee..095b795651 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -14,7 +14,6 @@ import time from multiprocessing import Process from subprocess import Popen import re -from copy import copy import mock # PyYAML @@ -80,7 +79,6 @@ class QueueStartStopTestMixin(QueueTestMixin): class MockCommonlySlowTestMixin(object): def __init__(self, *args, **kwargs): - #def setUp(self): from awx.api import generics mock.patch.object(generics, 'get_view_description', return_value=None).start() super(MockCommonlySlowTestMixin, self).__init__(*args, **kwargs) @@ -696,71 +694,6 @@ class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest): Base class for celery task tests. ''' -#class SetupTeardownOnceTestMixin(django.test.TestCase): -flag_setup_ran = False -self_backup = {} -class SetupTeardownOnceTestMixin(BaseTest): - - def __init__(self, *args, **kwargs): - global flag_setup_ran - self.setup_ran = flag_setup_ran - super(SetupTeardownOnceTestMixin, self).__init__(*args, **kwargs) - - @classmethod - def setUpClass(cls): - super(SetupTeardownOnceTestMixin, cls).setUpClass() - - ''' - def setUp(self): - global flag_setup_ran - super(SetupTeardownOnceTestMixin, self).setUp() - self.setup_ran = flag_setup_ran - ''' - - ''' - def _pre_setup(self): - if not self.setup_ran: - super(SetupTeardownOnceTestMixin, self)._pre_setup() - ''' - def _fixture_setup(self): - return - def _real_fixture_setup(self): - super(SetupTeardownOnceTestMixin, self)._fixture_setup() - - ''' - def _fixture_teardown(self): - return - def _post_teardown(self): - return - ''' - - def setup(self): - global flag_setup_ran - global self_backup - - if not self.setup_ran: - self._real_fixture_setup() - - if not flag_setup_ran: - self_backup = copy(self) - flag_setup_ran = True - else: - for k, v in self_backup.__dict__.iteritems(): - if k not in self_backup._keys_before: - setattr(self, k, v) - - - def should_setup(self): - self._keys_before = None - self._keys_before = self.__dict__.keys() - global flag_setup_ran - return not flag_setup_ran - - @classmethod - def tearDownClass(cls): - #super(SetupTeardownOnceTestMixin, cls._self)._fixture_teardown() - super(SetupTeardownOnceTestMixin, cls).tearDownClass() - # Helps with test cases. # Save all components of a uri (i.e. scheme, username, password, etc.) so that # when we construct a uri string and decompose it, we can verify the decomposition From 28d9b994eb12c2b167a21f14ec5e02e581691cca Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 10:11:51 -0400 Subject: [PATCH 04/26] restrict the running of jobs The following jobs require a credential to launch * InventoryUpdate without custom script * Job * AdHocJob --- awx/main/models/ad_hoc_commands.py | 11 +++++++++++ awx/main/models/inventory.py | 13 +++++++++++++ awx/main/models/jobs.py | 11 +++++++++++ 3 files changed, 35 insertions(+) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 1a48bbffcb..a1db3f6d73 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -201,6 +201,17 @@ class AdHocCommand(UnifiedJob): update_fields.append('name') super(AdHocCommand, self).save(*args, **kwargs) + # AdHocCommand Credential required + @property + def can_start(self): + if not super(AdHocCommand, self).can_start: + return False + + if not (self.credential and self.credential.active): + return False + + return True + class AdHocCommandEvent(CreatedModifiedModel): ''' diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6769efc28d..9bf848b1e0 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1247,6 +1247,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. From 7a0cb655419499e0774f6d79f798d0f94a8d33a7 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 12:37:35 -0400 Subject: [PATCH 05/26] require openstack host, username, pass --- awx/main/models/credential.py | 6 ++++++ 1 file changed, 6 insertions(+) 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): From a80602e40f98bece2cea4773cca275c9ba254bea Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 14:57:05 -0400 Subject: [PATCH 06/26] remove adhoc command credential required check from can_start() and rely on API only to enforce --- awx/main/models/ad_hoc_commands.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index a1db3f6d73..1a48bbffcb 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -201,17 +201,6 @@ class AdHocCommand(UnifiedJob): update_fields.append('name') super(AdHocCommand, self).save(*args, **kwargs) - # AdHocCommand Credential required - @property - def can_start(self): - if not super(AdHocCommand, self).can_start: - return False - - if not (self.credential and self.credential.active): - return False - - return True - class AdHocCommandEvent(CreatedModifiedModel): ''' From 50cbdf8535027e3f4292ec121e961ebc8223686f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 15:16:10 -0400 Subject: [PATCH 07/26] credential added to job launch test --- awx/main/tests/commands/commands_monolithic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) From 582842f54f74d71be215ae49a312029475740ea7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 19 May 2015 15:48:45 -0400 Subject: [PATCH 08/26] Add the ability to pass extra_vars when launching callback jobs. Update the docs to reflect passing json for extra_vars and the host config key --- .../templates/api/job_template_callback.md | 8 +++++-- awx/api/views.py | 9 ++++++-- awx/main/tests/jobs/jobs_monolithic.py | 23 ++++++++++++------- 3 files changed, 28 insertions(+), 12 deletions(-) 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 3da808b3e8..3e56b8a0ae 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1768,6 +1768,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. @@ -1822,8 +1825,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) 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])) From 82a808abc72e1b510d3db20fd1cff099da33feef Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 17:04:41 -0400 Subject: [PATCH 09/26] mock run_pexpect --- awx/main/tests/ad_hoc.py | 101 +++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index b5155ed155..73d5643d4f 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' From e6b834c4940d922f975c1d4e73e8c6e5a9683f43 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 19 May 2015 17:20:21 -0400 Subject: [PATCH 10/26] update task test to create credential before launching job --- awx/main/tests/tasks.py | 53 ++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index cfbb0bd401..db7a166ed2 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) From 51afab566be27d0ed74dfe135301326a55a41b56 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 20 May 2015 08:23:11 -0400 Subject: [PATCH 11/26] flake8 --- awx/main/models/inventory.py | 4 ++-- awx/main/tests/tasks.py | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 9bf848b1e0..fe58d67fc4 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1254,8 +1254,8 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions): if not super(InventoryUpdate, self).can_start: return False - if self.source != 'custom' \ - and not (self.credential and self.credential.active): + if (self.source != 'custom' + and not (self.credential and self.credential.active)): return False return True diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index db7a166ed2..0ef05567b3 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -347,22 +347,22 @@ class RunJobTest(BaseJobExecutionTest): def setUp(self): 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 + 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() From 6e42a69ce34e80f9d6c31810547645f519c81b45 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 20 May 2015 09:46:57 -0400 Subject: [PATCH 12/26] tests added --- awx/main/tests/inventory.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 71a1b826b7..ab3a20ffbd 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']})" @@ -1978,3 +1978,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) From 44cb9ea3970daf103f931262bc2c81f2ae91bb87 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 20 May 2015 11:36:21 -0400 Subject: [PATCH 13/26] pending vs. missing stdout message --- awx/main/models/unified_jobs.py | 8 +++-- awx/main/tests/__init__.py | 1 + awx/main/tests/unified_jobs.py | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 awx/main/tests/unified_jobs.py diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 0a34e18bca..3b96735bdb 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': 'stdout capture pending', + '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/unified_jobs.py b/awx/main/tests/unified_jobs.py new file mode 100644 index 0000000000..719bc45165 --- /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(), 'stdout capture pending') + From 299ece0b224934208cdbfbfc834f39a05e661737 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 May 2015 11:22:25 -0400 Subject: [PATCH 14/26] updating sockets to only init once. --- awx/ui/static/js/app.js | 73 ++++++++++++++++++- awx/ui/static/js/controllers/Home.js | 17 ++--- awx/ui/static/js/controllers/Inventories.js | 62 +++++++--------- awx/ui/static/js/controllers/JobDetail.js | 12 +-- awx/ui/static/js/controllers/JobStdout.js | 63 +++++++--------- awx/ui/static/js/controllers/Jobs.js | 46 +++++------- awx/ui/static/js/controllers/Portal.js | 2 +- awx/ui/static/js/controllers/Projects.js | 26 +------ awx/ui/static/js/controllers/Sockets.js | 5 -- .../js/services/job-status-graph-data.js | 2 +- awx/ui/static/js/widgets/DashboardJobs.js | 2 +- .../services/job-status-graph-data-test.js | 2 +- 12 files changed, 160 insertions(+), 152 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 2ee0160c33..bc8cda2691 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(); + }], + 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; + } }] } }). @@ -942,16 +978,49 @@ 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)+/.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 (/\/jobs\/(\d)+\/stdout/.test($location.$$url) || + /\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { + $rootScope.$emit('JobStatusChange-jobStdout', 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..1a24bde001 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -849,12 +849,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 +1094,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 +1447,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..5bbabd6665 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,37 +33,7 @@ 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; - } - } - }); - event_socket = Socket({ - scope: $scope, - endpoint: "job_events" - }); - event_socket.init(); - event_socket.on("job_events-" + job_id, function() { + $rootScope.event_socket.on("job_events-" + job_id, function() { if (api_complete) { event_queue++; } @@ -73,6 +41,30 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi } 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 @@ -283,5 +275,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/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/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/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(); }]); From 41836a91394f5ae331362286bed84b95b49a66e0 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 May 2015 13:51:34 -0400 Subject: [PATCH 15/26] fixed inv vars on UI --- awx/ui/static/js/controllers/Inventories.js | 73 ++++----------------- 1 file changed, 13 insertions(+), 60 deletions(-) diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 1a24bde001..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!', From 7f1c97b16733d87207af59dbad47197998eef71d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 20 May 2015 14:00:33 -0400 Subject: [PATCH 16/26] Include pk in inventory source name to keep it unique even with group renames. Fixes https://trello.com/c/PnmDWCai --- awx/main/management/commands/inventory_import.py | 5 ++--- awx/main/models/inventory.py | 8 +++++--- awx/main/tests/inventory.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) 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/inventory.py b/awx/main/models/inventory.py index 6769efc28d..e0a906c735 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: diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index ab3a20ffbd..edb14a5850 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -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 From 9adfb64d8e088814cbb154ba31af1c1a36013d9a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 May 2015 14:20:32 -0400 Subject: [PATCH 17/26] fixed unclosed div on password error fixes --- awx/ui/static/js/shared/form-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"; } } From 8485cd9f850bea79fd7f8826b825ee8115d326b0 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 20 May 2015 14:18:45 -0400 Subject: [PATCH 18/26] Pagination left arrow shown at all times for spacing Within the pagination widget, the left (previous) arrow will always be shown now, with functionality disabled if the user is on the first page of results --- awx/ui/static/js/helpers/PaginationHelpers.js | 9 ++++++++- awx/ui/static/js/shared/generator-helpers.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) 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/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"; From 65a28787d3d7f7b6e00d7b2223076382ee4e8106 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 May 2015 17:41:16 -0400 Subject: [PATCH 19/26] fixed order of socket routing --- awx/ui/static/js/app.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index bc8cda2691..268b0da772 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -992,6 +992,10 @@ var tower = angular.module('Tower', [ // 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') { @@ -1000,9 +1004,6 @@ var tower = angular.module('Tower', [ $rootScope.$emit('JobStatusChange-portal', data); } else if ($location.$$url === '/projects') { $rootScope.$emit('JobStatusChange-projects', data); - } else if (/\/jobs\/(\d)+\/stdout/.test($location.$$url) || - /\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { - $rootScope.$emit('JobStatusChange-jobStdout', data); } else if (/\/inventory\/(\d)+\/manage/.test($location.$$url)) { $rootScope.$emit('JobStatusChange-inventory', data); } From 17924aedffa07ae6342da0a32ad4d02ae7c0e774 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 May 2015 18:00:46 -0400 Subject: [PATCH 20/26] fixed stdout caputure pending getting prepended when the standard out is not pending anymore --- awx/ui/static/js/controllers/JobStdout.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/ui/static/js/controllers/JobStdout.js b/awx/ui/static/js/controllers/JobStdout.js index 5bbabd6665..4c27313b04 100644 --- a/awx/ui/static/js/controllers/JobStdout.js +++ b/awx/ui/static/js/controllers/JobStdout.js @@ -252,7 +252,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 From 7064c9bed45ed5be0f2f51ed18137d981a92d697 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 20 May 2015 15:26:44 -0400 Subject: [PATCH 21/26] Additional flake8 cleanup The flake8 command was identifying several warnings and errors. This change addresses the flake8 warnings and updates the setup.cfg with additional exclusions. If accepted, jenkins will be updated to use the flake8 command, rather than using the django_jenkins plugin. This will expedite jenkins testing. --- Makefile | 20 +++-- awx/plugins/callback/job_event_callback.py | 3 - awx/plugins/fact_caching/tower.py | 1 - awx/plugins/inventory/awxrest.py | 12 +-- awx/plugins/library/scan_services.py | 4 +- awx/settings/defaults.py | 20 +++-- awx/settings/development.py | 14 ++-- awx/settings/development_quiet.py | 4 +- awx/settings/production.py | 10 +-- config/wsgi.py | 2 +- setup.cfg | 4 +- tools/sosreport/tower.py | 87 ++++++++-------------- 12 files changed, 77 insertions(+), 104 deletions(-) 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/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..f1a03ba26c 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', @@ -247,7 +245,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 +257,7 @@ DEBUG_TOOLBAR_CONFIG = { # Use Django-devserver if installed. try: import devserver - INSTALLED_APPS += ('devserver',) + INSTALLED_APPS += (devserver.__name__,) except ImportError: pass @@ -444,7 +442,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 +607,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 +616,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 +625,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 +634,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/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) From 9aa41fdb8bb4c5d1d40485ff53aada1c219a7655 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 21 May 2015 10:47:38 -0400 Subject: [PATCH 22/26] added debugging for getting standard out events --- awx/ui/static/js/controllers/JobStdout.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/ui/static/js/controllers/JobStdout.js b/awx/ui/static/js/controllers/JobStdout.js index 4c27313b04..6572294084 100644 --- a/awx/ui/static/js/controllers/JobStdout.js +++ b/awx/ui/static/js/controllers/JobStdout.js @@ -33,7 +33,9 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi function openSockets() { + $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++; } From e068f7bb7faa5443ed87da00274139e44d377cb4 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 21 May 2015 11:28:23 -0400 Subject: [PATCH 23/26] fixed ui adhoc stdout socket endpoint and wrapping of std out --- awx/ui/static/js/app.js | 10 +++++----- awx/ui/static/js/controllers/JobStdout.js | 24 ++++++++++++++++------- awx/ui/static/less/stdout.less | 6 ++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 268b0da772..7cebb19339 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -267,13 +267,13 @@ var tower = angular.module('Tower', [ features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }], - jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { - if (!$rootScope.event_socket) { - $rootScope.event_socket = Socket({ + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.adhoc_event_socket) { + $rootScope.adhoc_event_socket = Socket({ scope: $rootScope, - endpoint: "job_events" + endpoint: "ad_hoc_command_events" }); - $rootScope.event_socket.init(); + $rootScope.adhoc_event_socket.init(); return true; } else { return true; diff --git a/awx/ui/static/js/controllers/JobStdout.js b/awx/ui/static/js/controllers/JobStdout.js index 6572294084..cbd6818e52 100644 --- a/awx/ui/static/js/controllers/JobStdout.js +++ b/awx/ui/static/js/controllers/JobStdout.js @@ -33,13 +33,23 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi function openSockets() { - $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++; - } - }); + 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++; + } + }); + } 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(); 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; +} From 809b905cd145bb78dfa1b4ad5cd58f0f060738d1 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 21 May 2015 13:29:15 -0400 Subject: [PATCH 24/26] changed stdout pending wording --- awx/main/models/unified_jobs.py | 2 +- awx/main/tests/unified_jobs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 3b96735bdb..f2ea8e4a9b 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -611,7 +611,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique job's result. """ msg = { - 'pending': 'stdout capture pending', + 'pending': 'Waiting for results...', 'missing': 'stdout capture is missing', } if self.result_stdout_text: diff --git a/awx/main/tests/unified_jobs.py b/awx/main/tests/unified_jobs.py index 719bc45165..607feeec4e 100644 --- a/awx/main/tests/unified_jobs.py +++ b/awx/main/tests/unified_jobs.py @@ -49,5 +49,5 @@ class UnifiedJobsUnitTest(SimpleTestCase): result = unified_job.result_stdout_raw_handle() self.assertIsInstance(result, StringIO) - self.assertEqual(result.read(), 'stdout capture pending') + self.assertEqual(result.read(), 'Waiting for results...') From 1b1d43dc59627e5be56a6dc3ce04f1b3ddeb806e Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 21 May 2015 14:13:13 -0400 Subject: [PATCH 25/26] Prevent launching ad hoc commands when license has expired. --- awx/api/permissions.py | 2 +- awx/api/views.py | 1 + awx/main/access.py | 3 ++- awx/main/tests/ad_hoc.py | 36 ++++++++++++++++++++++++++++++++++++ awx/main/tests/base.py | 7 +++++++ 5 files changed, 47 insertions(+), 2 deletions(-) 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/views.py b/awx/api/views.py index 3e56b8a0ae..26f49ada04 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2309,6 +2309,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/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 3445d9225e..5e0fe90fe3 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 time import mock # Django @@ -568,6 +569,13 @@ 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) @mock.patch('awx.main.tasks.BaseTask.run_pexpect', side_effect=run_pexpect_mock) def test_ad_hoc_command_detail(self, ignore): @@ -748,6 +756,13 @@ 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 @@ -1049,6 +1064,13 @@ 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 @@ -1100,6 +1122,13 @@ 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 @@ -1156,6 +1185,13 @@ 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 diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 095b795651..10b350d743 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -186,6 +186,13 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): 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)) From ebdc7a9648546190d4a6d28d016bb432bafeefbf Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 21 May 2015 17:04:25 -0400 Subject: [PATCH 26/26] flake8 fix --- awx/main/tests/ad_hoc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 5e0fe90fe3..45e0c095e7 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -6,7 +6,6 @@ import glob import os import subprocess import tempfile -import time import mock # Django