From 17ac2cee42542a3bfdbc95dc6ad0565003347ec5 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 5 Aug 2016 15:11:28 -0400 Subject: [PATCH 01/14] Put survey passwords in job field --- awx/api/serializers.py | 2 +- .../0030_v302_job_survey_passwords.py | 20 +++++++++++++++ .../0031_v302_migrate_survey_passwords.py | 18 +++++++++++++ awx/main/migrations/_save_password_keys.py | 25 +++++++++++++++++++ awx/main/models/jobs.py | 21 ++++++++-------- awx/main/models/unified_jobs.py | 7 ++++++ 6 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 awx/main/migrations/0030_v302_job_survey_passwords.py create mode 100644 awx/main/migrations/0031_v302_migrate_survey_passwords.py create mode 100644 awx/main/migrations/_save_password_keys.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3def9ae19b..fb8cd89db1 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1980,7 +1980,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): return ret if 'job_template' in ret and not obj.job_template: ret['job_template'] = None - if obj.job_template and obj.job_template.survey_enabled and 'extra_vars' in ret: + if 'extra_vars' in ret: ret['extra_vars'] = obj.display_extra_vars() return ret diff --git a/awx/main/migrations/0030_v302_job_survey_passwords.py b/awx/main/migrations/0030_v302_job_survey_passwords.py new file mode 100644 index 0000000000..fa6c2cd3fe --- /dev/null +++ b/awx/main/migrations/0030_v302_job_survey_passwords.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0029_v302_add_ask_skip_tags'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='survey_passwords', + field=jsonfield.fields.JSONField(default={}, editable=False, blank=True), + ), + ] diff --git a/awx/main/migrations/0031_v302_migrate_survey_passwords.py b/awx/main/migrations/0031_v302_migrate_survey_passwords.py new file mode 100644 index 0000000000..5eac01b853 --- /dev/null +++ b/awx/main/migrations/0031_v302_migrate_survey_passwords.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from awx.main.migrations import _save_password_keys +from awx.main.migrations import _migration_utils as migration_utils +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0030_v302_job_survey_passwords'), + ] + + operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), + migrations.RunPython(_save_password_keys.migrate_survey_passwords), + ] diff --git a/awx/main/migrations/_save_password_keys.py b/awx/main/migrations/_save_password_keys.py new file mode 100644 index 0000000000..3ff7b17562 --- /dev/null +++ b/awx/main/migrations/_save_password_keys.py @@ -0,0 +1,25 @@ +def survey_password_variables(survey_spec): + vars = [] + # Get variables that are type password + for survey_element in survey_spec['spec']: + if survey_element['type'] == 'password': + vars.append(survey_element['variable']) + return vars + + +def migrate_survey_passwords(apps, schema_editor): + '''Take the output of the Job Template password list for all that + have a survey enabled, and then save it into the job model. + ''' + Job = apps.get_model('main', 'Job') + for job in Job.objects.iterator(): + if not job.job_template: + continue + jt = job.job_template + if jt.survey_spec is not None and jt.survey_enabled: + password_list = survey_password_variables(jt.survey_spec) + hide_password_dict = {} + for password in password_list: + hide_password_dict[password] = "$encrypted$" + job.survey_passwords = hide_password_dict + job.save() diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a7c1c6041d..8a4ba4e1d3 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -513,6 +513,11 @@ class Job(UnifiedJob, JobOptions): editable=False, through='JobHostSummary', ) + survey_passwords = JSONField( + blank=True, + default={}, + editable=False, + ) @classmethod def _get_parent_field_name(cls): @@ -721,16 +726,12 @@ class Job(UnifiedJob, JobOptions): ''' Hides fields marked as passwords in survey. ''' - if self.extra_vars and self.job_template and self.job_template.survey_enabled: - try: - extra_vars = json.loads(self.extra_vars) - for key in self.job_template.survey_password_variables(): - if key in extra_vars: - extra_vars[key] = REPLACE_STR - return json.dumps(extra_vars) - except ValueError: - pass - return self.extra_vars + if self.survey_passwords: + extra_vars = json.loads(self.extra_vars) + extra_vars.update(self.survey_passwords) + return json.dumps(extra_vars) + else: + return self.extra_vars def _survey_search_and_replace(self, content): # Use job template survey spec to identify password fields. diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f7106eb7ab..77cafef746 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -343,6 +343,13 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio create_kwargs[field_name] = getattr(self, field_name) new_kwargs = self._update_unified_job_kwargs(**create_kwargs) unified_job = unified_job_class(**new_kwargs) + # For JobTemplate-based jobs with surveys, save list for perma-redaction + if hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False): + password_list = self.survey_password_variables() + hide_password_dict = {} + for password in password_list: + hide_password_dict[password] = REPLACE_STR + unified_job.survey_passwords = hide_password_dict unified_job.save() for field_name, src_field_value in m2m_fields.iteritems(): dest_field = getattr(unified_job, field_name) From 9f3d9fa78a338725594f6b693deed0a98fa4b0d6 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 5 Aug 2016 15:59:11 -0400 Subject: [PATCH 02/14] carry over survey passwords from old relaunched job --- awx/main/models/jobs.py | 4 ++-- awx/main/models/unified_jobs.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 8a4ba4e1d3..a04c1ab98e 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -26,7 +26,7 @@ from awx.main.models.unified_jobs import * # noqa from awx.main.models.notifications import NotificationTemplate from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import emit_websocket_notification -from awx.main.redact import PlainTextCleaner, REPLACE_STR +from awx.main.redact import PlainTextCleaner from awx.main.conf import tower_settings from awx.main.fields import ImplicitRoleField from awx.main.models.mixins import ResourceMixin @@ -248,7 +248,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): 'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule', 'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type', 'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled', - 'labels',] + 'labels', 'survey_passwords'] def resource_validation_data(self): ''' diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 77cafef746..52aa46904f 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -32,7 +32,7 @@ from djcelery.models import TaskMeta from awx.main.models.base import * # noqa from awx.main.models.schedules import Schedule from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates -from awx.main.redact import UriCleaner +from awx.main.redact import UriCleaner, REPLACE_STR __all__ = ['UnifiedJobTemplate', 'UnifiedJob'] @@ -344,7 +344,8 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio new_kwargs = self._update_unified_job_kwargs(**create_kwargs) unified_job = unified_job_class(**new_kwargs) # For JobTemplate-based jobs with surveys, save list for perma-redaction - if hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False): + if (hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False) and + not getattr(unified_job, 'survey_passwords', False)): password_list = self.survey_password_variables() hide_password_dict = {} for password in password_list: From 6559118f4002596f12c91276798becb14e48d9c9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 8 Aug 2016 13:06:07 -0400 Subject: [PATCH 03/14] tests for saving survey passwords to job --- awx/main/migrations/_save_password_keys.py | 4 +++- awx/main/tests/conftest.py | 10 ++++----- .../tests/functional/api/test_job_template.py | 21 ++++++++++++++++++- .../tests/functional/api/test_survey_spec.py | 3 ++- awx/main/tests/functional/conftest.py | 4 ++-- .../unit/models/test_job_template_unit.py | 7 ++++--- awx/main/tests/unit/models/test_job_unit.py | 17 ++++++++++++--- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/awx/main/migrations/_save_password_keys.py b/awx/main/migrations/_save_password_keys.py index 3ff7b17562..a5a231a92f 100644 --- a/awx/main/migrations/_save_password_keys.py +++ b/awx/main/migrations/_save_password_keys.py @@ -1,8 +1,10 @@ def survey_password_variables(survey_spec): vars = [] # Get variables that are type password + if 'spec' not in survey_spec: + return vars for survey_element in survey_spec['spec']: - if survey_element['type'] == 'password': + if 'type' in survey_element and survey_element['type'] == 'password': vars.append(survey_element['variable']) return vars diff --git a/awx/main/tests/conftest.py b/awx/main/tests/conftest.py index 470f43e661..1f21905fb9 100644 --- a/awx/main/tests/conftest.py +++ b/awx/main/tests/conftest.py @@ -26,16 +26,16 @@ def survey_spec_factory(): return create_survey_spec @pytest.fixture -def job_with_secret_key_factory(job_template_factory): +def job_template_with_survey_passwords_factory(job_template_factory): def rf(persisted): "Returns job with linked JT survey with password survey questions" objects = job_template_factory('jt', organization='org1', survey=[ {'variable': 'submitter_email', 'type': 'text', 'default': 'foobar@redhat.com'}, {'variable': 'secret_key', 'default': '6kQngg3h8lgiSTvIEb21', 'type': 'password'}, - {'variable': 'SSN', 'type': 'password'}], jobs=[1], persisted=persisted) - return objects.jobs[1] + {'variable': 'SSN', 'type': 'password'}], persisted=persisted) + return objects.job_template return rf @pytest.fixture -def job_with_secret_key_unit(job_with_secret_key_factory): - return job_with_secret_key_factory(persisted=False) +def job_template_with_survey_passwords_unit(job_template_with_survey_passwords_factory): + return job_template_with_survey_passwords_factory(persisted=False) diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index a5a961f88e..88437a0037 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -3,12 +3,14 @@ import mock # AWX from awx.api.serializers import JobTemplateSerializer, JobLaunchSerializer -from awx.main.models.jobs import JobTemplate +from awx.main.models.jobs import JobTemplate, Job from awx.main.models.projects import ProjectOptions +from awx.main.migrations import _save_password_keys as save_password_keys # Django from django.test.client import RequestFactory from django.core.urlresolvers import reverse +from django.apps import apps @property def project_playbooks(self): @@ -348,3 +350,20 @@ def test_disallow_template_delete_on_running_job(job_template_factory, delete, a objects.job_template.create_unified_job() delete_response = delete(reverse('api:job_template_detail', args=[objects.job_template.pk]), user=admin_user) assert delete_response.status_code == 409 + +@pytest.mark.django_db +def test_save_survey_passwords_to_job(job_template_with_survey_passwords): + """Test that when a new job is created, the survey_passwords field is + given all of the passwords that exist in the JT survey""" + job = job_template_with_survey_passwords.create_unified_job() + assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'} + +@pytest.mark.django_db +def test_save_survey_passwords_on_migration(job_template_with_survey_passwords): + """Test that when upgrading to 3.0.2, the jobs connected to a JT that has + a survey with passwords in it, the survey passwords get saved to the + job survey_passwords field.""" + Job.objects.create(job_template=job_template_with_survey_passwords) + save_password_keys.migrate_survey_passwords(apps, None) + job = job_template_with_survey_passwords.jobs.all()[0] + assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'} diff --git a/awx/main/tests/functional/api/test_survey_spec.py b/awx/main/tests/functional/api/test_survey_spec.py index dc7071fc11..d6cc512847 100644 --- a/awx/main/tests/functional/api/test_survey_spec.py +++ b/awx/main/tests/functional/api/test_survey_spec.py @@ -193,7 +193,8 @@ def test_launch_with_non_empty_survey_spec_no_license(job_template_factory, post @pytest.mark.django_db @pytest.mark.survey -def test_redact_survey_passwords_in_activity_stream(job_with_secret_key): +def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords): + job_template_with_survey_passwords.create_unified_job() AS_record = ActivityStream.objects.filter(object1='job').all()[0] changes_dict = json.loads(AS_record.changes) extra_vars = json.loads(changes_dict['extra_vars']) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index f970adc2e7..5e67dda1b5 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -206,8 +206,8 @@ def notification(notification_template): subject='email subject') @pytest.fixture -def job_with_secret_key(job_with_secret_key_factory): - return job_with_secret_key_factory(persisted=True) +def job_template_with_survey_passwords(job_template_with_survey_passwords_factory): + return job_template_with_survey_passwords_factory(persisted=True) @pytest.fixture def admin(user): diff --git a/awx/main/tests/unit/models/test_job_template_unit.py b/awx/main/tests/unit/models/test_job_template_unit.py index a25cce6f6c..b9a72edea5 100644 --- a/awx/main/tests/unit/models/test_job_template_unit.py +++ b/awx/main/tests/unit/models/test_job_template_unit.py @@ -35,6 +35,7 @@ def test_inventory_credential_contradictions(job_template_factory): assert 'credential' in validation_errors @pytest.mark.survey -def test_survey_password_list(job_with_secret_key_unit): - """Verify that survey_password_variables method gives a list of survey passwords""" - assert job_with_secret_key_unit.job_template.survey_password_variables() == ['secret_key', 'SSN'] +def test_job_template_survey_password_redaction(job_template_with_survey_passwords_unit): + """Tests the JobTemplate model's funciton to redact passwords from + extra_vars - used when creating a new job""" + assert job_template_with_survey_passwords_unit.survey_password_variables() == ['secret_key', 'SSN'] diff --git a/awx/main/tests/unit/models/test_job_unit.py b/awx/main/tests/unit/models/test_job_unit.py index a1791c59d5..1b66681dcf 100644 --- a/awx/main/tests/unit/models/test_job_unit.py +++ b/awx/main/tests/unit/models/test_job_unit.py @@ -2,6 +2,7 @@ import pytest import json from awx.main.tasks import RunJob +from awx.main.models import Job @pytest.fixture @@ -14,9 +15,19 @@ def job(mocker): 'launch_type': 'manual'}) @pytest.mark.survey -def test_job_redacted_extra_vars(job_with_secret_key_unit): - """Verify that this method redacts vars marked as passwords in a survey""" - assert json.loads(job_with_secret_key_unit.display_extra_vars()) == { +def test_job_survey_password_redaction(): + """Tests the Job model's funciton to redact passwords from + extra_vars - used when displaying job information""" + job = Job( + name="test-job-with-passwords", + extra_vars=json.dumps({ + 'submitter_email': 'foobar@redhat.com', + 'secret_key': '6kQngg3h8lgiSTvIEb21', + 'SSN': '123-45-6789'}), + survey_passwords={ + 'secret_key': '$encrypted$', + 'SSN': '$encrypted$'}) + assert json.loads(job.display_extra_vars()) == { 'submitter_email': 'foobar@redhat.com', 'secret_key': '$encrypted$', 'SSN': '$encrypted$'} From c1e340fbd6e7bbfce6cca72a9e8b47d4980e5633 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 9 Aug 2016 14:56:19 -0400 Subject: [PATCH 04/14] allow for 201 status_code from callback --- tools/scripts/request_tower_configuration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/scripts/request_tower_configuration.sh b/tools/scripts/request_tower_configuration.sh index 0e569ac5fd..86c90ac805 100644 --- a/tools/scripts/request_tower_configuration.sh +++ b/tools/scripts/request_tower_configuration.sh @@ -14,7 +14,7 @@ attempt=0 while [[ $attempt -lt $retry_attempts ]] do status_code=`curl -s -i --data "host_config_key=$2" http://$1/api/v1/job_templates/$3/callback/ | head -n 1 | awk '{print $2}'` - if [[ $status_code == 202 ]] + if [[ $status_code == 202 || $status_code == 201 ]] then exit 0 fi From a94e97366a1e02249e95766f80e278591c20b4db Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 10 Aug 2016 09:41:04 -0400 Subject: [PATCH 05/14] fix error processing survey vars --- awx/main/models/jobs.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a7c1c6041d..ac8cc3fbc7 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -446,11 +446,21 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): if field == 'extra_vars' and self.survey_enabled and self.survey_spec: # Accept vars defined in the survey and no others survey_vars = [question['variable'] for question in self.survey_spec.get('spec', [])] - for key in kwargs[field]: + extra_vars = kwargs[field] + if isinstance(extra_vars, basestring): + try: + extra_vars = json.loads(extra_vars) + except (ValueError, TypeError): + try: + extra_vars = yaml.safe_load(extra_vars) + assert isinstance(extra_vars, dict) + except (yaml.YAMLError, TypeError, AttributeError, AssertionError): + extra_vars = {} + for key in extra_vars: if key in survey_vars: - prompted_fields[field][key] = kwargs[field][key] + prompted_fields[field][key] = extra_vars[key] else: - ignored_fields[field][key] = kwargs[field][key] + ignored_fields[field][key] = extra_vars[key] else: ignored_fields[field] = kwargs[field] From fab0ff18d82ab41a0234d102219e8d5350081eee Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 10 Aug 2016 11:02:29 -0400 Subject: [PATCH 06/14] add unit test for survey vars as strings --- awx/main/tests/unit/models/test_job_template_unit.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/awx/main/tests/unit/models/test_job_template_unit.py b/awx/main/tests/unit/models/test_job_template_unit.py index a25cce6f6c..b5ba7b8301 100644 --- a/awx/main/tests/unit/models/test_job_template_unit.py +++ b/awx/main/tests/unit/models/test_job_template_unit.py @@ -1,4 +1,5 @@ import pytest +import json def test_missing_project_error(job_template_factory): @@ -34,6 +35,16 @@ def test_inventory_credential_contradictions(job_template_factory): assert 'inventory' in validation_errors assert 'credential' in validation_errors +def test_survey_answers_as_string(job_template_factory): + objects = job_template_factory( + 'job-template-with-survey', + survey=['var1'], + persisted=False) + jt = objects.job_template + user_extra_vars = json.dumps({'var1': 'asdf'}) + accepted, ignored = jt._accept_or_ignore_job_kwargs(extra_vars=user_extra_vars) + assert 'var1' in accepted['extra_vars'] + @pytest.mark.survey def test_survey_password_list(job_with_secret_key_unit): """Verify that survey_password_variables method gives a list of survey passwords""" From d8c713d5efa11dc825fc942dfc9ce79c2bb8eb74 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 10 Aug 2016 14:08:33 -0700 Subject: [PATCH 07/14] Making the username and password fields optional for email notifications --- awx/ui/client/src/notifications/add/add.controller.js | 6 ++++++ awx/ui/client/src/notifications/edit/edit.controller.js | 6 ++++++ .../client/src/notifications/notificationTemplates.form.js | 4 ---- .../client/src/notifications/shared/type-change.service.js | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index 148940e8a1..0b58f2e257 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -149,6 +149,12 @@ export default if(field.type === 'number'){ $scope[i] = Number($scope[i]); } + if(field.name === "username" && $scope.notification_type.value === "email" && value === null){ + $scope[i] = ""; + } + if(field.type === 'sensitive' && value === null){ + $scope[i] = ""; + } return $scope[i]; } diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index 4e0bc35772..44c50a71ce 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -223,6 +223,12 @@ export default if(field.type === 'number'){ $scope[i] = Number($scope[i]); } + if(field.name === "username" && $scope.notification_type.value === "email" && value === null){ + $scope[i] = ""; + } + if(field.type === 'sensitive' && value === null){ + $scope[i] = ""; + } return $scope[i]; } diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index d748ab96e4..cd0ff9d945 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -59,10 +59,6 @@ export default function() { username: { label: 'Username', type: 'text', - awRequiredWhen: { - reqExpression: "email_required", - init: "false" - }, ngShow: "notification_type.value == 'email' ", subForm: 'typeSubForm' }, diff --git a/awx/ui/client/src/notifications/shared/type-change.service.js b/awx/ui/client/src/notifications/shared/type-change.service.js index e7d63e51f5..0827e88b34 100644 --- a/awx/ui/client/src/notifications/shared/type-change.service.js +++ b/awx/ui/client/src/notifications/shared/type-change.service.js @@ -28,7 +28,7 @@ function () { obj.passwordLabel = ' Password'; obj.email_required = true; obj.port_required = true; - obj.password_required = true; + obj.password_required = false; break; case 'slack': obj.tokenLabel =' Token'; From c21e14292992bebb2b3ee1f612563ac466713117 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 11 Aug 2016 12:56:17 -0700 Subject: [PATCH 08/14] making ec2 credential optional for ec2 inventory and fixing the autopopulate for that field (it should not autopopulate) --- .../src/inventories/manage/groups/groups-add.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js index a816cacd3a..d1272f508b 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-add.controller.js @@ -100,6 +100,7 @@ // equal to case 'ec2' || 'rax' || 'azure' || 'azure_rm' || 'vmware' || 'satellite6' || 'cloudforms' || 'openstack' else{ var credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source)); + $scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false; CredentialList.basePath = credentialBasePath; LookUpInit({ scope: $scope, @@ -122,7 +123,7 @@ $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; - $scope.cloudCredentialRequired = source !== '' && source !== 'custom' ? true : false; + $scope.cloudCredentialRequired = source !== '' && source !== 'custom' && source !== 'ec2' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; From 5e4362da6992b7a52db98691f81abd2fe17fcccc Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 11 Aug 2016 13:09:56 -0700 Subject: [PATCH 09/14] making ec2 cred optional on group->edit --- .../src/inventories/manage/groups/groups-edit.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js index b008c0f888..941789f39d 100644 --- a/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js +++ b/awx/ui/client/src/inventories/manage/groups/groups-edit.controller.js @@ -100,6 +100,7 @@ else{ var credentialBasePath = (source.value === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source.value === '' ? '' : '?kind=' + (source.value)); CredentialList.basePath = credentialBasePath; + $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false; LookUpInit({ scope: $scope, url: credentialBasePath, @@ -122,7 +123,7 @@ // reset fields // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint $scope.source_region_choices = source.value === 'azure_rm' ? $scope.azure_regions : $scope[source.value + '_regions']; - $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' ? true : false; + $scope.cloudCredentialRequired = source.value !== '' && source.value !== 'custom' && source.value !== 'ec2' ? true : false; $scope.group_by = null; $scope.source_regions = null; $scope.credential = null; From f90b244fe65dfa0030f128f5828b9d6bb7395150 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 11 Aug 2016 17:34:35 -0400 Subject: [PATCH 10/14] Prevent ignored task from being displayed as failing. --- awx/api/views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index dfc7685aa0..85ad006ed3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. @@ -2987,7 +2986,7 @@ class JobJobTasksList(BaseJobEventsList): # need stats on grandchildren, sorted by child. queryset = (JobEvent.objects.filter(parent__parent=parent_task, parent__event__in=STARTING_EVENTS) - .values('parent__id', 'event', 'changed') + .values('parent__id', 'event', 'changed', 'failed') .annotate(num=Count('event')) .order_by('parent__id')) @@ -3048,10 +3047,13 @@ class JobJobTasksList(BaseJobEventsList): # make appropriate changes to the task data. for child_data in data.get(task_start_event.id, []): if child_data['event'] == 'runner_on_failed': - task_data['failed'] = True task_data['host_count'] += child_data['num'] task_data['reported_hosts'] += child_data['num'] - task_data['failed_count'] += child_data['num'] + if child_data['failed']: + task_data['failed'] = True + task_data['failed_count'] += child_data['num'] + else: + task_data['skipped_count'] += child_data['num'] elif child_data['event'] == 'runner_on_ok': task_data['host_count'] += child_data['num'] task_data['reported_hosts'] += child_data['num'] From ee66fd4aa54041db1fd0e7df2bb11872c585b931 Mon Sep 17 00:00:00 2001 From: James Laska Date: Mon, 8 Aug 2016 11:24:46 -0400 Subject: [PATCH 11/14] Make CloudForms inventory_script work Fixes a few flake8 issues while at it. --- awx/plugins/inventory/cloudforms.py | 68 ++++++++++++++++++----------- awx/settings/defaults.py | 10 +++++ 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/awx/plugins/inventory/cloudforms.py b/awx/plugins/inventory/cloudforms.py index 3de81d0bd2..8d9854974f 100755 --- a/awx/plugins/inventory/cloudforms.py +++ b/awx/plugins/inventory/cloudforms.py @@ -18,10 +18,11 @@ import json # http://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings requests.packages.urllib3.disable_warnings() + class CloudFormsInventory(object): def _empty_inventory(self): - return {"_meta" : {"hostvars" : {}}} + return {"_meta": {"hostvars": {}}} def __init__(self): ''' Main execution path ''' @@ -43,7 +44,7 @@ class CloudFormsInventory(object): # This doesn't exist yet and needs to be added if self.args.host: - data2 = { } + data2 = {} print json.dumps(data2, indent=2) def parse_cli_args(self): @@ -51,9 +52,9 @@ class CloudFormsInventory(object): parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms') parser.add_argument('--list', action='store_true', default=False, - help='List instances (default: False)') + help='List instances (default: False)') parser.add_argument('--host', action='store', - help='Get all the variables about a specific instance') + help='Get all the variables about a specific instance') self.args = parser.parse_args() def read_settings(self): @@ -97,30 +98,47 @@ class CloudFormsInventory(object): def get_hosts(self): ''' Gets host from CloudForms ''' - r = requests.get("https://" + self.cloudforms_hostname + "/api/vms?expand=resources&attributes=name,power_state", auth=(self.cloudforms_username,self.cloudforms_password), verify=False) - + r = requests.get("https://{0}/api/vms?expand=resources&attributes=all".format(self.cloudforms_hostname), + auth=(self.cloudforms_username, self.cloudforms_password), verify=False) obj = r.json() - #Remove objects that don't matter - del obj["count"] - del obj["subcount"] - del obj["name"] + # Create groups+hosts based on host data + for resource in obj.get('resources', []): - #Create a new list to grab VMs with power_state on to add to a new list - #I'm sure there is a cleaner way to do this - newlist = [] - getnext = False - for x in obj.items(): - for y in x[1]: - for z in y.items(): - if getnext == True: - newlist.append(z[1]) - getnext = False - if ( z[0] == "power_state" and z[1] == "on" ): - getnext = True - newdict = {'hosts': newlist} - newdict2 = {'Dynamic_CloudForms': newdict} - print json.dumps(newdict2, indent=2) + # Maintain backwards compat by creating `Dynamic_CloudForms` group + if 'Dynamic_CloudForms' not in self.inventory: + self.inventory['Dynamic_CloudForms'] = [] + self.inventory['Dynamic_CloudForms'].append(resource['name']) + + # Add host to desired groups + for key in ('vendor', 'type', 'location'): + if key in resource: + # Create top-level group + if key not in self.inventory: + self.inventory[key] = dict(children=[], vars={}, hosts=[]) + # if resource['name'] not in self.inventory[key]['hosts']: + # self.inventory[key]['hosts'].append(resource['name']) + + # Create sub-group + if resource[key] not in self.inventory: + self.inventory[resource[key]] = dict(children=[], vars={}, hosts=[]) + # self.inventory[resource[key]]['hosts'].append(resource['name']) + + # Add sub-group, as a child of top-level + if resource[key] not in self.inventory[key]['children']: + self.inventory[key]['children'].append(resource[key]) + + # Add host to sub-group + if resource['name'] not in self.inventory[resource[key]]: + self.inventory[resource[key]]['hosts'].append(resource['name']) + + # Delete 'actions' key + del resource['actions'] + + # Add _meta hostvars + self.inventory['_meta']['hostvars'][resource['name']] = resource + + print json.dumps(self.inventory, indent=2) # Run the script CloudFormsInventory() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index aa8f69866b..2998d15bb7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -682,6 +682,16 @@ SATELLITE6_HOST_FILTER = r'^.+$' SATELLITE6_EXCLUDE_EMPTY_GROUPS = True SATELLITE6_INSTANCE_ID_VAR = 'foreman.id' +# --------------------- +# ----- CloudForms ----- +# --------------------- +CLOUDFORMS_ENABLED_VAR = 'power_state' +CLOUDFORMS_ENABLED_VALUE = 'on' +CLOUDFORMS_GROUP_FILTER = r'^.+$' +CLOUDFORMS_HOST_FILTER = r'^.+$' +CLOUDFORMS_EXCLUDE_EMPTY_GROUPS = True +CLOUDFORMS_INSTANCE_ID_VAR = 'id' + # --------------------- # -- Activity Stream -- # --------------------- From efb66cad20dd4afe396b46075008c161ba46a7cf Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 12 Aug 2016 07:31:01 -0400 Subject: [PATCH 12/14] bail when status code is over 300 --- tools/scripts/request_tower_configuration.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/scripts/request_tower_configuration.sh b/tools/scripts/request_tower_configuration.sh index 86c90ac805..eb7431c6b4 100644 --- a/tools/scripts/request_tower_configuration.sh +++ b/tools/scripts/request_tower_configuration.sh @@ -14,7 +14,11 @@ attempt=0 while [[ $attempt -lt $retry_attempts ]] do status_code=`curl -s -i --data "host_config_key=$2" http://$1/api/v1/job_templates/$3/callback/ | head -n 1 | awk '{print $2}'` - if [[ $status_code == 202 || $status_code == 201 ]] + if [[ $status_code -ge 300 ]] + then + echo "${status_code} received, encountered problem, halting." + exit 1 + elif [[ $status_code -gt 200 ]] then exit 0 fi From ba101573d6e366f3530b35e5a4e8744b7e7345d9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 12 Aug 2016 11:15:15 -0400 Subject: [PATCH 13/14] interpret any code below 300 as success --- tools/scripts/request_tower_configuration.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/scripts/request_tower_configuration.sh b/tools/scripts/request_tower_configuration.sh index eb7431c6b4..4b3b731772 100644 --- a/tools/scripts/request_tower_configuration.sh +++ b/tools/scripts/request_tower_configuration.sh @@ -18,8 +18,7 @@ do then echo "${status_code} received, encountered problem, halting." exit 1 - elif [[ $status_code -gt 200 ]] - then + else exit 0 fi attempt=$(( attempt + 1 )) From 5467b233ebe019eeddabca885bd51bb334414a08 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 12 Aug 2016 16:28:57 -0400 Subject: [PATCH 14/14] fix credential kind options for list --- awx/ui/client/src/controllers/Credentials.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js index 49150e22c9..bc84930255 100644 --- a/awx/ui/client/src/controllers/Credentials.js +++ b/awx/ui/client/src/controllers/Credentials.js @@ -46,13 +46,13 @@ export function CredentialsList($scope, $rootScope, $location, $log, Wait('stop'); $('#prompt-modal').modal('hide'); - list.fields.kind.searchOptions = $scope.credential_kind_options; + list.fields.kind.searchOptions = $scope.credential_kind_options_list; // Translate the kind value for (i = 0; i < $scope.credentials.length; i++) { - for (j = 0; j < $scope.credential_kind_options.length; j++) { - if ($scope.credential_kind_options[j].value === $scope.credentials[i].kind) { - $scope.credentials[i].kind = $scope.credential_kind_options[j].label; + for (j = 0; j < $scope.credential_kind_options_list.length; j++) { + if ($scope.credential_kind_options_list[j].value === $scope.credentials[i].kind) { + $scope.credentials[i].kind = $scope.credential_kind_options_list[j].label; break; } } @@ -77,6 +77,15 @@ export function CredentialsList($scope, $rootScope, $location, $log, $scope.search(list.iterator); }); + // Load the list of options for Kind + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options_list', + callback: 'choicesReadyCredential' + }); + $scope.addCredential = function () { $state.transitionTo('credentials.add'); };