1# Copyright 2010 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"""Update class, used for manipulating source and cache data.
17
18These update classes are based around file synchronization rather than
19map synchronization.
20
21These classes contains all the business logic for updating cache objects.
22They also contain the code for reading, writing, and updating timestamps.
23"""
24
25__author__ = (
26    'jaq@google.com (Jamie Wilkinson)',
27    'vasilios@google.com (V Hoffman)',
28    'blaedd@google.com (David MacKinnon)',
29)
30
31import errno
32import os
33import tempfile
34import time
35
36from nss_cache import error
37from nss_cache.caches import cache_factory
38from nss_cache.update import updater
39
40
41class FileMapUpdater(updater.Updater):
42    """Updates simple map files like passwd, group, shadow, and netgroup."""
43
44    def UpdateCacheFromSource(self,
45                              cache,
46                              source,
47                              incremental=False,
48                              force_write=False,
49                              location=None):
50        """Update a single cache file, from a given source.
51
52        Args:
53          cache: A nss_cache.caches.Cache object.
54          source: A nss_cache.sources.Source object.
55          incremental: We ignore this.
56          force_write: A boolean flag forcing empty map updates when False,
57            defaults to False.
58          location: The optional location in the source of this map used by
59            automount to specify which automount map to get, defaults to None.
60
61        Returns:
62          An int indicating the success of an update (0 == good, fail otherwise).
63        """
64        return_val = 0
65
66        cache_filename = cache.GetCacheFilename()
67        if cache_filename is not None:
68            new_file_fd, new_file = tempfile.mkstemp(
69                dir=os.path.dirname(cache_filename),
70                prefix=os.path.basename(cache_filename),
71                suffix='.nsscache.tmp')
72        else:
73            raise error.CacheInvalid('Cache has no filename.')
74
75        self.log.debug('temp source filename: %s', new_file)
76        try:
77            # Writes the source to new_file.
78            # Current file is passed in to allow the source to do partial diffs.
79            # TODO(jaq): refactor this to pass in the whole cache, so that the source
80            # can decide how to reduce downloads, c.f. last-modify-timestamp for ldap.
81            source.GetFile(self.map_name,
82                           new_file,
83                           current_file=cache.GetCacheFilename(),
84                           location=location)
85            os.lseek(new_file_fd, 0, os.SEEK_SET)
86            # TODO(jaq): this sucks.
87            source_cache = cache_factory.Create(self.cache_options,
88                                                self.map_name)
89            source_map = source_cache.GetMap(new_file)
90
91            # Update the cache from the new file.
92            return_val += self._FullUpdateFromFile(cache, source_map,
93                                                   force_write)
94        finally:
95            try:
96                os.unlink(new_file)
97            except OSError as e:
98                # If we're using zsync source, it already renames the file for us.
99                if e.errno != errno.ENOENT:
100                    raise
101
102        return return_val
103
104    def _FullUpdateFromFile(self, cache, source_map, force_write=False):
105        """Write a new map into the provided cache (overwrites).
106
107        Args:
108          cache: A nss_cache.caches.Cache object.
109          source_map: The map whose contents we're replacing the cache with, that is
110            used for verification.
111          force_write: A boolean flag forcing empty map updates when False,
112            defaults to False.
113
114        Returns:
115          0 if succesful, non-zero indicating number of failures otherwise.
116
117        Raises:
118          EmptyMap: Update is an empty map, not raised if force_write=True.
119          InvalidMap:
120        """
121        return_val = 0
122
123        for entry in source_map:
124            if not entry.Verify():
125                raise error.InvalidMap('Map is not valid. Aborting')
126
127        if len(source_map) == 0 and not force_write:
128            raise error.EmptyMap(
129                'Source map empty during full update, aborting. '
130                'Use --force-write to override.')
131
132        return_val += cache.WriteMap(map_data=source_map)
133
134        # We did an update, write our timestamps unless there is an error.
135        if return_val == 0:
136            mtime = os.stat(cache.GetCacheFilename()).st_mtime
137            self.log.debug('Cache filename %s has mtime %d',
138                           cache.GetCacheFilename(), mtime)
139            self.WriteModifyTimestamp(mtime)
140            self.WriteUpdateTimestamp()
141
142        return return_val
143
144
145class FileAutomountUpdater(updater.Updater):
146    """Update an automount map.
147
148    Automount maps are a unique case.  They are not a single set of map entries,
149    they are a set of sets.  Updating automount maps require fetching the list
150    of maps and updating each map as well as the list of maps.
151
152    This class is written to re-use the individual update code in the
153    FileMapUpdater class.
154    """
155
156    # automount-specific options
157    OPT_LOCAL_MASTER = 'local_automount_master'
158
159    def __init__(self,
160                 map_name,
161                 timestamp_dir,
162                 cache_options,
163                 automount_mountpoint=None):
164        """Initialize automount-specific updater options.
165
166        Args:
167          map_name: A string representing the type of the map we are an Updater for.
168          timestamp_dir: A string with the directory containing our timestamp files.
169          cache_options: A dict containing the options for any caches we create.
170          automount_mountpoint: An optional string containing automount path info.
171        """
172        updater.Updater.__init__(self, map_name, timestamp_dir, cache_options,
173                                 automount_mountpoint)
174        self.local_master = False
175        if self.OPT_LOCAL_MASTER in cache_options:
176            if cache_options[self.OPT_LOCAL_MASTER] == 'yes':
177                self.local_master = True
178
179    def UpdateFromSource(self, source, incremental=False, force_write=False):
180        """Update the automount master map, and every map it points to.
181
182        We fetch a full copy of the master map everytime, and then use the
183        FileMapUpdater to write each map the master map points to, as well
184        as the master map itself.
185
186        During this process, the master map will be modified.  It starts
187        out pointing to other maps in the source, but when written it needs
188        to point to other maps in the cache instead.  For example, using ldap we
189        store this data in ldap:
190
191        map_entry.key = /auto
192        map_entry.location = ou=auto.auto,ou=automounts,dc=example,dc=com
193
194        We need to go back to ldap get the map in ou=auto.auto, but when it comes
195        time to write the master map to (for example) a file, we need to write
196        out the /etc/auto.master file with:
197
198        map_entry.key = /auto
199        map_entry.location = /etc/auto.auto
200
201        This is annoying :)  Since the keys are fixed, namely /auto is a mountpoint
202        that isn't going to change format, we expect each Cache implementation that
203        supports automount maps to support a GetMapLocation() method which returns
204        the correct cache location from the key.
205
206        Args:
207          source: An nss_cache.sources.Source object.
208          incremental: Not used by this class
209          force_write: A boolean flag forcing empty map updates when False,
210            defaults to False.
211
212        Returns:
213          An int indicating success of update (0 == good, fail otherwise).
214        """
215        return_val = 0
216
217        try:
218            if not self.local_master:
219                self.log.info('Retrieving automount master map.')
220                master_file = source.GetAutomountMasterFile(
221                    os.path.join(self.cache_options['dir'], 'auto.master'))
222            master_cache = cache_factory.Create(self.cache_options,
223                                                self.map_name, None)
224            master_map = master_cache.GetMap()
225        except error.CacheNotFound:
226            return 1
227
228        if self.local_master:
229            self.log.info('Using local master map to determine maps to update.')
230            # we need the local map to determine which of the other maps to update
231            cache = cache_factory.Create(self.cache_options,
232                                         self.map_name,
233                                         automount_mountpoint=None)
234            try:
235                local_master = cache.GetMap()
236            except error.CacheNotFound:
237                self.log.warning('Local master map specified but no map found! '
238                                 'No maps will update.')
239                return return_val + 1
240
241        # update specific maps, e.g. auto.home and auto.auto
242        for map_entry in master_map:
243            source_location = os.path.basename(map_entry.location)
244            mountpoint = map_entry.key  # e.g. /auto mountpoint
245            self.log.debug('Looking at mountpoint %s', mountpoint)
246
247            # create the cache to update
248            cache = cache_factory.Create(self.cache_options,
249                                         self.map_name,
250                                         automount_mountpoint=mountpoint)
251
252            # update the master map with the location of the map in the cache
253            # e.g. /etc/auto.auto replaces ou=auto.auto
254            map_entry.location = cache.GetMapLocation()
255            self.log.debug('Map location: %s', map_entry.location)
256
257            # if configured to use the local master map, skip any not defined there
258            if self.local_master:
259                if map_entry not in local_master:
260                    self.log.info('Skipping entry %s, not in map %s', map_entry,
261                                  local_master)
262                    continue
263            self.log.info('Updating mountpoint %s', map_entry.key)
264            # update this map (e.g. /etc/auto.auto)
265            update_obj = FileMapUpdater(self.map_name,
266                                        self.timestamp_dir,
267                                        self.cache_options,
268                                        automount_mountpoint=mountpoint)
269            return_val += update_obj.UpdateCacheFromSource(
270                cache, source, False, force_write, source_location)
271        # with sub-maps updated, write modified master map to disk if configured to
272        if not self.local_master:
273            # automount_mountpoint=None defaults to master
274            cache = cache_factory.Create(self.cache_options,
275                                         self.map_name,
276                                         automount_mountpoint=None)
277            update_obj = FileMapUpdater(self.map_name, self.timestamp_dir,
278                                        self.cache_options)
279            return_val += update_obj.FullUpdateFromMap(cache, master_file)
280
281        return return_val
282