cfg: Drop the default configuration file

- The configuration module defaults to values in the production configuration
file.

- If the file is found, it is read and the read values overwrite the defaults.
If the file is not found, no error is raised. This allows us to not ship the
configuration file. User may create the configuration if they want to change the
defaults. This eases upgrades when configuration is edited. This also make
FreedomBox robust to deployments where /etc/ is not populated by default such as
OSTree. It is also a good practice for daemons as followed by the likes of
systemd.

- If the file partly populated only the values read override the defaults and
the remaining values don't change. This allows the user to write simpler
configuration file.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2020-06-23 17:36:41 -07:00 committed by James Valleroy
parent 64b1c21fe0
commit 4263f9e2c8
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
7 changed files with 67 additions and 103 deletions

View File

@ -55,7 +55,7 @@ def fixture_load_cfg():
cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config' cfg_file = test_data_dir / 'etc' / 'plinth' / 'plinth.config'
cfg.read(str(cfg_file), str(root_dir)) cfg.read(str(cfg_file), str(root_dir))
yield cfg yield cfg
cfg.read() cfg.read(str(cfg_file), str(root_dir))
@pytest.fixture(name='develop_mode') @pytest.fixture(name='develop_mode')

View File

@ -1,36 +0,0 @@
[Path]
# directory locations
file_root = /usr/share/plinth
config_dir = /etc/plinth
data_dir = /var/lib/plinth
server_dir = /plinth
actions_dir = /usr/share/plinth/actions
doc_dir = /usr/share/freedombox
custom_static_dir = /var/www/plinth/custom/static
# file locations
store_file = %(data_dir)s/plinth.sqlite3
[Network]
host = 127.0.0.1
port = 8000
# Enable the following only if Plinth is behind a proxy server. The
# proxy server should properly clean and the following HTTP headers:
# X-Forwarded-For
# X-Forwarded-Host
# X-Forwarded-Proto
# If you enable these unnecessarily, this will lead to serious security
# problems. For more information, see
# https://docs.djangoproject.com/en/1.7/ref/settings/
#
# These are enabled by default in Plinth because the default
# configuration allows only connections from localhost
#
# Leave the values blank to disable
use_x_forwarded_for = True
use_x_forwarded_host = True
secure_proxy_ssl_header = HTTP_X_FORWARDED_PROTO
[Misc]
box_name = FreedomBox

View File

@ -10,3 +10,4 @@ rm_conffile /etc/plinth/modules-enabled/udiskie 0.39.0~
rm_conffile /etc/plinth/modules-enabled/restore 20.1~ rm_conffile /etc/plinth/modules-enabled/restore 20.1~
rm_conffile /etc/plinth/modules-enabled/repro 20.1~ rm_conffile /etc/plinth/modules-enabled/repro 20.1~
rm_conffile /etc/apt/preferences.d/50freedombox3.pref 20.5~ rm_conffile /etc/apt/preferences.d/50freedombox3.pref 20.5~
rm_conffile /etc/plinth/plinth.config 20.12~

View File

@ -1,4 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration parser and default values for configuration options.
"""
import configparser import configparser
import logging import logging
@ -6,28 +9,46 @@ import os
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
box_name = None # [Path] section
root = None root = None
file_root = None file_root = '/usr/share/plinth'
config_dir = None config_dir = '/etc/plinth'
data_dir = None data_dir = '/var/lib/plinth'
custom_static_dir = None custom_static_dir = '/var/www/plinth/custom/static'
store_file = None store_file = data_dir + '/plinth.sqlite3'
actions_dir = None actions_dir = '/usr/share/plinth/actions'
doc_dir = None doc_dir = '/usr/share/freedombox'
host = None server_dir = '/plinth'
port = None
use_x_forwarded_for = False # [Network] section
use_x_forwarded_host = False host = '127.0.0.1'
secure_proxy_ssl_header = None port = 8000
# Enable the following only if Plinth is behind a proxy server. The
# proxy server should properly clean and the following HTTP headers:
# X-Forwarded-For
# X-Forwarded-Host
# X-Forwarded-Proto
# If you enable these unnecessarily, this will lead to serious security
# problems. For more information, see
# https://docs.djangoproject.com/en/1.7/ref/settings/
#
# These are enabled by default in FreedomBox because the default
# configuration allows only connections from localhost
#
# Leave the values blank to disable
use_x_forwarded_for = True
use_x_forwarded_host = True
secure_proxy_ssl_header = 'HTTP_X_FORWARDED_PROTO'
# [Misc] section
box_name = 'FreedomBox'
# Other globals
develop = False develop = False
server_dir = '/'
config_file = None config_file = None
DEFAULT_CONFIG_FILE = '/etc/plinth/plinth.config'
DEFAULT_ROOT = '/'
def get_fallback_config_paths(): def get_fallback_config_paths():
"""Get config paths of the current source code folder""" """Get config paths of the current source code folder"""
@ -54,8 +75,8 @@ def read(config_path=None, root_directory=None):
config_path, root_directory = get_config_paths() config_path, root_directory = get_config_paths()
if not os.path.isfile(config_path): if not os.path.isfile(config_path):
msg = 'No plinth.config file could be found on path: %s' % config_path # Ignore missing configuration files
raise FileNotFoundError(msg) return
global config_file # pylint: disable-msg=invalid-name,global-statement global config_file # pylint: disable-msg=invalid-name,global-statement
config_file = config_path config_file = config_path
@ -87,9 +108,8 @@ def read(config_path=None, root_directory=None):
try: try:
value = parser.get(section, name) value = parser.get(section, name)
except (configparser.NoSectionError, configparser.NoOptionError): except (configparser.NoSectionError, configparser.NoOptionError):
logger.error('Configuration does not contain option: %s.%s', # Use default values for any missing keys in configuration
section, name) continue
raise
else: else:
if datatype == 'int': if datatype == 'int':
value = int(value) value = int(value)

View File

@ -1,5 +1,5 @@
[Misc] [Misc]
box_name = FreedomBox box_name = FreedomBoxTestMissingOptions
[Path] [Path]

View File

@ -1,2 +1,2 @@
[Misc] [Misc]
box_name = FreedomBox box_name = FreedomBoxTestMissingSections

View File

@ -6,6 +6,7 @@ Test module for configuration module.
import configparser import configparser
import logging import logging
import os import os
from unittest.mock import patch
import pytest import pytest
@ -23,54 +24,33 @@ logging.disable(logging.CRITICAL)
pytestmark = pytest.mark.usefixtures('load_cfg') pytestmark = pytest.mark.usefixtures('load_cfg')
@pytest.fixture(name='test_config_file') def test_read_default_config_file():
def fixture_test_config_file(load_cfg):
"""Test fixture to return the configuration file path"""
return cfg.get_config_paths()[0]
@pytest.fixture(name='test_config_dir')
def fixture_test_config_dir(load_cfg):
"""Test fixture to return the configuration file directory."""
return cfg.get_config_paths()[1]
def test_read_default_config_file(test_config_dir, test_config_file):
"""Verify that the default config file can be read correctly.""" """Verify that the default config file can be read correctly."""
config_file, config_dir = cfg.get_fallback_config_paths()
# Read the plinth.config file directly # Read the plinth.config file directly
parser = configparser.ConfigParser(defaults={'root': test_config_dir}) parser = configparser.ConfigParser(defaults={'root': config_dir})
parser.read(test_config_file) parser.read(config_file)
# Read the plinth.config file via the cfg module # Read the plinth.config file via the cfg module
cfg.read(test_config_file, test_config_dir) cfg.read(config_file, config_dir)
# Compare the two results # Compare the two results
compare_configurations(parser) compare_configurations(parser)
def test_read_primary_config_file(): @patch('plinth.cfg.get_config_paths')
def test_read_primary_config_file(get_config_paths):
"""Verify that the primary config file is used by default.""" """Verify that the primary config file is used by default."""
original_config_path = cfg.DEFAULT_CONFIG_FILE
original_root_directory = cfg.DEFAULT_ROOT
expected_config_path = CONFIG_FILE_WITH_MISSING_OPTIONS expected_config_path = CONFIG_FILE_WITH_MISSING_OPTIONS
root_directory = 'x-default-root' root_directory = 'x-default-root'
expected_root_directory = os.path.realpath(root_directory) expected_root_directory = os.path.realpath(root_directory)
get_config_paths.return_value = (expected_config_path, root_directory)
try: cfg.read()
cfg.DEFAULT_CONFIG_FILE = expected_config_path
cfg.DEFAULT_ROOT = root_directory assert cfg.config_file == expected_config_path
# reading the config file will fail, but still cfg.root and assert cfg.root == expected_root_directory
# cfg.config_file will be set for parsing the config file
try:
cfg.read()
except configparser.NoOptionError:
pass
assert cfg.config_file == expected_config_path
assert cfg.root == expected_root_directory
finally:
cfg.DEFAULT_CONFIG_FILE = original_config_path
cfg.DEFAULT_ROOT = original_root_directory
def test_read_fallback_config_file(): def test_read_fallback_config_file():
@ -86,20 +66,19 @@ def test_read_fallback_config_file():
def test_read_missing_config_file(): def test_read_missing_config_file():
"""Verify that an exception is raised when there's no config file.""" """Verify that an exception is raised when there's no config file."""
with pytest.raises(FileNotFoundError): cfg.read('x-non-existant-file', 'x-root-directory')
cfg.read('x-non-existant-file', 'x-root-directory')
def test_read_config_file_with_missing_sections(test_config_dir): def test_read_config_file_with_missing_sections():
"""Verify that missing configuration sections can be detected.""" """Verify that missing configuration sections can be detected."""
with pytest.raises(configparser.NoSectionError): cfg.read(CONFIG_FILE_WITH_MISSING_SECTIONS, TEST_CONFIG_DIR)
cfg.read(CONFIG_FILE_WITH_MISSING_SECTIONS, test_config_dir) assert cfg.box_name == 'FreedomBoxTestMissingSections'
def test_read_config_file_with_missing_options(test_config_dir): def test_read_config_file_with_missing_options():
"""Verify that missing configuration options can be detected.""" """Verify that missing configuration options can be detected."""
with pytest.raises(configparser.NoOptionError): cfg.read(CONFIG_FILE_WITH_MISSING_OPTIONS, TEST_CONFIG_DIR)
cfg.read(CONFIG_FILE_WITH_MISSING_OPTIONS, test_config_dir) assert cfg.box_name == 'FreedomBoxTestMissingOptions'
def compare_configurations(parser): def compare_configurations(parser):