| 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
| 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
| 3 | # |
| 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU Affero General Public License as published by |
| 6 | # the Free Software Foundation, either version 3 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU Affero General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU Affero General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | import copy |
| 18 | import logging |
| 19 | import os |
| 20 | import pkg_resources |
| 21 | |
| 22 | from configobj import ConfigObj, flatten_errors |
| 23 | from validate import Validator |
| 24 | |
| 25 | |
| 26 | _log = logging.getLogger(__name__) |
| 27 | |
| 28 | |
| 29 | CONFIG_SPEC_PATH = pkg_resources.resource_filename( |
| 30 | 'mediagoblin', 'config_spec.ini') |
| 31 | |
| 32 | |
| 33 | def _setup_defaults(config, config_path, extra_defaults=None): |
| 34 | """ |
| 35 | Setup DEFAULTS in a config object from an (absolute) config_path. |
| 36 | """ |
| 37 | extra_defaults = extra_defaults or {} |
| 38 | |
| 39 | config.setdefault('DEFAULT', {}) |
| 40 | config['DEFAULT']['here'] = os.path.dirname(config_path) |
| 41 | config['DEFAULT']['__file__'] = config_path |
| 42 | |
| 43 | for key, value in extra_defaults.items(): |
| 44 | config['DEFAULT'].setdefault(key, value) |
| 45 | |
| 46 | |
| 47 | def read_mediagoblin_config(config_path, config_spec_path=CONFIG_SPEC_PATH): |
| 48 | """ |
| 49 | Read a config object from config_path. |
| 50 | |
| 51 | Does automatic value transformation based on the config_spec. |
| 52 | Also provides %(__file__)s and %(here)s values of this file and |
| 53 | its directory respectively similar to paste deploy. |
| 54 | |
| 55 | Also reads for [plugins] section, appends all config_spec.ini |
| 56 | files from said plugins into the general config_spec specification. |
| 57 | |
| 58 | This function doesn't itself raise any exceptions if validation |
| 59 | fails, you'll have to do something |
| 60 | |
| 61 | Args: |
| 62 | - config_path: path to the config file |
| 63 | - config_spec_path: config file that provides defaults and value types |
| 64 | for validation / conversion. Defaults to mediagoblin/config_spec.ini |
| 65 | |
| 66 | Returns: |
| 67 | A tuple like: (config, validation_result) |
| 68 | ... where 'conf' is the parsed config object and 'validation_result' |
| 69 | is the information from the validation process. |
| 70 | """ |
| 71 | config_path = os.path.abspath(config_path) |
| 72 | |
| 73 | # PRE-READ of config file. This allows us to fetch the plugins so |
| 74 | # we can add their plugin specs to the general config_spec. |
| 75 | config = ConfigObj( |
| 76 | config_path, |
| 77 | interpolation="ConfigParser") |
| 78 | |
| 79 | # temporary bootstrap, just setup here and __file__... we'll do this again |
| 80 | _setup_defaults(config, config_path) |
| 81 | |
| 82 | # Now load the main config spec |
| 83 | config_spec = ConfigObj( |
| 84 | config_spec_path, |
| 85 | encoding="UTF8", list_values=False, _inspec=True) |
| 86 | |
| 87 | # HACK to get MediaGoblin running under Docker/Python 3. Without this line, |
| 88 | # `./bin/gmg dbupdate` fails as the configuration under 'DEFAULT' in |
| 89 | # config_spec still had %(here)s markers in it, when these should have been |
| 90 | # replaced with actual paths, resulting in |
| 91 | # "configobj.MissingInterpolationOption: missing option "here" in |
| 92 | # interpolation". This issue doesn't seem to appear when running on Guix, |
| 93 | # but adding this line also doesn't appear to cause problems on Guix. |
| 94 | _setup_defaults(config_spec, config_path) |
| 95 | |
| 96 | # Set up extra defaults that will be pushed into the rest of the |
| 97 | # configs. This is a combined extrapolation of defaults based on |
| 98 | mainconfig_defaults = copy.copy(config_spec.get("DEFAULT", {})) |
| 99 | mainconfig_defaults.update(config["DEFAULT"]) |
| 100 | |
| 101 | plugins = config.get("plugins", {}).keys() |
| 102 | plugin_configs = {} |
| 103 | |
| 104 | for plugin in plugins: |
| 105 | try: |
| 106 | plugin_config_spec_path = pkg_resources.resource_filename( |
| 107 | plugin, "config_spec.ini") |
| 108 | if not os.path.exists(plugin_config_spec_path): |
| 109 | continue |
| 110 | |
| 111 | plugin_config_spec = ConfigObj( |
| 112 | plugin_config_spec_path, |
| 113 | encoding="UTF8", list_values=False, _inspec=True) |
| 114 | _setup_defaults( |
| 115 | plugin_config_spec, config_path, mainconfig_defaults) |
| 116 | |
| 117 | if not "plugin_spec" in plugin_config_spec: |
| 118 | continue |
| 119 | |
| 120 | plugin_configs[plugin] = plugin_config_spec["plugin_spec"] |
| 121 | |
| 122 | except ImportError: |
| 123 | _log.warning( |
| 124 | "When setting up config section, could not import '%s'" % |
| 125 | plugin) |
| 126 | |
| 127 | # append the plugin specific sections of the config spec |
| 128 | config_spec["plugins"] = plugin_configs |
| 129 | |
| 130 | _setup_defaults(config_spec, config_path, mainconfig_defaults) |
| 131 | |
| 132 | config = ConfigObj( |
| 133 | config_path, |
| 134 | configspec=config_spec, |
| 135 | encoding="UTF8", |
| 136 | interpolation="ConfigParser") |
| 137 | |
| 138 | _setup_defaults(config, config_path, mainconfig_defaults) |
| 139 | |
| 140 | # For now the validator just works with the default functions, |
| 141 | # but in the future if we want to add additional validation/configuration |
| 142 | # functions we'd add them to validator.functions here. |
| 143 | # |
| 144 | # See also: |
| 145 | # http://www.voidspace.org.uk/python/validate.html#adding-functions |
| 146 | validator = Validator() |
| 147 | validation_result = config.validate(validator, preserve_errors=True) |
| 148 | |
| 149 | return config, validation_result |
| 150 | |
| 151 | |
| 152 | REPORT_HEADER = u"""\ |
| 153 | There were validation problems loading this config file: |
| 154 | -------------------------------------------------------- |
| 155 | """ |
| 156 | |
| 157 | |
| 158 | def generate_validation_report(config, validation_result): |
| 159 | """ |
| 160 | Generate a report if necessary of problems while validating. |
| 161 | |
| 162 | Returns: |
| 163 | Either a string describing for a user the problems validating |
| 164 | this config or None if there are no problems. |
| 165 | """ |
| 166 | report = [] |
| 167 | |
| 168 | # Organize the report |
| 169 | for entry in flatten_errors(config, validation_result): |
| 170 | # each entry is a tuple |
| 171 | section_list, key, error = entry |
| 172 | |
| 173 | if key is not None: |
| 174 | section_list.append(key) |
| 175 | else: |
| 176 | section_list.append(u'[missing section]') |
| 177 | |
| 178 | section_string = u':'.join(section_list) |
| 179 | |
| 180 | if error == False: |
| 181 | # We don't care about missing values for now. |
| 182 | continue |
| 183 | |
| 184 | report.append(u"%s = %s" % (section_string, error)) |
| 185 | |
| 186 | if report: |
| 187 | return REPORT_HEADER + u"\n".join(report) |
| 188 | else: |
| 189 | return None |