#!/usr/bin/python
# -*- Mode: Python; python-indent: 8; indent-tabs-mode: t -*-
"""
"""

import sys
import os
import re
from preupg.script_api import *


"""Preupgrade Assistant performs system upgradability assessment
and gathers information required for successful operating system upgrade.
Copyright (C) 2013 Red Hat Inc.
Jakub Mazanek <jmazanek@redhat.com>

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
the Free Software Foundation, either version 3 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, see <http://www.gnu.org/licenses/>."""
check_rpm_to (check_rpm="",check_bin="python")
#END GENERATED SECTION

# exit functions are exit_{pass,not_applicable, fixed, fail, etc.}
# logging functions are log_{error, warning, info, etc.}
# for logging in-place risk use functions log_{extreme, high, medium, slight}_risk
from operator import itemgetter as _itemgetter
from keyword import iskeyword as _iskeyword
import sys as _sys

if "add_pkg_to_kickstart" not in dir():
   from preupg.script_api import add_pkg_to_kickstart

if not ((is_pkg_installed("bind") and is_dist_native("bind")) or
        (is_pkg_installed("bind97") and is_dist_native("bind97"))):
    exit_not_applicable()

if (is_pkg_installed("bind97") and is_dist_native("bind97")):
    add_pkg_to_kickstart("bind")

def namedtuple(typename, field_names, verbose=False, rename=False):
    """Returns a new subclass of tuple with named fields.

    >>> Point = namedtuple('Point', 'x y')
    >>> Point.__doc__                   # docstring for the new class
    'Point(x, y)'
    >>> p = Point(11, y=22)             # instantiate with positional args or keywords
    >>> p[0] + p[1]                     # indexable like a plain tuple
    33
    >>> x, y = p                        # unpack like a regular tuple
    >>> x, y
    (11, 22)
    >>> p.x + p.y                       # fields also accessible by name
    33
    >>> d = p._asdict()                 # convert to a dictionary
    >>> d['x']
    11
    >>> Point(**d)                      # convert from a dictionary
    Point(x=11, y=22)
    >>> p._replace(x=100)               # _replace() is like str.replace() but targets named fields
    Point(x=100, y=22)

    """

    # Parse and validate the field names.  Validation serves two purposes,
    # generating informative error messages and preventing template injection attacks.
    if isinstance(field_names, basestring):
        field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas
    field_names = tuple(map(str, field_names))
    if rename:
        names = list(field_names)
        seen = set()
        for i, name in enumerate(names):
            if (not min(c.isalnum() or c=='_' for c in name) or _iskeyword(name)
                or not name or name[0].isdigit() or name.startswith('_')
                or name in seen):
                    names[i] = '_%d' % i
            seen.add(name)
        field_names = tuple(names)
    for name in (typename,) + field_names:
        if not min(c.isalnum() or c=='_' for c in name):
            raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name)
        if _iskeyword(name):
            raise ValueError('Type names and field names cannot be a keyword: %r' % name)
        if name[0].isdigit():
            raise ValueError('Type names and field names cannot start with a number: %r' % name)
    seen_names = set()
    for name in field_names:
        if name.startswith('_') and not rename:
            raise ValueError('Field names cannot start with an underscore: %r' % name)
        if name in seen_names:
            raise ValueError('Encountered a duplicate field name: %r' % name)
        seen_names.add(name)

    # Create and fill-in the class template
    numfields = len(field_names)
    argtxt = repr(field_names).replace("'", "")[1:-1]   # tuple repr without parens or quotes
    reprtxt = ', '.join('%s=%%r' % name for name in field_names)
    template = '''class %(typename)s(tuple):
        '%(typename)s(%(argtxt)s)' \n
        __slots__ = () \n
        _fields = %(field_names)r \n
        def __new__(_cls, %(argtxt)s):
            return _tuple.__new__(_cls, (%(argtxt)s)) \n
        @classmethod
        def _make(cls, iterable, new=tuple.__new__, len=len):
            'Make a new %(typename)s object from a sequence or iterable'
            result = new(cls, iterable)
            if len(result) != %(numfields)d:
                raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
            return result \n
        def __repr__(self):
            return '%(typename)s(%(reprtxt)s)' %% self \n
        def _asdict(self):
            'Return a new dict which maps field names to their values'
            return dict(zip(self._fields, self)) \n
        def _replace(_self, **kwds):
            'Return a new %(typename)s object replacing specified fields with new values'
            result = _self._make(map(kwds.pop, %(field_names)r, _self))
            if kwds:
                raise ValueError('Got unexpected field names: %%r' %% kwds.keys())
            return result \n
        def __getnewargs__(self):
            return tuple(self) \n\n''' % locals()
    for i, name in enumerate(field_names):
        template += '        %s = _property(_itemgetter(%d))\n' % (name, i)
    if verbose:
        print template

    # Execute the template string in a temporary namespace
    namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
                     _property=property, _tuple=tuple)
    try:
        exec template in namespace
    except SyntaxError, e:
        raise SyntaxError(e.message + ':\n' + template)
    result = namespace[typename]

    # For pickling to work, the __module__ variable needs to be set to the frame
    # where the named tuple is created.  Bypass this step in enviroments where
    # sys._getframe is not defined (Jython for example) or sys._getframe is not
    # defined for arguments greater than 0 (IronPython).
    try:
        result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    return result


ConfFile = namedtuple("ConfFile", ["path", "buffer"])

def file_exists():
    exists = os.path.isfile("/etc/named.conf")
    if exists == False:
       exit_not_applicable()

file_exists()

CONFIG_FILE = "/etc/named.conf"
FILES_TO_CHECK = []

FIXED_CONFIGS = {}

# Exit codes
EXIT_NOT_APPLICABLE = 0
EXIT_PASS = 1
EXIT_INFORMATIONAL = 2
EXIT_FIXED = 3
EXIT_FAIL = 4
EXIT_ERROR = 5


class SolutionText(object):
    """
    class for handling the construction of the solution text.
    """
    def __init__(self):
        self.header = """Some issues have been found in your BIND9 configuration.
Use the following solutions to fix them:"""
        self.tail = """For more information, see the BIND9 Administrator Reference
Manual located in the /usr/share/doc/bind-9.9.4/Bv9ARM.pdf file, and the 'DNS Servers'
section of Red Hat Enterprise Linux 7 Networking Guide."""
        self.solutions = []

    def add_solution(self, solution=""):
        if solution:
            self.solutions.append(solution)

    def get_text(self):
        text = self.header + "\n\n\n"
        for solution in self.solutions:
            text += solution + "\n\n\n"
        text += self.tail
        return text


# object used for creating solution text
sol_text = SolutionText()


#######################################################
### CONFIGURATION CHECKS PART - BEGIN
#######################################################


CONFIG_CHECKS = []


def register_check(check):
    """
    Function decorator that adds the configuration check into the list of checks.
    """
    CONFIG_CHECKS.append(check)
    return check


def run_checks(files_to_check):
    """
    Runs all available checks on files loaded into the files_to_check list.
    """
    gl_result = EXIT_PASS

    for check in CONFIG_CHECKS:
        log_info("Running check: \"" + check.__name__ + "\"")
        for fpath, buff in FILES_TO_CHECK:
            log_info("checking: \"" + fpath + "\"")
            result = check(fpath, buff)
            if result > gl_result:
                gl_result = result

    log_info("Running check: \"check_empty_zones_complex\"")
    result = check_empty_zones_complex()
    if result > gl_result:
        gl_result = result
    
    log_info("Running check: \"check_default_runtime_dir\"")
    result = check_default_runtime_dir()
    if result > gl_result:
        gl_result = result

    return gl_result


@register_check
def check_tcp_listen_queue(file_path, buff):
    """
    3581.	[bug]		Changed the tcp-listen-queue default to 10. [RT #33029]

    Default and minimum value changed from 3 to 10

    From bind-9.9.4 ARM:
    The listen queue depth. The default and minimum is 10. If the kernel supports the
    accept filter 'dataready' this also controls how many TCP connections that will be queued in
    kernel space waiting for some data before being passed to accept. Nonzero values less than 10
    will be silently raised. A value of 0 may also be used; on most platforms this sets the listen queue
    length to a system-defined default value.
    """
    pattern = re.compile("tcp-listen-queue\s*([0-9]+)\s*;")
    match_iter = pattern.finditer(buff)
    status = EXIT_PASS

    for match in match_iter:
        try:
            number = int(match.group(1))
        except ValueError:
            log_error("Value \"" + match.group(1) + "\" cannot be converted")
            return EXIT_ERROR
        # the new default and minimum value is "10"
        if number > 0 and number < 10:
            log_slight_risk("Found \"" + match.group(0) + "\" in \"" +
                            file_path + "\"")
            sol_text.add_solution(
"""The 'tcp-listen-queue' statement with a value less than 10:
-------------------------------------------------------
The value specified in the 'tcp-listen-queue' statement is less than 10.
Change your configuration to use at least the value of 10. BIND9 will silently ignore values less than 10 and it will use 10 instead.""")
            status = EXIT_INFORMATIONAL

    return status

@register_check
def dlz_driver_open(self, config):
    status = EXIT_INFORMATIONAL
    try:
        f = open(CONFIG_FILE, "r")
        config = f.readlines()
        f.close()
        for line in config:
            if re.match("(.*)--with-dlopen=yes(.*)", line):
                sol_text.add_solution(" The DLZ \"dlopen\" driver is now built by default. Remove the option from the configuration file.")
            elif re.match("(.*)--with-dlopen=no(.*)", line):
                sol_text.add_solution(" The DLZ \"dlopen\" driver is now built in by default. To disable it, use \"configure --without-dlopen\".")
    except IOError:
        raise
    return status

@register_check
def check_zone_statistics(file_path, buff):
    """
    3501.	[func]   zone-statistics now takes three options: 'full',
                    'terse', and 'none'. 'yes' and 'no' are retained as
                    synonyms for 'full' and 'terse', respectively. [RT #29165]

    The options changed, but they are still compatible, and they can be used in a new version.

    From bind-9.9.4 ARM:
    If full, the server will collect statistical data on all zones (unless specifically turned off
    on a per-zone basis by specifying zone-statistics terse or zone-statistics none in the zone state-
    ment). The default is terse, providing minimal statistics on zones (including name and current
    serial number, but not query type counters).

    For backward compatibility with earlier versions of BIND 9, the 'zone-statistics' option can also
    accept 'yes' or 'no', which have the same effect as 'full' and 'terse', respectively.
    """
    pattern = re.compile("zone-statistics\s*(yes|no)\s*;")
    match_iter = pattern.finditer(buff)
    status = EXIT_PASS

    for match in match_iter:
        log_slight_risk("Found \"" + match.group(0) + "\" in \"" +
                        file_path + "\"")
        sol_text.add_solution(
"""The 'zone-statistics' arguments changed:
------------------------------------
The arguments of the 'zone-statistics' option changed in the new version of BIND9.
Replace the argument 'yes' with 'full', or replace the argument 'no' with 'terse'. The old options are still recognised by BIND9, and silently converted.""")
        status = EXIT_INFORMATIONAL

    return status


@register_check
def check_masterfile_format(file_path, buff):
    """
    3180.	[func]		Local copies of slave zones are now saved in a raw
                            format by default to improve the startup performance.
                            'masterfile-format text;' can be used to override
                            the default, if desired. [RT #25867]

    The default format of the saved slave zone changed from 'text' to 'raw'.

    From bind-9.9.4 ARM:
    masterfile-format Specifies the file format of zone files (see Section 6.3.7). The default value is text,
    which is the standard textual representation, except for slave zones, in which the default value
    is raw. Files in other formats than text are typically expected to be generated by the named-
    compilezone tool, or dumped by named.
    """
    pattern_zone_str = "zone\s+\"(.+?)\"(\s|.)*?{(\s|.)*?}"
    pattern_slave_str = "type\s+slave"
    pattern_mff_str = "masterfile-format"
    status = EXIT_PASS

    # find slave zones without masterfile-format statement
    pattern_zone = re.compile(pattern_zone_str)
    pattern_sl_zone = re.compile(pattern_slave_str)
    pattern_mff = re.compile(pattern_mff_str)
    pattern_zone_iter = pattern_zone.finditer(buff)

    for zone in pattern_zone_iter:
        slave_statement = pattern_sl_zone.search(zone.group(0))
        # if slave zone
        if slave_statement:
            mff_statement = pattern_mff.search(zone.group(0))
            # if no masterfile-format statement
            if not mff_statement:
                log_medium_risk("Found slave zone \"" + zone.group(1) + "\" in \"" +
                                file_path + "\" without \"masterfile-format\" statement.")
                status = EXIT_FAIL
    
    if status == EXIT_FAIL:
        sol_text.add_solution(
"""slave zone definition without the 'masterfile-format' statement:
------------------------------------------------------------
In the new version of BIND9, slave zones are saved by default as a 'raw'
format after the zone transfer. Previously, the default format was 'text'.
You should use one of the following solutions:
- Remove saved slave zones files so that they are saved in the 'raw'
  format when transfered next time.
- Convert zones files to the 'raw' format using the 'named-compilezone'
  tool.
- Include the 'masterfile-format text;' statement in the slave zone
  definition statement.""")

    return status


################################################################
# These checks can not be run as the rest, as they need to check
# all configuration files at once.

def check_empty_zones_complex():
    """
    Check if there are any zones defined that are now included in empty zones.
    """
    status = EXIT_PASS

    new_ez = ["10.IN-ADDR.ARPA",
              "64.100.IN-ADDR.ARPA",
              "65.100.IN-ADDR.ARPA",
              "66.100.IN-ADDR.ARPA",
              "67.100.IN-ADDR.ARPA",
              "68.100.IN-ADDR.ARPA",
              "69.100.IN-ADDR.ARPA",
              "70.100.IN-ADDR.ARPA",
              "71.100.IN-ADDR.ARPA",
              "72.100.IN-ADDR.ARPA",
              "73.100.IN-ADDR.ARPA",
              "74.100.IN-ADDR.ARPA",
              "75.100.IN-ADDR.ARPA",
              "76.100.IN-ADDR.ARPA",
              "77.100.IN-ADDR.ARPA",
              "78.100.IN-ADDR.ARPA",
              "79.100.IN-ADDR.ARPA",
              "80.100.IN-ADDR.ARPA",
              "81.100.IN-ADDR.ARPA",
              "82.100.IN-ADDR.ARPA",
              "83.100.IN-ADDR.ARPA",
              "84.100.IN-ADDR.ARPA",
              "85.100.IN-ADDR.ARPA",
              "86.100.IN-ADDR.ARPA",
              "87.100.IN-ADDR.ARPA",
              "88.100.IN-ADDR.ARPA",
              "89.100.IN-ADDR.ARPA",
              "90.100.IN-ADDR.ARPA",
              "91.100.IN-ADDR.ARPA",
              "92.100.IN-ADDR.ARPA",
              "93.100.IN-ADDR.ARPA",
              "94.100.IN-ADDR.ARPA",
              "95.100.IN-ADDR.ARPA",
              "96.100.IN-ADDR.ARPA",
              "97.100.IN-ADDR.ARPA",
              "98.100.IN-ADDR.ARPA",
              "99.100.IN-ADDR.ARPA",
              "100.100.IN-ADDR.ARPA",
              "101.100.IN-ADDR.ARPA",
              "102.100.IN-ADDR.ARPA",
              "103.100.IN-ADDR.ARPA",
              "104.100.IN-ADDR.ARPA",
              "105.100.IN-ADDR.ARPA",
              "106.100.IN-ADDR.ARPA",
              "107.100.IN-ADDR.ARPA",
              "108.100.IN-ADDR.ARPA",
              "109.100.IN-ADDR.ARPA",
              "110.100.IN-ADDR.ARPA",
              "111.100.IN-ADDR.ARPA",
              "112.100.IN-ADDR.ARPA",
              "113.100.IN-ADDR.ARPA",
              "114.100.IN-ADDR.ARPA",
              "115.100.IN-ADDR.ARPA",
              "116.100.IN-ADDR.ARPA",
              "117.100.IN-ADDR.ARPA",
              "118.100.IN-ADDR.ARPA",
              "119.100.IN-ADDR.ARPA",
              "120.100.IN-ADDR.ARPA",
              "121.100.IN-ADDR.ARPA",
              "122.100.IN-ADDR.ARPA",
              "123.100.IN-ADDR.ARPA",
              "124.100.IN-ADDR.ARPA",
              "125.100.IN-ADDR.ARPA",
              "126.100.IN-ADDR.ARPA",
              "127.100.IN-ADDR.ARPA",
              "16.172.IN-ADDR.ARPA",
              "17.172.IN-ADDR.ARPA",
              "18.172.IN-ADDR.ARPA",
              "19.172.IN-ADDR.ARPA",
              "20.172.IN-ADDR.ARPA",
              "21.172.IN-ADDR.ARPA",
              "22.172.IN-ADDR.ARPA",
              "23.172.IN-ADDR.ARPA",
              "24.172.IN-ADDR.ARPA",
              "25.172.IN-ADDR.ARPA",
              "26.172.IN-ADDR.ARPA",
              "27.172.IN-ADDR.ARPA",
              "28.172.IN-ADDR.ARPA",
              "29.172.IN-ADDR.ARPA",
              "30.172.IN-ADDR.ARPA",
              "31.172.IN-ADDR.ARPA",
              "168.192.IN-ADDR.ARPA",
              "100.51.198.IN-ADDR.ARPA",
              "113.0.203.IN-ADDR.ARPA",
              "8.B.D.0.1.0.0.2.IP6.ARPA",
              ]

    # Create a global config
    configuration = ""
    for fpath, buff in FILES_TO_CHECK:
        configuration += buff + "\n"

    ez_disable_pattern = re.compile("empty-zones-enable\s+no")
    # Check if empty zones are not disabled globally
    found = ez_disable_pattern.findall(configuration)
    if found:
        return status

    # Check new empty zones
    for empty_zone in new_ez:
        pattern = re.compile("zone\s+\"" + empty_zone + "\"", re.IGNORECASE)
        pattern_dis = re.compile(
            "disable-empty-zone\s+\"" + empty_zone + "\"", re.IGNORECASE)
        found = pattern.findall(configuration)
        if found:
            # check if the empty zone is not disabled individually
            found_dis = pattern_dis.findall(configuration)
            if found_dis:
                continue
            status = EXIT_FAIL
            log_high_risk("Found a zone \"" + empty_zone + "\" in the BIND9 " +
                          "configuration. This zone will be overridden by a built-in " +
                          "empty zone if not disabled.")
    
    if status == EXIT_FAIL:
        sol_text.add_solution(
"""Zone declaration that conflicts with built-in empty zones:
----------------------------------------------------------
In the new version of BIND9, the list of automatically created empty
zones expanded. Your configuration contains a zone that is conflicting
with a built-in empty zone. You should use one of the following solutions:
- Disable the specific empty zone by using the 'disable-empty-zone <zone>;'
  statement.
- Disable empty zones globally by using the 'empty-zones-enable no;'
  statement.""")

    return status


def check_default_runtime_dir():
    """
    Check if there are any statements needed for the /var/run/ -> /run/ move in 'options'.
    """
    status = EXIT_PASS

    # Create a global config
    configuration = ""
    for fpath, buff in FILES_TO_CHECK:
        configuration += buff + "\n"

    pid_file_pattern = re.compile("pid-file\s+\"\/run\/named\/named\.pid\"")
    session_keyfile_pattern = re.compile("session-keyfile\s+\"\/run\/named\/session\.key\"")
    
    # Check for 'pid-file' statement
    found = pid_file_pattern.findall(configuration)
    if not found:
        ret = fix_pid_file_statement()
        if ret:
            status = EXIT_FIXED
        else:
            log_slight_risk("Did not find the \"pid-file\" statement in the BIND9 configuration.")
            status = EXIT_FAIL

    # Check for 'session-keyfile' statement
    found = session_keyfile_pattern.findall(configuration)
    if not found:
        ret = fix_session_keyfile_statement()
        if ret:
            status = EXIT_FIXED
        else:
            log_slight_risk("Did not find the \"session-keyfile\" statement in the BIND9 configuration.")
            status = EXIT_FAIL
    
    if status == EXIT_FAIL:
        sol_text.add_solution(
"""No 'pid-file' and/or 'session-keyfile' statement found:
-------------------------------------------------------
The directory used by named for runtime data has been moved from the BIND
default location '/var/run/named/' to a new location '/run/named/'.
As a result, the PID file has been moved from the default location
'/var/run/named/named.pid' to a new location '/run/named/named.pid'.
In addition, the session-key file has been moved to '/run/named/session.key'.
These locations need to be specified by statements in the options section.
To fix this, add the following statements into the options section of your BIND9 configuration:
- 'pid-file  "/run/named/named.pid";'
- 'session-keyfile  "/run/named/session.key";'""")
    else:
        sol_text.add_solution(
"""[FIXED] No 'pid-file' or 'session-keyfile' statement found:
-------------------------------------------------------
The directory used by named for runtime data has been moved from the BIND
default location '/var/run/named/' to a new location '/run/named/'.
As a result, the PID file has been moved from the default location
'/var/run/named/named.pid' to a new location '/run/named/named.pid'.
In addition, the session-key file has been moved to '/run/named/session.key'.
These locations need to be specified by statements in the options section.
To fix this, we added the following statements into the options section of your BIND9 configuration:
- 'pid-file  "/run/named/named.pid";'
- 'session-keyfile  "/run/named/session.key";'""")

    return status


#######################################################
### CONFIGURATION CHECKS PART - END
#######################################################
### CONFIGURATION fixes PART - BEGIN
#######################################################

def fix_pid_file_statement():
    """
    Adds the 'pid-file' statement into the named.conf file
    """
    try:
        new_config = FIXED_CONFIGS[CONFIG_FILE]
    except KeyError:
        try:
            f = open(CONFIG_FILE, "r")
            new_config = f.read()
            f.close()
        except IOError:
            raise

    options_pattern = re.compile("options\s+\{(([^\{\}]*)|(\{[^\{\}]*\};)|(.*?))*\};", re.DOTALL)
    matches = re.finditer(options_pattern, new_config)

    match = None
    for m in matches:
        match = m
        break

    if match:
        new_config = new_config[0:match.end()-2] + '\tpid-file "/run/named/named.pid";\n' + new_config[match.end()-2:]
        FIXED_CONFIGS[CONFIG_FILE] = new_config
        return True
    else:
        return False


def fix_session_keyfile_statement():
    """
    Adds the 'session-keyfile' statement into the named.conf file
    """
    try:
        new_config = FIXED_CONFIGS[CONFIG_FILE]
    except KeyError:
        try:
            f = open(CONFIG_FILE, "r")
            new_config = f.read()
            f.close()
        except IOError:
            raise

    options_pattern = re.compile("options\s+\{(([^\{\}]*)|(\{[^\{\}]*\};)|(.*?))*\};", re.DOTALL)
    matches = re.finditer(options_pattern, new_config)

    match = None
    for m in matches:
        match = m
        break

    if match:
        new_config = new_config[0:match.end()-2] + '\tsession-keyfile  "/run/named/session.key";\n' + new_config[match.end()-2:]
        FIXED_CONFIGS[CONFIG_FILE] = new_config
        return True
    else:
        return False


#######################################################
### CONFIGURATION fixes PART - END
#######################################################

def write_fixed_configs_to_disk(result):
    """
    Writes fixed configs in the respective directoriess
    """
    if result > EXIT_FIXED:
        output_dir = os.path.join(VALUE_TMP_PREUPGRADE, "dirtyconf")
        sol_text.add_solution("The configuration file(s) could not be fixed completely. There are still some issues that need a review.")
    else:
        output_dir = os.path.join(VALUE_TMP_PREUPGRADE, "cleanconf")
        sol_text.add_solution("The configuration file(s) have been completely fixed.")

    for path, buff in FIXED_CONFIGS.iteritems():
        curr_path = os.path.join(output_dir, path[1:])

        # create dirs to make sure they exist
        try:
            os.makedirs(os.path.dirname(curr_path))
        except OSError, e:
            # if the dir already exist (errno 17), pass
            if e.errno == 17:
                pass
            else:
                raise e

        try:
            f = open(curr_path, "w")
            f.write(buff)
            f.close()
            msg = "Written Fixed config file to '" + curr_path + "'"
            log_info(msg)
            sol_text.add_solution(msg)
        except IOError:
            pass


def is_config_changed():
    """
    Checks if configuration files changed
    """
    try:
        f = open(VALUE_ALLCHANGED, "r")
        files = f.read()
        f.close()
        for fpath, buff in FILES_TO_CHECK:
            found = re.findall(fpath, files)
            if found:
                return True
    except IOError:
        return False
    return False


def return_with_code(code):
    if code == EXIT_FAIL:
        exit_fail()
    elif code == EXIT_FIXED:
        exit_fixed()
    elif code == EXIT_NOT_APPLICABLE:
        exit_not_applicable()
    elif code == EXIT_PASS:
        exit_pass()
    elif code == EXIT_ERROR:
        exit_error()
    elif code == EXIT_INFORMATIONAL:
        exit_informational()
    else:
        exit_unknown()


def check_user(uid=0):
    """
    Checks if the effective user ID is the one passed as an argument
    """
    if os.geteuid() != uid:
        sys.stdout.write("Needs to be root.\n")
        log_error("The script needs to be run under the root account.")
        exit_error()


def remove_comments(string):
    """
    Removes the following types of comments from the passed string and returns it:
    // .*
    # .*
    /* (.|\n)* */
    """
    pattern = "(\/\*(.|\n)*\*\/)|(#.*\n)|(\/\/.*\n)"
    replacer = ""
    return re.sub(pattern, replacer, string)


def is_file_loaded(path=""):
    """
    Checks if the file with a given 'path' is already loaded in FILES_TO_CHECK
    """
    for f in FILES_TO_CHECK:
        if f.path == path:
            return True
    return False


def load_included_files():
    """
    Finds configuration files that are included in some configuration
    file, reads it, closes, and adds into the FILES_TO_CHECK list
    """
    pattern = re.compile("include\s*\"(.+?)\"\s*;")
    # find includes in all files
    for ch_file in FILES_TO_CHECK:
        includes = re.findall(pattern, ch_file.buffer)
        for include in includes:
            # don't include already loaded files -> prevent loops
            if is_file_loaded(include):
                continue
            try:
                f = open(include, 'r')
            except IOError:
                log_error("Cannot open the configuration file: \"" + include +
                          "\"" + "included by \"" + ch_file.path + "\"")
                exit_error()
            else:
                log_info("Include a statement found in \"" + ch_file.path + "\": " +
                         "loading file \"" + include + "\"")
                filtered_string = remove_comments(f.read())
                f.close()
                FILES_TO_CHECK.append(ConfFile(buffer=filtered_string,
                                               path=include))


def load_main_config():
    """
    Loads main CONFIG_FILE
    """
    try:
        f = open(CONFIG_FILE, 'r')
        log_info("Loading configuration file: \"" + CONFIG_FILE + "\"")
        filtered_string = remove_comments(f.read())
        f.close()
        FILES_TO_CHECK.append(ConfFile(buffer=filtered_string,
                                       path=CONFIG_FILE))
    except IOError:
        log_error(
            "Cannot open the configuration file: \"" + CONFIG_FILE + "\"")
        exit_error()


def main():
    check_user()
    load_main_config()
    load_included_files()
    # need to check also paths of included files
    if not is_config_changed():
        return_with_code(EXIT_PASS)    
    result = run_checks(FILES_TO_CHECK)
    # write the config into the respective dir
    write_fixed_configs_to_disk(result)
    # if there was some issue, write a solution text
    if result > EXIT_PASS:
        solution_file(sol_text.get_text())
    return_with_code(result)


if __name__ == "__main__":
    main()
