From fdcda43de6f214988a842073e7507e7c9c448810 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 7 Dec 2015 09:49:13 -0500 Subject: [PATCH 01/24] Initial Database Configuration bootstrap * Settings manifest, mapping internal settings to what can be used in the database along with type information etc. * Initial Database model * Helper object that overlays database settings on django settings --- awx/main/conf.py | 40 +++++++++ awx/main/models/configuration.py | 42 +++++++++ awx/settings/defaults.py | 142 +++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 awx/main/conf.py create mode 100644 awx/main/models/configuration.py diff --git a/awx/main/conf.py b/awx/main/conf.py new file mode 100644 index 0000000000..279e5c9da3 --- /dev/null +++ b/awx/main/conf.py @@ -0,0 +1,40 @@ +# Copyright (c) 2015 Ansible, Inc.. +# All Rights Reserved. + +import json +from django.conf import settings as django_settings +from awx.main.models.configuration import TowerSettings + +class TowerSettings(object): + + def __getattr__(self, key): + ts = TowerSettings.objects.filter(key=name) + if not ts.exists: + return getattr(django_settings, key) + ts = ts[0] + if ts.value_type == 'json': + converted_type = json.loads(ts.value) + elif ts.value_type == 'password': + converted_type = ts.value + elif ts.value_type == 'list': + converted_type = [x.strip() for x in a.split(',')] + else: + t = getattr(__builtin__, ts.value_type) + converted_type = t(ts.value) + return converted_type + + def create(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] + setting_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() diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py new file mode 100644 index 0000000000..b3aec6c6b7 --- /dev/null +++ b/awx/main/models/configuration.py @@ -0,0 +1,42 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Python + +# Django +from django.conf import settings +from django.db import models + +# Tower +from awx.main.models.base import CreatedModifiedModel + +class TowerSettings(CreatedModifiedModel): + + SETTINGS_TYPE_CHOICES = [ + ('string', "String"), + ('int', 'Integer'), + ('float', 'Decimal'), + ('json', 'JSON'), + ('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, + ) + diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6733f0d408..efabb6019f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -681,6 +681,148 @@ FACT_CACHE_PORT = 6564 ORG_ADMINS_CAN_SEE_ALL_USERS = True +TOWER_SETTINGS_MANIFEST = { + "PROJECTS_ROOT": { + "name": "Projects Root Directory", + "description": "Directory to store synced projects in and to look for manual projects", + "default": PROJECTS_ROOT, + "type": "string", + "category": "jobs", + }, + "JOBOUTPUT_ROOT": { + "name": "Job Standard Output Directory", + "description": "Directory to store job standard output files", + "default": JOBOUTPUT_ROOT, + "type": "string", + "category": "jobs", + }, + "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"s, + "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": "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", + }, +} # Logging configuration. LOGGING = { 'version': 1, From f53f3d805def9a01aa5ed6d3250a974e8ec1c6d3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 11 Dec 2015 16:57:11 -0500 Subject: [PATCH 02/24] View and some validation logic for database config * Fixing some bugs in the manifest definition * Database model and schema migration for tower settings * Initial View and Serializer implementation using a strategy of merging model instances and named tuples --- awx/api/generics.py | 1 + awx/api/serializers.py | 7 +- awx/api/urls.py | 6 +- awx/api/views.py | 27 ++ awx/main/migrations/0075_v300_changes.py | 544 +++++++++++++++++++++++ awx/main/models/__init__.py | 4 +- awx/main/models/configuration.py | 17 +- awx/settings/defaults.py | 8 +- 8 files changed, 600 insertions(+), 14 deletions(-) create mode 100644 awx/main/migrations/0075_v300_changes.py diff --git a/awx/api/generics.py b/awx/api/generics.py index a0f892210c..2707bb9d4a 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): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bf258cc524..6ba6b2c725 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -267,7 +267,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,)) @@ -2105,7 +2105,12 @@ 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') class AuthTokenSerializer(serializers.Serializer): diff --git a/awx/api/urls.py b/awx/api/urls.py index d177d6b9ba..4166dd9551 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -220,6 +220,9 @@ 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')) + v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), url(r'^ping/$', 'api_v1_ping_view'), @@ -230,7 +233,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..52459ee269 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 @@ -113,6 +114,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') @@ -2959,6 +2961,31 @@ class ActivityStreamDetail(RetrieveAPIView): # Okay, let it through. return super(type(self), self).get(request, *args, **kwargs) +class SettingsList(ListCreateAPIView): + + model = TowerSettings + serializer_class = TowerSettingsSerializer + new_in_300 = True + filter_backends = () + + def get_queryset(self): + SettingsTuple = namedtuple('Settings', ['key', 'description', 'category', 'value', 'value_type', 'user']) + # TODO: Filter by what the user can see + all_defined_settings = {s.key: SettingsTuple(s.key, s.description, s.category, s.value, 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 # 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/migrations/0075_v300_changes.py b/awx/main/migrations/0075_v300_changes.py new file mode 100644 index 0000000000..95500db57d --- /dev/null +++ b/awx/main/migrations/0075_v300_changes.py @@ -0,0 +1,544 @@ +# -*- 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']) + + + def backwards(self, orm): + # Deleting model 'TowerSettings' + db.delete_table(u'main_towersettings') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'ad_hoc_command': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.AdHocCommand']", 'symmetrical': 'False', 'blank': 'True'}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'custom_inventory_script': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.CustomInventoryScript']", 'symmetrical': 'False', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}), + 'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}), + 'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}), + 'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}), + 'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'main.adhoccommand': { + 'Meta': {'object_name': 'AdHocCommand', '_ormbases': ['main.UnifiedJob']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ad_hoc_commands'", 'symmetrical': 'False', 'through': "orm['main.AdHocCommandEvent']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_commands'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'module_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'module_name': ('django.db.models.fields.CharField', [], {'default': "'command'", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.adhoccommandevent': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('ad_hoc_command', 'host_name')]", 'object_name': 'AdHocCommandEvent'}, + 'ad_hoc_command': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ad_hoc_command_events'", 'to': "orm['main.AdHocCommand']"}), + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'ad_hoc_command_events'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'ordering': "('kind', 'name')", 'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'become_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'become_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'become_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'security_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.custominventoryscript': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'CustomInventoryScript'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'custom_inventory_scripts'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'script': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'groups'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'ordering': "('inventory', 'name')", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'hosts'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'hosts_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Job']"}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.instance': { + 'Meta': {'object_name': 'Instance'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}) + }, + 'main.inventory': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventorysources'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'default': 'None', 'related_name': "'inventory_source'", 'unique': 'True', 'null': 'True', 'to': "orm['main.Group']"}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventoryupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group_by': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'instance_filters': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.job': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Job', '_ormbases': ['main.UnifiedJob']}, + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_events_as_primary_host'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'job_events'", 'symmetrical': 'False', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.JobEvent']"}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'role': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host_name')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_host_summaries'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.joborigin': { + 'Meta': {'object_name': 'JobOrigin'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Instance']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'job_origin'", 'unique': 'True', 'to': "orm['main.UnifiedJob']"}) + }, + 'main.jobtemplate': { + 'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'become_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "'run'", 'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Project']", 'blank': 'True', 'null': 'True'}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'survey_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'survey_spec': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'run_ad_hoc_commands': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.schedule': { + 'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'extra_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'next_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'schedules'", 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.systemjob': { + 'Meta': {'ordering': "('id',)", 'object_name': 'SystemJob', '_ormbases': ['main.UnifiedJob']}, + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'system_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.SystemJobTemplate']", 'blank': 'True', 'null': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.systemjobtemplate': { + 'Meta': {'object_name': 'SystemJobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.team': { + 'Meta': {'ordering': "('organization__name', 'name')", 'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.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'] \ No newline at end of file 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/configuration.py b/awx/main/models/configuration.py index b3aec6c6b7..21ce5704ad 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -6,19 +6,22 @@ # Django from django.conf import settings 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'), - ('password', 'Password'), - ('list', 'List') + ('string', _("String")), + ('int', _('Integer')), + ('float', _('Decimal')), + ('json', _('JSON')), + ('password', _('Password')), + ('list', _('List')) ] key = models.CharField( diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index efabb6019f..3295663973 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -255,11 +255,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. @@ -728,7 +728,7 @@ TOWER_SETTINGS_MANIFEST = { "name": "Enable HTTP Basic Auth", "description": "Enable HTTP Basic Auth for the API Browser", "default": AUTH_BASIC_ENABLED, - "type": "bool" + "type": "bool", "category": "authentication", }, "AUTH_LDAP_SERVER_URI": { @@ -747,7 +747,7 @@ TOWER_SETTINGS_MANIFEST = { }, "RADIUS_PORT": { "name": "Radius Server Port", - "description": "Port on the Radius host for Radius Authentication"s, + "description": "Port on the Radius host for Radius Authentication", "default": RADIUS_PORT, "type": "string", "category": "authentication", From 273181e894ad06aeba2e552b0dafbab1b1fc1514 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 14 Dec 2015 15:09:10 -0500 Subject: [PATCH 03/24] Expand dbconfig support * Support updating settings values * Support activity stream endpoint * Support clearing value * Improve type conversion system for displaying values --- awx/api/serializers.py | 29 ++++++++++++++++++++++++ awx/api/urls.py | 3 ++- awx/api/views.py | 20 +++++++++++++++- awx/main/conf.py | 14 ++---------- awx/main/migrations/0075_v300_changes.py | 13 +++++++++++ awx/main/models/activity_stream.py | 1 + awx/main/models/configuration.py | 13 +++++++++++ awx/main/signals.py | 1 + 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 6ba6b2c725..e460fe7789 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2111,6 +2111,35 @@ 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): + print("kwargs {0}".format(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/urls.py b/awx/api/urls.py index 4166dd9551..2b3a93d852 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -221,7 +221,8 @@ activity_stream_urls = patterns('awx.api.views', ) settings_urls = patterns('awx.api.views', - url(r'^$', 'settings_list')) + url(r'^$', 'settings_list'), + url(r'^reset/$', 'settings_reset')) v1_urls = patterns('awx.api.views', url(r'^$', 'api_v1_root_view'), diff --git a/awx/api/views.py b/awx/api/views.py index 52459ee269..505783a292 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -70,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 TowerConfiguration def api_exception_handler(exc): ''' @@ -2971,7 +2972,12 @@ class SettingsList(ListCreateAPIView): def get_queryset(self): SettingsTuple = namedtuple('Settings', ['key', 'description', 'category', 'value', 'value_type', 'user']) # TODO: Filter by what the user can see - all_defined_settings = {s.key: SettingsTuple(s.key, s.description, s.category, s.value, s.value_type, s.user) for s in TowerSettings.objects.all()} + 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: @@ -2987,6 +2993,18 @@ class SettingsList(ListCreateAPIView): None)) return settings_actual +class SettingsReset(APIView): + + view_name = "Reset a settings value" + new_in_300 = True + + def post(self, request): + # TODO: RBAC + setting_key = request.DATA.get('key', None) + if setting_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 # lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). diff --git a/awx/main/conf.py b/awx/main/conf.py index 279e5c9da3..147afabe46 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -5,23 +5,13 @@ import json from django.conf import settings as django_settings from awx.main.models.configuration import TowerSettings -class TowerSettings(object): +class TowerConfiguration(object): def __getattr__(self, key): ts = TowerSettings.objects.filter(key=name) if not ts.exists: return getattr(django_settings, key) - ts = ts[0] - if ts.value_type == 'json': - converted_type = json.loads(ts.value) - elif ts.value_type == 'password': - converted_type = ts.value - elif ts.value_type == 'list': - converted_type = [x.strip() for x in a.split(',')] - else: - t = getattr(__builtin__, ts.value_type) - converted_type = t(ts.value) - return converted_type + return ts[0].value_converted def create(key, value): settings_manifest = django_settings.TOWER_SETTINGS_MANIFEST diff --git a/awx/main/migrations/0075_v300_changes.py b/awx/main/migrations/0075_v300_changes.py index 95500db57d..6c9b74b414 100644 --- a/awx/main/migrations/0075_v300_changes.py +++ b/awx/main/migrations/0075_v300_changes.py @@ -22,11 +22,23 @@ class Migration(SchemaMigration): )) 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': { @@ -91,6 +103,7 @@ class Migration(SchemaMigration): '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'}) 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/configuration.py b/awx/main/models/configuration.py index 21ce5704ad..b53d45dc63 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -20,6 +20,7 @@ class TowerSettings(CreatedModifiedModel): ('int', _('Integer')), ('float', _('Decimal')), ('json', _('JSON')), + ('bool', _('Boolean')), ('password', _('Password')), ('list', _('List')) ] @@ -43,3 +44,15 @@ class TowerSettings(CreatedModifiedModel): 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(',')] + else: + t = __builtins__[self.value_type] + converted_type = t(self.value) + return converted_type diff --git a/awx/main/signals.py b/awx/main/signals.py index 2f426b74b3..6eb9745830 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -306,6 +306,7 @@ model_serializer_mapping = { JobTemplate: JobTemplateSerializer, Job: JobSerializer, AdHocCommand: AdHocCommandSerializer, + TowerSettings: TowerSettingsSerializer, } def activity_stream_create(sender, instance, created, **kwargs): From 7867a58c007ed6de961f629ada40f860c8b78530 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 12:12:54 -0500 Subject: [PATCH 04/24] RBAC and settings reset * Initial super-user only rbac with notes for future user-settings support * Clearing individual and all settings back to defaults --- awx/api/views.py | 20 ++++++++++++++++---- awx/main/access.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 505783a292..dd07c57c0a 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2970,8 +2970,12 @@ class SettingsList(ListCreateAPIView): filter_backends = () def get_queryset(self): + # TODO: docs + if not 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']) - # TODO: Filter by what the user can see all_defined_settings = {s.key: SettingsTuple(s.key, s.description, s.category, @@ -2993,15 +2997,23 @@ class SettingsList(ListCreateAPIView): 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): - # TODO: RBAC - setting_key = request.DATA.get('key', None) - if setting_key is not None: + # 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) diff --git a/awx/main/access.py b/awx/main/access.py index 4af42b28f2..c043855873 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1563,6 +1563,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) @@ -1633,6 +1637,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) @@ -1658,3 +1686,4 @@ register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess) register_access(UnifiedJob, UnifiedJobAccess) register_access(ActivityStream, ActivityStreamAccess) register_access(CustomInventoryScript, CustomInventoryScriptAccess) +register_access(TowerSettings, TowerSettingsAccess) From 16aa34522c36b71aeecb8021c6758f6b1b94ce76 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 15:13:58 -0500 Subject: [PATCH 05/24] Fix up an export issue for tower_settings --- awx/main/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index 147afabe46..8e49d9a10f 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -8,8 +8,8 @@ from awx.main.models.configuration import TowerSettings class TowerConfiguration(object): def __getattr__(self, key): - ts = TowerSettings.objects.filter(key=name) - if not ts.exists: + ts = TowerSettings.objects.filter(key=key) + if not ts.exists(): return getattr(django_settings, key) return ts[0].value_converted @@ -28,3 +28,5 @@ class TowerConfiguration(object): else: settings_actual['value'] = value settings_actual.save() + +tower_settings = TowerConfiguration() From e97e60bd301b609fb75103e34f079c301748a5fc Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 15:14:16 -0500 Subject: [PATCH 06/24] Update settings location for certain values * PROJECTS_ROOT * JOBOUTPUT_ROOT * SCHEDULE_MAX_JOBS * STDOUT_MAX_BYTES_DISPLAY --- awx/api/serializers.py | 11 +++++++---- awx/api/views.py | 11 ++++++----- awx/main/models/configuration.py | 2 ++ awx/main/models/jobs.py | 5 +++-- awx/main/models/projects.py | 9 +++++---- awx/main/tasks.py | 9 +++++---- awx/main/tests/ad_hoc.py | 9 +++++---- awx/main/tests/base.py | 7 ++++--- awx/main/tests/projects.py | 5 +++-- awx/main/tests/tasks.py | 5 +++-- awx/main/utils.py | 14 +++++++------- 11 files changed, 50 insertions(+), 37 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e460fe7789..e14362c36d 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 @@ -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): diff --git a/awx/api/views.py b/awx/api/views.py index dd07c57c0a..a307144ed4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -70,7 +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 TowerConfiguration +from awx.main.conf import tower_settings def api_exception_handler(exc): ''' @@ -216,7 +216,7 @@ class ApiV1ConfigView(APIView): if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count(): data.update(dict( - project_base_dir = settings.PROJECTS_ROOT, + project_base_dir = tower_settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), )) @@ -2862,8 +2862,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: @@ -2971,7 +2972,7 @@ class SettingsList(ListCreateAPIView): def get_queryset(self): # TODO: docs - if not request.user.is_superuser: + 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 [] diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py index b53d45dc63..c7e8d07f68 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -52,6 +52,8 @@ class TowerSettings(CreatedModifiedModel): 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"] else: t = __builtins__[self.value_type] converted_type = t(self.value) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index dddd91dfc8..fdc7b74241 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/projects.py b/awx/main/models/projects.py index b90e29cd85..30c6c9d9d1 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -22,6 +22,7 @@ from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.unified_jobs import * # noqa from awx.main.utils import update_scm_url +from awx.main.conf import tower_settings __all__ = ['Project', 'ProjectUpdate'] @@ -45,9 +46,9 @@ class ProjectOptions(models.Model): @classmethod def get_local_path_choices(cls): - if os.path.exists(settings.PROJECTS_ROOT): - paths = [x.decode('utf-8') for x in os.listdir(settings.PROJECTS_ROOT) - if (os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x)) and + if os.path.exists(tower_settings.PROJECTS_ROOT): + paths = [x.decode('utf-8') for x in os.listdir(tower_settings.PROJECTS_ROOT) + if (os.path.isdir(os.path.join(tower_settings.PROJECTS_ROOT, x)) and not x.startswith('.') and not x.startswith('_'))] qs = Project.objects.filter(active=True) used_paths = qs.values_list('local_path', flat=True) @@ -143,7 +144,7 @@ class ProjectOptions(models.Model): def get_project_path(self, check_if_exists=True): local_path = os.path.basename(self.local_path) if local_path and not local_path.startswith('.'): - proj_path = os.path.join(settings.PROJECTS_ROOT, local_path) + proj_path = os.path.join(tower_settings.PROJECTS_ROOT, local_path) if not check_if_exists or os.path.exists(smart_str(proj_path)): return proj_path diff --git a/awx/main/tasks.py b/awx/main/tasks.py index eef1590d1a..3c6e18ba0f 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) @@ -536,9 +537,9 @@ class BaseTask(Task): cwd = self.build_cwd(instance, **kwargs) env = self.build_env(instance, **kwargs) safe_env = self.build_safe_env(instance, **kwargs) - if not os.path.exists(settings.JOBOUTPUT_ROOT): - os.makedirs(settings.JOBOUTPUT_ROOT) - stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (pk, str(uuid.uuid1()))) + if not os.path.exists(tower_settings.JOBOUTPUT_ROOT): + os.makedirs(tower_settings.JOBOUTPUT_ROOT) + stdout_filename = os.path.join(tower_settings.JOBOUTPUT_ROOT, "%d-%s.out" % (pk, str(uuid.uuid1()))) stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8') if self.should_use_proot(instance, **kwargs): if not check_proot_installed(): @@ -813,7 +814,7 @@ class RunJob(BaseTask): return self.get_path_to('..', 'playbooks') cwd = job.project.get_project_path() if not cwd: - root = settings.PROJECTS_ROOT + root = tower_settings.PROJECTS_ROOT raise RuntimeError('project local_path %s cannot be found in %s' % (job.project.local_path, root)) return cwd diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 957cd7c084..b5ca386c1b 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -20,6 +20,7 @@ from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.tests.base import BaseJobExecutionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK +from awx.main.conf import tower_settings __all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest'] @@ -325,13 +326,13 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): if not has_proot: self.skipTest('proot is not installed') # Enable proot for this test. - settings.AWX_PROOT_ENABLED = True + tower_settings.AWX_PROOT_ENABLED = True # Hide local settings path. - settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] + tower_settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] # Create list of paths that should not be visible to the command. hidden_paths = [ - os.path.join(settings.PROJECTS_ROOT, '*'), - os.path.join(settings.JOBOUTPUT_ROOT, '*'), + os.path.join(tower_settings.PROJECTS_ROOT, '*'), + os.path.join(tower_settings.JOBOUTPUT_ROOT, '*'), ] # Create a temp directory that should not be visible to the command. temp_path = tempfile.mkdtemp() diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index bdea0523a8..027b19e850 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -31,6 +31,7 @@ from awx.main.models import * # noqa from awx.main.management.commands.run_callback_receiver import CallbackReceiver from awx.main.management.commands.run_task_system import run_taskmanager from awx.main.utils import get_ansible_version +from awx.main.conf import tower_settings from awx.main.task_engine import TaskEngager as LicenseWriter from awx.sso.backends import LDAPSettings @@ -263,14 +264,14 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): if not name: name = self.unique_name('Project') - if not os.path.exists(settings.PROJECTS_ROOT): - os.makedirs(settings.PROJECTS_ROOT) + if not os.path.exists(tower_settings.PROJECTS_ROOT): + os.makedirs(tower_settings.PROJECTS_ROOT) # Create temp project directory. if unicode_prefix: tmp_prefix = u'\u2620tmp' else: tmp_prefix = 'tmp' - project_dir = tempfile.mkdtemp(prefix=tmp_prefix, dir=settings.PROJECTS_ROOT) + project_dir = tempfile.mkdtemp(prefix=tmp_prefix, dir=tower_settings.PROJECTS_ROOT) self._temp_paths.append(project_dir) # Create temp playbook in project (if playbook content is given). if playbook_content: diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index f698267a0c..e0cab8f03c 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -20,6 +20,7 @@ from django.utils.timezone import now # AWX from awx.main.models import * # noqa +from awx.main.conf import tower_settings from awx.main.tests.base import BaseTransactionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK, TEST_OPENSSH_KEY_DATA, TEST_OPENSSH_KEY_DATA_LOCKED from awx.main.utils import decrypt_field, update_scm_url @@ -150,7 +151,7 @@ class ProjectsTest(BaseTransactionTest): url = reverse('api:api_v1_config_view') response = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue('project_base_dir' in response) - self.assertEqual(response['project_base_dir'], settings.PROJECTS_ROOT) + self.assertEqual(response['project_base_dir'], tower_settings.PROJECTS_ROOT) self.assertTrue('project_local_paths' in response) self.assertEqual(set(response['project_local_paths']), set(Project.get_local_path_choices())) @@ -218,7 +219,7 @@ class ProjectsTest(BaseTransactionTest): self.assertEquals(results['count'], 0) # can add projects (super user) - project_dir = tempfile.mkdtemp(dir=settings.PROJECTS_ROOT) + project_dir = tempfile.mkdtemp(dir=tower_settings.PROJECTS_ROOT) self._temp_paths.append(project_dir) project_data = { 'name': 'My Test Project', diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index acdff99e29..81ee968b21 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -21,6 +21,7 @@ from crum import impersonate # AWX from awx.main.utils import * # noqa from awx.main.models import * # noqa +from awx.main.conf import tower_settings from awx.main.tests.base import BaseJobExecutionTest TEST_PLAYBOOK = u''' @@ -1412,8 +1413,8 @@ class RunJobTest(BaseJobExecutionTest): project_path = self.project.local_path job_template = self.create_test_job_template() extra_vars = { - 'projects_root': settings.PROJECTS_ROOT, - 'joboutput_root': settings.JOBOUTPUT_ROOT, + 'projects_root': tower_settings.PROJECTS_ROOT, + 'joboutput_root': tower_settings.JOBOUTPUT_ROOT, 'project_path': project_path, 'other_project_path': other_project_path, 'temp_path': temp_path, diff --git a/awx/main/utils.py b/awx/main/utils.py index 9e6a005dc1..ef19e42d8c 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,13 @@ def wrap_args_with_proot(args, cwd, **kwargs): - /var/log/supervisor - /tmp (except for own tmp files) ''' - from django.conf import settings + from awx.main.conf import tower_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 []) + tempfile.gettempdir(), tower_settings.PROJECTS_ROOT, + tower_settings.JOBOUTPUT_ROOT] + 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 +484,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 From dfd1ca4ae9cc8d4adef12057835b5aa5993bb4b1 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 15:37:26 -0500 Subject: [PATCH 07/24] Relocate AUTH_TOKEN_* settings reference --- awx/api/authentication.py | 4 ++-- awx/api/views.py | 2 +- awx/main/middleware.py | 3 ++- awx/main/models/organization.py | 9 +++++---- awx/main/tests/users.py | 3 ++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 72dccc61f4..85ad62963e 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -15,7 +15,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 +90,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/views.py b/awx/api/views.py index a307144ed4..b1c65f2f23 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -595,7 +595,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) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 3377ec8cb6..39e8bc7565 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -15,6 +15,7 @@ from django.conf import settings from awx import __version__ as version from awx.main.models import ActivityStream, Instance +from awx.main.comf import tower_settings from awx.api.authentication import TokenAuthentication @@ -117,6 +118,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/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/tests/users.py b/awx/main/tests/users.py index 322c73c522..1ce2c648c8 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): From 0404f6ebc33d83fc6dfeceed5d9370e73ef40e64 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 16:42:32 -0500 Subject: [PATCH 08/24] Add a note about caching --- awx/main/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/conf.py b/awx/main/conf.py index 8e49d9a10f..40646c2537 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -7,6 +7,7 @@ from awx.main.models.configuration import TowerSettings class TowerConfiguration(object): + # TODO: Caching so we don't have to hit the database every time for settings def __getattr__(self, key): ts = TowerSettings.objects.filter(key=key) if not ts.exists(): From a4d4e6d0fb4c94ddfe8e7d96065fb862bb6806d6 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 16:42:54 -0500 Subject: [PATCH 09/24] Comment out some settings tougher to implement Specifically ldap and radius --- awx/settings/defaults.py | 70 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 3295663973..5bff8359c7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -724,41 +724,41 @@ TOWER_SETTINGS_MANIFEST = { "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", - }, + # "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", From 35b19bf220ba038575a876f266da1d02597c6309 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 15 Dec 2015 16:44:08 -0500 Subject: [PATCH 10/24] Update settings references * PROOT * Pendo tracking state * ad hoc commands * activity stream * org admin visibility --- awx/api/views.py | 2 +- awx/main/access.py | 3 ++- awx/main/management/commands/inventory_import.py | 3 ++- awx/main/models/ad_hoc_commands.py | 7 ++++--- awx/main/registrar.py | 3 ++- awx/main/signals.py | 3 ++- awx/main/tasks.py | 14 +++++++------- awx/main/tests/ad_hoc.py | 4 ++-- 8 files changed, 22 insertions(+), 17 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index b1c65f2f23..1677bb37be 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -194,7 +194,7 @@ class ApiV1ConfigView(APIView): license_reader = TaskSerializer() license_data = license_reader.from_file(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, diff --git a/awx/main/access.py b/awx/main/access.py index c043855873..a786920b24 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -19,6 +19,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 +197,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) | 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/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/registrar.py b/awx/main/registrar.py index 6d01ec8d2d..20783745ad 100644 --- a/awx/main/registrar.py +++ b/awx/main/registrar.py @@ -14,7 +14,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 6eb9745830..483626a7c7 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -22,6 +22,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 +274,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) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3c6e18ba0f..5c888033ad 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -349,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): @@ -462,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) @@ -655,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 @@ -851,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: @@ -1476,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/ad_hoc.py b/awx/main/tests/ad_hoc.py index b5ca386c1b..095085f039 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -326,9 +326,9 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): if not has_proot: self.skipTest('proot is not installed') # Enable proot for this test. - tower_settings.AWX_PROOT_ENABLED = True + settings.AWX_PROOT_ENABLED = True # Hide local settings path. - tower_settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] + settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] # Create list of paths that should not be visible to the command. hidden_paths = [ os.path.join(tower_settings.PROJECTS_ROOT, '*'), From 9db80bd9fc98859f6f32d2d19cc4c735bea20ddc Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 16 Dec 2015 11:21:52 -0500 Subject: [PATCH 11/24] Support tower license within the database With backwards support for the file-based license --- awx/api/views.py | 6 ++---- awx/main/conf.py | 13 ++++++++++--- awx/main/middleware.py | 2 +- awx/main/models/configuration.py | 2 ++ awx/settings/defaults.py | 7 +++++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 1677bb37be..a87957b0e9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -192,7 +192,7 @@ 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 = tower_settings.PENDO_TRACKING_STATE if tower_settings.PENDO_TRACKING_STATE in ('off', 'anonymous', 'detailed') else 'off' @@ -264,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. diff --git a/awx/main/conf.py b/awx/main/conf.py index 40646c2537..915f361639 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -9,17 +9,24 @@ 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)) ts = TowerSettings.objects.filter(key=key) if not ts.exists(): - return getattr(django_settings, key) + try: + val_actual = getattr(django_settings, key) + except AttributeError: + val_actual = settings_manifest[key]['default'] + return val_actual return ts[0].value_converted - def create(key, 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] - setting_actual = TowerSettings.objects.filter(key=key) + settings_actual = TowerSettings.objects.filter(key=key) if not settings_actual.exists(): settings_actual = TowerSettings(key=key, description=settings_entry['description'], diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 39e8bc7565..4737ed74a8 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -15,7 +15,7 @@ from django.conf import settings from awx import __version__ as version from awx.main.models import ActivityStream, Instance -from awx.main.comf import tower_settings +from awx.main.conf import tower_settings from awx.api.authentication import TokenAuthentication diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py index c7e8d07f68..c3591dd31c 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -54,6 +54,8 @@ class TowerSettings(CreatedModifiedModel): 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) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 5bff8359c7..2e35c848f7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -822,6 +822,13 @@ TOWER_SETTINGS_MANIFEST = { "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 = { From 910f9bd4a3af4d33be452dedc808d0e010c801e7 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 10:48:15 -0500 Subject: [PATCH 12/24] Fixing up some flake8 issues --- awx/api/authentication.py | 1 - awx/main/access.py | 1 - awx/main/conf.py | 1 - awx/main/middleware.py | 1 - awx/main/models/configuration.py | 2 +- awx/main/models/projects.py | 1 - awx/main/registrar.py | 1 - awx/main/signals.py | 1 - awx/main/utils.py | 1 + 9 files changed, 2 insertions(+), 8 deletions(-) diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 85ad62963e..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 diff --git a/awx/main/access.py b/awx/main/access.py index a786920b24..ad5327a20b 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 diff --git a/awx/main/conf.py b/awx/main/conf.py index 915f361639..75be6280bd 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -1,7 +1,6 @@ # Copyright (c) 2015 Ansible, Inc.. # All Rights Reserved. -import json from django.conf import settings as django_settings from awx.main.models.configuration import TowerSettings diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 4737ed74a8..49077efa6f 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -11,7 +11,6 @@ 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 diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py index c3591dd31c..6b35ea4d77 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -2,9 +2,9 @@ # All Rights Reserved. # Python +import json # Django -from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ # Tower diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 30c6c9d9d1..c266f33971 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -8,7 +8,6 @@ import re import urlparse # Django -from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str diff --git a/awx/main/registrar.py b/awx/main/registrar.py index 20783745ad..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') diff --git a/awx/main/signals.py b/awx/main/signals.py index 483626a7c7..aebf2ac7ee 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 diff --git a/awx/main/utils.py b/awx/main/utils.py index ef19e42d8c..de95460201 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -463,6 +463,7 @@ def wrap_args_with_proot(args, cwd, **kwargs): - /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', From 5e1c98341b5adfa238429e8fb9aba155614b15f5 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 10:50:07 -0500 Subject: [PATCH 13/24] Removing a stray debug statement --- awx/api/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e14362c36d..c59614f53f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2137,7 +2137,6 @@ class TowerSettingsSerializer(BaseSerializer): return attrs def save_object(self, obj, **kwargs): - print("kwargs {0}".format(kwargs)) manifest_val = settings.TOWER_SETTINGS_MANIFEST[obj.key] obj.description = manifest_val['description'] obj.category = manifest_val['category'] From 0e98491dac9c165fa584abb819cef335609ec06d Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 11:22:45 -0500 Subject: [PATCH 14/24] Remove PROJECTS_ROOT and JOBOUTPUT_ROOT from stngs --- awx/api/views.py | 2 +- awx/main/models/projects.py | 9 +++++---- awx/main/tasks.py | 8 ++++---- awx/main/tests/ad_hoc.py | 4 ++-- awx/main/tests/base.py | 6 +++--- awx/main/tests/projects.py | 4 ++-- awx/main/tests/tasks.py | 4 ++-- awx/main/utils.py | 5 +++-- awx/settings/defaults.py | 14 -------------- 9 files changed, 22 insertions(+), 34 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index a87957b0e9..5efb16bbda 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -216,7 +216,7 @@ class ApiV1ConfigView(APIView): if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count(): data.update(dict( - project_base_dir = tower_settings.PROJECTS_ROOT, + project_base_dir = settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), )) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index c266f33971..f494a958f9 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -8,6 +8,7 @@ import re import urlparse # Django +from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str @@ -45,9 +46,9 @@ class ProjectOptions(models.Model): @classmethod def get_local_path_choices(cls): - if os.path.exists(tower_settings.PROJECTS_ROOT): - paths = [x.decode('utf-8') for x in os.listdir(tower_settings.PROJECTS_ROOT) - if (os.path.isdir(os.path.join(tower_settings.PROJECTS_ROOT, x)) and + if os.path.exists(settings.PROJECTS_ROOT): + paths = [x.decode('utf-8') for x in os.listdir(settings.PROJECTS_ROOT) + if (os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x)) and not x.startswith('.') and not x.startswith('_'))] qs = Project.objects.filter(active=True) used_paths = qs.values_list('local_path', flat=True) @@ -143,7 +144,7 @@ class ProjectOptions(models.Model): def get_project_path(self, check_if_exists=True): local_path = os.path.basename(self.local_path) if local_path and not local_path.startswith('.'): - proj_path = os.path.join(tower_settings.PROJECTS_ROOT, local_path) + proj_path = os.path.join(settings.PROJECTS_ROOT, local_path) if not check_if_exists or os.path.exists(smart_str(proj_path)): return proj_path diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 5c888033ad..622c533333 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -537,9 +537,9 @@ class BaseTask(Task): cwd = self.build_cwd(instance, **kwargs) env = self.build_env(instance, **kwargs) safe_env = self.build_safe_env(instance, **kwargs) - if not os.path.exists(tower_settings.JOBOUTPUT_ROOT): - os.makedirs(tower_settings.JOBOUTPUT_ROOT) - stdout_filename = os.path.join(tower_settings.JOBOUTPUT_ROOT, "%d-%s.out" % (pk, str(uuid.uuid1()))) + if not os.path.exists(settings.JOBOUTPUT_ROOT): + os.makedirs(settings.JOBOUTPUT_ROOT) + stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, "%d-%s.out" % (pk, str(uuid.uuid1()))) stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8') if self.should_use_proot(instance, **kwargs): if not check_proot_installed(): @@ -814,7 +814,7 @@ class RunJob(BaseTask): return self.get_path_to('..', 'playbooks') cwd = job.project.get_project_path() if not cwd: - root = tower_settings.PROJECTS_ROOT + root = settings.PROJECTS_ROOT raise RuntimeError('project local_path %s cannot be found in %s' % (job.project.local_path, root)) return cwd diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 095085f039..1e08d83635 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -331,8 +331,8 @@ class RunAdHocCommandTest(BaseAdHocCommandTest): settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')] # Create list of paths that should not be visible to the command. hidden_paths = [ - os.path.join(tower_settings.PROJECTS_ROOT, '*'), - os.path.join(tower_settings.JOBOUTPUT_ROOT, '*'), + os.path.join(settings.PROJECTS_ROOT, '*'), + os.path.join(settings.JOBOUTPUT_ROOT, '*'), ] # Create a temp directory that should not be visible to the command. temp_path = tempfile.mkdtemp() diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 027b19e850..cc675aecf4 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -264,14 +264,14 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): if not name: name = self.unique_name('Project') - if not os.path.exists(tower_settings.PROJECTS_ROOT): - os.makedirs(tower_settings.PROJECTS_ROOT) + if not os.path.exists(settings.PROJECTS_ROOT): + os.makedirs(settings.PROJECTS_ROOT) # Create temp project directory. if unicode_prefix: tmp_prefix = u'\u2620tmp' else: tmp_prefix = 'tmp' - project_dir = tempfile.mkdtemp(prefix=tmp_prefix, dir=tower_settings.PROJECTS_ROOT) + project_dir = tempfile.mkdtemp(prefix=tmp_prefix, dir=settings.PROJECTS_ROOT) self._temp_paths.append(project_dir) # Create temp playbook in project (if playbook content is given). if playbook_content: diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index e0cab8f03c..5cebd3fe4a 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -151,7 +151,7 @@ class ProjectsTest(BaseTransactionTest): url = reverse('api:api_v1_config_view') response = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue('project_base_dir' in response) - self.assertEqual(response['project_base_dir'], tower_settings.PROJECTS_ROOT) + self.assertEqual(response['project_base_dir'], settings.PROJECTS_ROOT) self.assertTrue('project_local_paths' in response) self.assertEqual(set(response['project_local_paths']), set(Project.get_local_path_choices())) @@ -219,7 +219,7 @@ class ProjectsTest(BaseTransactionTest): self.assertEquals(results['count'], 0) # can add projects (super user) - project_dir = tempfile.mkdtemp(dir=tower_settings.PROJECTS_ROOT) + project_dir = tempfile.mkdtemp(dir=settings.PROJECTS_ROOT) self._temp_paths.append(project_dir) project_data = { 'name': 'My Test Project', diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index 81ee968b21..90e4ca75ba 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -1413,8 +1413,8 @@ class RunJobTest(BaseJobExecutionTest): project_path = self.project.local_path job_template = self.create_test_job_template() extra_vars = { - 'projects_root': tower_settings.PROJECTS_ROOT, - 'joboutput_root': tower_settings.JOBOUTPUT_ROOT, + 'projects_root': settings.PROJECTS_ROOT, + 'joboutput_root': settings.JOBOUTPUT_ROOT, 'project_path': project_path, 'other_project_path': other_project_path, 'temp_path': temp_path, diff --git a/awx/main/utils.py b/awx/main/utils.py index de95460201..546c26e3e9 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -17,6 +17,7 @@ import contextlib import tempfile # Django REST Framework +from django.conf import settings from rest_framework.exceptions import ParseError, PermissionDenied from django.utils.encoding import smart_str from django.core.urlresolvers import reverse @@ -467,8 +468,8 @@ def wrap_args_with_proot(args, cwd, **kwargs): 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(), tower_settings.PROJECTS_ROOT, - tower_settings.JOBOUTPUT_ROOT] + tempfile.gettempdir(), settings.PROJECTS_ROOT, + settings.JOBOUTPUT_ROOT] 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): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 2e35c848f7..bf650f40e5 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -682,20 +682,6 @@ FACT_CACHE_PORT = 6564 ORG_ADMINS_CAN_SEE_ALL_USERS = True TOWER_SETTINGS_MANIFEST = { - "PROJECTS_ROOT": { - "name": "Projects Root Directory", - "description": "Directory to store synced projects in and to look for manual projects", - "default": PROJECTS_ROOT, - "type": "string", - "category": "jobs", - }, - "JOBOUTPUT_ROOT": { - "name": "Job Standard Output Directory", - "description": "Directory to store job standard output files", - "default": JOBOUTPUT_ROOT, - "type": "string", - "category": "jobs", - }, "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", From 4ae8d35cc31b685578d654dc085a382ae874155b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 11:26:42 -0500 Subject: [PATCH 15/24] Extra docs for callback plugins manifest entry --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index bf650f40e5..38a57d4cdd 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -775,7 +775,7 @@ TOWER_SETTINGS_MANIFEST = { }, "AWX_ANSIBLE_CALLBACK_PLUGINS": { "name": "Ansible Callback Plugins", - "description": "Extra Callback Plugins to be used when running jobs", + "description": "Colon Seperated Paths for extra callback plugins to be used when running jobs", "default": AWX_ANSIBLE_CALLBACK_PLUGINS, "type": "string", "category": "jobs", From b59e4d8111c1d9670f5c922842a7e063894497cb Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 11:34:01 -0500 Subject: [PATCH 16/24] Fix up some flake8 import issues --- awx/main/models/projects.py | 1 - awx/main/tests/ad_hoc.py | 1 - awx/main/tests/base.py | 1 - awx/main/tests/projects.py | 1 - awx/main/tests/tasks.py | 1 - awx/main/utils.py | 1 - 6 files changed, 6 deletions(-) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index f494a958f9..b90e29cd85 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -22,7 +22,6 @@ from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.unified_jobs import * # noqa from awx.main.utils import update_scm_url -from awx.main.conf import tower_settings __all__ = ['Project', 'ProjectUpdate'] diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 1e08d83635..957cd7c084 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -20,7 +20,6 @@ from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.tests.base import BaseJobExecutionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK -from awx.main.conf import tower_settings __all__ = ['RunAdHocCommandTest', 'AdHocCommandApiTest'] diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index cc675aecf4..bdea0523a8 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -31,7 +31,6 @@ from awx.main.models import * # noqa from awx.main.management.commands.run_callback_receiver import CallbackReceiver from awx.main.management.commands.run_task_system import run_taskmanager from awx.main.utils import get_ansible_version -from awx.main.conf import tower_settings from awx.main.task_engine import TaskEngager as LicenseWriter from awx.sso.backends import LDAPSettings diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 5cebd3fe4a..f698267a0c 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -20,7 +20,6 @@ from django.utils.timezone import now # AWX from awx.main.models import * # noqa -from awx.main.conf import tower_settings from awx.main.tests.base import BaseTransactionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK, TEST_OPENSSH_KEY_DATA, TEST_OPENSSH_KEY_DATA_LOCKED from awx.main.utils import decrypt_field, update_scm_url diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index 90e4ca75ba..acdff99e29 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -21,7 +21,6 @@ from crum import impersonate # AWX from awx.main.utils import * # noqa from awx.main.models import * # noqa -from awx.main.conf import tower_settings from awx.main.tests.base import BaseJobExecutionTest TEST_PLAYBOOK = u''' diff --git a/awx/main/utils.py b/awx/main/utils.py index 546c26e3e9..63b4a9441c 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -17,7 +17,6 @@ import contextlib import tempfile # Django REST Framework -from django.conf import settings from rest_framework.exceptions import ParseError, PermissionDenied from django.utils.encoding import smart_str from django.core.urlresolvers import reverse From 74c06cfc920ab249fe8d565f38bba117a1fb439b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 13:13:12 -0500 Subject: [PATCH 17/24] Fix up migration path These settings aren't available during the initial database migration so we'll use the defaults when bootstrapping the database --- awx/main/conf.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index 75be6280bd..f1b572783d 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -1,9 +1,14 @@ # 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 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 @@ -11,14 +16,20 @@ class TowerConfiguration(object): 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) - if not ts.exists(): - try: - val_actual = getattr(django_settings, key) - except AttributeError: - val_actual = settings_manifest[key]['default'] - return val_actual - return ts[0].value_converted + 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, 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 From d2f99ad549994533c4000c2586a9d8a2ace6ddcb Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 17 Dec 2015 14:00:17 -0500 Subject: [PATCH 18/24] Handle operationalerror for unit tests --- awx/main/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index f1b572783d..4a6b97037b 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -5,6 +5,7 @@ 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') @@ -26,7 +27,7 @@ class TowerConfiguration(object): val_actual = default_value return val_actual return ts[0].value_converted - except ProgrammingError, e: + 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 From ae12be9afa493a2b2d917a92c9021b284e316b64 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 5 Jan 2016 14:34:40 -0500 Subject: [PATCH 19/24] Fixing up settings reset issue This is done by not assigning the ActivityStream foreign key. Which is almost superflous at this point --- awx/api/generics.py | 2 +- awx/main/migrations/0075_v300_changes.py | 2 +- awx/main/signals.py | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 2707bb9d4a..65ce0fde90 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -161,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/main/migrations/0075_v300_changes.py b/awx/main/migrations/0075_v300_changes.py index 6c9b74b414..db25caa1f2 100644 --- a/awx/main/migrations/0075_v300_changes.py +++ b/awx/main/migrations/0075_v300_changes.py @@ -554,4 +554,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/signals.py b/awx/main/signals.py index aebf2ac7ee..04a1cfa00a 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -321,7 +321,12 @@ 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) + print("Instance: {}".format(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: @@ -348,7 +353,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: From 71c7dc16e3258f1bb60035534a85d88db9847d0e Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 5 Jan 2016 14:59:04 -0500 Subject: [PATCH 20/24] Removing stray debug statement --- awx/main/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 04a1cfa00a..8b0c22ec9d 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -321,7 +321,6 @@ def activity_stream_create(sender, instance, created, **kwargs): object1=object1, changes=json.dumps(model_to_dict(instance, model_serializer_mapping))) activity_entry.save() - print("Instance: {}".format(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. From f366b34847d684ac99fbea2e29f3b8430b33ec1c Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jan 2016 16:01:16 -0500 Subject: [PATCH 21/24] Adding database config unit tests --- awx/api/views.py | 1 + awx/main/tests/__init__.py | 1 + awx/main/tests/settings.py | 104 +++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 awx/main/tests/settings.py diff --git a/awx/api/views.py b/awx/api/views.py index 5efb16bbda..bdd5fdca93 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2965,6 +2965,7 @@ class SettingsList(ListCreateAPIView): model = TowerSettings serializer_class = TowerSettingsSerializer + authentication_classes = [TokenGetAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES new_in_300 = True filter_backends = () 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..bfcf79fe32 --- /dev/null +++ b/awx/main/tests/settings.py @@ -0,0 +1,104 @@ +# 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_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']) From 5c2f555b1ddb39172c59d3c04b06bcecc1a7ae57 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jan 2016 16:41:55 -0500 Subject: [PATCH 22/24] Updating new_in for 3.0 --- awx/api/templates/api/_new_in_awx.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 %} From 393c4c558c5a03257a7655ed1924424098bcd7cd Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 11 Jan 2016 16:42:12 -0500 Subject: [PATCH 23/24] pyflakes fix --- awx/main/tests/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/settings.py b/awx/main/tests/settings.py index bfcf79fe32..488533e8bf 100644 --- a/awx/main/tests/settings.py +++ b/awx/main/tests/settings.py @@ -2,7 +2,7 @@ # All Rights Reserved. from awx.main.tests.base import BaseTest -from awx.main.models import * #noqa +from awx.main.models import * # noqa from django.core.urlresolvers import reverse from django.test.utils import override_settings From 272e5829f28f35a38297f6509de83d72df167a02 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Tue, 12 Jan 2016 09:25:09 -0500 Subject: [PATCH 24/24] Setup instance during settings unit tests --- awx/main/tests/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/tests/settings.py b/awx/main/tests/settings.py index 488533e8bf..a727213454 100644 --- a/awx/main/tests/settings.py +++ b/awx/main/tests/settings.py @@ -43,6 +43,7 @@ class SettingsTest(BaseTest): def setUp(self): super(SettingsTest, self).setUp() + self.setup_instances() self.setup_users() def get_settings(self, expected_count=4):