diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 72dccc61f4..300c5cfc65 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -6,7 +6,6 @@ import urllib # Django from django.utils.timezone import now as tz_now -from django.conf import settings # Django REST Framework from rest_framework import authentication @@ -15,7 +14,7 @@ from rest_framework import HTTP_HEADER_ENCODING # AWX from awx.main.models import UnifiedJob, AuthToken - +from awx.main.conf import tower_settings class TokenAuthentication(authentication.TokenAuthentication): ''' @@ -90,7 +89,7 @@ class TokenAuthentication(authentication.TokenAuthentication): # Token invalidated due to session limit config being reduced # Session limit reached invalidation will also take place on authentication - if settings.AUTH_TOKEN_PER_USER != -1: + if tower_settings.AUTH_TOKEN_PER_USER != -1: if not token.in_valid_tokens(now=now): token.invalidate(reason='limit_reached') raise exceptions.AuthenticationFailed(AuthToken.reason_long('limit_reached')) diff --git a/awx/api/generics.py b/awx/api/generics.py index a0f892210c..65ce0fde90 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -144,6 +144,7 @@ class APIView(views.APIView): 'new_in_220': getattr(self, 'new_in_220', False), 'new_in_230': getattr(self, 'new_in_230', False), 'new_in_240': getattr(self, 'new_in_240', False), + 'new_in_300': getattr(self, 'new_in_300', False), } def get_description(self, html=False): @@ -160,7 +161,7 @@ class APIView(views.APIView): ''' ret = super(APIView, self).metadata(request) added_in_version = '1.2' - for version in ('2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'): + for version in ('3.0.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'): if getattr(self, 'new_in_%s' % version.replace('.', ''), False): added_in_version = version break diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bf258cc524..c59614f53f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -38,6 +38,7 @@ from awx.main.constants import SCHEDULEABLE_PROVIDERS from awx.main.models import * # noqa from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat from awx.main.redact import REPLACE_STR +from awx.main.conf import tower_settings from awx.api.license import feature_enabled @@ -267,7 +268,7 @@ class BaseSerializer(serializers.ModelSerializer): return choices def get_url(self, obj): - if obj is None: + if obj is None or not hasattr(obj, 'get_absolute_url'): return '' elif isinstance(obj, User): return reverse('api:user_detail', args=(obj.pk,)) @@ -521,8 +522,9 @@ class UnifiedJobSerializer(BaseSerializer): def get_result_stdout(self, obj): obj_size = obj.result_stdout_size - if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: - return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + if obj_size > tower_settings.STDOUT_MAX_BYTES_DISPLAY: + return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, + tower_settings.STDOUT_MAX_BYTES_DISPLAY) return obj.result_stdout class UnifiedJobListSerializer(UnifiedJobSerializer): @@ -569,8 +571,9 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer): def get_result_stdout(self, obj): obj_size = obj.result_stdout_size - if obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: - return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + if obj_size > tower_settings.STDOUT_MAX_BYTES_DISPLAY: + return "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, + tower_settings.STDOUT_MAX_BYTES_DISPLAY) return obj.result_stdout def get_types(self): @@ -2105,7 +2108,40 @@ class ActivityStreamSerializer(BaseSerializer): first_name = obj.actor.first_name, last_name = obj.actor.last_name) return summary_fields + +class TowerSettingsSerializer(BaseSerializer): + class Meta: + model = TowerSettings + fields = ('key', 'description', 'category', 'value', 'value_type', 'user') + read_only_fields = ('description', 'category', 'value_type', 'user') + + def from_native(self, data, files): + if data['key'] not in settings.TOWER_SETTINGS_MANIFEST: + self._errors = {'key': 'Key {0} is not a valid settings key'.format(data['key'])} + return + current_val = TowerSettings.objects.filter(key=data['key']) + if current_val.exists(): + current_val.delete() + manifest_val = settings.TOWER_SETTINGS_MANIFEST[data['key']] + data['description'] = manifest_val['description'] + data['category'] = manifest_val['category'] + data['value_type'] = manifest_val['type'] + return super(TowerSettingsSerializer, self).from_native(data, files) + + def validate(self, attrs): + manifest = settings.TOWER_SETTINGS_MANIFEST + if attrs['key'] not in manifest: + raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key".format(attrs['key'])])) + # TODO: Type checking/coercion, contextual validation + return attrs + + def save_object(self, obj, **kwargs): + manifest_val = settings.TOWER_SETTINGS_MANIFEST[obj.key] + obj.description = manifest_val['description'] + obj.category = manifest_val['category'] + obj.value_type = manifest_val['type'] + return super(TowerSettingsSerializer, self).save_object(obj, **kwargs) class AuthTokenSerializer(serializers.Serializer): diff --git a/awx/api/templates/api/_new_in_awx.md b/awx/api/templates/api/_new_in_awx.md index f953afcc14..4df45be686 100644 --- a/awx/api/templates/api/_new_in_awx.md +++ b/awx/api/templates/api/_new_in_awx.md @@ -5,4 +5,5 @@ {% if new_in_200 %}> _New in Ansible Tower 2.0.0_{% endif %} {% if new_in_220 %}> _New in Ansible Tower 2.2.0_{% endif %} {% if new_in_230 %}> _New in Ansible Tower 2.3.0_{% endif %} -{% if new_in_240 %}> _New in Ansible Tower 2.4.0_{% endif %} \ No newline at end of file +{% if new_in_240 %}> _New in Ansible Tower 2.4.0_{% endif %} +{% if new_in_300 %}> _New in Ansible Tower 3.0.0_{% endif %} diff --git a/awx/api/urls.py b/awx/api/urls.py index d177d6b9ba..2b3a93d852 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -220,6 +220,10 @@ activity_stream_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/$', 'activity_stream_detail'), ) +settings_urls = patterns('awx.api.views', + url(r'^$', 'settings_list'), + url(r'^reset/$', 'settings_reset')) + v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), url(r'^ping/$', 'api_v1_ping_view'), @@ -230,7 +234,8 @@ v1_urls = patterns('awx.api.views', url(r'^dashboard/$', 'dashboard_view'), url(r'^dashboard/graphs/jobs/$', 'dashboard_jobs_graph_view'), url(r'^dashboard/graphs/inventory/$', 'dashboard_inventory_graph_view'), - url(r'^schedules/', include(schedule_urls)), + url(r'^settings/', include(settings_urls)), + url(r'^schedules/', include(schedule_urls)), url(r'^organizations/', include(organization_urls)), url(r'^users/', include(user_urls)), url(r'^projects/', include(project_urls)), diff --git a/awx/api/views.py b/awx/api/views.py index c6f89bd116..bdd5fdca93 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -12,6 +12,7 @@ import socket import sys import errno from base64 import b64encode +from collections import namedtuple # Django from django.conf import settings @@ -69,6 +70,7 @@ from awx.api.renderers import * # noqa from awx.api.serializers import * # noqa from awx.fact.models import * # noqa from awx.main.utils import emit_websocket_notification +from awx.main.conf import tower_settings def api_exception_handler(exc): ''' @@ -113,6 +115,7 @@ class ApiV1RootView(APIView): data['authtoken'] = reverse('api:auth_token_view') data['ping'] = reverse('api:api_v1_ping_view') data['config'] = reverse('api:api_v1_config_view') + data['settings'] = reverse('api:settings_list') data['me'] = reverse('api:user_me_list') data['dashboard'] = reverse('api:dashboard_view') data['organizations'] = reverse('api:organization_list') @@ -189,9 +192,9 @@ class ApiV1ConfigView(APIView): '''Return various sitewide configuration settings.''' license_reader = TaskSerializer() - license_data = license_reader.from_file(show_key=request.user.is_superuser) + license_data = license_reader.from_database(show_key=request.user.is_superuser) - pendo_state = settings.PENDO_TRACKING_STATE if settings.PENDO_TRACKING_STATE in ('off', 'anonymous', 'detailed') else 'off' + pendo_state = tower_settings.PENDO_TRACKING_STATE if tower_settings.PENDO_TRACKING_STATE in ('off', 'anonymous', 'detailed') else 'off' data = dict( time_zone=settings.TIME_ZONE, @@ -261,9 +264,7 @@ class ApiV1ConfigView(APIView): # If the license is valid, write it to disk. if license_data['valid_key']: - fh = open(TASK_FILE, "w") - fh.write(data_actual) - fh.close() + tower_settings.LICENSE = data_actual # Spawn a task to ensure that MongoDB is started (or stopped) # as appropriate, based on whether the license uses it. @@ -592,7 +593,7 @@ class AuthTokenView(APIView): # Note: This header is normally added in the middleware whenever an # auth token is included in the request header. headers = { - 'Auth-Token-Timeout': int(settings.AUTH_TOKEN_EXPIRATION) + 'Auth-Token-Timeout': int(tower_settings.AUTH_TOKEN_EXPIRATION) } return Response({'token': token.key, 'expires': token.expires}, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -2859,8 +2860,9 @@ class UnifiedJobStdout(RetrieveAPIView): def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() obj_size = unified_job.result_stdout_size - if request.accepted_renderer.format != 'txt_download' and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: - response_message = "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, settings.STDOUT_MAX_BYTES_DISPLAY) + if request.accepted_renderer.format != 'txt_download' and obj_size > tower_settings.STDOUT_MAX_BYTES_DISPLAY: + response_message = "Standard Output too large to display (%d bytes), only download supported for sizes over %d bytes" % (obj_size, + tower_settings.STDOUT_MAX_BYTES_DISPLAY) if request.accepted_renderer.format == 'json': return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message}) else: @@ -2959,6 +2961,61 @@ class ActivityStreamDetail(RetrieveAPIView): # Okay, let it through. return super(type(self), self).get(request, *args, **kwargs) +class SettingsList(ListCreateAPIView): + + model = TowerSettings + serializer_class = TowerSettingsSerializer + authentication_classes = [TokenGetAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES + new_in_300 = True + filter_backends = () + + def get_queryset(self): + # TODO: docs + if not self.request.user.is_superuser: + # NOTE: Shortcutting the rbac class due to the merging of the settings manifest and the database + # we'll need to extend this more in the future when we have user settings + return [] + SettingsTuple = namedtuple('Settings', ['key', 'description', 'category', 'value', 'value_type', 'user']) + all_defined_settings = {s.key: SettingsTuple(s.key, + s.description, + s.category, + s.value_converted, + s.value_type, + s.user) for s in TowerSettings.objects.all()} + manifest_settings = settings.TOWER_SETTINGS_MANIFEST + settings_actual = [] + for settings_key in manifest_settings: + if settings_key in all_defined_settings: + settings_actual.append(all_defined_settings[settings_key]) + else: + m_entry = manifest_settings[settings_key] + settings_actual.append(SettingsTuple(settings_key, + m_entry['description'], + m_entry['category'], + m_entry['default'], + m_entry['type'], + None)) + return settings_actual + + def delete(self, request, *args, **kwargs): + if not request.user.can_access(self.model, 'delete', None): + raise PermissionDenied() + TowerSettings.objects.all().delete() + return Response() + +class SettingsReset(APIView): + + view_name = "Reset a settings value" + new_in_300 = True + + def post(self, request): + # NOTE: Extend more with user settings + if not request.user.can_access(TowerSettings, 'delete', None): + raise PermissionDenied() + settings_key = request.DATA.get('key', None) + if settings_key is not None: + TowerSettings.objects.filter(key=settings_key).delete() + return Response(status=status.HTTP_204_NO_CONTENT) # Create view functions for all of the class-based views to simplify inclusion # in URL patterns and reverse URL lookups, converting CamelCase names to diff --git a/awx/main/access.py b/awx/main/access.py index 18ae3a91b1..d24598c8e9 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -7,7 +7,6 @@ import sys import logging # Django -from django.conf import settings from django.db.models import F, Q from django.contrib.auth.models import User @@ -19,6 +18,7 @@ from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.api.license import LicenseForbids from awx.main.task_engine import TaskSerializer +from awx.main.conf import tower_settings __all__ = ['get_user_queryset', 'check_user_access'] @@ -196,7 +196,7 @@ class UserAccess(BaseAccess): qs = self.model.objects.filter(is_active=True).distinct() if self.user.is_superuser: return qs - if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and self.user.admin_of_organizations.filter(active=True).exists(): + if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and self.user.admin_of_organizations.filter(active=True).exists(): return qs return qs.filter( Q(pk=self.user.pk) | @@ -1566,6 +1566,10 @@ class ActivityStreamAccess(BaseAccess): ad_hoc_command_qs = self.user.get_queryset(AdHocCommand) qs.filter(ad_hoc_command__in=ad_hoc_command_qs) + # TowerSettings Filter + settings_qs = self.user.get_queryset(TowerSettings) + qs.filter(tower_settings__in=settings_qs) + # organization_qs = self.user.get_queryset(Organization) # user_qs = self.user.get_queryset(User) # inventory_qs = self.user.get_queryset(Inventory) @@ -1636,6 +1640,30 @@ class CustomInventoryScriptAccess(BaseAccess): return True return False + +class TowerSettingsAccess(BaseAccess): + ''' + - I can see settings when + - I am a super user + - I can edit settings when + - I am a super user + - I can clear settings when + - I am a super user + ''' + + model = TowerSettings + + def get_queryset(self): + if self.user.is_superuser: + return self.model.objects.all() + return self.model.objects.none() + + def can_change(self, obj, data): + return self.user.is_superuser + + def can_delete(self, obj): + return self.user.is_superuser + register_access(User, UserAccess) register_access(Organization, OrganizationAccess) register_access(Inventory, InventoryAccess) @@ -1661,3 +1689,4 @@ register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess) register_access(UnifiedJob, UnifiedJobAccess) register_access(ActivityStream, ActivityStreamAccess) register_access(CustomInventoryScript, CustomInventoryScriptAccess) +register_access(TowerSettings, TowerSettingsAccess) diff --git a/awx/main/conf.py b/awx/main/conf.py new file mode 100644 index 0000000000..4a6b97037b --- /dev/null +++ b/awx/main/conf.py @@ -0,0 +1,51 @@ +# Copyright (c) 2015 Ansible, Inc.. +# All Rights Reserved. + +import logging + +from django.conf import settings as django_settings +from django.db.utils import ProgrammingError +from django.db import OperationalError +from awx.main.models.configuration import TowerSettings + +logger = logging.getLogger('awx.main.conf') + +class TowerConfiguration(object): + + # TODO: Caching so we don't have to hit the database every time for settings + def __getattr__(self, key): + settings_manifest = django_settings.TOWER_SETTINGS_MANIFEST + if key not in settings_manifest: + raise AttributeError("Tower Setting with key '{0}' is not defined in the manifest".format(key)) + default_value = settings_manifest[key]['default'] + ts = TowerSettings.objects.filter(key=key) + try: + if not ts.exists(): + try: + val_actual = getattr(django_settings, key) + except AttributeError: + val_actual = default_value + return val_actual + return ts[0].value_converted + except (ProgrammingError, OperationalError), e: + # Database is not available yet, usually during migrations so lets use the default + logger.debug("Database settings not available yet, using defaults ({0})".format(e)) + return default_value + + def __setattr__(self, key, value): + settings_manifest = django_settings.TOWER_SETTINGS_MANIFEST + if key not in settings_manifest: + raise AttributeError("Tower Setting with key '{0}' does not exist".format(key)) + settings_entry = settings_manifest[key] + settings_actual = TowerSettings.objects.filter(key=key) + if not settings_actual.exists(): + settings_actual = TowerSettings(key=key, + description=settings_entry['description'], + category=settings_entry['category'], + value=value, + value_type=settings_entry['type']) + else: + settings_actual['value'] = value + settings_actual.save() + +tower_settings = TowerConfiguration() diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 97d5937533..ae5eeb8a25 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -28,6 +28,7 @@ from awx.main.models import * # noqa from awx.main.utils import ignore_inventory_computed_fields, check_proot_installed, wrap_args_with_proot from awx.main.signals import disable_activity_stream from awx.main.task_engine import TaskSerializer as LicenseReader +from awx.main.conf import tower_settings logger = logging.getLogger('awx.main.commands.inventory_import') @@ -356,7 +357,7 @@ class ExecutableJsonLoader(BaseLoader): data = {} stdout, stderr = '', '' try: - if self.is_custom and getattr(settings, 'AWX_PROOT_ENABLED', False): + if self.is_custom and getattr(tower_settings, 'AWX_PROOT_ENABLED', False): if not check_proot_installed(): raise RuntimeError("proot is not installed but is configured for use") kwargs = {'proot_temp_dir': self.source_dir} # TODO: Remove proot dir diff --git a/awx/main/middleware.py b/awx/main/middleware.py index f73758ad7d..37903886ac 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -11,10 +11,10 @@ from django.db import IntegrityError from django.http import HttpResponseRedirect from django.template.response import TemplateResponse from django.utils.functional import curry -from django.conf import settings from awx import __version__ as version from awx.main.models import ActivityStream, Instance +from awx.main.conf import tower_settings from awx.api.authentication import TokenAuthentication @@ -117,6 +117,6 @@ class AuthTokenTimeoutMiddleware(object): if not TokenAuthentication._get_x_auth_token_header(request): return response - response['Auth-Token-Timeout'] = int(settings.AUTH_TOKEN_EXPIRATION) + response['Auth-Token-Timeout'] = int(tower_settings.AUTH_TOKEN_EXPIRATION) return response diff --git a/awx/main/migrations/0075_v300_changes.py b/awx/main/migrations/0075_v300_changes.py new file mode 100644 index 0000000000..db25caa1f2 --- /dev/null +++ b/awx/main/migrations/0075_v300_changes.py @@ -0,0 +1,557 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'TowerSettings' + db.create_table(u'main_towersettings', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(default=None)), + ('modified', self.gf('django.db.models.fields.DateTimeField')(default=None)), + ('key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('description', self.gf('django.db.models.fields.TextField')()), + ('category', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('value', self.gf('django.db.models.fields.TextField')()), + ('value_type', self.gf('django.db.models.fields.CharField')(max_length=12)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='settings', null=True, to=orm['auth.User'])), + )) + db.send_create_signal('main', ['TowerSettings']) + + # Adding M2M table for field tower_settings on 'ActivityStream' + m2m_table_name = db.shorten_name(u'main_activitystream_tower_settings') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('activitystream', models.ForeignKey(orm['main.activitystream'], null=False)), + ('towersettings', models.ForeignKey(orm['main.towersettings'], null=False)) + )) + db.create_unique(m2m_table_name, ['activitystream_id', 'towersettings_id']) + + + def backwards(self, orm): + # Deleting model 'TowerSettings' + db.delete_table(u'main_towersettings') + + # Removing M2M table for field tower_settings on 'ActivityStream' + db.delete_table(db.shorten_name(u'main_activitystream_tower_settings')) + + + 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'}), + 'tower_settings': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.TowerSettings']", 'symmetrical': 'False', '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.towersettings': { + 'Meta': {'object_name': 'TowerSettings'}, + 'category': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'settings'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'value': ('django.db.models.fields.TextField', [], {}), + 'value_type': ('django.db.models.fields.CharField', [], {'max_length': '12'}) + }, + '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/__init__.py b/awx/main/models/__init__.py index 2926e7cf28..23cf591e6b 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -16,6 +16,7 @@ from awx.main.models.ad_hoc_commands import * # noqa from awx.main.models.schedules import * # noqa from awx.main.models.activity_stream import * # noqa from awx.main.models.ha import * # noqa +from awx.main.models.configuration import * # noqa # Monkeypatch Django serializer to ignore django-taggit fields (which break # the dumpdata command; see https://github.com/alex/django-taggit/issues/155). @@ -55,6 +56,7 @@ activity_stream_registrar.connect(Job) activity_stream_registrar.connect(AdHocCommand) # activity_stream_registrar.connect(JobHostSummary) # activity_stream_registrar.connect(JobEvent) -#activity_stream_registrar.connect(Profile) +# activity_stream_registrar.connect(Profile) activity_stream_registrar.connect(Schedule) activity_stream_registrar.connect(CustomInventoryScript) +activity_stream_registrar.connect(TowerSettings) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index b695831ada..f811c36507 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -53,6 +53,7 @@ class ActivityStream(models.Model): ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True) schedule = models.ManyToManyField("Schedule", blank=True) custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True) + tower_settings = models.ManyToManyField("TowerSettings", blank=True) def get_absolute_url(self): return reverse('api:activity_stream_detail', args=(self.pk,)) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 80520cdb1e..e47328844c 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -21,6 +21,7 @@ from jsonfield import JSONField from awx.main.models.base import * # noqa from awx.main.models.unified_jobs import * # noqa from awx.main.utils import decrypt_field +from awx.main.conf import tower_settings logger = logging.getLogger('awx.main.models.ad_hoc_commands') @@ -29,8 +30,8 @@ __all__ = ['AdHocCommand', 'AdHocCommandEvent'] class AdHocCommand(UnifiedJob): - MODULE_NAME_CHOICES = [(x,x) for x in settings.AD_HOC_COMMANDS] - MODULE_NAME_DEFAULT = 'command' if 'command' in settings.AD_HOC_COMMANDS else None + MODULE_NAME_CHOICES = [(x,x) for x in tower_settings.AD_HOC_COMMANDS] + MODULE_NAME_DEFAULT = 'command' if 'command' in tower_settings.AD_HOC_COMMANDS else None class Meta(object): app_label = 'main' @@ -104,7 +105,7 @@ class AdHocCommand(UnifiedJob): if type(self.module_name) not in (str, unicode): raise ValidationError("Invalid type for ad hoc command") module_name = self.module_name.strip() or 'command' - if module_name not in settings.AD_HOC_COMMANDS: + if module_name not in tower_settings.AD_HOC_COMMANDS: raise ValidationError('Unsupported module for ad hoc commands.') return module_name diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py new file mode 100644 index 0000000000..6b35ea4d77 --- /dev/null +++ b/awx/main/models/configuration.py @@ -0,0 +1,62 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Python +import json + +# Django +from django.db import models +from django.utils.translation import ugettext_lazy as _ +# Tower +from awx.main.models.base import CreatedModifiedModel + +class TowerSettings(CreatedModifiedModel): + + class Meta: + app_label = 'main' + + SETTINGS_TYPE_CHOICES = [ + ('string', _("String")), + ('int', _('Integer')), + ('float', _('Decimal')), + ('json', _('JSON')), + ('bool', _('Boolean')), + ('password', _('Password')), + ('list', _('List')) + ] + + key = models.CharField( + max_length=255, + unique=True + ) + description = models.TextField() + category = models.CharField(max_length=128) + value = models.TextField() + value_type = models.CharField( + max_length=12, + choices=SETTINGS_TYPE_CHOICES + ) + user = models.ForeignKey( + 'auth.User', + related_name='settings', + default=None, + null=True, + editable=False, + ) + + @property + def value_converted(self): + if self.value_type == 'json': + converted_type = json.loads(self.value) + elif self.value_type == 'password': + converted_type = self.value + elif self.value_type == 'list': + converted_type = [x.strip() for x in self.value.split(',')] + elif self.value_type == 'bool': + converted_type = self.value in [True, "true", "True", 1, "1", "yes"] + elif self.value_type == 'string': + converted_type = self.value + else: + t = __builtins__[self.value_type] + converted_type = t(self.value) + return converted_type diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index ebc6a0fa78..bd5fb252ba 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -24,6 +24,7 @@ from awx.main.models.unified_jobs import * # noqa from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import emit_websocket_notification from awx.main.redact import PlainTextCleaner +from awx.main.conf import tower_settings logger = logging.getLogger('awx.main.models.jobs') @@ -318,9 +319,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): @property def cache_timeout_blocked(self): - if Job.objects.filter(job_template=self, status__in=['pending', 'waiting', 'running']).count() > getattr(settings, 'SCHEDULE_MAX_JOBS', 10): + if Job.objects.filter(job_template=self, status__in=['pending', 'waiting', 'running']).count() > getattr(tower_settings, 'SCHEDULE_MAX_JOBS', 10): logger.error("Job template %s could not be started because there are more than %s other jobs from that template waiting to run" % - (self.name, getattr(settings, 'SCHEDULE_MAX_JOBS', 10))) + (self.name, getattr(tower_settings, 'SCHEDULE_MAX_JOBS', 10))) return True return False diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index f20f69f70b..0462902c75 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -18,6 +18,7 @@ from django.utils.translation import ugettext_lazy as _ # AWX from awx.main.fields import AutoOneToOneField from awx.main.models.base import * # noqa +from awx.main.conf import tower_settings __all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken'] @@ -242,7 +243,7 @@ class AuthToken(BaseModel): if not now: now = tz_now() if not self.pk or not self.is_expired(now=now): - self.expires = now + datetime.timedelta(seconds=settings.AUTH_TOKEN_EXPIRATION) + self.expires = now + datetime.timedelta(seconds=tower_settings.AUTH_TOKEN_EXPIRATION) if save: self.save() @@ -259,12 +260,12 @@ class AuthToken(BaseModel): if now is None: now = tz_now() invalid_tokens = AuthToken.objects.none() - if settings.AUTH_TOKEN_PER_USER != -1: + if tower_settings.AUTH_TOKEN_PER_USER != -1: invalid_tokens = AuthToken.objects.filter( user=user, expires__gt=now, reason='', - ).order_by('-created')[settings.AUTH_TOKEN_PER_USER:] + ).order_by('-created')[tower_settings.AUTH_TOKEN_PER_USER:] return invalid_tokens def generate_key(self): @@ -293,7 +294,7 @@ class AuthToken(BaseModel): valid_n_tokens_qs = self.user.auth_tokens.filter( expires__gt=now, reason='', - ).order_by('-created')[0:settings.AUTH_TOKEN_PER_USER] + ).order_by('-created')[0:tower_settings.AUTH_TOKEN_PER_USER] valid_n_tokens = valid_n_tokens_qs.values_list('key', flat=True) return bool(self.key in valid_n_tokens) diff --git a/awx/main/registrar.py b/awx/main/registrar.py index 6d01ec8d2d..c78bf22f7c 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -3,7 +3,6 @@ import logging -from django.conf import settings from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed logger = logging.getLogger('awx.main.registrar') @@ -14,7 +13,8 @@ class ActivityStreamRegistrar(object): self.models = [] def connect(self, model): - if not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True): + from awx.main.conf import tower_settings + if not getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True): return from awx.main.signals import activity_stream_create, activity_stream_update, activity_stream_delete, activity_stream_associate diff --git a/awx/main/signals.py b/awx/main/signals.py index 2f426b74b3..8b0c22ec9d 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -8,7 +8,6 @@ import threading import json # Django -from django.conf import settings from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.dispatch import receiver @@ -22,6 +21,7 @@ from awx.api.serializers import * # noqa from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, emit_websocket_notification from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates from awx.main.tasks import update_inventory_computed_fields +from awx.main.conf import tower_settings __all__ = [] @@ -273,7 +273,7 @@ def update_host_last_job_after_job_deleted(sender, **kwargs): class ActivityStreamEnabled(threading.local): def __init__(self): - self.enabled = getattr(settings, 'ACTIVITY_STREAM_ENABLED', True) + self.enabled = getattr(tower_settings, 'ACTIVITY_STREAM_ENABLED', True) def __nonzero__(self): return bool(self.enabled) @@ -306,6 +306,7 @@ model_serializer_mapping = { JobTemplate: JobTemplateSerializer, Job: JobSerializer, AdHocCommand: AdHocCommandSerializer, + TowerSettings: TowerSettingsSerializer, } def activity_stream_create(sender, instance, created, **kwargs): @@ -320,7 +321,11 @@ def activity_stream_create(sender, instance, created, **kwargs): object1=object1, changes=json.dumps(model_to_dict(instance, model_serializer_mapping))) activity_entry.save() - getattr(activity_entry, object1).add(instance) + #TODO: Weird situation where cascade SETNULL doesn't work + # it might actually be a good idea to remove all of these FK references since + # we don't really use them anyway. + if type(instance) is not TowerSettings: + getattr(activity_entry, object1).add(instance) def activity_stream_update(sender, instance, **kwargs): if instance.id is None: @@ -347,7 +352,8 @@ def activity_stream_update(sender, instance, **kwargs): object1=object1, changes=json.dumps(changes)) activity_entry.save() - getattr(activity_entry, object1).add(instance) + if type(instance) is not TowerSettings: + getattr(activity_entry, object1).add(instance) def activity_stream_delete(sender, instance, **kwargs): if not activity_stream_enabled: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index eef1590d1a..622c533333 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -44,6 +44,7 @@ from django.utils.timezone import now from awx.main.constants import CLOUD_PROVIDERS from awx.main.models import * # noqa from awx.main.queue import FifoQueue +from awx.main.conf import tower_settings from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, ignore_inventory_computed_fields, emit_websocket_notification, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot) @@ -348,7 +349,7 @@ class BaseTask(Task): python_paths.insert(0, local_site_packages) env['PYTHONPATH'] = os.pathsep.join(python_paths) if self.should_use_proot: - env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH + env['PROOT_TMP_DIR'] = tower_settings.AWX_PROOT_BASE_PATH return env def build_safe_env(self, instance, **kwargs): @@ -461,7 +462,7 @@ class BaseTask(Task): instance = self.update_model(instance.pk) if instance.cancel_flag: try: - if settings.AWX_PROOT_ENABLED: + if tower_settings.AWX_PROOT_ENABLED: # NOTE: Refactor this once we get a newer psutil across the board if not psutil: os.kill(child.pid, signal.SIGKILL) @@ -654,9 +655,9 @@ class RunJob(BaseTask): ''' 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) + if hasattr(tower_settings, 'AWX_ANSIBLE_CALLBACK_PLUGINS') and \ + tower_settings.AWX_ANSIBLE_CALLBACK_PLUGINS: + plugin_dirs.append(tower_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 @@ -850,7 +851,7 @@ class RunJob(BaseTask): ''' Return whether this task should use proot. ''' - return getattr(settings, 'AWX_PROOT_ENABLED', False) + return getattr(tower_settings, 'AWX_PROOT_ENABLED', False) def pre_run_hook(self, job, **kwargs): if job.job_type == PERM_INVENTORY_SCAN: @@ -1475,7 +1476,7 @@ class RunAdHocCommand(BaseTask): ''' Return whether this task should use proot. ''' - return getattr(settings, 'AWX_PROOT_ENABLED', False) + return getattr(tower_settings, 'AWX_PROOT_ENABLED', False) def post_run_hook(self, ad_hoc_command, **kwargs): ''' diff --git a/awx/main/tests/__init__.py b/awx/main/tests/__init__.py index c3d0fd60a0..4f1e89274e 100644 --- a/awx/main/tests/__init__.py +++ b/awx/main/tests/__init__.py @@ -19,3 +19,4 @@ from awx.main.tests.commands import * # noqa from awx.main.tests.fact import * # noqa from awx.main.tests.unified_jobs import * # noqa from awx.main.tests.ha import * # noqa +from awx.main.tests.settings import * # noqa diff --git a/awx/main/tests/settings.py b/awx/main/tests/settings.py new file mode 100644 index 0000000000..a727213454 --- /dev/null +++ b/awx/main/tests/settings.py @@ -0,0 +1,105 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved. + +from awx.main.tests.base import BaseTest +from awx.main.models import * # noqa + +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +TEST_TOWER_SETTINGS_MANIFEST = { + "TEST_SETTING_INT": { + "name": "An Integer Field", + "description": "An Integer Field", + "default": 1, + "type": "int", + "category": "test" + }, + "TEST_SETTING_STRING": { + "name": "A String Field", + "description": "A String Field", + "default": "test", + "type": "string", + "category": "test" + }, + "TEST_SETTING_BOOL": { + "name": "A Bool Field", + "description": "A Bool Field", + "default": True, + "type": "bool", + "category": "test" + }, + "TEST_SETTING_LIST": { + "name": "A List Field", + "description": "A List Field", + "default": ["A", "Simple", "List"], + "type": "list", + "category": "test" + } +} + +@override_settings(TOWER_SETTINGS_MANIFEST=TEST_TOWER_SETTINGS_MANIFEST) +class SettingsTest(BaseTest): + + def setUp(self): + super(SettingsTest, self).setUp() + self.setup_instances() + self.setup_users() + + def get_settings(self, expected_count=4): + result = self.get(reverse('api:settings_list'), expect=200) + self.assertEqual(result['count'], expected_count) + return result['results'] + + def get_individual_setting(self, setting): + all_settings = self.get_settings() + setting_actual = None + for setting_item in all_settings: + if setting_item['key'] == setting: + setting_actual = setting_item + break + self.assertIsNotNone(setting_actual) + return setting_actual + + def set_setting(self, key, value): + self.post(reverse('api:settings_list'), data={"key": key, "value": value}, expect=201) + + def test_get_settings(self): + # Regular user should see nothing (no user settings yet) + with self.current_user(self.normal_django_user): + self.get_settings(expected_count=0) + # anonymous user should get a 401 + self.get(reverse('api:settings_list'), expect=401) + # super user can see everything + with self.current_user(self.super_django_user): + self.get_settings(expected_count=len(TEST_TOWER_SETTINGS_MANIFEST)) + + def test_set_and_reset_settings(self): + settings_reset = reverse('api:settings_reset') + with self.current_user(self.super_django_user): + # Set and reset a single setting + setting_int = self.get_individual_setting('TEST_SETTING_INT') + self.assertEqual(setting_int['value'], TEST_TOWER_SETTINGS_MANIFEST['TEST_SETTING_INT']['default']) + self.set_setting('TEST_SETTING_INT', 2) + setting_int = self.get_individual_setting('TEST_SETTING_INT') + self.assertEqual(setting_int['value'], 2) + self.post(settings_reset, data={"key": 'TEST_SETTING_INT'}, expect=204) + setting_int = self.get_individual_setting('TEST_SETTING_INT') + self.assertEqual(setting_int['value'], TEST_TOWER_SETTINGS_MANIFEST['TEST_SETTING_INT']['default']) + + def test_clear_all_settings(self): + settings_list = reverse('api:settings_list') + with self.current_user(self.super_django_user): + self.set_setting('TEST_SETTING_INT', 2) + self.set_setting('TEST_SETTING_STRING', "foo") + self.set_setting('TEST_SETTING_BOOL', False) + self.set_setting('TEST_SETTING_LIST', [1,2,3]) + all_settings = self.get_settings() + for setting_entry in all_settings: + self.assertNotEqual(setting_entry['value'], + TEST_TOWER_SETTINGS_MANIFEST[setting_entry['key']]['default']) + self.delete(settings_list, expect=200) + all_settings = self.get_settings() + for setting_entry in all_settings: + self.assertEqual(setting_entry['value'], + TEST_TOWER_SETTINGS_MANIFEST[setting_entry['key']]['default']) diff --git a/awx/main/tests/users.py b/awx/main/tests/users.py index 5cea474875..78aca9da4b 100644 --- a/awx/main/tests/users.py +++ b/awx/main/tests/users.py @@ -16,6 +16,7 @@ from django.test.utils import override_settings # AWX from awx.main.models import * # noqa from awx.main.tests.base import BaseTest +from awx.main.conf import tower_settings __all__ = ['AuthTokenTimeoutTest', 'AuthTokenLimitTest', 'AuthTokenProxyTest', 'UsersTest', 'LdapTest'] @@ -38,7 +39,7 @@ class AuthTokenTimeoutTest(BaseTest): response = self._generic_rest(dashboard_url, expect=200, method='get', return_response_object=True, client_kwargs=kwargs) self.assertIn('Auth-Token-Timeout', response) - self.assertEqual(response['Auth-Token-Timeout'], str(settings.AUTH_TOKEN_EXPIRATION)) + self.assertEqual(response['Auth-Token-Timeout'], str(tower_settings.AUTH_TOKEN_EXPIRATION)) class AuthTokenLimitTest(BaseTest): def setUp(self): diff --git a/awx/main/utils.py b/awx/main/utils.py index 9e6a005dc1..63b4a9441c 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -448,8 +448,8 @@ def build_proot_temp_dir(): ''' Create a temporary directory for proot to use. ''' - from django.conf import settings - path = tempfile.mkdtemp(prefix='ansible_tower_proot_', dir=settings.AWX_PROOT_BASE_PATH) + from awx.main.conf import tower_settings + path = tempfile.mkdtemp(prefix='ansible_tower_proot_', dir=tower_settings.AWX_PROOT_BASE_PATH) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return path @@ -462,13 +462,14 @@ def wrap_args_with_proot(args, cwd, **kwargs): - /var/log/supervisor - /tmp (except for own tmp files) ''' + from awx.main.conf import tower_settings from django.conf import settings new_args = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '-v', str(getattr(settings, 'AWX_PROOT_VERBOSITY', '0')), '-r', '/'] hide_paths = ['/etc/tower', '/var/lib/awx', '/var/log', tempfile.gettempdir(), settings.PROJECTS_ROOT, settings.JOBOUTPUT_ROOT] - hide_paths.extend(getattr(settings, 'AWX_PROOT_HIDE_PATHS', None) or []) + hide_paths.extend(getattr(tower_settings, 'AWX_PROOT_HIDE_PATHS', None) or []) for path in sorted(set(hide_paths)): if not os.path.exists(path): continue @@ -484,7 +485,7 @@ def wrap_args_with_proot(args, cwd, **kwargs): show_paths = [cwd, kwargs['private_data_dir']] else: show_paths = [cwd] - show_paths.extend(getattr(settings, 'AWX_PROOT_SHOW_PATHS', None) or []) + show_paths.extend(getattr(tower_settings, 'AWX_PROOT_SHOW_PATHS', None) or []) for path in sorted(set(show_paths)): if not os.path.exists(path): continue diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d61f0fac10..de05ef930f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -262,11 +262,11 @@ SERVER_EMAIL = 'root@localhost' # Default email address to use for various automated correspondence from # the site managers. -DEFAULT_FROM_EMAIL = 'webmaster@localhost' +DEFAULT_FROM_EMAIL = 'tower@localhost' # Subject-line prefix for email messages send with django.core.mail.mail_admins # or ...mail_managers. Make sure to include the trailing space. -EMAIL_SUBJECT_PREFIX = '[AWX] ' +EMAIL_SUBJECT_PREFIX = '[Tower] ' # The email backend to use. For possible shortcuts see django.core.mail. # The default is to use the SMTP backend. @@ -688,6 +688,141 @@ FACT_CACHE_PORT = 6564 ORG_ADMINS_CAN_SEE_ALL_USERS = True +TOWER_SETTINGS_MANIFEST = { + "SCHEDULE_MAX_JOBS": { + "name": "Maximum Scheduled Jobs", + "description": "Maximum number of the same job template that can be waiting to run when launching from a schedule before no more are created", + "default": SCHEDULE_MAX_JOBS, + "type": "int", + "category": "jobs", + }, + "STDOUT_MAX_BYTES_DISPLAY": { + "name": "Standard Output Maximum Display Size", + "description": "Maximum Size of Standard Output in bytes to display before requiring the output be downloaded", + "default": STDOUT_MAX_BYTES_DISPLAY, + "type": "int", + "category": "jobs", + }, + "AUTH_TOKEN_EXPIRATION": { + "name": "Idle Time Force Log Out", + "description": "Number of seconds that a user is inactive before they will need to login again", + "type": "int", + "default": AUTH_TOKEN_EXPIRATION, + "category": "authentication", + }, + "AUTH_TOKEN_PER_USER": { + "name": "Maximum number of simultaneous logins", + "description": "Maximum number of simultaneous logins a user may have. To disable enter -1", + "type": "int", + "default": AUTH_TOKEN_PER_USER, + "category": "authentication", + }, + # "AUTH_BASIC_ENABLED": { + # "name": "Enable HTTP Basic Auth", + # "description": "Enable HTTP Basic Auth for the API Browser", + # "default": AUTH_BASIC_ENABLED, + # "type": "bool", + # "category": "authentication", + # }, + # "AUTH_LDAP_SERVER_URI": { + # "name": "LDAP Server URI", + # "description": "URI Location of the LDAP Server", + # "default": AUTH_LDAP_SERVER_URI, + # "type": "string", + # "category": "authentication", + # }, + # "RADIUS_SERVER": { + # "name": "Radius Server Host", + # "description": "Host to communicate with for Radius Authentication", + # "default": RADIUS_SERVER, + # "type": "string", + # "category": "authentication", + # }, + # "RADIUS_PORT": { + # "name": "Radius Server Port", + # "description": "Port on the Radius host for Radius Authentication", + # "default": RADIUS_PORT, + # "type": "string", + # "category": "authentication", + # }, + # "RADIUS_SECRET": { + # "name": "Radius Server Secret", + # "description": "Secret used when negotiating with the Radius server", + # "default": RADIUS_SECRET, + # "type": "string", + # "category": "authentication", + # }, + "AWX_PROOT_ENABLED": { + "name": "Enable PRoot for Job Execution", + "description": "Isolates an Ansible job from protected parts of the Tower system to prevent exposing sensitive information", + "default": AWX_PROOT_ENABLED, + "type": "bool", + "category": "jobs", + }, + "AWX_PROOT_HIDE_PATHS": { + "name": "Paths to hide from PRoot jobs", + "description": "Extra paths to hide from PRoot isolated processes", + "default": AWX_PROOT_HIDE_PATHS, + "type": "list", + "category": "jobs", + }, + "AWX_PROOT_SHOW_PATHS": { + "name": "Paths to expose to PRoot jobs", + "description": "Explicit whitelist of paths to expose to PRoot jobs", + "default": AWX_PROOT_SHOW_PATHS, + "type": "list", + "category": "jobs", + }, + "AWX_PROOT_BASE_PATH": { + "name": "Base PRoot execution path", + "description": "The location that PRoot will create its temporary working directory", + "default": AWX_PROOT_BASE_PATH, + "type": "string", + "category": "jobs", + }, + "AWX_ANSIBLE_CALLBACK_PLUGINS": { + "name": "Ansible Callback Plugins", + "description": "Colon Seperated Paths for extra callback plugins to be used when running jobs", + "default": AWX_ANSIBLE_CALLBACK_PLUGINS, + "type": "string", + "category": "jobs", + }, + "PENDO_TRACKING_STATE": { + "name": "Analytics Tracking State", + "description": "Enable or Disable Analytics Tracking", + "default": PENDO_TRACKING_STATE, + "type": "string", + "category": "ui", + }, + "AD_HOC_COMMANDS": { + "name": "Ansible Modules Allowed for Ad Hoc Jobs", + "description": "A colon-seperated whitelist of modules allowed to be used by ad-hoc jobs", + "default": AD_HOC_COMMANDS, + "type": "list", + "category": "jobs", + }, + "ACTIVITY_STREAM_ENABLED": { + "name": "Enable Activity Stream", + "description": "Enable capturing activity for the Tower activity stream", + "default": ACTIVITY_STREAM_ENABLED, + "type": "bool", + "category": "system", + }, + "ORG_ADMINS_CAN_SEE_ALL_USERS": { + "name": "All Users Visible to Organization Admins", + "description": "Controls whether any Organization Admin can view all users, even those not associated with their Organization", + "default": ORG_ADMINS_CAN_SEE_ALL_USERS, + "type": "bool", + "category": "system", + }, + "LICENSE": { + "name": "Tower License", + "description": "Controls what features and functionality is enabled in Tower.", + "default": "{}", + "type": "string", + "category": "system", + }, +} # Logging configuration. LOGGING = { 'version': 1,