From 273181e894ad06aeba2e552b0dafbab1b1fc1514 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 14 Dec 2015 15:09:10 -0500 Subject: [PATCH] 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):