From e427234aec894e2166a38e3d9b7bff73637bccea Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 11 Sep 2014 17:34:31 -0400 Subject: [PATCH] Implements https://trello.com/c/bEMQtVjz - API/UI changes to support su username/password. Adds force_handlers, skip_tasks and start_at_task options to jobs, only exposed via API. --- awx/api/serializers.py | 10 +- .../commands/run_callback_receiver.py | 8 +- awx/main/migrations/0054_v210_changes.py | 496 ++++++++++++++++++ awx/main/models/credential.py | 22 +- awx/main/models/jobs.py | 65 ++- awx/main/tasks.py | 23 +- awx/main/tests/jobs.py | 17 +- awx/main/tests/projects.py | 8 +- awx/main/tests/tasks.py | 126 ++++- awx/ui/static/js/controllers/Credentials.js | 40 +- awx/ui/static/js/forms/Credentials.js | 55 +- awx/ui/static/js/helpers/Credentials.js | 21 + 12 files changed, 809 insertions(+), 82 deletions(-) create mode 100644 awx/main/migrations/0054_v210_changes.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 976524c103..92e314fb99 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1135,13 +1135,15 @@ class CredentialSerializer(BaseSerializer): ssh_key_data = serializers.WritableField(required=False, default='') ssh_key_unlock = serializers.WritableField(required=False, default='') sudo_password = serializers.WritableField(required=False, default='') + su_password = serializers.WritableField(required=False, default='') vault_password = serializers.WritableField(required=False, default='') class Meta: model = Credential fields = ('*', 'user', 'team', 'kind', 'cloud', 'host', 'username', 'password', 'project', 'ssh_key_data', 'ssh_key_unlock', - 'sudo_username', 'sudo_password', 'vault_password') + 'sudo_username', 'sudo_password', 'su_username', + 'su_password', 'vault_password') def to_native(self, obj): ret = super(CredentialSerializer, self).to_native(obj) @@ -1180,7 +1182,8 @@ class JobOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'job_type', 'inventory', 'project', 'playbook', 'credential', 'cloud_credential', 'forks', 'limit', - 'verbosity', 'extra_vars', 'job_tags') + 'verbosity', 'extra_vars', 'job_tags', 'force_handlers', + 'skip_tags', 'start_at_task') def get_related(self, obj): res = super(JobOptionsSerializer, self).get_related(obj) @@ -1296,6 +1299,9 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): data.setdefault('verbosity', job_template.verbosity) data.setdefault('extra_vars', job_template.extra_vars) data.setdefault('job_tags', job_template.job_tags) + data.setdefault('force_handlers', job_template.force_handlers) + data.setdefault('skip_tags', job_template.skip_tags) + data.setdefault('start_at_task', job_template.start_at_task) return super(JobSerializer, self).from_native(data, files) def to_native(self, obj): diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index 038a0af25e..ff8851bee8 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -174,9 +174,11 @@ class CallbackReceiver(object): with transaction.atomic(): # If we're not in verbose mode, wipe out any module # arguments. - i = data['event_data'].get('res', {}).get('invocation', {}) - if verbose == 0 and 'module_args' in i: - i['module_args'] = '' + res = data['event_data'].get('res', {}) + if isinstance(res, dict): + i = res.get('invocation', {}) + if verbose == 0 and 'module_args' in i: + i['module_args'] = '' # Create a new JobEvent object. job_event = JobEvent(**data) diff --git a/awx/main/migrations/0054_v210_changes.py b/awx/main/migrations/0054_v210_changes.py new file mode 100644 index 0000000000..762515e3c5 --- /dev/null +++ b/awx/main/migrations/0054_v210_changes.py @@ -0,0 +1,496 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Job.force_handlers' + db.add_column(u'main_job', 'force_handlers', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'Job.skip_tags' + db.add_column(u'main_job', 'skip_tags', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'Job.start_at_task' + db.add_column(u'main_job', 'start_at_task', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'Credential.su_username' + db.add_column(u'main_credential', 'su_username', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'Credential.su_password' + db.add_column(u'main_credential', 'su_password', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'JobTemplate.force_handlers' + db.add_column(u'main_jobtemplate', 'force_handlers', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'JobTemplate.skip_tags' + db.add_column(u'main_jobtemplate', 'skip_tags', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + # Adding field 'JobTemplate.start_at_task' + db.add_column(u'main_jobtemplate', 'start_at_task', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Job.force_handlers' + db.delete_column(u'main_job', 'force_handlers') + + # Deleting field 'Job.skip_tags' + db.delete_column(u'main_job', 'skip_tags') + + # Deleting field 'Job.start_at_task' + db.delete_column(u'main_job', 'start_at_task') + + # Deleting field 'Credential.su_username' + db.delete_column(u'main_credential', 'su_username') + + # Deleting field 'Credential.su_password' + db.delete_column(u'main_credential', 'su_password') + + # Deleting field 'JobTemplate.force_handlers' + db.delete_column(u'main_jobtemplate', 'force_handlers') + + # Deleting field 'JobTemplate.skip_tags' + db.delete_column(u'main_jobtemplate', 'skip_tags') + + # Deleting field 'JobTemplate.start_at_task' + db.delete_column(u'main_jobtemplate', 'start_at_task') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}), + 'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}), + 'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}), + 'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}), + 'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'ordering': "('kind', 'name')", 'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'groups'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'ordering': "('inventory', 'name')", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'hosts'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'hosts_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Job']"}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventory': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventorysources'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'default': 'None', 'related_name': "'inventory_source'", 'unique': 'True', 'null': 'True', 'to': "orm['main.Group']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventoryupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.job': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Job', '_ormbases': ['main.UnifiedJob']}, + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_events_as_primary_host'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'job_events'", 'symmetrical': 'False', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.JobEvent']"}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'role': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host_name')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_host_summaries'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.jobtemplate': { + 'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'survey_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'survey_spec': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.schedule': { + 'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'next_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'schedules'", 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.team': { + 'Meta': {'ordering': "('organization__name', 'name')", 'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.unifiedjob': { + 'Meta': {'object_name': 'UnifiedJob'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'dependent_jobs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'dependent_jobs_rel_+'", 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'elapsed': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '3'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'finished': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'job_explanation': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjob_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'result_stdout_file': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_stdout_text': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.Schedule']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'start_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'started': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjob_unified_jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.unifiedjobtemplate': { + 'Meta': {'unique_together': "[('polymorphic_ctype', 'name')]", 'object_name': 'UnifiedJobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_current_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'has_schedules': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'last_job_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'next_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'next_schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_next_schedule+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Schedule']"}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjobtemplate_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32'}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index cdde8655c7..446b303981 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -39,7 +39,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ] PASSWORD_FIELDS = ('password', 'ssh_key_data', 'ssh_key_unlock', - 'sudo_password', 'vault_password') + 'sudo_password', 'su_password', 'vault_password') class Meta: app_label = 'main' @@ -126,6 +126,18 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): default='', help_text=_('Sudo password (or "ASK" to prompt the user).'), ) + su_username = models.CharField( + max_length=1024, + blank=True, + default='', + help_text=_('Su username for a job using this credential.'), + ) + su_password = models.CharField( + max_length=1024, + blank=True, + default='', + help_text=_('Su password (or "ASK" to prompt the user).'), + ) vault_password = models.CharField( max_length=1024, blank=True, @@ -155,6 +167,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): def needs_sudo_password(self): return self.kind == 'ssh' and self.sudo_password == 'ASK' + @property + def needs_su_password(self): + return self.kind == 'ssh' and self.su_password == 'ASK' + @property def needs_vault_password(self): return self.kind == 'ssh' and self.vault_password == 'ASK' @@ -162,7 +178,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): @property def passwords_needed(self): needed = [] - for field in ('password', 'sudo_password', 'ssh_key_unlock', 'vault_password'): + for field in ('password', 'sudo_password', 'su_password', 'ssh_key_unlock', 'vault_password'): if getattr(self, 'needs_%s' % field): needed.append(field) return needed @@ -308,6 +324,8 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): def clean(self): if self.user and self.team: raise ValidationError('Credential cannot be assigned to both a user and team') + if (self.sudo_username or self.sudo_password) and (self.su_username or self.su_password): + raise ValidationError('Credential cannot specify both sudo username/password and su username/password') def _validate_unique_together_with_null(self, unique_check, exclude=None): # Based on existing Django model validation code, except it doesn't diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 6bf9937149..e30485be36 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -52,8 +52,9 @@ __all__ = ['JobTemplate', 'Job', 'JobHostSummary', 'JobEvent'] class JobOptions(BaseModel): ''' + Common options for job templates and jobs. ''' - + class Meta: abstract = True @@ -115,6 +116,20 @@ class JobOptions(BaseModel): blank=True, default='', ) + force_handlers = models.BooleanField( + blank=True, + default=False, + ) + skip_tags = models.CharField( + max_length=1024, + blank=True, + default='', + ) + start_at_task = models.CharField( + max_length=1024, + blank=True, + default='', + ) extra_vars_dict = VarsDictProperty('extra_vars', True) @@ -135,6 +150,18 @@ class JobOptions(BaseModel): ) return cred + @property + def passwords_needed_to_start(self): + '''Return list of password field names needed to start the job.''' + needed = [] + if self.credential: + for pw in self.credential.passwords_needed: + if pw == 'password': + needed.append('ssh_password') + else: + needed.append(pw) + return needed + class JobTemplate(UnifiedJobTemplate, JobOptions): ''' @@ -174,7 +201,8 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): def _get_unified_job_field_names(cls): return ['name', 'description', 'job_type', 'inventory', 'project', 'playbook', 'credential', 'cloud_credential', 'forks', - 'limit', 'verbosity', 'extra_vars', 'job_tags'] + 'limit', 'verbosity', 'extra_vars', 'job_tags', + 'force_handlers', 'skip_tags', 'start_at_task'] def create_job(self, **kwargs): ''' @@ -190,26 +218,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): Return whether job template can be used to start a new job without requiring any user input. ''' - needed = [] - if self.credential: - for pw in self.credential.passwords_needed: - if pw == 'password': - needed.append('ssh_password') - else: - needed.append(pw) - return bool(self.credential and not len(needed)) - - @property - def passwords_needed_to_start(self): - '''Return list of password field names needed to start the job.''' - needed = [] - if self.credential: - for pw in self.credential.passwords_needed: - if pw == 'password': - needed.append('ssh_password') - else: - needed.append(pw) - return needed + return bool(self.credential and not len(self.passwords_needed_to_start)) @property def variables_needed_to_start(self): @@ -337,18 +346,6 @@ class Job(UnifiedJob, JobOptions): return self.job_template.ask_variables_on_launch return False - @property - def passwords_needed_to_start(self): - '''Return list of password field names needed to start the job.''' - needed = [] - if self.credential: - for pw in self.credential.passwords_needed: - if pw == 'password': - needed.append('ssh_password') - else: - needed.append(pw) - return needed - def get_passwords_needed_to_start(self): return self.passwords_needed_to_start diff --git a/awx/main/tasks.py b/awx/main/tasks.py index beeba7844c..f39e89ec39 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -479,13 +479,13 @@ class RunJob(BaseTask): def build_passwords(self, job, **kwargs): ''' - Build a dictionary of passwords for SSH private key, SSH user, sudo + Build a dictionary of passwords for SSH private key, SSH user, sudo/su and ansible-vault. ''' passwords = super(RunJob, self).build_passwords(job, **kwargs) creds = job.credential if creds: - for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password', 'vault_password'): + for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password', 'su_password', 'vault_password'): if field == 'ssh_password': value = kwargs.get(field, decrypt_field(creds, 'password')) else: @@ -550,10 +550,11 @@ class RunJob(BaseTask): optionally using ssh-agent for public/private key authentication. ''' creds = job.credential - ssh_username, sudo_username = '', '' + ssh_username, sudo_username, su_username = '', '', '' if creds: ssh_username = kwargs.get('username', creds.username) sudo_username = kwargs.get('sudo_username', creds.sudo_username) + su_username = kwargs.get('su_username', creds.su_username) # Always specify the normal SSH user as root by default. Since this # task is normally running in the background under a service account, # it doesn't make sense to rely on ansible-playbook's default of using @@ -567,9 +568,12 @@ class RunJob(BaseTask): args.extend(['-u', ssh_username]) if 'ssh_password' in kwargs.get('passwords', {}): args.append('--ask-pass') - # However, we should only specify sudo user if explicitly given by the - # credentials, otherwise, the playbook will be forced to run using - # sudo, which may not always be the desired behavior. + # We only specify sudo/su user and password if explicitly given by the + # credential. Credential should never specify both sudo and su. + if su_username: + args.extend(['-R', su_username]) + if 'su_password' in kwargs.get('passwords', {}): + args.append('--ask-su-pass') if sudo_username: args.extend(['-U', sudo_username]) if 'sudo_password' in kwargs.get('passwords', {}): @@ -591,12 +595,18 @@ class RunJob(BaseTask): if job.forks: # FIXME: Max limit? args.append('--forks=%d' % job.forks) + if job.force_handlers: + args.append('--force-handlers') if job.limit: args.extend(['-l', job.limit]) if job.verbosity: args.append('-%s' % ('v' * min(3, job.verbosity))) if job.job_tags: args.extend(['-t', job.job_tags]) + if job.skip_tags: + args.append('--skip-tags=%s' % job.skip_tags) + if job.start_at_task: + args.append('--start-at-task=%s' % job.start_at_task) # Define special extra_vars for Tower, combine with job.extra_vars. extra_vars = { @@ -642,6 +652,7 @@ class RunJob(BaseTask): d[re.compile(r'^Enter passphrase for .*:\s*?$', re.M)] = 'ssh_key_unlock' d[re.compile(r'^Bad passphrase, try again for .*:\s*?$', re.M)] = '' d[re.compile(r'^sudo password.*:\s*?$', re.M)] = 'sudo_password' + d[re.compile(r'^su password.*:\s*?$', re.M)] = 'su_password' d[re.compile(r'^SSH password:\s*?$', re.M)] = 'ssh_password' d[re.compile(r'^Password:\s*?$', re.M)] = 'ssh_password' d[re.compile(r'^Vault password:\s*?$', re.M)] = 'vault_password' diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index 93336c5694..1f0e2bc175 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -622,13 +622,16 @@ class BaseJobTestMixin(BaseTestMixin): class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): - JOB_TEMPLATE_FIELDS = ('id', 'type', 'url', 'related', 'summary_fields', 'created', - 'modified', 'name', 'description', 'job_type', - 'inventory', 'project', 'playbook', 'credential', - 'cloud_credential', 'forks', 'limit', 'verbosity', - 'extra_vars', 'ask_variables_on_launch', 'job_tags', - 'host_config_key', 'status', 'next_job_run', - 'has_schedules', 'last_job_run', 'last_job_failed', 'survey_enabled') + JOB_TEMPLATE_FIELDS = ('id', 'type', 'url', 'related', 'summary_fields', + 'created', 'modified', 'name', 'description', + 'job_type', 'inventory', 'project', 'playbook', + 'credential', 'use_su_credential', 'sudo_su_flag', + 'cloud_credential', 'force_handlers', 'forks', + 'limit', 'verbosity', 'extra_vars', + 'ask_variables_on_launch', 'job_tags', 'skip_tags', + 'start_at_task', 'host_config_key', 'status', + 'next_job_run', 'has_schedules', 'last_job_run', + 'last_job_failed', 'survey_enabled') def test_get_job_template_list(self): url = reverse('api:job_template_list') diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 90e7dc6fff..4ba27c4056 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -34,7 +34,7 @@ TEST_PLAYBOOK = '''- hosts: mygroup command: test 1 = 1 ''' -class ProjectsTest(BaseTest): +class ProjectsTest(BaseTransactionTest): # tests for users, projects, and teams @@ -541,6 +541,12 @@ class ProjectsTest(BaseTest): sudo_username=None) self.post(url, data, expect=400) + # Test trying to pass both sudo_username and su_username. + with self.current_user(self.super_django_user): + data = dict(name='zyq', user=self.super_django_user.pk, kind='ssh', + sudo_username='sudouser', su_username='suuser') + self.post(url, data, expect=400) + # Test with encrypted ssh key and no unlock password. with self.current_user(self.super_django_user): data = dict(name='wxy', user=self.super_django_user.pk, kind='ssh', diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index 4bf4e7affa..274835d8a6 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -39,6 +39,24 @@ TEST_PLAYBOOK2 = '''- name: test failed command: test 1 = 0 ''' +TEST_PLAYBOOK_WITH_TAGS = u''' +- name: test with tags + hosts: test-group + gather_facts: False + tasks: + - name: should fail but skipped using --start-at-task="start here" + command: test 1 = 0 + tags: runme + - name: start here + command: test 1 = 1 + tags: runme + - name: should fail but skipped using --skip-tags=skipme + command: test 1 = 0 + tags: skipme + - name: should fail but skipped without runme tag + command: test 1 = 0 +''' + TEST_EXTRA_VARS_PLAYBOOK = ''' - name: test extra vars hosts: test-group @@ -315,6 +333,8 @@ class RunJobTest(BaseCeleryTest): 'password': '', 'sudo_username': '', 'sudo_password': '', + 'su_username': '', + 'su_password': '', 'vault_password': '', } opts.update(kwargs) @@ -796,7 +816,8 @@ class RunJobTest(BaseCeleryTest): def test_extra_job_options(self): self.create_test_project(TEST_EXTRA_VARS_PLAYBOOK) # Test with extra_vars containing misc whitespace. - job_template = self.create_test_job_template(forks=3, verbosity=2, + job_template = self.create_test_job_template(force_handlers=True, + forks=3, verbosity=2, extra_vars=u'{\n\t"abc": 1234\n}') job = self.create_test_job(job_template=job_template) self.assertEqual(job.status, 'new') @@ -804,9 +825,10 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start()) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') - self.assertTrue('--forks=3' in job.job_args) - self.assertTrue('-vv' in job.job_args) - self.assertTrue('-e' in job.job_args) + self.assertTrue('"--force-handlers"' in job.job_args) + self.assertTrue('"--forks=3"' in job.job_args) + self.assertTrue('"-vv"' in job.job_args) + self.assertTrue('"-e"' in job.job_args) # Test with extra_vars as key=value (old format). job_template2 = self.create_test_job_template(extra_vars='foo=1') job2 = self.create_test_job(job_template=job_template2) @@ -833,7 +855,7 @@ class RunJobTest(BaseCeleryTest): job = Job.objects.get(pk=job.pk) self.assertTrue(len(job.job_args) > 1024) self.check_job_result(job, 'successful') - self.assertTrue('-e' in job.job_args) + self.assertTrue('"-e"' in job.job_args) def test_limit_option(self): self.create_test_project(TEST_PLAYBOOK) @@ -844,7 +866,7 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start()) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') - self.assertTrue('-l' in job.job_args) + self.assertTrue('"-l"' in job.job_args) def test_limit_option_with_group_pattern_and_ssh_key(self): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA) @@ -856,9 +878,24 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start()) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') - self.assertTrue('--private-key=' in job.job_args) + self.assertTrue('"--private-key=' in job.job_args) self.assertFalse('ssh-agent' in job.job_args) + def test_tag_and_task_options(self): + self.create_test_project(TEST_PLAYBOOK_WITH_TAGS) + job_template = self.create_test_job_template(job_tags='runme', + skip_tags='skipme', + start_at_task='start here') + job = self.create_test_job(job_template=job_template) + self.assertEqual(job.status, 'new') + self.assertFalse(job.passwords_needed_to_start) + self.assertTrue(job.signal_start()) + job = Job.objects.get(pk=job.pk) + self.check_job_result(job, 'successful') + self.assertTrue('"-t"' in job.job_args) + self.assertTrue('"--skip-tags=' in job.job_args) + self.assertTrue('"--start-at-task=' in job.job_args) + def test_ssh_username_and_password(self): self.create_test_credential(username='sshuser', password='sshpass') self.create_test_project(TEST_PLAYBOOK) @@ -869,8 +906,8 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start()) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') - self.assertTrue('-u' in job.job_args) - self.assertTrue('--ask-pass' in job.job_args) + self.assertTrue('"-u"' in job.job_args) + self.assertTrue('"--ask-pass"' in job.job_args) def test_ssh_ask_password(self): self.create_test_credential(password='ASK') @@ -885,7 +922,7 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start(ssh_password='sshpass')) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') - self.assertTrue('--ask-pass' in job.job_args) + self.assertTrue('"--ask-pass"' in job.job_args) def test_sudo_username_and_password(self): self.create_test_credential(sudo_username='sudouser', @@ -900,8 +937,12 @@ class RunJobTest(BaseCeleryTest): # 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(job, ('successful', 'failed')) - self.assertTrue('-U' in job.job_args) - self.assertTrue('--ask-sudo-pass' in job.job_args) + self.assertTrue('"-U"' in job.job_args) + self.assertTrue('"--ask-sudo-pass"' in job.job_args) + self.assertFalse('"-s"' in job.job_args) + self.assertFalse('"-R"' in job.job_args) + self.assertFalse('"--ask-su-pass"' in job.job_args) + self.assertFalse('"-S"' in job.job_args) def test_sudo_ask_password(self): self.create_test_credential(sudo_password='ASK') @@ -911,13 +952,58 @@ class RunJobTest(BaseCeleryTest): self.assertEqual(job.status, 'new') self.assertTrue(job.passwords_needed_to_start) self.assertTrue('sudo_password' in job.passwords_needed_to_start) + self.assertFalse('su_password' in job.passwords_needed_to_start) self.assertFalse(job.signal_start()) self.assertTrue(job.signal_start(sudo_password='sudopass')) job = Job.objects.get(pk=job.pk) # Job may fail if current user doesn't have password-less sudo # privileges, but we're mainly checking the command line arguments. self.assertTrue(job.status in ('successful', 'failed')) - self.assertTrue('--ask-sudo-pass' in job.job_args) + self.assertTrue('"--ask-sudo-pass"' in job.job_args) + self.assertFalse('"-s"' in job.job_args) + self.assertFalse('"-R"' in job.job_args) + self.assertFalse('"--ask-su-pass"' in job.job_args) + self.assertFalse('"-S"' in job.job_args) + + def test_su_username_and_password(self): + self.create_test_credential(su_username='suuser', + su_password='supass') + self.create_test_project(TEST_PLAYBOOK) + job_template = self.create_test_job_template() + job = self.create_test_job(job_template=job_template) + self.assertEqual(job.status, 'new') + self.assertFalse(job.passwords_needed_to_start) + self.assertTrue(job.signal_start()) + job = Job.objects.get(pk=job.pk) + # Job may fail, but we're mainly checking the command line arguments. + self.check_job_result(job, ('successful', 'failed')) + self.assertTrue('"-R"' in job.job_args) + self.assertTrue('"--ask-su-pass"' in job.job_args) + self.assertFalse('"-S"' in job.job_args) + self.assertFalse('"-U"' in job.job_args) + self.assertFalse('"--ask-sudo-pass"' in job.job_args) + self.assertFalse('"-s"' in job.job_args) + + def test_su_ask_password(self): + self.create_test_credential(su_password='ASK') + self.create_test_project(TEST_PLAYBOOK) + job_template = self.create_test_job_template() + job = self.create_test_job(job_template=job_template) + self.assertEqual(job.status, 'new') + self.assertTrue(job.passwords_needed_to_start) + self.assertTrue('su_password' in job.passwords_needed_to_start) + self.assertFalse('sudo_password' in job.passwords_needed_to_start) + self.assertFalse(job.signal_start()) + self.assertTrue(job.signal_start(su_password='supass')) + job = Job.objects.get(pk=job.pk) + # Job may fail, but we're mainly checking the command line arguments. + self.assertTrue(job.status in ('successful', 'failed')) + self.assertTrue('"--ask-su-pass"' in job.job_args) + self.assertFalse('"-S"' in job.job_args) + self.assertFalse('"-R"' in job.job_args) + self.assertFalse('"-U"' in job.job_args) + self.assertFalse('"--ask-sudo-pass"' in job.job_args) + self.assertFalse('"-s"' in job.job_args) def test_unlocked_ssh_key(self): self.create_test_credential(ssh_key_data=TEST_SSH_KEY_DATA) @@ -929,7 +1015,7 @@ class RunJobTest(BaseCeleryTest): self.assertTrue(job.signal_start()) job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'successful') - self.assertTrue('--private-key=' in job.job_args) + self.assertTrue('"--private-key=' in job.job_args) self.assertFalse('ssh-agent' in job.job_args) def test_locked_ssh_key_with_password(self): @@ -991,10 +1077,10 @@ class RunJobTest(BaseCeleryTest): job = Job.objects.get(pk=job.pk) if Version(self.ansible_version) >= Version('1.5'): self.check_job_result(job, 'successful') - self.assertTrue('--ask-vault-pass' in job.job_args) + self.assertTrue('"--ask-vault-pass"' in job.job_args) else: self.check_job_result(job, 'failed') - self.assertFalse('--ask-vault-pass' in job.job_args) + self.assertFalse('"--ask-vault-pass"' in job.job_args) def test_vault_ask_password(self): self.create_test_credential(vault_password='ASK') @@ -1010,10 +1096,10 @@ class RunJobTest(BaseCeleryTest): job = Job.objects.get(pk=job.pk) if Version(self.ansible_version) >= Version('1.5'): self.check_job_result(job, 'successful') - self.assertTrue('--ask-vault-pass' in job.job_args) + self.assertTrue('"--ask-vault-pass"' in job.job_args) else: self.check_job_result(job, 'failed') - self.assertFalse('--ask-vault-pass' in job.job_args) + self.assertFalse('"--ask-vault-pass"' in job.job_args) def test_vault_bad_password(self): self.create_test_credential(vault_password='not it') @@ -1026,9 +1112,9 @@ class RunJobTest(BaseCeleryTest): job = Job.objects.get(pk=job.pk) self.check_job_result(job, 'failed') if Version(self.ansible_version) >= Version('1.5'): - self.assertTrue('--ask-vault-pass' in job.job_args) + self.assertTrue('"--ask-vault-pass"' in job.job_args) else: - self.assertFalse('--ask-vault-pass' in job.job_args) + self.assertFalse('"--ask-vault-pass"' in job.job_args) def _test_cloud_credential_environment_variables(self, kind): if kind == 'aws': diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index ccab6efca8..0611a425ec 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -136,7 +136,7 @@ CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeP function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit, LookUpInit, UserList, TeamList, - GetBasePath, GetChoices, Empty, KindChange, OwnerChange, FormSave) { + GetBasePath, GetChoices, Empty, KindChange, OwnerChange, LoginMethodChange, FormSave) { ClearScope(); @@ -209,6 +209,17 @@ function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $routePar OwnerChange({ scope: $scope }); } + if (!Empty($routeParams.su_username) || !Empty($routeParams.su_password)) { + $scope.login_method = 'su'; + LoginMethodChange({ scope: $scope }); + } else if (!Empty($routeParams.sudo_username) || !Empty($routeParams.sudo_password)) { + $scope.login_method = 'sudo'; + LoginMethodChange({ scope: $scope }); + } else { + $scope.login_method = ''; + LoginMethodChange({ scope: $scope }); + } + // Handle Kind change $scope.kindChange = function () { KindChange({ scope: $scope, form: form, reset: true }); @@ -228,6 +239,11 @@ function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $routePar OwnerChange({ scope: $scope }); }; + // Handle Login Method change + $scope.loginMethodChange = function () { + LoginMethodChange({ scope: $scope }); + }; + // Reset defaults $scope.formReset = function () { //DebugForm({ scope: $scope, form: CredentialForm }); @@ -266,13 +282,13 @@ function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $routePar CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GenerateList', 'SearchInit', 'PaginateInit', - 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'OwnerChange', 'FormSave' + 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'OwnerChange', 'LoginMethodChange', 'FormSave' ]; function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, Prompt, GetBasePath, GetChoices, - KindChange, UserList, TeamList, LookUpInit, Empty, OwnerChange, FormSave, Stream, Wait) { + KindChange, UserList, TeamList, LookUpInit, Empty, OwnerChange, LoginMethodChange, FormSave, Stream, Wait) { ClearScope(); @@ -338,6 +354,7 @@ function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routePa reset: false }); OwnerChange({ scope: $scope }); + LoginMethodChange({ scope: $scope }); Wait('stop'); }); @@ -377,6 +394,15 @@ function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routePa } master.owner = $scope.owner; + if (!Empty($scope.su_username) || !Empty($scope.su_password)) { + $scope.login_method = 'su'; + } else if (!Empty($scope.sudo_username) || !Empty($scope.sudo_password)) { + $scope.login_method = 'sudo'; + } else { + $scope.login_method = ''; + } + master.login_method = $scope.login_method; + for (i = 0; i < $scope.credential_kind_options.length; i++) { if ($scope.credential_kind_options[i].value === data.kind) { $scope.kind = $scope.credential_kind_options[i]; @@ -445,6 +471,11 @@ function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routePa OwnerChange({ scope: $scope }); }; + // Handle Login Method change + $scope.loginMethodChange = function () { + LoginMethodChange({ scope: $scope }); + }; + // Handle Kind change $scope.kindChange = function () { KindChange({ scope: $scope, form: form, reset: true }); @@ -459,6 +490,7 @@ function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routePa setAskCheckboxes(); KindChange({ scope: $scope, form: form, reset: false }); OwnerChange({ scope: $scope }); + LoginMethodChange({ scope: $scope }); }; // Related set: Add button @@ -538,5 +570,5 @@ function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routePa CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', 'KindChange', 'UserList', 'TeamList', 'LookUpInit', - 'Empty', 'OwnerChange', 'FormSave', 'Stream', 'Wait' + 'Empty', 'OwnerChange', 'LoginMethodChange', 'FormSave', 'Stream', 'Wait' ]; \ No newline at end of file diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index ece3d98dc2..b7fb38d685 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -309,10 +309,29 @@ angular.module('CredentialFormDefinition', []) awPassMatch: true, associated: 'ssh_key_unlock' }, + "login_method": { + label: "Login Method", // FIXME: Confirm this label is ok? + type: 'radio_group', + ngChange: "loginMethodChange()", + options: [{ + label: 'None', // FIXME: Maybe 'Default' or 'SSH only' instead? + value: '', + selected: true + }, { + label: 'Sudo', + value: 'sudo' + }, { + label: 'Su', + value: 'su' + }], + awPopOver: "

A credential may optionally provide a sudo username and password or su username and password to use when running a playbook.

", + dataPlacement: 'right', + dataContainer: "body" + }, "sudo_username": { label: 'Sudo Username', type: 'text', - ngShow: "kind.value == 'ssh'", + ngShow: "kind.value == 'ssh' && login_method == 'sudo'", addRequired: false, editRequired: false, autocomplete: false @@ -320,7 +339,7 @@ angular.module('CredentialFormDefinition', []) "sudo_password": { label: 'Sudo Password', type: 'password', - ngShow: "kind.value == 'ssh'", + ngShow: "kind.value == 'ssh' && login_method == 'sudo'", addRequired: false, editRequired: false, ngChange: "clearPWConfirm('sudo_password_confirm')", @@ -332,13 +351,43 @@ angular.module('CredentialFormDefinition', []) "sudo_password_confirm": { label: 'Confirm Sudo Password', type: 'password', - ngShow: "kind.value == 'ssh'", + ngShow: "kind.value == 'ssh' && login_method == 'sudo'", addRequired: false, editRequired: false, awPassMatch: true, associated: 'sudo_password', autocomplete: false }, + "su_username": { + label: 'Su Username', + type: 'text', + ngShow: "kind.value == 'ssh' && login_method == 'su'", + addRequired: false, + editRequired: false, + autocomplete: false + }, + "su_password": { + label: 'Su Password', + type: 'password', + ngShow: "kind.value == 'ssh' && login_method == 'su'", + addRequired: false, + editRequired: false, + ngChange: "clearPWConfirm('su_password_confirm')", + ask: true, + clear: true, + associated: 'su_password_confirm', + autocomplete: false + }, + "su_password_confirm": { + label: 'Confirm Su Password', + type: 'password', + ngShow: "kind.value == 'ssh' && login_method == 'su'", + addRequired: false, + editRequired: false, + awPassMatch: true, + associated: 'su_password', + autocomplete: false + }, "project": { label: "Project", type: 'text', diff --git a/awx/ui/static/js/helpers/Credentials.js b/awx/ui/static/js/helpers/Credentials.js index 9dfcf2e5ea..5ce5807268 100644 --- a/awx/ui/static/js/helpers/Credentials.js +++ b/awx/ui/static/js/helpers/Credentials.js @@ -102,6 +102,9 @@ angular.module('CredentialsHelper', ['Utilities']) scope.sudo_username = null; scope.sudo_password = null; scope.sudo_password_confirm = null; + scope.su_username = null; + scope.su_password = null; + scope.su_password_confirm = null; } // Collapse or open help widget based on whether scm value is selected @@ -143,6 +146,24 @@ angular.module('CredentialsHelper', ['Utilities']) ]) +.factory('LoginMethodChange', [ + function () { + return function (params) { + var scope = params.scope, + login_method = scope.login_method; + if (login_method !== 'sudo') { + scope.sudo_username = null; + scope.sudo_password = null; + } + if (login_method !== 'su') { + scope.su_username = null; + scope.su_password = null; + } + }; + } +]) + + .factory('FormSave', ['$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', function ($location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait) { return function (params) {