from __future__ import absolute_import
import itertools
import socket
import time
import sys
import re
import os

if sys.version_info >= (3, 0, 0,):
    from collections.abc import MutableSequence
else:
    ## This syntax is not supported in Python 3...
    from collections import MutableSequence

from ciscoconfparse.protocol_values import ASA_TCP_PORTS, ASA_UDP_PORTS
from dns.exception import DNSException
from dns.resolver import Resolver
from dns import reversename, query, zone

if sys.version_info[0] < 3:
    from ipaddr import IPv4Network, IPv6Network, IPv4Address, IPv6Address
else:
    from ipaddress import IPv4Network, IPv6Network, IPv4Address, IPv6Address
""" ccp_util.py - Parse, Query, Build, and Modify IOS-style configurations
     Copyright (C) 2014-2015, 2018-2019 David Michael Pennington

     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/>.

     If you need to contact the author, you can do so by emailing:
     mike [~at~] pennington [/dot\] net
"""

_IPV6_REGEX_STR = r"""(?!:::\S+?$)       # Negative Lookahead for 3 colons
 (?P<addr>                               # Begin a group named 'addr'
 (?P<opt1>{0}(?::{0}){{7}})              # no double colons, option 1
|(?P<opt2>(?:{0}:){{1}}(?::{0}){{1,6}})  # match fe80::1
|(?P<opt3>(?:{0}:){{2}}(?::{0}){{1,5}})  # match fe80:a::1
|(?P<opt4>(?:{0}:){{3}}(?::{0}){{1,4}})  # match fe80:a:b::1
|(?P<opt5>(?:{0}:){{4}}(?::{0}){{1,3}})  # match fe80:a:b:c::1
|(?P<opt6>(?:{0}:){{5}}(?::{0}){{1,2}})  # match fe80:a:b:c:d::1
|(?P<opt7>(?:{0}:){{6}}(?::{0}){{1,1}})  # match fe80:a:b:c:d:e::1
|(?P<opt8>:(?::{0}){{1,7}})              # leading double colons
|(?P<opt9>(?:{0}:){{1,7}}:)              # trailing double colons
|(?P<opt10>(?:::))                       # bare double colons (default route)
)                                        # End group named 'addr'
""".format(
    r"[0-9a-fA-F]{1,4}"
)
_IPV6_REGEX_STR_COMPRESSED1 = r"""(?!:::\S+?$)(?P<addr1>(?P<opt1_1>{0}(?::{0}){{7}})|(?P<opt1_2>(?:{0}:){{1}}(?::{0}){{1,6}})|(?P<opt1_3>(?:{0}:){{2}}(?::{0}){{1,5}})|(?P<opt1_4>(?:{0}:){{3}}(?::{0}){{1,4}})|(?P<opt1_5>(?:{0}:){{4}}(?::{0}){{1,3}})|(?P<opt1_6>(?:{0}:){{5}}(?::{0}){{1,2}})|(?P<opt1_7>(?:{0}:){{6}}(?::{0}){{1,1}})|(?P<opt1_8>:(?::{0}){{1,7}})|(?P<opt1_9>(?:{0}:){{1,7}}:)|(?P<opt1_10>(?:::)))""".format(
    r"[0-9a-fA-F]{1,4}"
)
_IPV6_REGEX_STR_COMPRESSED2 = r"""(?!:::\S+?$)(?P<addr2>(?P<opt2_1>{0}(?::{0}){{7}})|(?P<opt2_2>(?:{0}:){{1}}(?::{0}){{1,6}})|(?P<opt2_3>(?:{0}:){{2}}(?::{0}){{1,5}})|(?P<opt2_4>(?:{0}:){{3}}(?::{0}){{1,4}})|(?P<opt2_5>(?:{0}:){{4}}(?::{0}){{1,3}})|(?P<opt2_6>(?:{0}:){{5}}(?::{0}){{1,2}})|(?P<opt2_7>(?:{0}:){{6}}(?::{0}){{1,1}})|(?P<opt2_8>:(?::{0}){{1,7}})|(?P<opt2_9>(?:{0}:){{1,7}}:)|(?P<opt2_10>(?:::)))""".format(
    r"[0-9a-fA-F]{1,4}"
)
_IPV6_REGEX_STR_COMPRESSED3 = r"""(?!:::\S+?$)(?P<addr3>(?P<opt3_1>{0}(?::{0}){{7}})|(?P<opt3_2>(?:{0}:){{1}}(?::{0}){{1,6}})|(?P<opt3_3>(?:{0}:){{2}}(?::{0}){{1,5}})|(?P<opt3_4>(?:{0}:){{3}}(?::{0}){{1,4}})|(?P<opt3_5>(?:{0}:){{4}}(?::{0}){{1,3}})|(?P<opt3_6>(?:{0}:){{5}}(?::{0}){{1,2}})|(?P<opt3_7>(?:{0}:){{6}}(?::{0}){{1,1}})|(?P<opt3_8>:(?::{0}){{1,7}})|(?P<opt3_9>(?:{0}:){{1,7}}:)|(?P<opt3_10>(?:::)))""".format(
    r"[0-9a-fA-F]{1,4}"
)

_CISCO_RANGE_ATOM_STR = r"""\d+\s*\-*\s*\d*"""
_CISCO_RANGE_STR = r"""^(?P<line_prefix>[a-zA-Z\s]*)(?P<slot_prefix>[\d\/]*\d+\/)*(?P<range_text>(\s*{0})*)$""".format(
    _CISCO_RANGE_ATOM_STR
)

_RGX_IPV6ADDR = re.compile(_IPV6_REGEX_STR, re.VERBOSE)

_RGX_IPV4ADDR = re.compile(r"^(?P<addr>\d+\.\d+\.\d+\.\d+)")
_RGX_IPV4ADDR_NETMASK = re.compile(
    r"""
     (?:
       ^(?P<addr0>\d+\.\d+\.\d+\.\d+)$
      |(?:^
         (?:(?P<addr1>\d+\.\d+\.\d+\.\d+))(?:\s+|\/)(?:(?P<netmask>\d+\.\d+\.\d+\.\d+))
       $)
      |^(?:\s*(?P<addr2>\d+\.\d+\.\d+\.\d+)(?:\/(?P<masklen>\d+))\s*)$
    )
    """,
    re.VERBOSE,
)

_RGX_CISCO_RANGE = re.compile(_CISCO_RANGE_STR)


def is_valid_ipv4_addr(input=""):
    """Check if this is a valid IPv4 string"""
    assert input != ""
    if _RGX_IPV4ADDR.search(input):
        return True
    return False


def is_valid_ipv6_addr(input=""):
    """Check if this is a valid IPv6 string"""
    assert input != ""
    if _RGX_IPV6ADDR.search(input):
        return True
    return False


## Emulate the old behavior of ipaddr.IPv4Network in Python2, which can use
##    IPv4Network with a host address.  Google removed that in Python3's
##    ipaddress.py module
class IPv4Obj(object):
    """An object to represent IPv4 addresses and IPv4Networks.  When :class:`~ccp_util.IPv4Obj` objects are compared or sorted, shorter masks are greater than longer masks. After comparing mask length, numerically higher IP addresses are greater than numerically lower IP addresses.

    Kwargs:
        - arg (str): A string containing an IPv4 address, and optionally a netmask or masklength.  The following address/netmask formats are supported: "10.1.1.1/24", "10.1.1.1 255.255.255.0", "10.1.1.1/255.255.255.0"

    Attributes:
        - network_object : An IPv4Network object
        - ip_object  : An IPv4Address object
        - ip : An IPv4Address object
        - as_binary_tuple (tuple): The address as a tuple of zero-padded binary strings
        - as_hex_tuple (tuple): The address as a tuple of zero-padded 8-bit hex strings
        - as_decimal (int): The ip address as a decimal integer
        - network : An IPv4Network object
        - network_object : An IPv4Network object
        - netmask (str): A string representing the netmask
        - prefixlen (int): An integer representing the length of the netmask
        - prefixlength (int): An integer representing the length of the netmask
        - broadcast (str): A string representing the broadcast address
        - hostmask (str): A string representing the hostmask
        - numhosts (int): An integer representing the number of hosts contained in the network

    Returns:
        - an instance of :class:`~ccp_util.IPv4Obj`.

>>> from ciscoconfparse.ccp_util import IPv4Obj
>>> net = IPv4Obj('172.16.1.0/24')
>>> net
<IPv4Obj 172.16.1.0/24>
>>> net.ip
IPv4Address('172.16.1.0')
>>> net.ip + 1
IPv4Address('172.16.1.1')
>>> str(net.ip+1)
'172.16.1.1'
>>> net.network
IPv4Address('172.16.1.0')
>>> net.network_object
IPv4Network('172.16.1.0/24')
>>> str(net.network_object)
'172.16.1.0/24'
>>> net.prefixlen
24
>>> net.network_object.iterhosts()
<generator object iterhosts at 0x7f00bfcce730>
>>>
    """

    def __init__(self, arg="127.0.0.1/32", strict=False):

        # RGX_IPV4ADDR = re.compile(r'^(\d+\.\d+\.\d+\.\d+)')
        # RGX_IPV4ADDR_NETMASK = re.compile(r'(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)')

        self.arg = arg
        self.dna = "IPv4Obj"
        try:
            mm = _RGX_IPV4ADDR_NETMASK.search(arg)
        except TypeError:
            if getattr(arg, "dna", "") == "IPv4Obj":
                ip_str = "{0}/{1}".format(str(arg.ip_object), arg.prefixlen)
                self.network_object = IPv4Network(ip_str, strict=False)
                self.ip_object = IPv4Address(str(arg.ip_object))
                return None
            elif isinstance(arg, IPv4Network):
                self.network_object = arg
                self.ip_object = IPv4Address(str(arg).split("/")[0])
                return None
            elif isinstance(arg, IPv4Address):
                self.network_object = IPv4Network(str(arg) + "/32")
                self.ip_object = IPv4Address(str(arg).split("/")[0])
                return None
            elif isinstance(arg, int):
                self.ip_object = IPv4Address(arg)
                self.network_object = IPv4Network(
                    str(self.ip_object) + "/32", strict=False
                )
                return None
            else:
                raise ValueError(
                    "IPv4Obj doesn't understand how to parse {0}".format(arg)
                )

        ERROR = "IPv4Obj couldn't parse '{0}'".format(arg)
        assert not (mm is None), ERROR

        mm_result = mm.groupdict()
        addr = (
            mm_result["addr0"]
            or mm_result["addr1"]
            or mm_result["addr2"]
            or "127.0.0.1"
        )

        ## Normalize addr if we get zero-padded strings, i.e. 172.001.001.001
        addr = ".".join([str(int(ii)) for ii in addr.split(".")])

        masklen = int(mm_result["masklen"] or 32)
        netmask = mm_result["netmask"]
        if netmask:
            ## ALWAYS check for the netmask first
            self.network_object = IPv4Network(
                "{0}/{1}".format(addr, netmask), strict=strict
            )
            self.ip_object = IPv4Address("{0}".format(addr))
        else:
            self.network_object = IPv4Network(
                "{0}/{1}".format(addr, masklen), strict=strict
            )
            self.ip_object = IPv4Address("{0}".format(addr))

    def __repr__(self):
        return """<IPv4Obj {0}/{1}>""".format(str(self.ip_object), self.prefixlen)

    def __eq__(self, val):
        try:
            if self.network_object == val.network_object:
                return True
            return False
        except (Exception) as e:
            errmsg = "'{0}' cannot compare itself to '{1}': {2}".format(
                self.__repr__(), val, e
            )
            raise ValueError(errmsg)

    def __gt__(self, val):
        try:
            val_in_self = self.__contains__(val)
            val_nobj = getattr(val, "network_object")
            self_nobj = getattr(self, "network_object")
            val_dec = getattr(val, "as_decimal")
            self_dec = getattr(self, "as_decimal")

            if val_in_self:
                # Sort shorter masks as lower...
                return False
            elif self_nobj == val_nobj:
                return self_dec > val_dec
            else:
                return self_nobj > val_nobj
        except:
            errmsg = "{0} cannot compare itself to '{1}'".format(self.__repr__(), val)
            raise ValueError(errmsg)

    def __lt__(self, val):
        try:
            val_in_self = self.__contains__(val)
            val_nobj = getattr(val, "network_object")
            self_nobj = getattr(self, "network_object")
            val_dec = getattr(val, "as_decimal")
            self_dec = getattr(self, "as_decimal")

            if val_in_self:
                # Sort shorter masks as lower...
                return True
            elif self_nobj == val_nobj:
                return self_dec < val_dec
            else:
                return self_nobj < val_nobj
        except:
            errmsg = "{0} cannot compare itself to '{1}'".format(self.__repr__(), val)
            raise ValueError(errmsg)

    def __int__(self):
        """Return this object as an integer"""
        return self.as_decimal

    def __index__(self):
        """Return this object as an integer (used for hex() and bin() operations)"""
        return self.as_decimal

    def __add__(self, val):
        """Add an integer to IPv4Obj() and return an IPv4Obj()"""
        assert isinstance(val, int), "Cannot add type: '{}' to {}".format(
            type(val), self
        )
        orig_prefixlen = self.prefixlen
        total = self.as_decimal + val
        assert total <= 4294967295, "Max IPv4 integer exceeded"
        assert total >= 0, "Min IPv4 integer exceeded"
        retval = IPv4Obj(total)
        retval.prefixlen = orig_prefixlen
        return retval

    def __sub__(self, val):
        """Subtract an integer from IPv4Obj() and return an IPv4Obj()"""
        assert isinstance(val, int), "Cannot subtract type: '{}' from {}".format(
            type(val), self
        )
        orig_prefixlen = self.prefixlen
        total = self.as_decimal - val
        assert total <= 4294967295, "Max IPv4 integer exceeded"
        assert total >= 0, "Min IPv4 integer exceeded"
        retval = IPv4Obj(total)
        retval.prefixlen = orig_prefixlen
        return retval

    def __contains__(self, val):
        # Used for "foo in bar"... python calls bar.__contains__(foo)
        try:
            if self.network_object.prefixlen == 0:
                return True
            elif self.network_object.prefixlen > val.network_object.prefixlen:
                # obvious shortcut... if this object's mask is longer than
                #    val, this object cannot contain val
                return False
            else:
                # return (val.network in self.network)
                return (self.network <= val.network) and (
                    self.broadcast >= val.broadcast
                )

        except (Exception) as e:
            raise ValueError(
                "Could not check whether '{0}' is contained in '{1}': {2}".format(
                    val, self, e
                )
            )

    def __hash__(self):
        # Python3 needs __hash__()
        return hash(str(self.ip_object)) + hash(str(self.prefixlen))

    def __iter__(self):
        return self.network_object.__iter__()

    def __next__(self):
        ## For Python3 iteration...
        return self.network_object.__next__()

    def next(self):
        ## For Python2 iteration...
        return self.network_object.__next__()

    @property
    def ip(self):
        """Returns the address as an IPv4Address object."""
        return self.ip_object

    @property
    def netmask(self):
        """Returns the network mask as an IPv4Address object."""
        return self.network_object.netmask

    @property
    def prefixlen(self):
        """Returns the length of the network mask as an integer."""
        return self.network_object.prefixlen

    @prefixlen.setter
    def prefixlen(self, arg):
        """prefixlen setter method"""
        self.network_object = IPv4Network(
            "{0}/{1}".format(str(self.ip_object), arg), strict=False
        )

    @property
    def prefixlength(self):
        """Returns the length of the network mask as an integer."""
        return self.prefixlen

    @property
    def exploded(self):
        """Returns the IPv4 object in exploded form"""
        return self.ip_object.exploded

    @property
    def packed(self):
        """Returns the IPv4 object in packed binary form"""
        return self.ip_object.packed

    @property
    def broadcast(self):
        """Returns the broadcast address as an IPv4Address object."""
        if sys.version_info[0] < 3:
            return self.network_object.broadcast
        else:
            return self.network_object.broadcast_address

    @property
    def network(self):
        """Returns an IPv4Network object, which represents this network.
        """
        if sys.version_info[0] < 3:
            return self.network_object.network
        else:
            ## The ipaddress module returns an "IPAddress" object in Python3...
            return IPv4Network("{0}".format(self.network_object.compressed))

    @property
    def hostmask(self):
        """Returns the host mask as an IPv4Address object."""
        return self.network_object.hostmask

    @property
    def version(self):
        """Returns the version of the object as an integer.  i.e. 4"""
        return 4

    @property
    def numhosts(self):
        """Returns the total number of IP addresses in this network, including broadcast and the "subnet zero" address"""
        if sys.version_info[0] < 3:
            return self.network_object.numhosts
        else:
            return 2 ** (32 - self.network_object.prefixlen)

    @property
    def as_decimal(self):
        """Returns the IP address as a decimal integer"""
        num_strings = str(self.ip).split(".")
        num_strings.reverse()  # reverse the order
        return sum([int(num) * (256 ** idx) for idx, num in enumerate(num_strings)])

    @property
    def as_zeropadded(self):
        """Returns the IP address as a zero-padded string (useful when sorting)"""
        num_strings = str(self.ip).split(".")
        return ".".join(["{0:03}".format(int(num)) for num in num_strings])

    @property
    def as_zeropadded_network(self):
        """Returns the IP network as a zero-padded string (useful when sorting)"""
        num_strings = str(self.network).split(".")
        return ".".join(["{0:03}".format(int(num)) for num in num_strings])

    @property
    def as_binary_tuple(self):
        """Returns the IP address as a tuple of zero-padded binary strings"""
        return tuple(["{0:08b}".format(int(num)) for num in str(self.ip).split(".")])

    @property
    def as_hex_tuple(self):
        """Returns the IP address as a tuple of zero-padded hex strings"""
        return tuple(["{0:02x}".format(int(num)) for num in str(self.ip).split(".")])

    @property
    def as_cidr_addr(self):
        """Returns a string with the address in CIDR notation"""
        return str(self.ip) + "/" + str(self.prefixlen)

    @property
    def as_cidr_net(self):
        """Returns a string with the network in CIDR notation"""
        return str(self.network)

    @property
    def is_multicast(self):
        """Returns a boolean for whether this is a multicast address"""
        return self.network_object.is_multicast

    @property
    def is_private(self):
        """Returns a boolean for whether this is a private address"""
        return self.network_object.is_private

    @property
    def is_reserved(self):
        """Returns a boolean for whether this is a reserved address"""
        return self.network_object.is_reserved


## Emulate the old behavior of ipaddr.IPv6Network in Python2, which can use
##    IPv6Network with a host address.  Google removed that in Python3's
##    ipaddress.py module
class IPv6Obj(object):
    """An object to represent IPv6 addresses and IPv6Networks.  When :class:`~ccp_util.IPv6Obj` objects are compared or sorted, shorter masks are greater than longer masks. After comparing mask length, numerically higher IP addresses are greater than numerically lower IP addresses.

    Kwargs:
        - arg (str): A string containing an IPv6 address, and optionally a netmask or masklength.  The following address/netmask formats are supported: "2001::dead:beef", "2001::dead:beef/64",

    Attributes:
        - network_object : An IPv6Network object
        - ip_object  : An IPv6Address object
        - ip : An IPv6Address object
        - as_binary_tuple (tuple): The ipv6 address as a tuple of zero-padded binary strings
        - as_decimal (int): The ipv6 address as a decimal integer
        - as_hex_tuple (tuple): The ipv6 address as a tuple of zero-padded 8-bit hex strings
        - network (str): A string representing the network address
        - netmask (str): A string representing the netmask
        - prefixlen (int): An integer representing the length of the netmask
        - broadcast: raises `NotImplementedError`; IPv6 doesn't use broadcast
        - hostmask (str): A string representing the hostmask
        - numhosts (int): An integer representing the number of hosts contained in the network

    Returns:
        - an instance of :class:`~ccp_util.IPv6Obj`.

    """

    def __init__(self, arg="::1/128", strict=False):

        # arg= _RGX_IPV6ADDR_NETMASK.sub(r'\1/\2', arg) # mangle IOS: 'addr mask'
        self.arg = arg
        self.dna = "IPv6Obj"

        try:
            mm = _RGX_IPV6ADDR.search(arg)
        except TypeError:
            if getattr(arg, "dna", "") == "IPv6Obj":
                ip_str = "{0}/{1}".format(str(arg.ip_object), arg.prefixlen)
                self.network_object = IPv6Network(ip_str, strict=False)
                self.ip_object = IPv6Address(str(arg.ip_object))
                return None
            elif isinstance(arg, IPv6Network):
                self.network_object = arg
                self.ip_object = IPv6Address(str(arg).split("/")[0])
                return None
            elif isinstance(arg, IPv6Address):
                self.network_object = IPv6Network(str(arg) + "/128")
                self.ip_object = IPv6Address(str(arg).split("/")[0])
                return None
            elif isinstance(arg, int):
                self.ip_object = IPv6Address(arg)
                self.network_object = IPv6Network(
                    str(self.ip_object) + "/128", strict=False
                )
                return None
            else:
                raise ValueError(
                    "IPv6Obj doesn't understand how to parse {0}".format(arg)
                )

        assert not (mm is None), "IPv6Obj couldn't parse {0}".format(arg)
        self.network_object = IPv6Network(arg, strict=strict)
        self.ip_object = IPv6Address(mm.group(1))

    # 'address_exclude', 'compare_networks', 'hostmask', 'ipv4_mapped', 'iter_subnets', 'iterhosts', 'masked', 'max_prefixlen', 'netmask', 'network', 'numhosts', 'overlaps', 'prefixlen', 'sixtofour', 'subnet', 'supernet', 'teredo', 'with_hostmask', 'with_netmask', 'with_prefixlen'

    def __repr__(self):
        return """<IPv6Obj {0}/{1}>""".format(str(self.ip_object), self.prefixlen)

    def __eq__(self, val):
        try:
            if self.network_object == val.network_object:
                return True
            return False
        except (Exception) as e:
            errmsg = "'{0}' cannot compare itself to '{1}': {2}".format(
                self.__repr__(), val, e
            )
            raise ValueError(errmsg)

    def __gt__(self, val):
        try:
            val_in_self = self.__contains__(val)
            val_nobj = getattr(val, "network_object")
            self_nobj = getattr(self, "network_object")
            val_dec = getattr(val, "as_decimal")
            self_dec = getattr(self, "as_decimal")

            if val_in_self:
                # Sort shorter masks as higher...
                return False
            elif self_nobj == val_nobj:
                return self_dec > val_dec
            else:
                return self_nobj > val_nobj
        except:
            errmsg = "{0} cannot compare itself to '{1}'".format(self.__repr__(), val)
            raise ValueError(errmsg)

    def __lt__(self, val):
        try:
            val_in_self = self.__contains__(val)
            val_nobj = getattr(val, "network_object")
            self_nobj = getattr(self, "network_object")
            val_dec = getattr(val, "as_decimal")
            self_dec = getattr(self, "as_decimal")

            if val_in_self:
                # Sort shorter masks as higher...
                return True
            elif self_nobj == val_nobj:
                return self_dec < val_dec
            else:
                return self_nobj < val_nobj
        except:
            errmsg = "{0} cannot compare itself to '{1}'".format(self.__repr__(), val)
            raise ValueError(errmsg)

    def __int__(self):
        """Return this object as an integer"""
        return self.as_decimal

    def __index__(self):
        """Return this object as an integer (used for hex() and bin() operations)"""
        return self.as_decimal

    def __add__(self, val):
        """Add an integer to IPv6Obj() and return an IPv6Obj()"""
        assert isinstance(val, int), "Cannot add type: '{}' to {}".format(
            type(val), self
        )
        orig_prefixlen = self.prefixlen
        total = self.as_decimal + val
        error = "Max IPv6 integer exceeded"
        assert total <= 340282366920938463463374607431768211455, error
        assert total >= 0, "Min IPv4 integer exceeded"
        retval = IPv6Obj(total)
        retval.prefixlen = orig_prefixlen
        return retval

    def __sub__(self, val):
        """Subtract an integer from IPv6Obj() and return an IPv6Obj()"""
        assert isinstance(val, int), "Cannot subtract type: '{}' from {}".format(
            type(val), self
        )
        orig_prefixlen = self.prefixlen
        total = self.as_decimal - val
        error = "Max IPv6 integer exceeded"
        assert total <= 340282366920938463463374607431768211455, error
        assert total >= 0, "Min IPv4 integer exceeded"
        retval = IPv6Obj(total)
        retval.prefixlen = orig_prefixlen
        return retval

    def __contains__(self, val):
        # Used for "foo in bar"... python calls bar.__contains__(foo)
        try:
            if self.network_object.prefixlen == 0:
                return True
            elif self.network_object.prefixlen > val.network_object.prefixlen:
                # obvious shortcut... if this object's mask is longer than
                #    val, this object cannot contain val
                return False
            else:
                # NOTE: We cannot use the same algorithm as IPv4Obj.__contains__() because IPv6Obj doesn't have .broadcast
                # return (val.network in self.network)
                return (self.network <= val.network) and (
                    (self.as_decimal + self.numhosts - 1)
                    >= (val.as_decimal + val.numhosts - 1)
                )

        except (Exception) as e:
            raise ValueError(
                "Could not check whether '{0}' is contained in '{1}': {2}".format(
                    val, self, e
                )
            )

    def __hash__(self):
        # Python3 needs __hash__()
        return hash(str(self.ip_object)) + hash(str(self.prefixlen))

    def __iter__(self):
        return self.network_object.__iter__()

    def __next__(self):
        ## For Python3 iteration...
        return self.network_object.__next__()

    def next(self):
        ## For Python2 iteration...
        return self.network_object.__next__()

    @property
    def ip(self):
        """Returns the address as an IPv6Address object."""
        return self.ip_object

    @property
    def netmask(self):
        """Returns the network mask as an IPv6Address object."""
        return self.network_object.netmask

    @property
    def prefixlen(self):
        """Returns the length of the network mask as an integer."""
        return self.network_object.prefixlen

    @prefixlen.setter
    def prefixlen(self, arg):
        """prefixlen setter method"""
        self.network_object = IPv6Network(
            "{0}/{1}".format(str(self.ip_object), arg), strict=False
        )

    @property
    def prefixlength(self):
        """Returns the length of the network mask as an integer."""
        return self.prefixlen

    @property
    def compressed(self):
        """Returns the IPv6 object in compressed form"""
        return self.network_object.compressed

    @property
    def exploded(self):
        """Returns the IPv6 object in exploded form"""
        return self.ip_object.exploded

    @property
    def packed(self):
        """Returns the IPv6 object in packed binary form"""
        return self.ip_object.packed

    @property
    def broadcast(self):
        raise NotImplementedError("IPv6 does not have broadcasts")

    @property
    def network(self):
        """Returns an IPv6Network object, which represents this network.
        """
        if sys.version_info[0] < 3:
            return self.network_object.network
        else:
            ## The ipaddress module returns an "IPAddress" object in Python3...
            return IPv6Network("{0}".format(self.network_object.compressed))

    @property
    def hostmask(self):
        """Returns the host mask as an IPv6Address object."""
        return self.network_object.hostmask

    @property
    def version(self):
        """Returns the version of the object as an integer.  i.e. 4"""
        return 6

    @property
    def numhosts(self):
        """Returns the total number of IP addresses in this network, including broadcast and the "subnet zero" address"""
        if sys.version_info[0] < 3:
            return self.network_object.numhosts
        else:
            return 2 ** (128 - self.network_object.prefixlen)

    @property
    def as_decimal(self):
        """Returns the IP address as a decimal integer"""
        num_strings = str(self.ip.exploded).split(":")
        num_strings.reverse()  # reverse the order
        return sum(
            [int(num, 16) * (65536 ** idx) for idx, num in enumerate(num_strings)]
        )

    @property
    def as_binary_tuple(self):
        """Returns the IPv6 address as a tuple of zero-padded 8-bit binary strings"""
        nested_list = [
            ["{0:08b}".format(int(ii, 16)) for ii in [num[0:2], num[2:4]]]
            for num in str(self.ip.exploded).split(":")
        ]
        return tuple(itertools.chain(*nested_list))

    @property
    def as_hex_tuple(self):
        """Returns the IPv6 address as a tuple of zero-padded 8-bit hex strings"""
        nested_list = [
            ["{0:02x}".format(int(ii, 16)) for ii in [num[0:2], num[2:4]]]
            for num in str(self.ip.exploded).split(":")
        ]
        return tuple(itertools.chain(*nested_list))

    @property
    def as_cidr_addr(self):
        """Returns a string with the address in CIDR notation"""
        return str(self.ip) + "/" + str(self.prefixlen)

    @property
    def as_cidr_net(self):
        """Returns a string with the network in CIDR notation"""
        return str(self.network)

    @property
    def is_multicast(self):
        """Returns a boolean for whether this is a multicast address"""
        return self.network_object.is_multicast

    @property
    def is_private(self):
        """Returns a boolean for whether this is a private address"""
        return self.network_object.is_private

    @property
    def is_reserved(self):
        """Returns a boolean for whether this is a reserved address"""
        return self.network_object.is_reserved

    @property
    def is_link_local(self):
        """Returns a boolean for whether this is an IPv6 link-local address"""
        return self.network_object.is_link_local

    @property
    def is_site_local(self):
        """Returns a boolean for whether this is an IPv6 site-local address"""
        return self.network_object.is_site_local

    @property
    def is_unspecified(self):
        """Returns a boolean for whether this address is not otherwise 
        classified"""
        return self.network_object.is_unspecified

    @property
    def teredo(self):
        return self.network_object.teredo

    @property
    def sixtofour(self):
        return self.network_object.sixtofour


class L4Object(object):
    """Object for Transport-layer protocols; the object ensures that logical operators (such as le, gt, eq, and ne) are parsed correctly, as well as mapping service names to port numbers"""

    def __init__(self, protocol="", port_spec="", syntax=""):
        self.protocol = protocol
        self.port_list = list()
        self.syntax = syntax

        try:
            port_spec = port_spec.strip()
        except:
            port_spec = port_spec

        if syntax == "asa":
            if protocol == "tcp":
                ports = ASA_TCP_PORTS
            elif protocol == "udp":
                ports = ASA_UDP_PORTS
            else:
                raise NotImplementedError(
                    "'{0}' is not supported: '{0}'".format(protocol)
                )
        else:
            raise NotImplementedError("This syntax is unknown: '{0}'".format(syntax))

        if "eq " in port_spec:
            port_str = re.split("\s+", port_spec)[-1]
            self.port_list = [int(ports.get(port_str, port_str))]
        elif re.search(r"^\S+$", port_spec):
            # Technically, 'eq ' is optional...
            self.port_list = [int(ports.get(port_spec, port_spec))]
        elif "range " in port_spec:
            port_tmp = re.split("\s+", port_spec)[1:]
            self.port_list = range(
                int(ports.get(port_tmp[0], port_tmp[0])),
                int(ports.get(port_tmp[1], port_tmp[1])) + 1,
            )
        elif "lt " in port_spec:
            port_str = re.split("\s+", port_spec)[-1]
            self.port_list = range(1, int(ports.get(port_str, port_str)))
        elif "gt " in port_spec:
            port_str = re.split("\s+", port_spec)[-1]
            self.port_list = range(int(ports.get(port_str, port_str)) + 1, 65535)
        elif "neq " in port_spec:
            port_str = re.split("\s+", port_spec)[-1]
            tmp = set(range(1, 65535))
            tmp.remove(int(port_str))
            self.port_list = sorted(tmp)

    def __eq__(self, val):
        if (self.protocol == val.protocol) and (self.port_list == val.port_list):
            return True
        return False

    def __repr__(self):
        return "<L4Object {0} {1}>".format(self.protocol, self.port_list)


class DNSResponse(object):
    """A universal DNS Response object

    Kwargs:
        - query_type (str): A string containing the DNS record to lookup
        - result_str (str): A string containing the DNS Response
        - input (str): The DNS query string
        - duration (float): The query duration in seconds 

    Attributes:
        - query_type (str): A string containing the DNS record to lookup
        - result_str (str): A string containing the DNS Response
        - input (str): The DNS query string
        - has_error (bool): Indicates the query resulted in an error when True
        - error_str (str): The error returned by dnspython
        - duration (float): The query duration in seconds 
        - preference (int): The MX record's preference (default: -1)

    Returns:
        A :class:`~ccp_util.DNSResponse` instance
"""

    def __init__(self, query_type="", result_str="", input="", duration=0.0):
        self.query_type = query_type
        self.result_str = result_str
        self.input = input
        self.duration = duration  # Query duration in seconds

        self.has_error = False
        self.error_str = ""
        self.preference = -1  # MX Preference

    def __str__(self):
        return self.result_str

    def __repr__(self):
        if not self.has_error:
            return '<DNSResponse "{0}" result_str="{1}">'.format(
                self.query_type, self.result_str
            )
        else:
            return '<DNSResponse "{0}" error="{1}">'.format(
                self.query_type, self.error_str
            )


def dns_query(input="", query_type="", server="", timeout=2.0):
    """A unified IPv4 & IPv6 DNS lookup interface; this is essentially just a wrapper around dnspython's API.  When you query a PTR record, you can use an IPv4 or IPv6 address (which will automatically be converted into an in-addr.arpa name.  This wrapper only supports a subset of DNS records: 'A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', and 'TXT'

    Kwargs:
        - input (str): A string containing the DNS record to lookup
        - query_type (str): A string containing the DNS record type (SOA not supported)
        - server (str): A string containing the fqdn or IP address of the dns server
        - timeout (float): DNS lookup timeout duration (default: 2.0 seconds)

    Returns:
        A set([]) of :class:`~ccp_util.DNSResponse` instances.  Refer to the DNSResponse object in these docs for more information.

>>> from ciscoconfparse.ccp_util import dns_query
>>> dns_query('www.pennington.net', "A", "4.2.2.2", timeout=0.5)
set([<DNSResponse "A" result_str="65.19.187.2">])
>>> response_set = dns_query('www.pennington.net', 'A', '4.2.2.2')
>>> aa = response_set.pop()
>>> aa.result_str
'65.19.187.2'
>>> aa.error_str
''
>>>
    """

    valid_records = set(["A", "AAAA", "AXFR", "CNAME", "MX", "NS", "PTR", "TXT"])
    query_type = query_type.upper()
    assert query_type in valid_records
    assert server != ""
    assert float(timeout) > 0
    assert input != ""
    intput = input.strip()
    retval = set([])
    resolver = Resolver()
    resolver.server = [socket.gethostbyname(server)]
    resolver.timeout = float(timeout)
    resolver.lifetime = float(timeout)
    start = time.time()
    if (query_type == "A") or (query_type == "AAAA"):
        try:
            answer = resolver.query(input, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type,
                    duration=duration,
                    input=input,
                    result_str=str(result.address),
                )
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    elif query_type == "AXFR":
        """This is a hack: return text of zone transfer, instead of axfr objs"""
        _zone = zone.from_xfr(query.xfr(server, input, lifetime=timeout))
        return [_zone[node].to_text(node) for node in _zone.nodes.keys()]
    elif query_type == "CNAME":
        try:
            answer = resolver.query(input, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type,
                    duration=duration,
                    input=input,
                    result_str=str(result.target),
                )
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    elif query_type == "MX":
        try:
            answer = resolver.query(input, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type, input=input, result_str=str(result.target)
                )
                response.preference = int(result.preference)
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    elif query_type == "NS":
        try:
            answer = resolver.query(input, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type,
                    duration=duration,
                    input=input,
                    result_str=str(result.target),
                )
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    elif query_type == "PTR":
        if is_valid_ipv4_addr(input) or is_valid_ipv6_addr(input):
            inaddr = reversename.from_address(input)
        elif "in-addr.arpa" in input.lower():
            inaddr = input
        else:
            raise ValueError('Cannot query PTR record for "{0}"'.format(input))

        try:
            answer = resolver.query(inaddr, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type,
                    duration=duration,
                    input=inaddr,
                    result_str=str(result.target),
                )
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    elif query_type == "TXT":
        try:
            answer = resolver.query(input, query_type)
            duration = time.time() - start
            for result in answer:
                response = DNSResponse(
                    query_type=query_type,
                    duration=duration,
                    input=inaddr,
                    result_str=str(result.strings),
                )
                retval.add(response)
        except DNSException as e:
            duration = time.time() - start
            response = DNSResponse(
                input=input, duration=duration, query_type=query_type
            )
            response.has_error = True
            response.error_str = e
            retval.add(response)
    return retval


def dns_lookup(input, timeout=3, server=""):
    """Perform a simple DNS lookup, return results in a dictionary"""
    resolver = Resolver()
    resolver.timeout = float(timeout)
    resolver.lifetime = float(timeout)
    if server:
        resolver.nameservers = [server]
    try:
        records = resolver.query(input, "A")
        return {
            "addrs": [ii.address for ii in records],
            "error": "",
            "name": input,
        }
    except DNSException as e:
        return {
            "addrs": [],
            "error": repr(e),
            "name": input,
        }


def dns6_lookup(input, timeout=3, server=""):
    """Perform a simple DNS lookup, return results in a dictionary"""
    resolver = Resolver()
    resolver.timeout = float(timeout)
    resolver.lifetime = float(timeout)
    if server:
        resolver.nameservers = [server]
    try:
        records = resolver.query(input, "AAAA")
        return {
            "addrs": [ii.address for ii in records],
            "error": "",
            "name": input,
        }
    except DNSException as e:
        return {
            "addrs": [],
            "error": repr(e),
            "name": input,
        }


_REVERSE_DNS_REGEX = re.compile(r"^\s*\d+\.\d+\.\d+\.\d+\s*$")


def reverse_dns_lookup(input, timeout=3, server=""):
    """Perform a simple reverse DNS lookup, return results in a dictionary"""
    assert _REVERSE_DNS_REGEX.search(input), "Invalid address format: '{0}'".format(
        input
    )
    resolver = Resolver()
    resolver.timeout = float(timeout)
    resolver.lifetime = float(timeout)
    if server:
        resolver.nameservers = [server]
    try:
        tmp = input.strip().split(".")
        tmp.reverse()
        inaddr = ".".join(tmp) + ".in-addr.arpa"
        records = resolver.query(inaddr, "PTR")
        return {
            "name": records[0].to_text(),
            "lookup": inaddr,
            "error": "",
            "addr": input,
        }
    except DNSException as e:
        return {
            "addrs": [],
            "lookup": inaddr,
            "error": repr(e),
            "name": input,
        }


class CiscoRange(MutableSequence):
    """Explode Cisco ranges into a list of explicit items... examples below...

>>> from ciscoconfparse.ccp_util import CiscoRange
>>> CiscoRange('1-3,5,9-11,13')
<CiscoRange 1-3,5,9-11,13>
>>> CiscoRange('Eth1/1-3,7')
<CiscoRange Eth1/1-3,7>
>>> CiscoRange()
<CiscoRange none>
    """

    def __init__(self, text="", result_type=str):
        super(CiscoRange, self).__init__()
        self.text = text
        self.result_type = result_type
        if text:
            (
                self.line_prefix,
                self.slot_prefix,
                self.range_text,
            ) = self._parse_range_text()
            self._list = self._range()
        else:
            self.line_prefix = ""
            self.slot_prefix = ""
            self._list = list()

    def __repr__(self):
        if len(self._list) == 0:
            return """<CiscoRange none>"""
        else:
            return """<CiscoRange {0}>""".format(self.compressed_str)

    def __len__(self):
        return len(self._list)

    def __getitem__(self, ii):
        return self._list[ii]

    def __delitem__(self, ii):
        del self._list[ii]

    def __setitem__(self, ii, val):
        return self._list[ii]

    def __str__(self):
        return self.__repr__()

    # Github issue #124
    def __eq__(self, other):
        assert hasattr(other, "line_prefix")
        self_prefix_str = self.line_prefix + self.slot_prefix
        other_prefix_str = other.line_prefix + other.slot_prefix
        cmp1 = self_prefix_str.lower() == other_prefix_str.lower()
        cmp2 = sorted(self._list) == sorted(other._list)
        return cmp1 and cmp2

    def insert(self, ii, val):
        ## Insert something at index ii
        for idx, obj in enumerate(CiscoRange(val, result_type=self.result_type)):
            self._list.insert(ii + idx, obj)

        # Prune out any duplicate entries, and sort...
        self._list = sorted(map(self.result_type, set(self._list)))
        return self

    def append(self, val):
        list_idx = len(self._list)
        self.insert(list_idx, val)
        return self

    def _parse_range_text(self):
        tmp = self.text.split(",")
        mm = _RGX_CISCO_RANGE.search(tmp[0])

        ERROR = "CiscoRange() couldn't parse '{0}'".format(self.text)
        assert not (mm is None), ERROR

        mm_result = mm.groupdict()
        line_prefix = mm_result.get("line_prefix", "") or ""
        slot_prefix = mm_result.get("slot_prefix", "") or ""
        if len(tmp[1:]) > 1:
            range_text = mm_result["range_text"] + "," + ",".join(tmp[1:])
        elif len(tmp[1:]) == 1:
            range_text = mm_result["range_text"] + "," + tmp[1]
        elif len(tmp[1:]) == 0:
            range_text = mm_result["range_text"]
        return line_prefix, slot_prefix, range_text

    def _parse_dash_range(self, text):
        """Parse a dash Cisco range into a discrete list of items"""
        retval = list()
        for range_atom in text.split(","):
            try:
                begin, end = range_atom.split("-")
            except ValueError:
                ## begin and end are the same number
                begin, end = range_atom, range_atom
            begin, end = int(begin.strip()), int(end.strip()) + 1
            assert begin > -1
            assert end > begin
            retval.extend(range(begin, end))
        return list(set(retval))

    def _range(self):
        """Enumerate all values in the CiscoRange()"""

        def combine(arg):
            return self.line_prefix + self.slot_prefix + str(arg)

        return [
            self.result_type(ii)
            for ii in map(combine, self._parse_dash_range(self.range_text))
        ]

    def remove(self, arg):
        remove_obj = CiscoRange(arg)
        for ii in remove_obj:
            try:
                ## Remove arg, even if duplicated... Ref Github issue #126
                while True:
                    index = self.index(self.result_type(ii))
                    self.pop(index)
            except ValueError:
                pass
        return self

    @property
    def as_list(self):
        return self._list

    ## Github issue #125
    @property
    def compressed_str(self):
        """Return a text string with a compressed csv of values

>>> from ciscoconfparse.ccp_util import CiscoRange
>>> range_obj = CiscoRange('1,3,5,6,7')
>>> range_obj.compressed_str
'1,3,5-7'
>>>
        """
        retval = list()
        prefix_str = self.line_prefix + self.slot_prefix

        # Build a list of integers (without prefix_str)
        input = list()
        for ii in self._list:
            try:
                unicode_ii = str(ii, "utf-8")  # Python2.7...
            except:
                unicode_ii = str(ii)
            ii = re.sub(r"^{0}(\d+)$".format(prefix_str), "\g<1>", unicode_ii)
            input.append(int(ii))

        if len(input) == 0:  # Special case, handle empty list
            return ""

        # source - https://stackoverflow.com/a/51227915/667301
        input = sorted(list(set(input)))
        range_list = [input[0]]
        for ii in range(len(input)):
            if ii + 1 < len(input) and ii - 1 > -1:
                if (input[ii] - input[ii - 1] == 1) and (
                    input[ii + 1] - input[ii] == 1
                ):
                    if range_list[-1] != "-":
                        range_list += ["-"]
                    else:
                        range_list = range_list
                else:
                    range_list += [input[ii]]
        if len(input) > 1:
            range_list += [input[len(input) - 1]]

        # Build the return value from range_list...
        retval = prefix_str + str(range_list[0])
        for ii in range(1, len(range_list)):
            if str(type(range_list[ii])) != str(type(range_list[ii - 1])):
                retval += str(range_list[ii])
            else:
                retval += "," + str(range_list[ii])

        return retval