From b7d907ed49d4bd1c8950890e6fb7fc8dffd84f56 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 26 Sep 2013 15:54:30 -0400 Subject: [PATCH] AC-505 Initial inventory import models and API support. --- awx/main/access.py | 41 ++ awx/main/base_views.py | 1 + awx/main/migrations/0016_v14_changes.py | 430 ++++++++++++++++++ awx/main/models/__init__.py | 355 ++++++++++++++- awx/main/serializers.py | 62 +++ awx/main/tasks.py | 36 +- awx/main/tests/commands.py | 4 +- awx/main/tests/scripts.py | 4 +- awx/main/urls.py | 13 + awx/main/views.py | 68 +++ .../inventory.py => plugins/inventory/awx.py} | 10 +- 11 files changed, 1006 insertions(+), 18 deletions(-) create mode 100644 awx/main/migrations/0016_v14_changes.py rename awx/{scripts/inventory.py => plugins/inventory/awx.py} (95%) diff --git a/awx/main/access.py b/awx/main/access.py index eebeb2e6a8..297e2dc753 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -427,6 +427,45 @@ class GroupAccess(BaseAccess): return False return True +class InventorySourceAccess(BaseAccess): + ''' + I can see inventory sources whenever I can see their group. + I can change inventory sources whenever I can change their group. + ''' + + model = InventorySource + + def get_queryset(self): + qs = self.model.objects.filter(active=True).distinct() + qs = qs.select_related('created_by', 'group') + groups_qs = self.user.get_queryset(Group) + return qs.filter(group__in=groups_qs) + + def can_read(self, obj): + return obj and self.user.can_access(Group, 'read', obj.group) + + def can_add(self, data): + # Automatically created from group. + return False + + def can_change(self, obj, data): + # Checks for admin or change permission on group. + return obj and self.user.can_access(Group, 'change', obj.group, None) + +class InventoryUpdateAccess(BaseAccess): + ''' + I can see inventory updates when I can see the inventory source. + I can change inventory updates whenever I can change their source. + ''' + + model = InventoryUpdate + + def get_queryset(self): + qs = InventoryUpdate.objects.filter(active=True).distinct() + qs = qs.select_related('created_by', 'group') + inventory_sources_qs = self.user.get_queryset(InventorySource) + return qs.filter(inventory_source__in=inventory_sources_qs) + class CredentialAccess(BaseAccess): ''' I can see credentials when: @@ -970,6 +1009,8 @@ register_access(Organization, OrganizationAccess) register_access(Inventory, InventoryAccess) register_access(Host, HostAccess) register_access(Group, GroupAccess) +register_access(InventorySource, InventorySourceAccess) +register_access(InventoryUpdate, InventoryUpdateAccess) register_access(Credential, CredentialAccess) register_access(Team, TeamAccess) register_access(Project, ProjectAccess) diff --git a/awx/main/base_views.py b/awx/main/base_views.py index 5858d7cdaa..0ac7b3e9c1 100644 --- a/awx/main/base_views.py +++ b/awx/main/base_views.py @@ -53,6 +53,7 @@ class APIView(views.APIView): return { 'docstring': type(self).__doc__ or '', 'new_in_13': getattr(self, 'new_in_13', False), + 'new_in_14': getattr(self, 'new_in_14', False), } def get_description(self, html=False): diff --git a/awx/main/migrations/0016_v14_changes.py b/awx/main/migrations/0016_v14_changes.py new file mode 100644 index 0000000000..b480df3a30 --- /dev/null +++ b/awx/main/migrations/0016_v14_changes.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'InventoryUpdate' + db.create_table(u'main_inventoryupdate', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('modified', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'inventoryupdate', 'app_label': 'main'}(class)s_created+", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('modified_by', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'inventoryupdate', 'app_label': 'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('inventory_source', self.gf('django.db.models.fields.related.ForeignKey')(related_name='inventory_updates', to=orm['main.InventorySource'])), + ('cancel_flag', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('status', self.gf('django.db.models.fields.CharField')(default='new', max_length=20)), + ('failed', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('job_args', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('job_cwd', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('job_env', self.gf('jsonfield.fields.JSONField')(default={}, blank=True)), + ('result_stdout', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('result_traceback', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('celery_task_id', self.gf('django.db.models.fields.CharField')(default='', max_length=100, blank=True)), + )) + db.send_create_signal('main', ['InventoryUpdate']) + + # Adding model 'InventorySource' + db.create_table(u'main_inventorysource', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('modified', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'inventorysource', 'app_label': u'main'}(class)s_created+", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('modified_by', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name="{'class': 'inventorysource', 'app_label': u'main'}(class)s_modified+", null=True, on_delete=models.SET_NULL, to=orm['auth.User'])), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('group', self.gf('awx.main.fields.AutoOneToOneField')(related_name='inventory_source', unique=True, to=orm['main.Group'])), + ('source', self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True)), + ('source_path', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('source_env', self.gf('django.db.models.fields.TextField')(default='', blank=True)), + ('source_username', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('source_password', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('source_regions', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('source_tags', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True)), + ('overwrite_hosts', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('overwrite_vars', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('keep_vars', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('update_on_launch', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('current_update', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='inventory_source_as_current_update+', null=True, to=orm['main.InventoryUpdate'])), + ('last_update', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='inventory_source_as_last_update+', null=True, to=orm['main.InventoryUpdate'])), + ('last_update_failed', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('last_updated', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True)), + ('status', self.gf('django.db.models.fields.CharField')(default='none', max_length=32, null=True)), + )) + db.send_create_signal(u'main', ['InventorySource']) + + + def backwards(self, orm): + # Deleting model 'InventoryUpdate' + db.delete_table(u'main_inventoryupdate') + + # Deleting model 'InventorySource' + db.delete_table(u'main_inventorysource') + + + 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', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 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', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + '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'}) + }, + u'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': {'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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'}), + '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'}), + 'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_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', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'}) + }, + 'main.group': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('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']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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']"}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}), + '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': u"orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': {'unique_together': "(('name', 'organization'),)", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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']"}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + u'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': u\'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_current_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'inventory_source'", 'unique': 'True', 'to': "orm['main.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'keep_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_source_as_last_update+'", 'null': 'True', 'to': "orm['main.InventoryUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventorysource\', \'app_label\': u\'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'overwrite_hosts': ('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_env': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'source_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', '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_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32', 'null': 'True'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate'}, + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'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'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': u"orm['main.InventorySource']"}), + '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventoryupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.job': { + 'Meta': {'object_name': 'Job'}, + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': u"orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + '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_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'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'job\', \'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'}), + 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + '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'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + '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', [], {'related_name': "'job_events_as_primary_host'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_events'", 'blank': 'True', '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'children'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobEvent']", 'blank': 'True', 'null': 'True'}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + u'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + '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', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}), + 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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': {'object_name': 'JobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'jobtemplate\', \'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'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': u"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', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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': u"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']"}) + }, + u'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 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', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + u'main.project': { + 'Meta': {'object_name': 'Project'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_current_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'project_as_last_update+'", 'null': 'True', 'to': "orm['main.ProjectUpdate']"}), + 'last_update_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'project\', \'app_label\': u\'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'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', '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_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'scm_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'null': 'True', 'blank': 'True'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'null': 'True', 'blank': 'True'}), + 'scm_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32', 'null': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate'}, + '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', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'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'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'projectupdate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': u"orm['main.Project']"}), + 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}) + }, + 'main.team': { + 'Meta': {'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + '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', [], {'unique': 'True', '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': u"orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) + } + } + + complete_apps = ['main'] \ No newline at end of file diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index e629f558fe..a957834369 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -40,8 +40,8 @@ from awx.main.utils import encrypt_field, decrypt_field __all__ = ['PrimordialModel', 'Organization', 'Team', 'Project', 'ProjectUpdate', 'Credential', 'Inventory', 'Host', 'Group', - 'Permission', 'JobTemplate', 'Job', 'JobHostSummary', 'JobEvent', - 'AuthToken', + 'InventorySource', 'InventoryUpdate', 'Permission', 'JobTemplate', + 'Job', 'JobHostSummary', 'JobEvent', 'AuthToken', 'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_STATUS_CHOICES'] @@ -254,6 +254,14 @@ class Host(CommonModelNameNotUnique): last_job_host_summary = models.ForeignKey('JobHostSummary', blank=True, null=True, default=None, on_delete=models.SET_NULL, related_name='hosts_as_last_job_summary+') has_active_failures = models.BooleanField(default=False, editable=False) + # FIXME: Track which inventory source(s) created this host. + #inventory_sources = models.ManyToManyField( + # 'InventorySource', + # related_name='synced_hosts', + # blank=True, + # editable=False, + #) + def __unicode__(self): return self.name @@ -317,6 +325,14 @@ class Group(CommonModelNameNotUnique): hosts = models.ManyToManyField('Host', related_name='groups', blank=True) has_active_failures = models.BooleanField(default=False, editable=False) + # FIXME: Track which inventory source(s) created this group. + #inventory_sources = models.ManyToManyField( + # 'InventorySource', + # related_name='synced_groups', + # blank=True, + # editable=False, + #) + def __unicode__(self): return self.name @@ -407,6 +423,341 @@ class Group(CommonModelNameNotUnique): def job_events(self): return JobEvent.objects.filter(host__in=self.all_hosts) +class InventorySource(PrimordialModel): + + # FIXME: Track inventory source for import via management command? + + SOURCE_CHOICES = [ + ('file', _('Local File or Script')), + ('rackspace', _('Rackspace Cloud Servers')), + ('ec2', _('Amazon EC2')), + ] + + PASSWORD_FIELDS = ('source_password',) + + INVENTORY_SOURCE_STATUS_CHOICES = [ + ('none', _('No External Source')), + ('never updated', _('Never Updated')), + ('updating', _('Updating')), + ('failed', _('Failed')), + ('successful', _('Successful')), + ] + + group = AutoOneToOneField( + 'Group', + related_name='inventory_source', + editable=False, + ) + source = models.CharField( + max_length=32, + choices=SOURCE_CHOICES, + blank=True, + default='', + ) + source_path = models.CharField( + max_length=1024, + blank=True, + default='', + ) + source_env = models.TextField( + blank=True, + default='', + ) + source_username = models.CharField( + max_length=1024, + blank=True, + default='', + ) + source_password = models.CharField( + max_length=1024, + blank=True, + default='', + ) + source_regions = models.CharField( + max_length=1024, + blank=True, + default='', + ) + source_tags = models.CharField( + max_length=1024, + blank=True, + default='', + ) + overwrite_hosts = models.BooleanField( + default=False, + ) + overwrite_vars = models.BooleanField( + default=False, + ) + keep_vars = models.BooleanField( + default=False, + ) + update_on_launch = models.BooleanField( + default=False, + ) + current_update = models.ForeignKey( + 'InventoryUpdate', + null=True, + default=None, + editable=False, + related_name='inventory_source_as_current_update+', + ) + last_update = models.ForeignKey( + 'InventoryUpdate', + null=True, + default=None, + editable=False, + related_name='inventory_source_as_last_update+', + ) + last_update_failed = models.BooleanField( + default=False, + editable=False, + ) + last_updated = models.DateTimeField( + null=True, + default=None, + editable=False, + ) + status = models.CharField( + max_length=32, + choices=INVENTORY_SOURCE_STATUS_CHOICES, + default='none', + editable=False, + null=True, + ) + + def save(self, *args, **kwargs): + new_instance = not bool(self.pk) + # If update_fields has been specified, add our field names to it, + # if it hasn't been specified, then we're just doing a normal save. + update_fields = kwargs.get('update_fields', []) + # When first saving to the database, don't store any password field + # values, but instead save them until after the instance is created. + if new_instance: + for field in self.PASSWORD_FIELDS: + value = getattr(self, field, '') + setattr(self, '_saved_%s' % field, value) + setattr(self, field, '') + # Otherwise, store encrypted values to the database. + else: + for field in self.PASSWORD_FIELDS: + encrypted = encrypt_field(self, field, True) + if getattr(self, field) != encrypted: + setattr(self, field, encrypted) + if field not in update_fields: + update_fields.append(field) + # Update status and last_updated fields. + updated_fields = self.set_status_and_last_updated(save=False) + for field in updated_fields: + if field not in update_fields: + update_fields.append(field) + # Do the actual save. + super(InventorySource, self).save(*args, **kwargs) + # After saving a new instance for the first time (to get a primary + # key), set the password fields and save again. + if new_instance: + update_fields=[] + for field in self.PASSWORD_FIELDS: + saved_value = getattr(self, '_saved_%s' % field, '') + if getattr(self, field) != saved_value: + setattr(self, field, saved_value) + update_fields.append(field) + if update_fields: + self.save(update_fields=update_fields) + + @property + def needs_source_password(self): + return self.source and self.source_password == 'ASK' + + @property + def source_passwords_needed(self): + needed = [] + for field in ('source_password',): + if getattr(self, 'needs_%s' % field): + needed.append(field) + return needed + + def set_status_and_last_updated(self, save=True): + # Determine current status. + if self.source: + if self.current_update: + status = 'updating' + elif not self.last_update: + status = 'never updated' + elif self.last_update_failed: + status = 'failed' + else: + status = 'successful' + else: + status = 'none' + # Determine current last_updated timestamp. + last_updated = None + if self.source and self.last_update: + last_updated = self.last_update.modified + # Update values if changed. + update_fields = [] + if self.status != status: + self.status = status + update_fields.append('status') + if self.last_updated != last_updated: + self.last_updated = last_updated + update_fields.append('last_updated') + if save and update_fields: + self.save(update_fields=update_fields) + return update_fields + + @property + def can_update(self): + # FIXME: Prevent update when another one is active! + return bool(self.source) + + def update(self, **kwargs): + if self.can_update: + needed = self.source_passwords_needed + opts = dict([(field, kwargs.get(field, '')) for field in needed]) + if not all(opts.values()): + return + inventory_update = self.inventory_updates.create() + inventory_update.start(**opts) + return inventory_update + + def get_absolute_url(self): + return reverse('main:inventory_source_detail', args=(self.pk,)) + +class InventoryUpdate(PrimordialModel): + ''' + Internal job for tracking inventory updates from external sources. + ''' + + class Meta: + app_label = 'main' + + inventory_source = models.ForeignKey( + 'InventorySource', + related_name='inventory_updates', + on_delete=models.CASCADE, + editable=False, + ) + cancel_flag = models.BooleanField( + blank=True, + default=False, + editable=False, + ) + status = models.CharField( + max_length=20, + choices=JOB_STATUS_CHOICES, + default='new', + editable=False, + ) + failed = models.BooleanField( + default=False, + editable=False, + ) + job_args = models.TextField( + blank=True, + default='', + editable=False, + ) + job_cwd = models.CharField( + max_length=1024, + blank=True, + default='', + editable=False, + ) + job_env = JSONField( + blank=True, + default={}, + editable=False, + ) + result_stdout = models.TextField( + blank=True, + default='', + editable=False, + ) + result_traceback = models.TextField( + blank=True, + default='', + editable=False, + ) + celery_task_id = models.CharField( + max_length=100, + blank=True, + default='', + editable=False, + ) + + def __unicode__(self): + return u'%s-%s-%s' % (self.created, self.id, self.status) + + def save(self, *args, **kwargs): + # Get status before save... + status_before = self.status or 'new' + if self.pk: + inventory_update_before = InventoryUpdate.objects.get(pk=self.pk) + if inventory_update_before.status != self.status: + status_before = inventory_update_before.status + self.failed = bool(self.status in ('failed', 'error', 'canceled')) + super(InventoryUpdate, self).save(*args, **kwargs) + # If status changed, update inventory. + if self.status != status_before: + if self.status in ('pending', 'waiting', 'running'): + inventory_source = self.inventory_source + if inventory_source.current_update != self: + inventory_source.current_update = self + inventory_source.save(update_fields=['current_update']) + elif self.status in ('successful', 'failed', 'error', 'canceled'): + inventory_source = self.inventory_source + if inventory_source.current_update == self: + inventory_source.current_update = None + inventory_source.last_update = self + inventory_source.last_update_failed = self.failed + inventory_source.save(update_fields=['current_update', 'last_update', + 'last_update_failed']) + + def get_absolute_url(self): + return reverse('main:inventory_update_detail', args=(self.pk,)) + + @property + def celery_task(self): + try: + if self.celery_task_id: + return TaskMeta.objects.get(task_id=self.celery_task_id) + except TaskMeta.DoesNotExist: + pass + + @property + def can_start(self): + return bool(self.status == 'new') + + def start(self, **kwargs): + from awx.main.tasks import RunInventoryUpdate + needed = self.inventory_source.source_passwords_needed + opts = dict([(field, kwargs.get(field, '')) for field in needed]) + if not all(opts.values()): + return False + self.status = 'pending' + self.save(update_fields=['status']) + task_result = RunInventoryUpdate().delay(self.pk, **opts) + # Reload inventory update from database so we don't clobber results + # from RunInventoryUpdate (mainly from tests when using Django 1.4.x). + inventory_update = InventoryUpdate.objects.get(pk=self.pk) + # The TaskMeta instance in the database isn't created until the worker + # starts processing the task, so we can only store the task ID here. + inventory_update.celery_task_id = task_result.task_id + inventory_update.save(update_fields=['celery_task_id']) + return True + + @property + def can_cancel(self): + return bool(self.status in ('pending', 'waiting', 'running')) + + def cancel(self): + if self.can_cancel: + if not self.cancel_flag: + self.cancel_flag = True + self.save(update_fields=['cancel_flag']) + return self.cancel_flag + class Credential(CommonModelNameNotUnique): ''' A credential contains information about how to talk to a remote set of hosts diff --git a/awx/main/serializers.py b/awx/main/serializers.py index 1e4cb67530..b7aa1184a7 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -488,6 +488,7 @@ class GroupSerializer(BaseSerializerWithVariables): inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), job_events = reverse('main:group_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('main:group_job_host_summaries_list', args=(obj.pk,)), + inventory_source = reverse('main:inventory_source_detail', args=(obj.inventory_source.pk,)), )) return res @@ -541,6 +542,67 @@ class GroupVariableDataSerializer(BaseVariableDataSerializer): model = Group fields = ('variables',) +class InventorySourceSerializer(BaseSerializer): + + source_password = serializers.WritableField(required=False, default='') + + class Meta: + model = InventorySource + fields = ('id', 'url', 'related', 'summary_fields', 'created', + 'modified', 'group', 'source', 'source_path', 'source_env', + 'source_username', 'source_password', 'source_regions', + 'source_tags', 'overwrite_hosts', 'overwrite_vars', + 'keep_vars', 'update_on_launch', 'last_update_failed', + 'status', 'last_updated') + + def to_native(self, obj): + ret = super(InventorySourceSerializer, self).to_native(obj) + # Replace the actual encrypted value with the string $encrypted$. + for field in InventorySource.PASSWORD_FIELDS: + if field in ret and unicode(ret[field]).startswith('$encrypted$'): + ret[field] = '$encrypted$' + return ret + + def restore_object(self, attrs, instance=None): + # If the value sent to the API startswith $encrypted$, ignore it. + for field in InventorySource.PASSWORD_FIELDS: + if unicode(attrs.get(field, '')).startswith('$encrypted$'): + attrs.pop(field, None) + instance = super(InventorySourceSerializer, self).restore_object(attrs, instance) + return instance + + def get_related(self, obj): + res = super(InventorySourceSerializer, self).get_related(obj) + res.update(dict( + group = reverse('main:group_detail', args=(obj.group.pk,)), + update = reverse('main:inventory_source_update_view', args=(obj.pk,)), + inventory_updates = reverse('main:inventory_source_updates_list', args=(obj.pk,)), + )) + if obj.current_update: + res['current_update'] = reverse('main:inventory_update_detail', + args=(obj.current_update.pk,)) + if obj.last_update: + res['last_update'] = reverse('main:inventory_update_detail', + args=(obj.last_update.pk,)) + return res + +class InventoryUpdateSerializer(BaseSerializer): + + class Meta: + model = InventoryUpdate + fields = ('id', 'url', 'related', 'summary_fields', 'created', + 'modified', 'inventory_source', 'status', 'failed', + 'result_stdout', 'result_traceback', 'job_args', 'job_cwd', + 'job_env') + + def get_related(self, obj): + res = super(InventoryUpdateSerializer, self).get_related(obj) + res.update(dict( + inventory_source = reverse('main:inventory_source_detail', args=(obj.inventory_source.pk,)), + cancel = reverse('main:inventory_update_cancel', args=(obj.pk,)), + )) + return res + class TeamSerializer(BaseSerializer): class Meta: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 78d0029804..2857da92b1 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -27,10 +27,10 @@ from django.conf import settings from django.utils.timezone import now # AWX -from awx.main.models import Job, ProjectUpdate +from awx.main.models import Job, ProjectUpdate, InventoryUpdate from awx.main.utils import get_ansible_version, decrypt_field, update_scm_url -__all__ = ['RunJob', 'RunProjectUpdate'] +__all__ = ['RunJob', 'RunProjectUpdate', 'RunInventoryImport'] logger = logging.getLogger('awx.main.tasks') @@ -231,12 +231,12 @@ class BaseTask(Task): Run the job/task using ansible-playbook and capture its output. ''' instance = self.update_model(pk) - if not self.pre_run_check(instance, **kwargs): - return - instance = self.update_model(pk, status='running') status, stdout, tb = 'error', '', '' output_replacements = [] try: + if not self.pre_run_check(instance, **kwargs): + return + instance = self.update_model(pk, status='running') kwargs['ssh_key_path'] = self.build_ssh_key_path(instance, **kwargs) kwargs['passwords'] = self.build_passwords(instance, **kwargs) args = self.build_args(instance, **kwargs) @@ -322,7 +322,8 @@ class RunJob(BaseTask): # it doesn't make sense to rely on ansible-playbook's default of using # the current user. ssh_username = ssh_username or 'root' - inventory_script = self.get_path_to('..', 'scripts', 'inventory.py') + inventory_script = self.get_path_to('..', 'plugins', 'inventory', + 'awx.py') args = ['ansible-playbook', '-i', inventory_script] if job.job_type == 'check': args.append('--check') @@ -622,3 +623,26 @@ class RunProjectUpdate(BaseTask): return False else: return False + +class RunInventoryUpdate(BaseTask): + + name = 'run_inventory_update' + model = InventoryUpdate + + def build_env(self, inventory_update, **kwargs): + ''' + Build environment dictionary for inventory import. + ''' + env = super(RunInventoryUpdate, self).build_env(inventory_update, **kwargs) + # FIXME + return env + + def build_args(self, inventory_update, **kwargs): + ''' + Build command line argument list for running inventory import. + ''' + # FIXME + return ['echo', 'FIXME'] + + def build_cwd(self, inventory_update, **kwargs): + return self.get_path_to('..', 'plugins', 'inventory') diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index 4355907282..df0fb46470 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -575,8 +575,8 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): #os.environ.setdefault('REST_API_TOKEN', # self.super_django_user.auth_token.key) os.environ['INVENTORY_ID'] = str(old_inv.pk) - source = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', - 'inventory.py') + source = os.path.join(os.path.dirname(__file__), '..', '..', 'plugins', + 'inventory', 'awx.py') result, stdout, stderr = self.run_command('inventory_import', inventory_id=new_inv.pk, source=source) diff --git a/awx/main/tests/scripts.py b/awx/main/tests/scripts.py index 17e9c3a3a2..7fb0aaaa70 100644 --- a/awx/main/tests/scripts.py +++ b/awx/main/tests/scripts.py @@ -130,8 +130,8 @@ class InventoryScriptTest(BaseScriptTest): os.environ.setdefault('REST_API_URL', rest_api_url) #os.environ.setdefault('REST_API_TOKEN', # self.super_django_user.auth_token.key) - name = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', - 'inventory.py') + name = os.path.join(os.path.dirname(__file__), '..', '..', 'plugins', + 'inventory', 'awx.py') return self.run_script(name, *args, **options) def test_without_inventory_id(self): diff --git a/awx/main/urls.py b/awx/main/urls.py index 6719435c31..2070a6db9a 100644 --- a/awx/main/urls.py +++ b/awx/main/urls.py @@ -86,6 +86,17 @@ group_urls = patterns('awx.main.views', url(r'^(?P[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'), ) +inventory_source_urls = patterns('awx.main.views', + url(r'^(?P[0-9]+)/$', 'inventory_source_detail'), + url(r'^(?P[0-9]+)/update/$', 'inventory_source_update_view'), + url(r'^(?P[0-9]+)/inventory_updates/$', 'inventory_source_updates_list'), +) + +inventory_update_urls = patterns('awx.main.views', + url(r'^(?P[0-9]+)/$', 'inventory_update_detail'), + url(r'^(?P[0-9]+)/cancel/$', 'inventory_update_cancel'), +) + credential_urls = patterns('awx.main.views', url(r'^$', 'credential_list'), url(r'^(?P[0-9]+)/$', 'credential_detail'), @@ -136,6 +147,8 @@ v1_urls = patterns('awx.main.views', url(r'^inventories/', include(inventory_urls)), url(r'^hosts/', include(host_urls)), url(r'^groups/', include(group_urls)), + url(r'^inventory_sources/', include(inventory_source_urls)), + url(r'^inventory_updates/', include(inventory_update_urls)), url(r'^credentials/', include(credential_urls)), url(r'^permissions/', include(permission_urls)), url(r'^job_templates/', include(job_template_urls)), diff --git a/awx/main/views.py b/awx/main/views.py index 7fab76a1e5..8248f41032 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -680,6 +680,74 @@ class InventoryTreeView(RetrieveAPIView): }) return d +class InventorySourceDetail(RetrieveUpdateAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + new_in_14 = True + +class InventorySourceUpdatesList(SubListAPIView): + + model = InventoryUpdate + serializer_class = InventoryUpdateSerializer + parent_model = InventorySource + relationship = 'inventory_updates' + new_in_14 = True + +class InventorySourceUpdateView(GenericAPIView): + + model = InventorySource + new_in_14 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_update=obj.can_update, + ) + if obj.source: + data['passwords_needed_to_update'] = obj.source_passwords_needed + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_update: + inventory_update = obj.update(**request.DATA) + if not inventory_update: + data = dict(passwords_needed_to_update=obj.source_passwords_needed) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + headers = {'Location': inventory_update.get_absolute_url()} + return Response(status=status.HTTP_202_ACCEPTED, headers=headers) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class InventoryUpdateDetail(RetrieveAPIView): + + model = InventoryUpdate + serializer_class = InventoryUpdateSerializer + new_in_14 = True + +class InventoryUpdateCancel(GenericAPIView): + + model = InventoryUpdate + is_job_cancel = True + new_in_14 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_cancel=obj.can_cancel, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_cancel: + result = obj.cancel() + return Response(status=status.HTTP_202_ACCEPTED) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + class JobTemplateList(ListCreateAPIView): model = JobTemplate diff --git a/awx/scripts/inventory.py b/awx/plugins/inventory/awx.py similarity index 95% rename from awx/scripts/inventory.py rename to awx/plugins/inventory/awx.py index a03ae5baf0..e8ad1a6eea 100755 --- a/awx/scripts/inventory.py +++ b/awx/plugins/inventory/awx.py @@ -45,9 +45,10 @@ try: except ImportError: # If running from an AWX installation, use the local version of requests if # if cannot be found globally. - local_site_packages = os.path.join(os.path.dirname(__file__), '..', 'lib', - 'site-packages') - sys.path.insert(0, local_site_packages) + local_site_packages = os.path.join(os.path.dirname(__file__), '..', '..', + 'lib', 'site-packages') + if os.path.exists(local_site_packages): + sys.path.insert(0, local_site_packages) import requests class TokenAuth(requests.auth.AuthBase): @@ -121,9 +122,6 @@ class InventoryScript(object): except Exception, e: # Always return an empty hash on stdout, even when an error occurs. sys.stdout.write(json.dumps({})) - #print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), repr(e) - #if hasattr(e, 'response'): - # print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), e.response.content if self.options.get('traceback', False): raise sys.stderr.write(str(e) + '\n')