192ec7
#!/usr/bin/python -tt
192ec7
# -*- coding: utf-8 -*-
192ec7
#
192ec7
# This script will help you with migration squid-3.3 conf files to squid-3.5 conf files
192ec7
# Copyright (C) 2016 Red Hat, Inc.
192ec7
#
192ec7
# This program is free software; you can redistribute it and/or modify
192ec7
# it under the terms of the GNU General Public License as published by
192ec7
# he Free Software Foundation; either version 2 of the License, or
192ec7
# (at your option) any later version.
192ec7
#
192ec7
# This program is distributed in the hope that it will be useful,
192ec7
# but WITHOUT ANY WARRANTY; without even the implied warranty of
192ec7
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
192ec7
# GNU General Public License for more details.
192ec7
#
192ec7
# You should have received a copy of the GNU General Public License along
192ec7
# with this program; if not, write to the Free Software Foundation, Inc.,
192ec7
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
192ec7
#
192ec7
# Authors: Lubos Uhliarik <luhliari@redhat.com>
192ec7
192ec7
import sys
192ec7
import os
192ec7
import re
192ec7
import shutil
192ec7
import traceback
192ec7
import argparse
192ec7
import glob
192ec7
192ec7
class ConfMigration:
192ec7
    RE_LOG_ACCESS="log_access\s+(\w+)\s+"
192ec7
    RE_LOG_ACCESS_DENY_REP="access_log none "
192ec7
    RE_LOG_ACCESS_ALLOW_REP="access_log daemon:/var/log/squid/access.log squid "
192ec7
    RE_LOG_ACCESS_TEXT="log_access"
192ec7
192ec7
    RE_LOG_ICAP="log_icap\s+"
192ec7
    RE_LOG_ICAP_REP="icap_log daemon:/var/log/squid/icap.log "
192ec7
    RE_LOG_ICAP_TEXT="log_icap"
192ec7
192ec7
    RE_HIER_STOPLIST="hierarchy_stoplist\s+(.*)"
192ec7
    RE_HIER_STOPLIST_REP="acl %s url_regex %s\nalways_direct allow %s"
192ec7
    RE_HIER_STOPLIST_TEXT="hierarchy_stoplist"
192ec7
192ec7
    HIER_ACL_NAME="migrated_hs_%d_%d"
192ec7
192ec7
    RE_INCLUDE_CHECK="\s*include\s+(.*)"
192ec7
192ec7
    COMMENT_FMT="# migrated automatically by squid-migrate-conf, the original configuration was: %s\n%s"
192ec7
192ec7
    DEFAULT_SQUID_CONF="/etc/squid/squid.conf"
192ec7
    DEFAULT_BACKUP_EXT=".bak"
192ec7
    DEFAULT_LEVEL_INDENT=3
192ec7
192ec7
    MAX_NESTED_INCLUDES=16
192ec7
192ec7
    def __init__(self, args, level=0, squid_conf='', conf_seq=0):
192ec7
        self.args = args
192ec7
192ec7
        if squid_conf:
192ec7
            self.squid_conf = squid_conf
192ec7
        else:
192ec7
            self.squid_conf = args.squid_conf
192ec7
        self.write_changes = args.write_changes
192ec7
        self.debug = args.debug
192ec7
192ec7
        self.conf_seq = conf_seq
192ec7
        self.acl_seq = 0
192ec7
192ec7
        self.line_num = 0
192ec7
        self.level = level
192ec7
        if (not os.path.isfile(self.squid_conf)):
192ec7
            sys.stderr.write("%sError: the config file %s does not exist\n" % (self.get_prefix_str(), self.squid_conf))
192ec7
            sys.exit(1)
192ec7
192ec7
        self.squid_bak_conf = self.get_backup_name()
192ec7
192ec7
        self.migrated_squid_conf_data = []
192ec7
        self.squid_conf_data = None
192ec7
192ec7
192ec7
        print ("Migrating: " + self.squid_conf)
192ec7
192ec7
    def print_info(self, text=''):
192ec7
        if (self.debug):
192ec7
            print "%s%s" % (self.get_prefix_str(), text)
192ec7
192ec7
    def get_backup_name(self):
192ec7
        file_idx = 1
192ec7
        tmp_fn = self.squid_conf + self.DEFAULT_BACKUP_EXT
192ec7
192ec7
        while (os.path.isfile(tmp_fn)):
192ec7
            tmp_fn = self.squid_conf + self.DEFAULT_BACKUP_EXT + str(file_idx)
192ec7
            file_idx = file_idx + 1
192ec7
192ec7
        return tmp_fn
192ec7
192ec7
    #
192ec7
    #  From squid config documentation:
192ec7
    #
192ec7
    #  Configuration options can be included using the "include" directive.
192ec7
    #  Include takes a list of files to include. Quoting and wildcards are
192ec7
    #  supported.
192ec7
    #
192ec7
    #  For example,
192ec7
    #
192ec7
    #  include /path/to/included/file/squid.acl.config
192ec7
    #
192ec7
    #  Includes can be nested up to a hard-coded depth of 16 levels.
192ec7
    #  This arbitrary restriction is to prevent recursive include references
192ec7
    #  from causing Squid entering an infinite loop whilst trying to load
192ec7
    #  configuration files.
192ec7
    #
192ec7
    def check_include(self, line=''):
192ec7
        m = re.match(self.RE_INCLUDE_CHECK, line)
192ec7
        include_list = ""
192ec7
        if not (m is None):
192ec7
             include_list = re.split('\s+', m.group(1))
192ec7
             for include_file_re in include_list:
192ec7
                 # included file can be written in regexp syntax
192ec7
                 for include_file in glob.glob(include_file_re):
192ec7
                     self.print_info("A config file %s was found and it will be included" % (include_file))
192ec7
                     if os.path.isfile(include_file):
192ec7
                         self.print_info("Migrating the included config file %s" % (include_file))
192ec7
                         conf = ConfMigration(self.args, self.level+1, include_file, self.conf_seq+1)
192ec7
                         conf.migrate()
192ec7
192ec7
                 # check, if included file exists
192ec7
                 if (len(glob.glob(include_file_re)) == 0 and not (os.path.isfile(include_file_re))):
192ec7
                     self.print_info("The config file %s does not exist." % (include_file_re))
192ec7
192ec7
    def print_sub_text(self, text, new_str):
192ec7
        if self.write_changes:
192ec7
            print "File: '%s', line: %d - the directive %s was replaced by %s" % (self.squid_conf, self.line_num, text, new_str)
192ec7
        else:
192ec7
            print "File: '%s', line: %d - the directive %s could be replaced by %s" % (self.squid_conf, self.line_num, text, new_str)
192ec7
192ec7
    def add_conf_comment(self, old_line, line):
192ec7
        return self.COMMENT_FMT % (old_line, line)
192ec7
192ec7
    def sub_line_ad(self, line, line_re, allow_sub, deny_sub, text):
192ec7
        new_line = line
192ec7
        m = re.match(line_re, line)
192ec7
        if not (m is None):
192ec7
            # check, if allow or deny was used and select coresponding sub
192ec7
            sub_text = allow_sub
192ec7
            if (re.match('allow', m.group(1), re.IGNORECASE)):
192ec7
                new_line = re.sub(line_re, sub_text, line)
192ec7
            elif (re.match('deny', m.group(1), re.IGNORECASE)):
192ec7
                sub_text = deny_sub
192ec7
                new_line = re.sub(line_re, sub_text, line)
192ec7
192ec7
            # print out, if there was any change and add comment to conf line, if so
192ec7
            if not (new_line is line):
192ec7
                self.print_sub_text(text + " " +  m.group(1), sub_text)
192ec7
                new_line = self.add_conf_comment(line, new_line)
192ec7
192ec7
        return new_line
192ec7
192ec7
    def sub_line(self, line, line_re, sub, text):
192ec7
        new_line = line
192ec7
        m = re.match(line_re, line)
192ec7
        if not (m is None):
192ec7
            new_line = re.sub(line_re, sub, line)
192ec7
192ec7
            # print out, if there was any change and add comment to conf line, if so
192ec7
            if not (new_line is line):
192ec7
                self.print_sub_text(text, sub)
192ec7
                new_line = self.add_conf_comment(line, new_line)
192ec7
192ec7
        return new_line
192ec7
192ec7
    def rep_hier_stoplist(self, line, sub, words):
192ec7
        wordlist = words.split(' ')
192ec7
192ec7
        esc_wordlist = []
192ec7
        for w in wordlist:
192ec7
            esc_wordlist.append(re.escape(w))
192ec7
192ec7
        # unique acl name for hierarchy_stoplist acl
192ec7
        acl_name = self.HIER_ACL_NAME % (self.conf_seq, self.acl_seq)
192ec7
        return sub % (acl_name, ' '.join(esc_wordlist), acl_name)
192ec7
192ec7
    def sub_hier_stoplist(self, line, line_re, sub, text):
192ec7
        new_line = line
192ec7
        m = re.match(line_re, line)
192ec7
        if (not (m is None)):
192ec7
            new_line = self.rep_hier_stoplist(line, sub, m.group(1))
192ec7
192ec7
        # print out, if there was any change and add comment to conf line, if so
192ec7
        if not (new_line is line):
192ec7
            self.print_sub_text(text, sub)
192ec7
            new_line = self.add_conf_comment(line, new_line)
192ec7
192ec7
        return new_line
192ec7
192ec7
    def process_conf_lines(self):
192ec7
        for line in self.squid_conf_data.split(os.linesep):
192ec7
192ec7
            # do not migrate comments
192ec7
            if not line.strip().startswith('#'):
192ec7
               self.check_include(line)
192ec7
               line = self.sub_line_ad(line, self.RE_LOG_ACCESS, self.RE_LOG_ACCESS_ALLOW_REP, self.RE_LOG_ACCESS_DENY_REP, self.RE_LOG_ACCESS_TEXT)
192ec7
               line = self.sub_line(line, self.RE_LOG_ICAP, self.RE_LOG_ICAP_REP, self.RE_LOG_ICAP_TEXT)
192ec7
               line = self.sub_hier_stoplist(line, self.RE_HIER_STOPLIST, self.RE_HIER_STOPLIST_REP, self.RE_HIER_STOPLIST_TEXT)
192ec7
192ec7
            self.migrated_squid_conf_data.append(line)
192ec7
192ec7
            self.line_num = self.line_num + 1
192ec7
192ec7
    def migrate(self):
192ec7
        # prevent infinite loop
192ec7
        if (self.level > ConfMigration.MAX_NESTED_INCLUDES):
192ec7
            sys.stderr.write("WARNING: the maximum number of nested includes was reached\n")
192ec7
            return
192ec7
192ec7
        self.read_conf()
192ec7
        self.process_conf_lines()
192ec7
        if self.write_changes:
192ec7
            if (not (set(self.migrated_squid_conf_data) == set(self.squid_conf_data.split(os.linesep)))):
192ec7
                self.write_conf()
192ec7
192ec7
        self.print_info("The migration finished successfully")
192ec7
192ec7
    def get_prefix_str(self):
192ec7
        return (("    " * int(self.level)) + "["+  self.squid_conf + "@%d]: " % (self.line_num))
192ec7
192ec7
    def read_conf(self):
192ec7
        self.print_info("Reading squid conf: " + self.squid_conf)
192ec7
        try:
192ec7
           self.in_file = open(self.squid_conf, 'r')
192ec7
           self.squid_conf_data = self.in_file.read()
192ec7
           self.in_file.close()
192ec7
        except Exception as e:
192ec7
           sys.stderr.write("%sError: %s\n" % (self.get_prefix_str(), e))
192ec7
           sys.exit(1)
192ec7
192ec7
    def write_conf(self):
192ec7
        self.print_info("Creating backup conf: %s" % (self.squid_bak_conf))
192ec7
        self.print_info("Writing changes to: %s" % (self.squid_conf))
192ec7
        try:
192ec7
           shutil.copyfile(self.squid_conf, self.squid_bak_conf)
192ec7
           self.out_file = open(self.squid_conf, "w")
192ec7
           self.out_file.write(os.linesep.join(self.migrated_squid_conf_data))
192ec7
           self.out_file.close()
192ec7
        except Exception as e:
192ec7
           sys.stderr.write("%s Error: %s\n" % (self.get_prefix_str(), e))
192ec7
           sys.exit(1)
192ec7
192ec7
def parse_args():
192ec7
    parser = argparse.ArgumentParser(description='The script migrates the squid 3.3 configuration files to configuration files which are compatible with squid 3.5.')
192ec7
    parser.add_argument('--conf', dest='squid_conf', action='store',
192ec7
                        default=ConfMigration.DEFAULT_SQUID_CONF,
192ec7
                        help='specify filename of squid configuration (default: %s)' % (ConfMigration.DEFAULT_SQUID_CONF))
192ec7
    parser.add_argument('--write-changes', dest='write_changes', action='store_true',
192ec7
                        default=False,
192ec7
                        help='The changes are written to corresponding configuration files')
192ec7
    parser.add_argument('--debug', dest="debug", action='store_true', default=False, help='print debug messages to stderr')
192ec7
    return parser.parse_args()
192ec7
192ec7
if __name__ == '__main__':
192ec7
    # parse args from command line
192ec7
    args = parse_args()
192ec7
192ec7
    # check if config file exists
192ec7
    if (not os.path.exists(args.squid_conf)):
192ec7
        sys.stderr.write("Error: the file %s does not exist\n" % (args.squid_conf))
192ec7
        sys.exit(1)
192ec7
192ec7
    # change working directory
192ec7
    script_dir = os.getcwd()
192ec7
    if (os.path.dirname(args.squid_conf)):
192ec7
        os.chdir(os.path.dirname(args.squid_conf))
192ec7
192ec7
    # start migration
192ec7
    try:
192ec7
        conf = ConfMigration(args, 0)
192ec7
        conf.migrate()
192ec7
    finally:
192ec7
        print ""
192ec7
192ec7
        if not args.write_changes:
192ec7
            print "The changes have NOT been written to config files.\nUse the --write-changes option to write the changes"
192ec7
        else:
192ec7
            print "The changes have been written to config files!"
192ec7
192ec7
        os.chdir(script_dir)