/ letsencrypt-apache / letsencrypt_apache / augeas_configurator.py
augeas_configurator.py
  1  """Class of Augeas Configurators."""
  2  import logging
  3  
  4  import augeas
  5  
  6  from letsencrypt import errors
  7  from letsencrypt import reverter
  8  from letsencrypt.plugins import common
  9  
 10  logger = logging.getLogger(__name__)
 11  
 12  
 13  class AugeasConfigurator(common.Plugin):
 14      """Base Augeas Configurator class.
 15  
 16      :ivar config: Configuration.
 17      :type config: :class:`~letsencrypt.interfaces.IConfig`
 18  
 19      :ivar aug: Augeas object
 20      :type aug: :class:`augeas.Augeas`
 21  
 22      :ivar str save_notes: Human-readable configuration change notes
 23      :ivar reverter: saves and reverts checkpoints
 24      :type reverter: :class:`letsencrypt.reverter.Reverter`
 25  
 26      """
 27      def __init__(self, *args, **kwargs):
 28          super(AugeasConfigurator, self).__init__(*args, **kwargs)
 29  
 30          # Set Augeas flags to not save backup (we do it ourselves)
 31          # Set Augeas to not load anything by default
 32          my_flags = augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD
 33          self.aug = augeas.Augeas(flags=my_flags)
 34          self.save_notes = ""
 35  
 36          # See if any temporary changes need to be recovered
 37          # This needs to occur before VirtualHost objects are setup...
 38          # because this will change the underlying configuration and potential
 39          # vhosts
 40          self.reverter = reverter.Reverter(self.config)
 41          self.recovery_routine()
 42  
 43      def check_parsing_errors(self, lens):
 44          """Verify Augeas can parse all of the lens files.
 45  
 46          :param str lens: lens to check for errors
 47  
 48          :raises .errors.PluginError: If there has been an error in parsing with
 49              the specified lens.
 50  
 51          """
 52          error_files = self.aug.match("/augeas//error")
 53  
 54          for path in error_files:
 55              # Check to see if it was an error resulting from the use of
 56              # the httpd lens
 57              lens_path = self.aug.get(path + "/lens")
 58              # As aug.get may return null
 59              if lens_path and lens in lens_path:
 60                  msg = (
 61                      "There has been an error in parsing the file (%s): %s",
 62                      # Strip off /augeas/files and /error
 63                      path[13:len(path) - 6], self.aug.get(path + "/message"))
 64                  raise errors.PluginError(msg)
 65  
 66      # TODO: Cleanup this function
 67      def save(self, title=None, temporary=False):
 68          """Saves all changes to the configuration files.
 69  
 70          This function first checks for save errors, if none are found,
 71          all configuration changes made will be saved. According to the
 72          function parameters.
 73  
 74          :param str title: The title of the save. If a title is given, the
 75              configuration will be saved as a new checkpoint and put in a
 76              timestamped directory.
 77  
 78          :param bool temporary: Indicates whether the changes made will
 79              be quickly reversed in the future (ie. challenges)
 80  
 81          :raises .errors.PluginError: If there was an error in Augeas, in an
 82              attempt to save the configuration, or an error creating a checkpoint
 83  
 84          """
 85          save_state = self.aug.get("/augeas/save")
 86          self.aug.set("/augeas/save", "noop")
 87          # Existing Errors
 88          ex_errs = self.aug.match("/augeas//error")
 89          try:
 90              # This is a noop save
 91              self.aug.save()
 92          except (RuntimeError, IOError):
 93              self._log_save_errors(ex_errs)
 94              # Erase Save Notes
 95              self.save_notes = ""
 96              raise errors.PluginError(
 97                  "Error saving files, check logs for more info.")
 98  
 99          # Retrieve list of modified files
100          # Note: Noop saves can cause the file to be listed twice, I used a
101          # set to remove this possibility. This is a known augeas 0.10 error.
102          save_paths = self.aug.match("/augeas/events/saved")
103  
104          # If the augeas tree didn't change, no files were saved and a backup
105          # should not be created
106          if save_paths:
107              save_files = set()
108              for path in save_paths:
109                  save_files.add(self.aug.get(path)[6:])
110  
111              try:
112                  # Create Checkpoint
113                  if temporary:
114                      self.reverter.add_to_temp_checkpoint(
115                          save_files, self.save_notes)
116                  else:
117                      self.reverter.add_to_checkpoint(save_files, self.save_notes)
118              except errors.ReverterError as err:
119                  raise errors.PluginError(str(err))
120  
121          if title and not temporary:
122              try:
123                  self.reverter.finalize_checkpoint(title)
124              except errors.ReverterError as err:
125                  raise errors.PluginError(str(err))
126  
127          self.aug.set("/augeas/save", save_state)
128          self.save_notes = ""
129          self.aug.save()
130  
131      def _log_save_errors(self, ex_errs):
132          """Log errors due to bad Augeas save.
133  
134          :param list ex_errs: Existing errors before save
135  
136          """
137          # Check for the root of save problems
138          new_errs = self.aug.match("/augeas//error")
139          # logger.error("During Save - %s", mod_conf)
140          logger.error("Unable to save files: %s. Attempted Save Notes: %s",
141                       ", ".join(err[13:len(err) - 6] for err in new_errs
142                                 # Only new errors caused by recent save
143                                 if err not in ex_errs), self.save_notes)
144  
145      # Wrapper functions for Reverter class
146      def recovery_routine(self):
147          """Revert all previously modified files.
148  
149          Reverts all modified files that have not been saved as a checkpoint
150  
151          :raises .errors.PluginError: If unable to recover the configuration
152  
153          """
154          try:
155              self.reverter.recovery_routine()
156          except errors.ReverterError as err:
157              raise errors.PluginError(str(err))
158          # Need to reload configuration after these changes take effect
159          self.aug.load()
160  
161      def revert_challenge_config(self):
162          """Used to cleanup challenge configurations.
163  
164          :raises .errors.PluginError: If unable to revert the challenge config.
165  
166          """
167          try:
168              self.reverter.revert_temporary_config()
169          except errors.ReverterError as err:
170              raise errors.PluginError(str(err))
171          self.aug.load()
172  
173      def rollback_checkpoints(self, rollback=1):
174          """Rollback saved checkpoints.
175  
176          :param int rollback: Number of checkpoints to revert
177  
178          :raises .errors.PluginError: If there is a problem with the input or
179              the function is unable to correctly revert the configuration
180  
181          """
182          try:
183              self.reverter.rollback_checkpoints(rollback)
184          except errors.ReverterError as err:
185              raise errors.PluginError(str(err))
186          self.aug.load()
187  
188      def view_config_changes(self):
189          """Show all of the configuration changes that have taken place.
190  
191          :raises .errors.PluginError: If there is a problem while processing
192              the checkpoints directories.
193  
194          """
195          try:
196              self.reverter.view_config_changes()
197          except errors.ReverterError as err:
198              raise errors.PluginError(str(err))