def test_parse_complex(self):
        cfg = NginxConfigParser(complex_config)
        tree = cfg.simplify()
        indexed_tree = cfg.tree

        # common structure
        assert_that(tree, has_key('http'))
        assert_that(tree, has_key('events'))

        # http
        http = tree['http']
        assert_that(http, has_key('server'))
        assert_that(http, has_key('upstream'))
        assert_that(http, has_key('include'))
        assert_that(http['server'], is_(instance_of(list)))
        assert_that(http['server'], has_length(11))

        # upstream
        upstream = http['upstream']
        assert_that(upstream, has_length(2))

        # ifs
        for server in http['server']:
            if server.get('listen', '') == '127.0.0.3:10122':
                assert_that(server, has_item('if'))

        # check index tree
        x1_location_index = indexed_tree['http'][0]['server'][0][0]['location']['/'][1]
        x2_return_index = indexed_tree['http'][0]['server'][1][0]['location']['/'][0]['return'][1]
        assert_that(cfg.index[x1_location_index], equal_to((0, 8)))  # root file, line number 8
        assert_that(cfg.index[x2_return_index], equal_to((0, 9)))  # root file, line number 9
    def test_parse_ssl(self):
        """
        This test case specifically checks to see that none of the excluded directives (SSL focused) are parsed.
        """

        cfg = NginxConfigParser(ssl_config)
        tree = cfg.simplify()

        assert_that(tree, has_key('server'))

        # ssl
        for directive in IGNORED_DIRECTIVES:
            assert_that(tree['server'][1], is_not(has_item(directive)))
    def test_parse_rewrites(self):
        cfg = NginxConfigParser(rewrites_config)
        tree = cfg.simplify()

        # common structure
        assert_that(tree, has_key('http'))

        # http
        http = tree['http']
        assert_that(http, has_key('server'))

        # rewrites
        for server in http['server']:
            if server.get('server_name', '') == 'mb.some.org localhost melchior melchior.some.org':
                assert_that(server, has_item('rewrite'))
    def test_parse_simple(self):
        cfg = NginxConfigParser(simple_config)
        tree = cfg.simplify()
        indexed_tree = cfg.tree

        # common structure
        assert_that(tree, has_key('http'))
        assert_that(tree, has_key('events'))

        # http
        http = tree['http']
        assert_that(http, has_key('server'))
        assert_that(http, has_key('types'))
        assert_that(http, has_key('include'))
        assert_that(http['server'], is_(instance_of(list)))
        assert_that(http['server'], has_length(2))

        # server
        server = http['server'][1]
        assert_that(server, has_key('listen'))
        assert_that(server, has_key('location'))
        assert_that(server['location'], is_(instance_of(dict)))

        # location
        location = server['location']
        assert_that(location, has_key('/basic_status'))

        # nested location
        assert_that(http['server'][0]['location']['/'], has_key('location'))

        # included mimes
        mimes = http['types']
        assert_that(mimes, has_key('application/java-archive'))

        # check index tree
        worker_connections_index = indexed_tree['events'][0]['worker_connections'][1]
        basic_status_index = indexed_tree['http'][0]['server'][1][0]['location']['/basic_status'][1]
        stub_status_in_basic_index = indexed_tree['http'][0]['server'][1][0]['location']['/basic_status'][0]['stub_status'][1]
        proxy_pass_index = indexed_tree['http'][0]['server'][0][0]['location']['/'][0]['proxy_pass'][1]

        assert_that(cfg.index[worker_connections_index], equal_to((0, 6)))  # root file, line number 6
        assert_that(cfg.index[basic_status_index], equal_to((0, 67)))  # root file, line number 65
        assert_that(cfg.index[stub_status_in_basic_index], equal_to((0, 69)))  # root file, line number 66
        assert_that(cfg.index[proxy_pass_index], equal_to((2, 13)))  # third loaded file, line number 13
    def test_parse_map_lua_perl(self):
        cfg = NginxConfigParser(map_lua_perl)
        tree = cfg.simplify()

        # common structure
        assert_that(tree, has_key('http'))

        # http
        http = tree['http']
        assert_that(http, has_key('server'))
        assert_that(http, has_key('map'))
        assert_that(http, has_key('perl_set'))

        # lua
        for server in http['server']:
            if server.get('server_name', '') == '127.0.0.1':
                assert_that(server, has_item('lua_shared_dict'))

                for location, data in server['location'].iteritems():
                    if location == '= /some/':
                        assert_that(data, has_item('rewrite_by_lua'))
    def test_parse_huge(self):
        cfg = NginxConfigParser(huge_config)
        tree = cfg.simplify()
        indexed_tree = cfg.tree

        # common structure
        assert_that(tree, has_key('http'))
        assert_that(tree, has_key('events'))

        # http
        http = tree['http']
        assert_that(http, has_key('server'))
        assert_that(http, has_key('include'))
        assert_that(http['server'], is_(instance_of(list)))
        assert_that(http['server'], has_length(8))

        # map
        http_map = http['map']
        assert_that(http_map, equal_to({'$dirname $diruri': {'default': '"dirindex.html"', 'include': ['"dir.map"']}}))

        # check index tree
        books_location_index = indexed_tree['http'][0]['server'][2][0]['location']['/books/'][1]
        assert_that(cfg.index[books_location_index], equal_to((0, 134)))  # root file, line number 134
    def __init__(self, filename, binary=None, prefix=None):
        self.filename = filename
        self.binary = binary
        self.prefix = prefix
        self.log_formats = {}
        self.access_logs = {}
        self.error_logs = []
        self.stub_status = []
        self.plus_status = []
        self.parser = NginxConfigParser(filename)

        # initial parsing
        self.tree = self.parser.tree
        self.files = self.parser.files
        self.index = self.parser.index
        self.parser_errors = self.parser.errors
        self.test_errors = []

        # go through and collect all logical data
        self.__recursive_search(subtree=self.parser.simplify())

        # try to locate and use default logs (PREFIX/logs/*)
        self.add_default_logs()
class NginxConfig(object):
    """
    Nginx config representation
    Parses configs with all includes, etc

    Main tasks:
    - find all log formats
    - find all access logs
    - find all error logs
    - find stub_status url
    """

    def __init__(self, filename, binary=None, prefix=None):
        self.filename = filename
        self.binary = binary
        self.prefix = prefix
        self.log_formats = {}
        self.access_logs = {}
        self.error_logs = []
        self.stub_status = []
        self.plus_status = []
        self.parser = NginxConfigParser(filename)

        # initial parsing
        self.tree = self.parser.tree
        self.files = self.parser.files
        self.index = self.parser.index
        self.parser_errors = self.parser.errors
        self.test_errors = []

        # go through and collect all logical data
        self.__recursive_search(subtree=self.parser.simplify())

        # try to locate and use default logs (PREFIX/logs/*)
        self.add_default_logs()

    def __recursive_search(self, subtree=None, ctx=None):
        """
        Searches needed data in config's tree

        :param subtree: dict with tree to parse
        :param ctx: dict with context
        """
        ctx = ctx if ctx is not None else {}
        subtree = subtree if subtree is not None else {}

        for key, value in subtree.iteritems():
            if key == "error_log":
                error_logs = value if isinstance(value, list) else [value]
                for er_log_definition in error_logs:
                    if er_log_definition == "off":
                        continue

                    log_name = er_log_definition.split(" ")[0]
                    log_name = re.sub("['\"]", "", log_name)  # remove all ' and "
                    if log_name.startswith("syslog"):
                        continue
                    elif not log_name.startswith("/"):
                        log_name = "%s/%s" % (self.prefix, log_name)

                    if log_name not in self.error_logs:
                        self.error_logs.append(log_name)
            elif key == "access_log":
                access_logs = value if isinstance(value, list) else [value]
                for ac_log_definition in access_logs:
                    if ac_log_definition == "off":
                        continue

                    parts = filter(lambda x: x, ac_log_definition.split(" "))
                    log_format = None if len(parts) == 1 else parts[1]
                    log_name = parts[0]
                    log_name = re.sub("['\"]", "", log_name)  # remove all ' and "

                    if log_name.startswith("syslog"):
                        continue
                    elif not log_name.startswith("/"):
                        log_name = "%s/%s" % (self.prefix, log_name)

                    self.access_logs[log_name] = log_format
            elif key == "log_format":
                for k, v in value.iteritems():
                    self.log_formats[k] = v
            elif key == "server" and isinstance(value, list) and "upstream" not in ctx:
                for server in value:

                    current_ctx = copy.copy(ctx)
                    if server.get("listen") is None:
                        # if no listens specified, then use default *:80 and *:8000
                        listen = ["80", "8000"]
                    else:
                        listen = server.get("listen")
                    listen = listen if isinstance(listen, list) else [listen]

                    ctx["ip_port"] = []
                    for item in listen:
                        listen_first_part = item.split(" ")[0]
                        addr, port = self.__parse_listen(listen_first_part)
                        if addr in ("*", "0.0.0.0"):
                            addr = "127.0.0.1"
                        elif addr == "[::]":
                            addr = "[::1]"
                        ctx["ip_port"].append((addr, port))

                    if "server_name" in server:
                        ctx["server_name"] = server.get("server_name")

                    self.__recursive_search(subtree=server, ctx=ctx)
                    ctx = current_ctx
            elif key == "upstream":
                for upstream, upstream_info in value.iteritems():
                    current_ctx = copy.copy(ctx)
                    ctx["upstream"] = upstream
                    self.__recursive_search(subtree=upstream_info, ctx=ctx)
                    ctx = current_ctx
            elif key == "location":
                for location, location_info in value.iteritems():
                    current_ctx = copy.copy(ctx)
                    ctx["location"] = location
                    self.__recursive_search(subtree=location_info, ctx=ctx)
                    ctx = current_ctx
            elif key == "stub_status" and ctx and "ip_port" in ctx:
                for url in self.__status_url(ctx):
                    if url not in self.stub_status:
                        self.stub_status.append(url)
            elif key == "status" and ctx and "ip_port" in ctx:
                for url in self.__status_url(ctx, server_preferred=True):
                    if url not in self.plus_status:
                        self.plus_status.append(url)
            elif isinstance(value, dict):
                self.__recursive_search(subtree=value, ctx=ctx)
            elif isinstance(value, list):
                for next_subtree in value:
                    if isinstance(next_subtree, dict):
                        self.__recursive_search(subtree=next_subtree, ctx=ctx)

    def __status_url(self, ctx, server_preferred=False):
        result = []
        location = ctx.get("location", "/")

        # remove all modifiers
        location_parts = location.split(" ")
        final_location_part = location_parts[-1]

        for ip_port in ctx.get("ip_port"):
            addr, port = ip_port
            if server_preferred and "server_name" in ctx:
                if isinstance(ctx["server_name"], list):
                    addr = ctx["server_name"][0].split(" ")[0]
                else:
                    addr = ctx["server_name"].split(" ")[0]

            result.append("%s:%s%s" % (addr, port, final_location_part))

        return result

    def run_test(self):
        """
        Tests the configuration using nginx -t
        Saves event info if syntax check was not successful
        """
        start_time = time.time()
        context.log.info("running %s -t -c %s" % (self.binary, self.filename))
        if self.binary:
            try:
                _, nginx_t_err = subp.call("%s -t -c %s" % (self.binary, self.filename), check=False)
                for line in nginx_t_err:
                    if "syntax is" in line and "syntax is ok" not in line:
                        self.test_errors.append(line)
            except Exception, e:
                exception_name = e.__class__.__name__
                context.log.error("failed to %s -t -c %s due to %s" % (self.binary, self.filename, exception_name))
                context.log.debug("additional info:", exc_info=True)
        end_time = time.time()
        return end_time - start_time