def get_fields(self): fields = super(SettingSingletonSerializer, self).get_fields() try: category_slug = self.context['view'].kwargs.get('category_slug', 'all') except (KeyError, AttributeError): category_slug = '' for key in settings_registry.get_registered_settings(category_slug=category_slug): if self.instance and not hasattr(self.instance, key): continue extra_kwargs = {} # Make LICENSE read-only here; LICENSE is only updated via /api/v2/config/ if key == 'LICENSE': extra_kwargs['read_only'] = True field = settings_registry.get_setting_field(key, mixin_class=SettingFieldMixin, for_user=bool(category_slug == 'user'), **extra_kwargs) fields[key] = field return fields
def perform_destroy(self, instance): settings_change_list = [] for setting in self.get_queryset().exclude(key='LICENSE'): if settings_registry.get_setting_field(setting.key).read_only: continue setting.delete() settings_change_list.append(setting.key) if settings_change_list and 'migrate_to_database_settings' not in sys.argv: handle_setting_changes.delay(settings_change_list) # When TOWER_URL_BASE is deleted from the API, reset it to the hostname # used to make the request as a default. if hasattr(instance, 'TOWER_URL_BASE'): url = '{}://{}'.format(self.request.scheme, self.request.get_host()) if settings.TOWER_URL_BASE != url: settings.TOWER_URL_BASE = url
def _discover_settings(self, registered_settings): if self.verbosity >= 1: self.stdout.write( self.style.HEADING( 'Discovering settings to be migrated and commented:')) # Determine which settings need to be commented/migrated. to_migrate = collections.OrderedDict() to_comment = collections.OrderedDict() patterns = self._get_settings_file_patterns() for name in registered_settings: comment_error, migrate_error = None, None files_to_comment = [] try: files_to_comment = self._check_if_needs_comment(patterns, name) except Exception as e: comment_error = 'Error commenting {0}: {1!r}'.format(name, e) if not self.skip_errors: raise CommandError(comment_error) if files_to_comment: to_comment[name] = files_to_comment migrate_value = empty if files_to_comment: migrate_value = self._check_if_needs_migration(name) if migrate_value is not empty: field = settings_registry.get_setting_field(name) assert not field.read_only try: data = field.to_representation(migrate_value) setting_value = field.run_validation(data) db_value = field.to_representation(setting_value) to_migrate[name] = db_value except Exception as e: to_comment.pop(name) migrate_error = 'Unable to assign value {0!r} to setting "{1}: {2!s}".'.format( migrate_value, name, e) if not self.skip_errors: raise CommandError(migrate_error) self._display_tbd(name, files_to_comment, migrate_value, comment_error, migrate_error) if self.verbosity == 1 and not to_migrate and not to_comment: self.stdout.write(' No settings found to migrate or comment!') return (to_migrate, to_comment)
def post(self, request, *args, **kwargs): defaults = dict() for key in settings_registry.get_registered_settings( category_slug='logging'): try: defaults[key] = settings_registry.get_setting_field( key).get_default() except serializers.SkipField: defaults[key] = None obj = type('Settings', (object, ), defaults)() serializer = self.get_serializer(obj, data=request.data) serializer.is_valid(raise_exception=True) # Special validation specific to logging test. errors = {} for key in ['LOG_AGGREGATOR_TYPE', 'LOG_AGGREGATOR_HOST']: if not request.data.get(key, ''): errors[key] = 'This field is required.' if errors: raise ValidationError(errors) if request.data.get('LOG_AGGREGATOR_PASSWORD', '').startswith('$encrypted$'): serializer.validated_data['LOG_AGGREGATOR_PASSWORD'] = getattr( settings, 'LOG_AGGREGATOR_PASSWORD', '') try: class MockSettings: pass mock_settings = MockSettings() for k, v in serializer.validated_data.items(): setattr(mock_settings, k, v) mock_settings.LOG_AGGREGATOR_LEVEL = 'DEBUG' if mock_settings.LOG_AGGREGATOR_PROTOCOL.upper() == 'UDP': UDPHandler.perform_test(mock_settings) return Response(status=status.HTTP_201_CREATED) else: BaseHTTPSHandler.perform_test(mock_settings) except LoggingConnectivityException as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_200_OK)
def _check_if_needs_migration(self, setting): # Check whether the current value differs from the default. default_value = settings.DEFAULTS_SNAPSHOT.get(setting, empty) if default_value is empty and setting != 'LICENSE': field = settings_registry.get_setting_field(setting, read_only=True) try: default_value = field.get_default() except SkipField: pass current_value = getattr(settings, setting, empty) if setting == 'CUSTOM_LOGIN_INFO' and current_value in {empty, ''}: local_settings_file = self._get_local_settings_file() try: if os.path.exists(local_settings_file): local_settings = json.load(open(local_settings_file)) current_value = local_settings.get('custom_login_info', '') except Exception as e: if not self.skip_errors: raise CommandError( 'Error reading custom login info from {0}: {1!r}'. format(local_settings_file, e)) if setting == 'CUSTOM_LOGO' and current_value in {empty, ''}: custom_logo_file = self._get_custom_logo_file() try: if os.path.exists(custom_logo_file): custom_logo_data = open(custom_logo_file).read() if custom_logo_data: current_value = 'data:image/png;base64,{}'.format( base64.b64encode(custom_logo_data)) else: current_value = '' except Exception as e: if not self.skip_errors: raise CommandError( 'Error reading custom logo from {0}: {1!r}'.format( custom_logo_file, e)) if current_value != default_value: if current_value is empty: current_value = None return current_value return empty
def get_object(self): settings_qs = self.get_queryset() registered_settings = settings_registry.get_registered_settings( category_slug=self.category_slug, ) all_settings = {} for setting in settings_qs: all_settings[setting.key] = setting.value for key in registered_settings: if key in all_settings or self.category_slug == 'changed': continue try: field = settings_registry.get_setting_field( key, for_user=bool(self.category_slug == 'user')) all_settings[key] = field.get_default() except serializers.SkipField: all_settings[key] = None all_settings[ 'user'] = self.request.user if self.category_slug == 'user' else None obj = type('Settings', (object, ), all_settings)() self.check_object_permissions(self.request, obj) return obj
def get_object(self): settings_qs = self.get_queryset() registered_settings = settings_registry.get_registered_settings( category_slug=self.category_slug, features_enabled=get_licensed_features(), slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[ get_request_version(self.request)]) all_settings = {} for setting in settings_qs: all_settings[setting.key] = setting.value for key in registered_settings: if key in all_settings or self.category_slug == 'changed': continue try: field = settings_registry.get_setting_field( key, for_user=bool(self.category_slug == 'user')) all_settings[key] = field.get_default() except serializers.SkipField: all_settings[key] = None all_settings[ 'user'] = self.request.user if self.category_slug == 'user' else None obj = type('Settings', (object, ), all_settings)() self.check_object_permissions(self.request, obj) return obj
def finalize_response(self, request, response, *args, **kwargs): """ Log warning for 400 requests. Add header with elapsed time. """ # # If the URL was rewritten, and we get a 404, we should entirely # replace the view in the request context with an ApiErrorView() # Without this change, there will be subtle differences in the BrowseableAPIRenderer # # These differences could provide contextual clues which would allow # anonymous users to determine if usernames were valid or not # (e.g., if an anonymous user visited `/api/v2/users/valid/`, and got a 404, # but also saw that the page heading said "User Detail", they might notice # that's a difference in behavior from a request to `/api/v2/users/not-valid/`, which # would show a page header of "Not Found"). Changing the view here # guarantees that the rendered response will look exactly like the response # when you visit a URL that has no matching URL paths in `awx.api.urls`. # if response.status_code == 404 and 'awx.named_url_rewritten' in request.environ: self.headers.pop('Allow', None) response = super(APIView, self).finalize_response(request, response, *args, **kwargs) view = ApiErrorView() setattr(view, 'request', request) response.renderer_context['view'] = view return response if response.status_code >= 400: msg_data = { 'status_code': response.status_code, 'user_name': request.user, 'url_path': request.path, 'remote_addr': request.META.get('REMOTE_ADDR', None), } if type(response.data) is dict: msg_data['error'] = response.data.get('error', response.status_text) elif type(response.data) is list: msg_data['error'] = ", ".join( list( map(lambda x: x.get('error', response.status_text), response.data))) else: msg_data['error'] = response.status_text try: status_msg = getattr( settings, 'API_400_ERROR_LOG_FORMAT').format(**msg_data) except Exception as e: if getattr(settings, 'API_400_ERROR_LOG_FORMAT', None): logger.error( "Unable to format API_400_ERROR_LOG_FORMAT setting, defaulting log message: {}" .format(e)) status_msg = settings_registry.get_setting_field( 'API_400_ERROR_LOG_FORMAT').get_default().format( **msg_data) if hasattr(self, '__init_request_error__'): response = self.handle_exception(self.__init_request_error__) if response.status_code == 401: response.data['detail'] += _( ' To establish a login session, visit') + ' /api/login/.' logger.info(status_msg) else: logger.warning(status_msg) response = super(APIView, self).finalize_response(request, response, *args, **kwargs) time_started = getattr(self, 'time_started', None) response['X-API-Product-Version'] = get_awx_version() response['X-API-Product-Name'] = server_product_name() response['X-API-Node'] = settings.CLUSTER_HOST_ID if time_started: time_elapsed = time.time() - self.time_started response['X-API-Time'] = '%0.3fs' % time_elapsed if getattr(settings, 'SQL_DEBUG', False): queries_before = getattr(self, 'queries_before', 0) q_times = [ float(q['time']) for q in connection.queries[queries_before:] ] response['X-API-Query-Count'] = len(q_times) response['X-API-Query-Time'] = '%0.3fs' % sum(q_times) if getattr(self, 'deprecated', False): response[ 'Warning'] = '299 awx "This resource has been deprecated and will be removed in a future release."' # noqa return response
def _migrate_settings(self, registered_settings): patterns = self._get_settings_file_patterns() # Determine which settings need to be commented/migrated. if self.verbosity >= 1: self.stdout.write( self.style.HEADING( 'Discovering settings to be migrated and commented:')) to_migrate = collections.OrderedDict() to_comment = collections.OrderedDict() for name in registered_settings: comment_error, migrate_error = None, None files_to_comment = [] try: files_to_comment = self._check_if_needs_comment(patterns, name) except Exception as e: comment_error = 'Error commenting {0}: {1!r}'.format(name, e) if not self.skip_errors: raise CommandError(comment_error) if files_to_comment: to_comment[name] = files_to_comment migrate_value = empty if files_to_comment: migrate_value = self._check_if_needs_migration(name) if migrate_value is not empty: field = settings_registry.get_setting_field(name) assert not field.read_only try: data = field.to_representation(migrate_value) setting_value = field.run_validation(data) db_value = field.to_representation(setting_value) to_migrate[name] = db_value except Exception as e: to_comment.pop(name) migrate_error = 'Unable to assign value {0!r} to setting "{1}: {2!s}".'.format( migrate_value, name, e) if not self.skip_errors: raise CommandError(migrate_error) self._display_tbd(name, files_to_comment, migrate_value, comment_error, migrate_error) if self.verbosity == 1 and not to_migrate and not to_comment: self.stdout.write(' No settings found to migrate or comment!') # Now migrate those settings to the database. if self.verbosity >= 1: if self.dry_run: self.stdout.write( self.style.HEADING( 'Migrating settings to database (dry-run):')) else: self.stdout.write( self.style.HEADING('Migrating settings to database:')) if not to_migrate: self.stdout.write(' No settings to migrate!') for name, db_value in to_migrate.items(): display_value = json.dumps(db_value, indent=4) setting = Setting.objects.filter( key=name, user__isnull=True).order_by('pk').first() action = 'No Change' if not setting: action = 'Migrated' if not self.dry_run: Setting.objects.create(key=name, user=None, value=db_value) elif setting.value != db_value or type( setting.value) != type(db_value): action = 'Updated' if not self.dry_run: setting.value = db_value setting.save(update_fields=['value']) self._display_migrate(name, action, display_value) # Now comment settings in settings files. if self.verbosity >= 1: if bool(self.dry_run or self.no_comment): self.stdout.write( self.style.HEADING( 'Commenting settings in files (dry-run):')) else: self.stdout.write( self.style.HEADING('Commenting settings in files:')) if not to_comment: self.stdout.write(' No settings to comment!') if to_comment: to_comment_patterns = [] license_file_to_comment = None local_settings_file_to_comment = None custom_logo_file_to_comment = None for files_to_comment in to_comment.values(): for file_to_comment in files_to_comment: if file_to_comment == self._get_license_file(): license_file_to_comment = file_to_comment elif file_to_comment == self._get_local_settings_file(): local_settings_file_to_comment = file_to_comment elif file_to_comment == self._get_custom_logo_file(): custom_logo_file_to_comment = file_to_comment elif file_to_comment not in to_comment_patterns: to_comment_patterns.append(file_to_comment) # Run once in dry-run mode to catch any errors from updating the files. diffs = comment_assignments(to_comment_patterns, to_comment.keys(), dry_run=True, backup_suffix=self.backup_suffix) # Then, if really updating, run again. if not self.dry_run and not self.no_comment: diffs = comment_assignments(to_comment_patterns, to_comment.keys(), dry_run=False, backup_suffix=self.backup_suffix) if license_file_to_comment: diffs.extend(self._comment_license_file(dry_run=False)) if local_settings_file_to_comment: diffs.extend( self._comment_local_settings_file(dry_run=False)) if custom_logo_file_to_comment: diffs.extend(self._comment_custom_logo_file(dry_run=False)) self._display_comment(diffs)