Blob Blame History Raw
#!/usr/bin/python -tt
# -*- coding: utf-8 -*-
#
# This script will help you with migration squid-3.3 conf files to squid-3.5 conf files
# Copyright (C) 2016 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# he Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Authors: Lubos Uhliarik <luhliari@redhat.com>

import sys
import os
import re
import shutil
import traceback
import argparse
import glob

class ConfMigration:
    RE_LOG_ACCESS="log_access\s+(\w+)\s+"
    RE_LOG_ACCESS_DENY_REP="access_log none "
    RE_LOG_ACCESS_ALLOW_REP="access_log daemon:/var/log/squid/access.log squid "
    RE_LOG_ACCESS_TEXT="log_access"

    RE_LOG_ICAP="log_icap\s+"
    RE_LOG_ICAP_REP="icap_log daemon:/var/log/squid/icap.log "
    RE_LOG_ICAP_TEXT="log_icap"

    RE_HIER_STOPLIST="hierarchy_stoplist\s+(.*)"
    RE_HIER_STOPLIST_REP="acl %s url_regex %s\nalways_direct allow %s"
    RE_HIER_STOPLIST_TEXT="hierarchy_stoplist"

    HIER_ACL_NAME="migrated_hs_%d_%d"

    RE_INCLUDE_CHECK="\s*include\s+(.*)"

    COMMENT_FMT="# migrated automatically by squid-migrate-conf, the original configuration was: %s\n%s"

    DEFAULT_SQUID_CONF="/etc/squid/squid.conf"
    DEFAULT_BACKUP_EXT=".bak"
    DEFAULT_LEVEL_INDENT=3

    MAX_NESTED_INCLUDES=16

    def __init__(self, args, level=0, squid_conf='', conf_seq=0):
        self.args = args

        if squid_conf:
            self.squid_conf = squid_conf
        else:
            self.squid_conf = args.squid_conf
        self.write_changes = args.write_changes
        self.debug = args.debug

        self.conf_seq = conf_seq
        self.acl_seq = 0

        self.line_num = 0
        self.level = level
        if (not os.path.isfile(self.squid_conf)):
            sys.stderr.write("%sError: the config file %s does not exist\n" % (self.get_prefix_str(), self.squid_conf))
            sys.exit(1)

        self.squid_bak_conf = self.get_backup_name()

        self.migrated_squid_conf_data = []
        self.squid_conf_data = None


        print ("Migrating: " + self.squid_conf)

    def print_info(self, text=''):
        if (self.debug):
            print "%s%s" % (self.get_prefix_str(), text)

    def get_backup_name(self):
        file_idx = 1
        tmp_fn = self.squid_conf + self.DEFAULT_BACKUP_EXT

        while (os.path.isfile(tmp_fn)):
            tmp_fn = self.squid_conf + self.DEFAULT_BACKUP_EXT + str(file_idx)
            file_idx = file_idx + 1

        return tmp_fn

    #
    #  From squid config documentation:
    #
    #  Configuration options can be included using the "include" directive.
    #  Include takes a list of files to include. Quoting and wildcards are
    #  supported.
    #
    #  For example,
    #
    #  include /path/to/included/file/squid.acl.config
    #
    #  Includes can be nested up to a hard-coded depth of 16 levels.
    #  This arbitrary restriction is to prevent recursive include references
    #  from causing Squid entering an infinite loop whilst trying to load
    #  configuration files.
    #
    def check_include(self, line=''):
        m = re.match(self.RE_INCLUDE_CHECK, line)
        include_list = ""
        if not (m is None):
             include_list = re.split('\s+', m.group(1))
             for include_file_re in include_list:
                 # included file can be written in regexp syntax
                 for include_file in glob.glob(include_file_re):
                     self.print_info("A config file %s was found and it will be included" % (include_file))
                     if os.path.isfile(include_file):
                         self.print_info("Migrating the included config file %s" % (include_file))
                         conf = ConfMigration(self.args, self.level+1, include_file, self.conf_seq+1)
                         conf.migrate()

                 # check, if included file exists
                 if (len(glob.glob(include_file_re)) == 0 and not (os.path.isfile(include_file_re))):
                     self.print_info("The config file %s does not exist." % (include_file_re))

    def print_sub_text(self, text, new_str):
        if self.write_changes:
            print "File: '%s', line: %d - the directive %s was replaced by %s" % (self.squid_conf, self.line_num, text, new_str)
        else:
            print "File: '%s', line: %d - the directive %s could be replaced by %s" % (self.squid_conf, self.line_num, text, new_str)

    def add_conf_comment(self, old_line, line):
        return self.COMMENT_FMT % (old_line, line)

    def sub_line_ad(self, line, line_re, allow_sub, deny_sub, text):
        new_line = line
        m = re.match(line_re, line)
        if not (m is None):
            # check, if allow or deny was used and select coresponding sub
            sub_text = allow_sub
            if (re.match('allow', m.group(1), re.IGNORECASE)):
                new_line = re.sub(line_re, sub_text, line)
            elif (re.match('deny', m.group(1), re.IGNORECASE)):
                sub_text = deny_sub
                new_line = re.sub(line_re, sub_text, line)

            # print out, if there was any change and add comment to conf line, if so
            if not (new_line is line):
                self.print_sub_text(text + " " +  m.group(1), sub_text)
                new_line = self.add_conf_comment(line, new_line)

        return new_line

    def sub_line(self, line, line_re, sub, text):
        new_line = line
        m = re.match(line_re, line)
        if not (m is None):
            new_line = re.sub(line_re, sub, line)

            # print out, if there was any change and add comment to conf line, if so
            if not (new_line is line):
                self.print_sub_text(text, sub)
                new_line = self.add_conf_comment(line, new_line)

        return new_line

    def rep_hier_stoplist(self, line, sub, words):
        wordlist = words.split(' ')

        esc_wordlist = []
        for w in wordlist:
            esc_wordlist.append(re.escape(w))

        # unique acl name for hierarchy_stoplist acl
        acl_name = self.HIER_ACL_NAME % (self.conf_seq, self.acl_seq)
        return sub % (acl_name, ' '.join(esc_wordlist), acl_name)

    def sub_hier_stoplist(self, line, line_re, sub, text):
        new_line = line
        m = re.match(line_re, line)
        if (not (m is None)):
            new_line = self.rep_hier_stoplist(line, sub, m.group(1))

        # print out, if there was any change and add comment to conf line, if so
        if not (new_line is line):
            self.print_sub_text(text, sub)
            new_line = self.add_conf_comment(line, new_line)

        return new_line

    def process_conf_lines(self):
        for line in self.squid_conf_data.split(os.linesep):

            # do not migrate comments
            if not line.strip().startswith('#'):
               self.check_include(line)
               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)
               line = self.sub_line(line, self.RE_LOG_ICAP, self.RE_LOG_ICAP_REP, self.RE_LOG_ICAP_TEXT)
               line = self.sub_hier_stoplist(line, self.RE_HIER_STOPLIST, self.RE_HIER_STOPLIST_REP, self.RE_HIER_STOPLIST_TEXT)

            self.migrated_squid_conf_data.append(line)

            self.line_num = self.line_num + 1

    def migrate(self):
        # prevent infinite loop
        if (self.level > ConfMigration.MAX_NESTED_INCLUDES):
            sys.stderr.write("WARNING: the maximum number of nested includes was reached\n")
            return

        self.read_conf()
        self.process_conf_lines()
        if self.write_changes:
            if (not (set(self.migrated_squid_conf_data) == set(self.squid_conf_data.split(os.linesep)))):
                self.write_conf()

        self.print_info("The migration finished successfully")

    def get_prefix_str(self):
        return (("    " * int(self.level)) + "["+  self.squid_conf + "@%d]: " % (self.line_num))

    def read_conf(self):
        self.print_info("Reading squid conf: " + self.squid_conf)
        try:
           self.in_file = open(self.squid_conf, 'r')
           self.squid_conf_data = self.in_file.read()
           self.in_file.close()
        except Exception as e:
           sys.stderr.write("%sError: %s\n" % (self.get_prefix_str(), e))
           sys.exit(1)

    def write_conf(self):
        self.print_info("Creating backup conf: %s" % (self.squid_bak_conf))
        self.print_info("Writing changes to: %s" % (self.squid_conf))
        try:
           shutil.copyfile(self.squid_conf, self.squid_bak_conf)
           self.out_file = open(self.squid_conf, "w")
           self.out_file.write(os.linesep.join(self.migrated_squid_conf_data))
           self.out_file.close()
        except Exception as e:
           sys.stderr.write("%s Error: %s\n" % (self.get_prefix_str(), e))
           sys.exit(1)

def parse_args():
    parser = argparse.ArgumentParser(description='The script migrates the squid 3.3 configuration files to configuration files which are compatible with squid 3.5.')
    parser.add_argument('--conf', dest='squid_conf', action='store',
                        default=ConfMigration.DEFAULT_SQUID_CONF,
                        help='specify filename of squid configuration (default: %s)' % (ConfMigration.DEFAULT_SQUID_CONF))
    parser.add_argument('--write-changes', dest='write_changes', action='store_true',
                        default=False,
                        help='The changes are written to corresponding configuration files')
    parser.add_argument('--debug', dest="debug", action='store_true', default=False, help='print debug messages to stderr')
    return parser.parse_args()

if __name__ == '__main__':
    # parse args from command line
    args = parse_args()

    # check if config file exists
    if (not os.path.exists(args.squid_conf)):
        sys.stderr.write("Error: the file %s does not exist\n" % (args.squid_conf))
        sys.exit(1)

    # change working directory
    script_dir = os.getcwd()
    if (os.path.dirname(args.squid_conf)):
        os.chdir(os.path.dirname(args.squid_conf))

    # start migration
    try:
        conf = ConfMigration(args, 0)
        conf.migrate()
    finally:
        print ""

        if not args.write_changes:
            print "The changes have NOT been written to config files.\nUse the --write-changes option to write the changes"
        else:
            print "The changes have been written to config files!"

        os.chdir(script_dir)