1# Copyright 2007 Google Inc. 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16"""Configuration classes for nss_cache module. 17 18These classes perform command line and file-based configuration loading 19and parsing for the nss_cache module. 20""" 21 22__author__ = 'vasilios@google.com (Vasilios Hoffman)' 23 24from configparser import ConfigParser 25import logging 26import re 27 28# known nss map types. 29MAP_PASSWORD = 'passwd' 30MAP_GROUP = 'group' 31MAP_SHADOW = 'shadow' 32MAP_NETGROUP = 'netgroup' 33MAP_AUTOMOUNT = 'automount' 34MAP_SSHKEY = 'sshkey' 35 36# accepted commands. 37CMD_HELP = 'help' 38CMD_REPAIR = 'repair' 39CMD_STATUS = 'status' 40CMD_UPDATE = 'update' 41CMD_VERIFY = 'verify' 42 43# default file locations 44FILE_NSSWITCH = '/etc/nsswitch.conf' 45 46# update method types 47UPDATER_FILE = 'file' 48UPDATER_MAP = 'map' 49 50 51class Config(object): 52 """Data container for runtime configuration information. 53 54 Global information such as the command, configured maps, etc, are 55 loaded into this object. Source and cache configuration 56 information is also stored here. 57 58 However since each map can be configured against a different 59 source and cache implementation we have to store per-map 60 configuration information. This is done via a Config().options 61 dictionary with the map name as the key and a MapOptions object as 62 the value. 63 """ 64 65 # default config file. 66 NSSCACHE_CONFIG = '/usr/local/etc/nsscache.conf' 67 68 # known config file option names 69 OPT_SOURCE = 'source' 70 OPT_CACHE = 'cache' 71 OPT_MAPS = 'maps' 72 OPT_LOCKFILE = 'lockfile' 73 OPT_TIMESTAMP_DIR = 'timestamp_dir' 74 75 def __init__(self, env): 76 """Initialize defaults for data we hold. 77 78 Args: 79 env: dictionary of environment variables (typically os.environ) 80 """ 81 # override constants based on ENV vars 82 if 'NSSCACHE_CONFIG' in env: 83 self.config_file = env['NSSCACHE_CONFIG'] 84 else: 85 self.config_file = self.NSSCACHE_CONFIG 86 87 # default values 88 self.command = None 89 self.help_command = None 90 self.maps = [] 91 self.options = {} 92 self.lockfile = None 93 self.timestamp_dir = None 94 self.log = logging.getLogger(__name__) 95 96 def __repr__(self): 97 """String representation of this object.""" 98 # self.options is of variable length so we are forced to do 99 # some fugly concatenation here to print our config in a 100 # readable fashion. 101 string = (('<Config:\n\tcommand=%r\n\thelp_command=%r\n\tmaps=%r' 102 '\n\tlockfile=%r\n\ttimestamp_dir=%s') % 103 (self.command, self.help_command, self.maps, self.lockfile, 104 self.timestamp_dir)) 105 for key in self.options: 106 string = '%s\n\t%s=%r' % (string, key, self.options[key]) 107 return '%s\n>' % string 108 109 110class MapOptions(object): 111 """Data container for individual maps. 112 113 Each map is configured against a source and cache. The dictionaries 114 used by the source and cache implementations are stored here. 115 """ 116 117 def __init__(self): 118 """Initialize default values.""" 119 self.cache = {} 120 self.source = {} 121 122 def __repr__(self): 123 """String representation of this object.""" 124 return '<MapOptions cache=%r source=%r>' % (self.cache, self.source) 125 126 127# 128# Configuration itself is done through module-level methods. These 129# methods are below. 130# 131def LoadConfig(configuration): 132 """Load the on-disk configuration file and merge it into config. 133 134 Args: 135 configuration: a config.Config object 136 137 Raises: 138 error.NoConfigFound: no configuration file was found 139 """ 140 parser = ConfigParser() 141 142 # load config file 143 configuration.log.debug('Attempting to parse configuration file: %s', 144 configuration.config_file) 145 parser.read(configuration.config_file) 146 147 # these are required, and used as defaults for each section 148 default = 'DEFAULT' 149 default_source = FixValue(parser.get(default, Config.OPT_SOURCE)) 150 default_cache = FixValue(parser.get(default, Config.OPT_CACHE)) 151 152 # this is also required, but global only 153 # TODO(v): make this default to /var/lib/nsscache before next release 154 configuration.timestamp_dir = FixValue( 155 parser.get(default, Config.OPT_TIMESTAMP_DIR)) 156 157 # optional defaults 158 if parser.has_option(default, Config.OPT_LOCKFILE): 159 configuration.lockfile = FixValue( 160 parser.get(default, Config.OPT_LOCKFILE)) 161 162 if not configuration.maps: 163 # command line did not override 164 maplist = FixValue(parser.get(default, Config.OPT_MAPS)) 165 # special case for empty string, or split(',') will return a 166 # non-empty list 167 if maplist: 168 configuration.maps = [m.strip() for m in maplist.split(',')] 169 else: 170 configuration.maps = [] 171 172 # build per-map source and cache dictionaries and store 173 # them in MapOptions() objects. 174 for map_name in configuration.maps: 175 map_options = MapOptions() 176 177 source = default_source 178 cache = default_cache 179 180 # override source and cache if necessary 181 if parser.has_section(map_name): 182 if parser.has_option(map_name, Config.OPT_SOURCE): 183 source = FixValue(parser.get(map_name, Config.OPT_SOURCE)) 184 if parser.has_option(map_name, Config.OPT_CACHE): 185 cache = FixValue(parser.get(map_name, Config.OPT_CACHE)) 186 187 # load source and cache default options 188 map_options.source = Options(parser.items(default), source) 189 map_options.cache = Options(parser.items(default), cache) 190 191 # overide with any section-specific options 192 if parser.has_section(map_name): 193 options = Options(parser.items(map_name), source) 194 map_options.source.update(options) 195 options = Options(parser.items(map_name), cache) 196 map_options.cache.update(options) 197 198 # used to instantiate the specific cache/source 199 map_options.source['name'] = source 200 map_options.cache['name'] = cache 201 202 # save final MapOptions() in the parent config object 203 configuration.options[map_name] = map_options 204 205 configuration.log.info('Configured maps are: %s', 206 ', '.join(configuration.maps)) 207 208 configuration.log.debug('loaded configuration: %r', configuration) 209 210 211def Options(items, name): 212 """Returns a dict of options specific to an implementation. 213 214 This is used to retrieve a dict of options for a given 215 implementation. We look for configuration options in the form of 216 name_option and ignore the rest. 217 218 Args: 219 items: [('key1', 'value1'), ('key2, 'value2'), ...] 220 name: 'foo' 221 Returns: 222 dictionary of option:value pairs 223 """ 224 options = {} 225 option_re = re.compile(r'^%s_(.+)' % name) 226 for item in items: 227 match = option_re.match(item[0]) 228 if match: 229 options[match.group(1)] = FixValue(item[1]) 230 231 return options 232 233 234def FixValue(value): 235 """Helper function to fix values loaded from a config file. 236 237 Currently we strip bracketed quotes as well as convert numbers to 238 floats for configuration parameters expecting numerical data types. 239 240 Args: 241 value: value to be converted 242 243 Returns: 244 fixed value 245 """ 246 # Strip quotes if necessary. 247 if ((value.startswith('"') and value.endswith('"')) or 248 (value.startswith('\'') and value.endswith('\''))): 249 value = value[1:-1] 250 251 # Convert to float if necessary. Python converts between floats and ints 252 # on demand, but won't attempt string conversion automagically. 253 # 254 # Caveat: '1' becomes 1.0, however python treats it reliably as 1 255 # for native comparisons to int types, and if an int type is needed 256 # explicitly the caller will have to cast. This is simplist. 257 try: 258 value = int(value) 259 except ValueError: 260 try: 261 value = float(value) 262 except ValueError: 263 return value 264 265 return value 266 267 268def ParseNSSwitchConf(nsswitch_filename): 269 """Parse /etc/nsswitch.conf and return the sources for each map. 270 271 Args: 272 nsswitch_filename: Full path to an nsswitch.conf to parse. See manpage 273 nsswitch.conf(5) for full details on the format expected. 274 275 Returns: 276 a dictionary keyed by map names and containing a list of sources 277 for each map. 278 """ 279 with open(nsswitch_filename, 'r') as nsswitch_file: 280 281 nsswitch = {} 282 283 map_re = re.compile(r'^([a-z]+): *(.*)$') 284 for line in nsswitch_file: 285 match = map_re.match(line) 286 if match: 287 sources = match.group(2).split() 288 nsswitch[match.group(1)] = sources 289 290 return nsswitch 291 292 293def VerifyConfiguration(conf, nsswitch_filename=FILE_NSSWITCH): 294 """Verify that the system configuration matches the nsscache configuration. 295 296 Checks that NSS configuration has the cache listed for each map that 297 is configured in the nsscache configuration, i.e. that the system is 298 configured to use the maps we are building. 299 300 Args: 301 conf: a Configuration 302 nsswitch_filename: optionally the name of the file to parse 303 Returns: 304 (warnings, errors) a tuple counting the number of warnings and 305 errors detected 306 """ 307 (warnings, errors) = (0, 0) 308 if not conf.maps: 309 logging.error('No maps are configured.') 310 errors += 1 311 312 # Verify that at least one supported module is configured in nsswitch.conf. 313 nsswitch = ParseNSSwitchConf(nsswitch_filename) 314 for configured_map in conf.maps: 315 if configured_map == 'sshkey': 316 continue 317 if conf.options[configured_map].cache['name'] == 'nssdb': 318 nss_module_name = 'db' 319 if conf.options[configured_map].cache['name'] == 'files': 320 nss_module_name = 'files' 321 if ('cache_filename_suffix' in conf.options[configured_map].cache 322 and 323 conf.options[configured_map].cache['cache_filename_suffix'] 324 == 'cache'): 325 # We are configured for libnss-cache for this map. 326 nss_module_name = 'cache' 327 else: 328 # TODO(jaq): default due to hysterical raisins 329 nss_module_name = 'db' 330 331 if nss_module_name not in nsswitch[configured_map]: 332 logging.warning(('nsscache is configured to build maps for %r, ' 333 'but NSS is not configured (in %r) to use it'), 334 configured_map, nsswitch_filename) 335 warnings += 1 336 return (warnings, errors) 337