From 6244265de012dcf6e278d4f813f5cff48530bed9 Mon Sep 17 00:00:00 2001 From: Leo Simons Date: Thu, 7 Aug 2014 11:45:36 +0200 Subject: [PATCH] Fancy line editor for Ian --- .../debian/config/opt/cloud/bin/line_edit.py | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100755 systemvm/patches/debian/config/opt/cloud/bin/line_edit.py diff --git a/systemvm/patches/debian/config/opt/cloud/bin/line_edit.py b/systemvm/patches/debian/config/opt/cloud/bin/line_edit.py new file mode 100755 index 00000000000..5918883ea96 --- /dev/null +++ b/systemvm/patches/debian/config/opt/cloud/bin/line_edit.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +import re +import shutil +import os + + +class LineEdit(object): + """Helper for LineEditingFile that keeps track of one edit.""" + def __init__(self, search, sub, *sub_args, **kwargs): + if len(sub_args) > 0: + sub = sub % sub_args + flags = kwargs.get('flags', 0) + self.pattern = re.compile(search, flags=flags) + self.sub = sub + self.count = kwargs.get('count', 0) # max subs to make + self.subs = 0 # subs made so far + + +class LineEditingFile(object): + """ + Atomic, conservative, by-line editing of configuration files. + + Will not touch the file if there are no changes to do. + Reasonably efficient for large files, though files with a long time + before their first match will use memory. + + + Given a vhosts file such as: + >>> with open('doctest-vhosts.conf', 'w') as f: + ... f.write(''' + ... Listen foo:80 + ... + ... DocRoot /var/www + ... + ... + ... Listen other:80 + ... + ... DocRoot /var/www + ... + ... ''') + ... + + To replace the hostname for the first virtualhost entry: + >>> new_hostname = 'fooooo' + >>> with LineEditingFile('doctest-vhosts.conf') as f: + ... f.replace(r'', '', new_hostname, count=1, flags=re.I) + ... f.replace(r'Listen .*?:80', 'Listen %s:80', new_hostname, count=1, flags=re.I) + ... + + Be careful with the matches! + A second invocation of the same rule will edit the second vhost: + >>> new_hostname = 'fooooo' + >>> with LineEditingFile('doctest-vhosts.conf') as f: + ... f.replace(r'', '', new_hostname, count=1, flags=re.I) + ... + + To move all hosts from port 80 to port 8080: + >>> with LineEditingFile('doctest-vhosts.conf') as f: + ... f.replace(r'', '', flags=re.I) + ... f.replace(r'Listen (.*?):80', 'Listen \\\\1:80', flags=re.I) + ... + + (please note in this example there's a double escape of the backreference + \\\\1, to make the example work with doctest) + + Since this example already matched all files, a second invocation does nothing: + >>> with LineEditingFile('doctest-vhosts.conf') as f: + ... f.replace(r'', '', flags=re.I) + ... + + It's also acceptable to not make any edits at all: + >>> with LineEditingFile('doctest-vhosts.conf') as f: + ... pass + ... + + You don't _have_ to use a with statement: + >>> f = LineEditingFile('doctest-vhosts.conf') + >>> f.replace(r'DocRoot /var/www', 'DocRoot /var/www/html', flags=re.I) + >>> changes = f.commit() + >>> print changes + 2 + >>> + + Cleanup of the example vhosts.conf: + >>> # noinspection PyBroadException + >>> try: + ... os.unlink('doctest-vhosts.conf') + ... os.unlink('doctest-vhosts.conf.bak') + ... os.unlink('doctest-vhosts.conf.new') + ... except: + ... pass + ... + """ + + def __init__(self, filename): + self.filename = filename + self.changed = False + self.edits = [] + + def __enter__(self): + return self + + def replace(self, search, sub, *sub_args, **kwargs): + edit = LineEdit(search, sub, *sub_args, **kwargs) + self.edits.append(edit) + + # noinspection PyUnusedLocal + def __exit__(self, exc, value, traceback): + if exc is not None: + return False # return false results in re-raise + + self.commit() + + def commit(self): + changes = 0 + changed_file = None + changed_filename = self.filename + '.new' + try: + lines = [] + backup_filename = self.filename + '.bak' + # noinspection PyUnusedLocal + stat = None + with open(self.filename, 'r') as orig: + stat = os.fstat(orig.fileno()) + for line in orig: + changed_line = line + for edit in self.edits: + remaining_count = 0 + if edit.count != 0: + remaining_count = edit.count - edit.subs + if remaining_count < 0: + raise Exception("Made too many edits") + elif remaining_count == 0: + continue + changed_line, subs = edit.pattern.subn( + edit.sub, line, remaining_count) + if changed_line != line: + if changed_file is None: + logging.debug("Editing file %s" % self.filename) + logging.debug(" - %s" % line[:-1]) + logging.debug(" + %s" % changed_line[:-1]) + changes += subs + edit.subs += subs + if changes == 0: # buffer until we find a change + lines.append(changed_line) + elif changed_file is None: # found first change, flush buffer + changed_file = open(changed_filename, 'w') + if hasattr(os, 'fchmod'): + os.fchmod(changed_file.fileno(), # can cause OSError which aborts + stat.st_mode) + if hasattr(os, 'fchown'): + os.fchown(changed_file.fileno(), # can cause OSError which aborts + stat.st_uid, stat.st_gid) + changed_file.writelines(lines) + changed_file.write(changed_line) + del lines # reclaim buffer memory + else: # already flushed, just write + changed_file.write(changed_line) + + if changes == 0: + logging.info("No edits need for file %s" % + self.filename) + else: + changed_file.close() + changed_file = None + if os.path.exists(backup_filename): # back up the original + os.unlink(backup_filename) + shutil.copy(self.filename, backup_filename) + os.rename(changed_filename, self.filename) # the swap + logging.info("Edited file %s (%d changes)" % + (self.filename, changes)) + finally: + if changed_file is not None: # failed, clean up + changed_file.close() + os.unlink(changed_filename) + return changes + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + import doctest + doctest.testmod()