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