diff --git a/Makefile b/Makefile index a6bffca063..d7e7f091c2 100644 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ clean-bundle: # Remove temporary build files, compiled Python files. clean: clean-rpm clean-deb clean-grunt clean-ui clean-tar clean-packer clean-bundle rm -rf awx/lib/site-packages - rm- rf awx/lib/.deps_built + rm -rf awx/lib/.deps_built rm -rf dist/* rm -rf tmp mkdir tmp @@ -700,5 +700,13 @@ docker-compose: docker-compose -f tools/docker-compose.yml up --no-recreate docker-compose-test: - cd tools && docker-compose run --service-ports tower /bin/bash + cd tools && docker-compose run --rm --service-ports tower /bin/bash +mongo-debug-ui: + docker run -it --rm --name mongo-express --link tools_mongo_1:mongo -e ME_CONFIG_OPTIONS_EDITORTHEME=ambiance -e ME_CONFIG_BASICAUTH_USERNAME=admin -e ME_CONFIG_BASICAUTH_PASSWORD=password -p 8081:8081 knickers/mongo-express + +mongo-container: + docker run -it --link tools_mongo_1:mongo --rm mongo sh -c 'exec mongo "$MONGO_PORT_27017_TCP_ADDR:$MONGO_PORT_27017_TCP_PORT/system_tracking_dev"' + +psql-container: + docker run -it --link tools_postgres_1:postgres --rm postgres:9.4.1 sh -c 'exec psql -h "$$POSTGRES_PORT_5432_TCP_ADDR" -p "$$POSTGRES_PORT_5432_TCP_PORT" -U postgres' diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 8c1eceac97..72dccc61f4 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -50,7 +50,10 @@ class TokenAuthentication(authentication.TokenAuthentication): auth = TokenAuthentication._get_x_auth_token_header(request).split() if not auth or auth[0].lower() != 'token': auth = authentication.get_authorization_header(request).split() - if not auth or auth[0].lower() != 'token': + # Prefer basic auth over cookie token + if auth and auth[0].lower() == 'basic': + return None + elif not auth or auth[0].lower() != 'token': auth = TokenAuthentication._get_auth_token_cookie(request).split() if not auth or auth[0].lower() != 'token': return None diff --git a/awx/api/license.py b/awx/api/license.py index acdc508614..60d10f5dcd 100644 --- a/awx/api/license.py +++ b/awx/api/license.py @@ -31,4 +31,17 @@ def feature_enabled(name): return False # Return the correct feature flag. - return get_license()['features'].get(name, False) + return license['features'].get(name, False) + +def feature_exists(name): + """Return True if the requested feature is enabled, False otherwise. + If the feature does not exist, raise KeyError. + """ + license = get_license() + + # Sanity check: If there is no license, the feature is considered + # to be off. + if 'features' not in license: + return False + + return name in license['features'] diff --git a/awx/api/views.py b/awx/api/views.py index 4741c827c2..c101b43bed 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -60,7 +60,7 @@ from awx.api.utils.decorators import paginated from awx.api.filters import MongoFilterBackend from awx.api.generics import get_view_name from awx.api.generics import * # noqa -from awx.api.license import feature_enabled, LicenseForbids +from awx.api.license import feature_enabled, feature_exists, LicenseForbids from awx.main.models import * # noqa from awx.main.utils import * # noqa from awx.api.permissions import * # noqa @@ -527,6 +527,11 @@ class AuthView(APIView): data = SortedDict() err_backend, err_message = request.session.get('social_auth_error', (None, None)) for name, backend in load_backends(settings.AUTHENTICATION_BACKENDS).items(): + if (not feature_exists('enterprise_auth') and + not feature_enabled('ldap')) or \ + (not feature_enabled('enterprise_auth') and + name in ['saml', 'radius']): + continue login_url = reverse('social:begin', args=(name,)) complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,))) backend_data = { diff --git a/awx/main/access.py b/awx/main/access.py index d0278270f6..b4a798fe21 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -262,6 +262,7 @@ class OrganizationAccess(BaseAccess): self.user in obj.admins.all()) def can_delete(self, obj): + self.check_license(feature='multiple_organizations') return self.can_change(obj, None) class InventoryAccess(BaseAccess): @@ -672,22 +673,22 @@ class ProjectAccess(BaseAccess): - I am on a team associated with the project. - I have been explicitly granted permission to run/check jobs using the project. - - I created it (for now?). + - I created the project but it isn't associated with an organization I can change/delete when: - I am a superuser. - I am an admin in an organization associated with the project. - - I created it (for now?). + - I created the project but it isn't associated with an organization ''' model = Project def get_queryset(self): qs = Project.objects.filter(active=True).distinct() - qs = qs.select_related('created_by', 'modified_by', 'credential', 'current_update', 'last_update') + qs = qs.select_related('modified_by', 'credential', 'current_update', 'last_update') if self.user.is_superuser: return qs team_ids = set(Team.objects.filter(users__in=[self.user]).values_list('id', flat=True)) - qs = qs.filter(Q(created_by=self.user) | + qs = qs.filter(Q(created_by=self.user, organizations__isnull=True) | Q(organizations__admins__in=[self.user], organizations__active=True) | Q(organizations__users__in=[self.user], organizations__active=True) | Q(teams__in=team_ids)) @@ -719,7 +720,7 @@ class ProjectAccess(BaseAccess): def can_change(self, obj, data): if self.user.is_superuser: return True - if obj.created_by == self.user: + if obj.created_by == self.user and not obj.organizations.filter(active=True).count(): return True if obj.organizations.filter(active=True, admins__in=[self.user]).exists(): return True @@ -1605,7 +1606,7 @@ class CustomInventoryScriptAccess(BaseAccess): model = CustomInventoryScript def get_queryset(self): - qs = self.model.objects.filter(active=True, organization__active=True).distinct() + qs = self.model.objects.filter(active=True).distinct() if not self.user.is_superuser: qs = qs.filter(Q(organization__admins__in=[self.user]) | Q(organization__users__in=[self.user])) return qs diff --git a/awx/main/management/commands/cleanup_authtokens.py b/awx/main/management/commands/cleanup_authtokens.py new file mode 100644 index 0000000000..65a8d67e6b --- /dev/null +++ b/awx/main/management/commands/cleanup_authtokens.py @@ -0,0 +1,34 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Python +import logging + +# Django +from django.db import transaction +from django.core.management.base import BaseCommand +from django.utils.timezone import now + +# AWX +from awx.main.models import * # noqa + +class Command(BaseCommand): + ''' + Management command to cleanup expired auth tokens + ''' + + help = 'Cleanup expired auth tokens.' + + def init_logging(self): + self.logger = logging.getLogger('awx.main.commands.cleanup_authtokens') + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(message)s')) + self.logger.addHandler(handler) + self.logger.propagate = False + + @transaction.atomic + def handle(self, *args, **options): + self.init_logging() + tokens_removed = AuthToken.objects.filter(expires__lt=now()) + self.logger.log(99, "Removing %d expired auth tokens" % tokens_removed.count()) + tokens_removed.delete() diff --git a/awx/main/management/commands/cleanup_deleted.py b/awx/main/management/commands/cleanup_deleted.py index 74b6c68106..b6fd5360e5 100644 --- a/awx/main/management/commands/cleanup_deleted.py +++ b/awx/main/management/commands/cleanup_deleted.py @@ -118,3 +118,10 @@ class Command(BaseCommand): self.logger.log(99, "Removed %d items", n_deleted_items) else: self.logger.log(99, "Would have removed %d items", n_deleted_items) + + tokens_removed = AuthToken.objects.filter(expires__lt=now()) + if not self.dry_run: + self.logger.log(99, "Removed %d expired auth tokens" % tokens_removed.count()) + tokens_removed.delete() + else: + self.logger.log(99, "Would have removed %d expired auth tokens" % tokens_removed.count()) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 4028a722bd..97d5937533 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -883,14 +883,14 @@ class Command(NoArgsCommand): all_obj = self.inventory all_name = 'inventory' db_variables = all_obj.variables_dict - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: db_variables = self.all_group.variables else: db_variables.update(self.all_group.variables) if db_variables != all_obj.variables_dict: all_obj.variables = json.dumps(db_variables) all_obj.save(update_fields=['variables']) - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: self.logger.info('%s variables replaced from "all" group', all_name.capitalize()) else: self.logger.info('%s variables updated from "all" group', all_name.capitalize()) @@ -920,14 +920,14 @@ class Command(NoArgsCommand): for group in self.inventory.groups.filter(name__in=group_names): mem_group = self.all_group.all_groups[group.name] db_variables = group.variables_dict - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: db_variables = mem_group.variables else: db_variables.update(mem_group.variables) if db_variables != group.variables_dict: group.variables = json.dumps(db_variables) group.save(update_fields=['variables']) - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: self.logger.info('Group "%s" variables replaced', group.name) else: self.logger.info('Group "%s" variables updated', group.name) @@ -959,7 +959,7 @@ class Command(NoArgsCommand): def _update_db_host_from_mem_host(self, db_host, mem_host): # Update host variables. db_variables = db_host.variables_dict - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: db_variables = mem_host.variables else: db_variables.update(mem_host.variables) @@ -994,7 +994,7 @@ class Command(NoArgsCommand): else: self.logger.info('Host "%s" instance_id added', mem_host.name) if 'variables' in update_fields: - if self.overwrite_vars or self.overwrite: + if self.overwrite_vars: self.logger.info('Host "%s" variables replaced', mem_host.name) else: self.logger.info('Host "%s" variables updated', mem_host.name) diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index e8566c4f6c..cccf07b4db 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -130,6 +130,7 @@ class CallbackReceiver(object): 'playbook_on_task_start', 'playbook_on_no_hosts_matched', 'playbook_on_no_hosts_remaining', + 'playbook_on_include', 'playbook_on_import_for_host', 'playbook_on_not_import_for_host'): parent = job_parent_events.get('playbook_on_play_start', None) diff --git a/awx/main/migrations/0074_v240_changes.py b/awx/main/migrations/0074_v240_changes.py new file mode 100644 index 0000000000..deeea4131d --- /dev/null +++ b/awx/main/migrations/0074_v240_changes.py @@ -0,0 +1,519 @@ +# -*- 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): + # Changing field 'CustomInventoryScript.organization' + db.alter_column(u'main_custominventoryscript', 'organization_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.SET_NULL, to=orm['main.Organization'])) + + def backwards(self, orm): + # Changing field 'CustomInventoryScript.organization' + db.alter_column(u'main_custominventoryscript', 'organization_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['main.Organization'])) + + 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']"}), + 'ad_hoc_command': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.AdHocCommand']", 'symmetrical': 'False', 'blank': 'True'}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'custom_inventory_script': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.CustomInventoryScript']", '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.adhoccommand': { + 'Meta': {'object_name': 'AdHocCommand', '_ormbases': ['main.UnifiedJob']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ad_hoc_commands'", 'symmetrical': 'False', 'through': "orm['main.AdHocCommandEvent']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'module_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'module_name': ('django.db.models.fields.CharField', [], {'default': "'command'", '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.adhoccommandevent': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('ad_hoc_command', 'host_name')]", 'object_name': 'AdHocCommandEvent'}, + 'ad_hoc_command': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_command_events'", 'to': "orm['main.AdHocCommand']"}), + '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': "'ad_hoc_command_events'", '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'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}) + }, + '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'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', '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'}), + 'become_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'become_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'become_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': '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'}), + 'security_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', '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'}), + '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.custominventoryscript': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'CustomInventoryScript'}, + '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\': \'custominventoryscript\', \'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\': \'custominventoryscript\', \'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': "'custom_inventory_scripts'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'script': ('django.db.models.fields.TextField', [], {'default': "''", '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.instance': { + 'Meta': {'object_name': 'Instance'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}) + }, + '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']"}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + '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_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', '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'}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': '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_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', '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']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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', [], {'default': "'run'", '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', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + '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.joborigin': { + 'Meta': {'object_name': 'JobOrigin'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Instance']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'job_origin'", 'unique': 'True', 'to': "orm['main.UnifiedJob']"}) + }, + 'main.jobtemplate': { + 'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'become_enabled': ('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', [], {'default': "'run'", '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', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + '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']"}), + 'run_ad_hoc_commands': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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', 'blank': 'True'}), + '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'}), + 'extra_data': ('jsonfield.fields.JSONField', [], {'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\': \'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.systemjob': { + 'Meta': {'ordering': "('id',)", 'object_name': 'SystemJob', '_ormbases': ['main.UnifiedJob']}, + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'system_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.SystemJobTemplate']", 'blank': 'True', 'null': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.systemjobtemplate': { + 'Meta': {'object_name': 'SystemJobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + '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'] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 4fd165339f..58875a9630 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1281,7 +1281,9 @@ class CustomInventoryScript(CommonModelNameNotUnique): 'Organization', related_name='custom_inventory_scripts', help_text=_('Organization owning this inventory script'), - on_delete=models.CASCADE, + blank=False, + null=True, + on_delete=models.SET_NULL, ) def get_absolute_url(self): diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 7f9f73a81a..f20f69f70b 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -53,6 +53,12 @@ class Organization(CommonModel): def __unicode__(self): return self.name + def mark_inactive(self, save=True): + for script in self.custom_inventory_scripts.all(): + script.organization = None + script.save() + super(Organization, self).mark_inactive(save=save) + class Team(CommonModelNameNotUnique): ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index a34d6e3973..8b83383737 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -162,6 +162,7 @@ def handle_work_error(self, task_id, subtasks=None): print('Executing error task id %s, subtasks: %s' % (str(self.request.id), str(subtasks))) first_task = None + first_task_id = None first_task_type = '' first_task_name = '' if subtasks is not None: @@ -184,13 +185,14 @@ def handle_work_error(self, task_id, subtasks=None): break if first_task is None: first_task = instance + first_task_id = instance.id first_task_type = each_task['type'] first_task_name = instance_name if instance.celery_task_id != task_id: instance.status = 'failed' instance.failed = True - instance.job_explanation = "Previous Task Failed: %s for %s with celery task id: %s" % \ - (first_task_type, first_task_name, task_id) + instance.job_explanation = 'Previous Task Failed: {"task_type": "%s", "task_name": "%s", "task_id": "%s"}' % \ + (first_task_type, first_task_name, first_task_id) instance.save() instance.socketio_emit_status("failed") @@ -636,12 +638,17 @@ class RunJob(BaseTask): Build environment dictionary for ansible-playbook. ''' plugin_dir = self.get_path_to('..', 'plugins', 'callback') + plugin_dirs = [plugin_dir] + if hasattr(settings, 'AWX_ANSIBLE_CALLBACK_PLUGINS') and \ + settings.AWX_ANSIBLE_CALLBACK_PLUGINS: + plugin_dirs.append(settings.AWX_ANSIBLE_CALLBACK_PLUGINS) + plugin_path = ':'.join(plugin_dirs) env = super(RunJob, self).build_env(job, **kwargs) # Set environment variables needed for inventory and job event # callbacks to work. env['JOB_ID'] = str(job.pk) env['INVENTORY_ID'] = str(job.inventory.pk) - env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir + env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_path env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_TOKEN'] = job.task_auth_token or '' env['CALLBACK_CONSUMER_PORT'] = str(settings.CALLBACK_CONSUMER_PORT) diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index b3a0268c7d..a7a6fe116f 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -192,6 +192,20 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): self._temp_paths.append(license_path) os.environ['AWX_LICENSE_FILE'] = license_path + def create_basic_license_file(self, instance_count=100, license_date=int(time.time() + 3600)): + writer = LicenseWriter( + company_name='AWX', + contact_name='AWX Admin', + contact_email='awx@example.com', + license_date=license_date, + instance_count=instance_count, + license_type='basic') + handle, license_path = tempfile.mkstemp(suffix='.json') + os.close(handle) + writer.write_file(license_path) + self._temp_paths.append(license_path) + os.environ['AWX_LICENSE_FILE'] = license_path + def create_expired_license_file(self, instance_count=1000, grace_period=False): license_date = time.time() - 1 if not grace_period: diff --git a/awx/main/tests/commands/commands_monolithic.py b/awx/main/tests/commands/commands_monolithic.py index f6055d27cf..d13bbe05f2 100644 --- a/awx/main/tests/commands/commands_monolithic.py +++ b/awx/main/tests/commands/commands_monolithic.py @@ -828,7 +828,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): host_names = set(new_inv.hosts.filter(active=True).values_list('name', flat=True)) self.assertEqual(expected_host_names, host_names) expected_inv_vars = {'vara': 'A', 'varc': 'C'} - if overwrite or overwrite_vars: + if overwrite_vars: expected_inv_vars.pop('varc') self.assertEqual(new_inv.variables_dict, expected_inv_vars) for host in new_inv.hosts.filter(active=True): @@ -846,7 +846,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): for group in new_inv.groups.filter(active=True): if group.name == 'servers': expected_vars = {'varb': 'B', 'vard': 'D'} - if overwrite or overwrite_vars: + if overwrite_vars: expected_vars.pop('vard') self.assertEqual(group.variables_dict, expected_vars) children = set(group.children.filter(active=True).values_list('name', flat=True)) diff --git a/awx/main/tests/organizations.py b/awx/main/tests/organizations.py index 655fa430c7..8378ab12cb 100644 --- a/awx/main/tests/organizations.py +++ b/awx/main/tests/organizations.py @@ -267,6 +267,11 @@ class OrganizationsTest(BaseTest): # look at what we got back from the post, make sure we added an org last_org = Organization.objects.order_by('-pk')[0] self.assertTrue(data1['url'].endswith("/%d/" % last_org.pk)) + + # Test that not even super users can create an organization with a basic license + self.create_basic_license_file() + cant_org = dict(name='silly user org', description='4815162342') + self.post(self.collection(), cant_org, expect=402, auth=self.get_super_credentials()) def test_post_item_subobjects_projects(self): @@ -437,6 +442,10 @@ class OrganizationsTest(BaseTest): # also check that DELETE on the collection doesn't work self.delete(self.collection(), expect=405, auth=self.get_super_credentials()) + # Test that not even super users can delete an organization with a basic license + self.create_basic_license_file() + self.delete(urls[2], expect=402, auth=self.get_super_credentials()) + def test_invalid_post_data(self): url = reverse('api:organization_list') # API should gracefully handle data of an invalid type. diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 6628dd3714..f698267a0c 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -209,7 +209,7 @@ class ProjectsTest(BaseTransactionTest): self.assertEquals(results['count'], 10) # org admin results = self.get(projects, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(results['count'], 10) + self.assertEquals(results['count'], 9) # user on a team results = self.get(projects, expect=200, auth=self.get_other_credentials()) self.assertEquals(results['count'], 5) @@ -300,6 +300,17 @@ class ProjectsTest(BaseTransactionTest): got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials()) self.assertEquals(got['count'], 2) + # Verify that creatorship doesn't imply access if access is removed + a_new_proj = self.make_project(created_by=self.other_django_user, playbook_content=TEST_PLAYBOOK) + self.organizations[0].admins.add(self.other_django_user) + self.organizations[0].projects.add(a_new_proj) + proj_detail = reverse('api:project_detail', args=(a_new_proj.pk,)) + self.patch(proj_detail, data=dict(description="test"), expect=200, auth=self.get_other_credentials()) + self.organizations[0].admins.remove(self.other_django_user) + self.patch(proj_detail, data=dict(description="test_now"), expect=403, auth=self.get_other_credentials()) + self.delete(proj_detail, expect=403, auth=self.get_other_credentials()) + a_new_proj.delete() + # ===================================================================== # TEAMS diff --git a/awx/plugins/callback/job_event_callback.py b/awx/plugins/callback/job_event_callback.py index 2369868c87..9fdfa4e23a 100644 --- a/awx/plugins/callback/job_event_callback.py +++ b/awx/plugins/callback/job_event_callback.py @@ -426,6 +426,8 @@ class JobCallbackModule(BaseCallbackModule): def v2_playbook_on_stats(self, stats): self.playbook_on_stats(stats) + def v2_playbook_on_include(self, included_file): + self._log_event('playbook_on_include', included_file=included_file) class AdHocCommandCallbackModule(BaseCallbackModule): ''' diff --git a/awx/plugins/fact_caching/tower.py b/awx/plugins/fact_caching/tower.py index 362b9b261c..ce965f9b11 100755 --- a/awx/plugins/fact_caching/tower.py +++ b/awx/plugins/fact_caching/tower.py @@ -35,7 +35,10 @@ import time import datetime from copy import deepcopy from ansible import constants as C -from ansible.cache.base import BaseCacheModule +try: + from ansible.cache.base import BaseCacheModule +except: + from ansible.plugins.cache.base import BaseCacheModule try: import zmq @@ -46,7 +49,6 @@ except ImportError: class CacheModule(BaseCacheModule): def __init__(self, *args, **kwargs): - # Basic in-memory caching for typical runs self._cache = {} self._cache_prev = {} @@ -108,16 +110,15 @@ class CacheModule(BaseCacheModule): module = self.identify_new_module(key, value) # Assume ansible fact triggered the set if no new module found facts = self.filter_ansible_facts(value) if not module else dict({ module : value[module]}) - - self._cache_prev = deepcopy(self._cache) self._cache[key] = value - + self._cache_prev = deepcopy(self._cache) packet = { 'host': key, 'inventory_id': os.environ['INVENTORY_ID'], 'facts': facts, 'date_key': self.date_key, } + # Emit fact data to tower for processing self.socket.send_json(packet) self.socket.recv() diff --git a/awx/plugins/inventory/gce.py b/awx/plugins/inventory/gce.py index 5fe3db93f8..b13c194a6e 100755 --- a/awx/plugins/inventory/gce.py +++ b/awx/plugins/inventory/gce.py @@ -66,7 +66,7 @@ Examples: $ ansible -i gce.py us-central1-a -m shell -a "/bin/uname -a" Use the GCE inventory script to print out instance specific information - $ plugins/inventory/gce.py --host my_instance + $ contrib/inventory/gce.py --host my_instance Author: Eric Johnson Version: 0.0.1 @@ -112,9 +112,9 @@ class GceInventory(object): # Just display data for specific host if self.args.host: - print self.json_format_dict(self.node_to_dict( + print(self.json_format_dict(self.node_to_dict( self.get_instance(self.args.host)), - pretty=self.args.pretty) + pretty=self.args.pretty)) sys.exit(0) # Otherwise, assume user wants all instances grouped @@ -237,7 +237,7 @@ class GceInventory(object): '''Gets details about a specific instance ''' try: return self.driver.ex_get_node(instance_name) - except Exception, e: + except Exception as e: return None def group_instances(self): @@ -257,7 +257,10 @@ class GceInventory(object): tags = node.extra['tags'] for t in tags: - tag = 'tag_%s' % t + if t.startswith('group-'): + tag = t[6:] + else: + tag = 'tag_%s' % t if groups.has_key(tag): groups[tag].append(name) else: groups[tag] = [name] diff --git a/awx/plugins/inventory/rax.py b/awx/plugins/inventory/rax.py index 10b72d322b..0028f54d20 100755 --- a/awx/plugins/inventory/rax.py +++ b/awx/plugins/inventory/rax.py @@ -153,6 +153,8 @@ import warnings import collections import ConfigParser +from six import iteritems + from ansible.constants import get_config, mk_boolean try: @@ -167,6 +169,9 @@ except ImportError: print('pyrax is required for this module') sys.exit(1) +from time import time + + NON_CALLABLES = (basestring, bool, dict, int, list, type(None)) @@ -214,7 +219,7 @@ def host(regions, hostname): print(json.dumps(hostvars, sort_keys=True, indent=4)) -def _list(regions): +def _list_into_cache(regions): groups = collections.defaultdict(list) hostvars = collections.defaultdict(dict) images = {} @@ -242,7 +247,7 @@ def _list(regions): if cs is None: warnings.warn( 'Connecting to Rackspace region "%s" has caused Pyrax to ' - 'return a NoneType. Is this a valid region?' % region, + 'return None. Is this a valid region?' % region, RuntimeWarning) continue for server in cs.servers.list(): @@ -264,7 +269,7 @@ def _list(regions): hostvars[server.name]['rax_region'] = region - for key, value in server.metadata.iteritems(): + for key, value in iteritems(server.metadata): groups['%s_%s_%s' % (prefix, key, value)].append(server.name) groups['instance-%s' % server.id].append(server.name) @@ -334,7 +339,31 @@ def _list(regions): if hostvars: groups['_meta'] = {'hostvars': hostvars} - print(json.dumps(groups, sort_keys=True, indent=4)) + + with open(get_cache_file_path(regions), 'w') as cache_file: + json.dump(groups, cache_file) + + +def get_cache_file_path(regions): + regions_str = '.'.join([reg.strip().lower() for reg in regions]) + ansible_tmp_path = os.path.join(os.path.expanduser("~"), '.ansible', 'tmp') + if not os.path.exists(ansible_tmp_path): + os.makedirs(ansible_tmp_path) + return os.path.join(ansible_tmp_path, + 'ansible-rax-%s-%s.cache' % ( + pyrax.identity.username, regions_str)) + + +def _list(regions, refresh_cache=True): + if (not os.path.exists(get_cache_file_path(regions)) or + refresh_cache or + (time() - os.stat(get_cache_file_path(regions))[-1]) > 600): + # Cache file doesn't exist or older than 10m or refresh cache requested + _list_into_cache(regions) + + with open(get_cache_file_path(regions), 'r') as cache_file: + groups = json.load(cache_file) + print(json.dumps(groups, sort_keys=True, indent=4)) def parse_args(): @@ -344,6 +373,9 @@ def parse_args(): group.add_argument('--list', action='store_true', help='List active servers') group.add_argument('--host', help='List details about the specific host') + parser.add_argument('--refresh-cache', action='store_true', default=False, + help=('Force refresh of cache, making API requests to' + 'RackSpace (default: False - use cache files)')) return parser.parse_args() @@ -382,7 +414,7 @@ def setup(): pyrax.keyring_auth(keyring_username, region=region) else: pyrax.set_credential_file(creds_file, region=region) - except Exception, e: + except Exception as e: sys.stderr.write("%s: %s\n" % (e, e.message)) sys.exit(1) @@ -410,7 +442,7 @@ def main(): args = parse_args() regions = setup() if args.list: - _list(regions) + _list(regions, refresh_cache=args.refresh_cache) elif args.host: host(regions, args.host) sys.exit(0) diff --git a/awx/plugins/inventory/vmware.py b/awx/plugins/inventory/vmware.py index 27330b8bcd..8f723a638d 100755 --- a/awx/plugins/inventory/vmware.py +++ b/awx/plugins/inventory/vmware.py @@ -28,6 +28,8 @@ take precedence over options present in the INI file. An INI file is not required if these options are specified using environment variables. ''' +from __future__ import print_function + import collections import json import logging @@ -37,6 +39,8 @@ import sys import time import ConfigParser +from six import text_type + # Disable logging message trigged by pSphere/suds. try: from logging import NullHandler @@ -95,7 +99,7 @@ class VMwareInventory(object): Saves the value to cache with the name given. ''' if self.config.has_option('defaults', 'cache_dir'): - cache_dir = self.config.get('defaults', 'cache_dir') + cache_dir = os.path.expanduser(self.config.get('defaults', 'cache_dir')) if not os.path.exists(cache_dir): os.makedirs(cache_dir) cache_file = os.path.join(cache_dir, name) @@ -115,7 +119,7 @@ class VMwareInventory(object): else: cache_max_age = 0 cache_stat = os.stat(cache_file) - if (cache_stat.st_mtime + cache_max_age) < time.time(): + if (cache_stat.st_mtime + cache_max_age) >= time.time(): with open(cache_file) as cache: return json.load(cache) return default @@ -147,7 +151,7 @@ class VMwareInventory(object): seen = seen or set() if isinstance(obj, ManagedObject): try: - obj_unicode = unicode(getattr(obj, 'name')) + obj_unicode = text_type(getattr(obj, 'name')) except AttributeError: obj_unicode = () if obj in seen: @@ -164,7 +168,7 @@ class VMwareInventory(object): obj_info = self._get_obj_info(val, depth - 1, seen) if obj_info != (): d[attr] = obj_info - except Exception, e: + except Exception as e: pass return d elif isinstance(obj, SudsObject): @@ -207,8 +211,8 @@ class VMwareInventory(object): host_info[k] = v try: host_info['ipAddress'] = host.config.network.vnic[0].spec.ip.ipAddress - except Exception, e: - print >> sys.stderr, e + except Exception as e: + print(e, file=sys.stderr) host_info = self._flatten_dict(host_info, prefix) if ('%s_ipAddress' % prefix) in host_info: host_info['ansible_ssh_host'] = host_info['%s_ipAddress' % prefix] diff --git a/awx/plugins/inventory/windows_azure.py b/awx/plugins/inventory/windows_azure.py index 921a99cdfc..d566b0c4d3 100755 --- a/awx/plugins/inventory/windows_azure.py +++ b/awx/plugins/inventory/windows_azure.py @@ -51,7 +51,7 @@ try: from azure import WindowsAzureError from azure.servicemanagement import ServiceManagementService except ImportError as e: - print "failed=True msg='`azure` library required for this script'" + print("failed=True msg='`azure` library required for this script'") sys.exit(1) @@ -70,8 +70,8 @@ class AzureInventory(object): # Cache setting defaults. # These can be overridden in settings (see `read_settings`). cache_dir = os.path.expanduser('~') - self.cache_path_cache = '%s/.ansible-azure.cache' % cache_dir - self.cache_path_index = '%s/.ansible-azure.index' % cache_dir + self.cache_path_cache = os.path.join(cache_dir, '.ansible-azure.cache') + self.cache_path_index = os.path.join(cache_dir, '.ansible-azure.index') self.cache_max_age = 0 # Read settings and parse CLI arguments @@ -103,15 +103,13 @@ class AzureInventory(object): # Add the `['_meta']['hostvars']` information. hostvars = {} if len(data) > 0: - for host in set(reduce(lambda x, y: x + y, - [i for i in data.values()])): - if host is not None: - hostvars[host] = self.get_host(host, jsonify=False) + for host in set([h for hosts in data.values() for h in hosts if h]): + hostvars[host] = self.get_host(host, jsonify=False) data['_meta'] = {'hostvars': hostvars} # JSONify the data. data_to_print = self.json_format_dict(data, pretty=True) - print data_to_print + print(data_to_print) def get_host(self, hostname, jsonify=True): """Return information about the given hostname, based on what @@ -153,9 +151,9 @@ class AzureInventory(object): # Cache related if config.has_option('azure', 'cache_path'): - cache_path = os.path.expanduser(config.get('azure', 'cache_path')) - self.cache_path_cache = cache_path + '/ansible-azure.cache' - self.cache_path_index = cache_path + '/ansible-azure.index' + cache_path = os.path.expandvars(os.path.expanduser(config.get('azure', 'cache_path'))) + self.cache_path_cache = os.path.join(cache_path, 'ansible-azure.cache') + self.cache_path_index = os.path.join(cache_path, 'ansible-azure.index') if config.has_option('azure', 'cache_max_age'): self.cache_max_age = config.getint('azure', 'cache_max_age') @@ -197,9 +195,9 @@ class AzureInventory(object): for cloud_service in self.sms.list_hosted_services(): self.add_deployments(cloud_service) except WindowsAzureError as e: - print "Looks like Azure's API is down:" - print - print e + print("Looks like Azure's API is down:") + print("") + print(e) sys.exit(1) def add_deployments(self, cloud_service): @@ -209,12 +207,10 @@ class AzureInventory(object): try: for deployment in self.sms.get_hosted_service_properties(cloud_service.service_name,embed_detail=True).deployments.deployments: self.add_deployment(cloud_service, deployment) - #if deployment.deployment_slot == "Production": - # self.add_deployment(cloud_service, deployment) except WindowsAzureError as e: - print "Looks like Azure's API is down:" - print - print e + print("Looks like Azure's API is down:") + print("") + print(e) sys.exit(1) def add_deployment(self, cloud_service, deployment): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9684479542..4dd6a017d7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -448,6 +448,9 @@ AWX_JOB_TEMPLATE_HISTORY = 10 # The directory in which proot will create new temporary directories for its root AWX_PROOT_BASE_PATH = "/tmp" +# User definable ansible callback plugins +AWX_ANSIBLE_CALLBACK_PLUGINS = "" + # Enable Pendo on the UI, possible values are 'off', 'anonymous', and 'detailed' PENDO_TRACKING_STATE = "off" diff --git a/awx/templates/rest_framework/api.html b/awx/templates/rest_framework/api.html index baf7013be5..2129ca2bc8 100644 --- a/awx/templates/rest_framework/api.html +++ b/awx/templates/rest_framework/api.html @@ -5,7 +5,7 @@ {% block style %} {{ block.super }} - +