| 1 | # -*- coding: iso-8859-15 -*- |
|---|
| 2 | """IP4 address range set implementation. |
|---|
| 3 | |
|---|
| 4 | Implements an IPv4-range type. |
|---|
| 5 | |
|---|
| 6 | Copyright (C) 2006, Heiko Wundram. |
|---|
| 7 | Released under the MIT-license. |
|---|
| 8 | """ |
|---|
| 9 | |
|---|
| 10 | # Version information |
|---|
| 11 | # ------------------- |
|---|
| 12 | |
|---|
| 13 | __author__ = "Heiko Wundram <me@modelnine.org>" |
|---|
| 14 | __version__ = "0.2" |
|---|
| 15 | __revision__ = "3" |
|---|
| 16 | __date__ = "2006-01-20" |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | # Imports |
|---|
| 20 | # ------- |
|---|
| 21 | |
|---|
| 22 | import intset |
|---|
| 23 | import socket |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | # IP4Range class |
|---|
| 27 | # -------------- |
|---|
| 28 | |
|---|
| 29 | class IP4Range(intset.IntSet): |
|---|
| 30 | """IP4 address range class with efficient storage of address ranges. |
|---|
| 31 | Supports all set operations.""" |
|---|
| 32 | |
|---|
| 33 | _MINIP4 = 0 |
|---|
| 34 | _MAXIP4 = (1<<32) - 1 |
|---|
| 35 | _UNITYTRANS = "".join([chr(n) for n in range(256)]) |
|---|
| 36 | _IPREMOVE = "0123456789." |
|---|
| 37 | |
|---|
| 38 | def __init__(self,*args): |
|---|
| 39 | """Initialize an ip4range class. The constructor accepts an unlimited |
|---|
| 40 | number of arguments that may either be tuples in the form (start,stop), |
|---|
| 41 | integers, longs or strings, where start and stop in a tuple may |
|---|
| 42 | also be of the form integer, long or string. |
|---|
| 43 | |
|---|
| 44 | Passing an integer or long means passing an IPv4-address that's already |
|---|
| 45 | been converted to integer notation, whereas passing a string specifies |
|---|
| 46 | an address where this conversion still has to be done. A string |
|---|
| 47 | address may be in the following formats: |
|---|
| 48 | |
|---|
| 49 | - 1.2.3.4 - a plain address, interpreted as a single address |
|---|
| 50 | - 1.2.3 - a set of addresses, interpreted as 1.2.3.0-1.2.3.255 |
|---|
| 51 | - localhost - hostname to look up, interpreted as single address |
|---|
| 52 | - 1.2.3<->5 - a set of addresses, interpreted as 1.2.3.0-1.2.5.255 |
|---|
| 53 | - 1.2.0.0/16 - a set of addresses, interpreted as 1.2.0.0-1.2.255.255 |
|---|
| 54 | |
|---|
| 55 | Only the first three notations are valid if you use a string address in |
|---|
| 56 | a tuple, whereby notation 2 is interpreted as 1.2.3.0 if specified as |
|---|
| 57 | lower bound and 1.2.3.255 if specified as upper bound, not as a range |
|---|
| 58 | of addresses. |
|---|
| 59 | |
|---|
| 60 | Specifying a range is done with the <-> operator. This is necessary |
|---|
| 61 | because '-' might be present in a hostname. '<->' shouldn't be, ever. |
|---|
| 62 | """ |
|---|
| 63 | |
|---|
| 64 | # Special case copy constructor. |
|---|
| 65 | if len(args) == 1 and isinstance(args[0],IP4Range): |
|---|
| 66 | super(IP4Range,self).__init__(args[0]) |
|---|
| 67 | return |
|---|
| 68 | |
|---|
| 69 | # Convert arguments to tuple syntax. |
|---|
| 70 | args = list(args) |
|---|
| 71 | for i in range(len(args)): |
|---|
| 72 | argval = args[i] |
|---|
| 73 | if isinstance(argval,str): |
|---|
| 74 | if "<->" in argval: |
|---|
| 75 | # Type 4 address. |
|---|
| 76 | args[i] = self._parseRange(*argval.split("<->",1)) |
|---|
| 77 | continue |
|---|
| 78 | elif "/" in argval: |
|---|
| 79 | # Type 5 address. |
|---|
| 80 | args[i] = self._parseMask(*argval.split("/",1)) |
|---|
| 81 | else: |
|---|
| 82 | # Type 1, 2 or 3. |
|---|
| 83 | args[i] = self._parseAddrRange(argval) |
|---|
| 84 | elif isinstance(argval,tuple): |
|---|
| 85 | if len(tuple) <> 2: |
|---|
| 86 | raise ValueError("Tuple is of invalid length.") |
|---|
| 87 | addr1, addr2 = argval |
|---|
| 88 | if isinstance(addr1,str): |
|---|
| 89 | addr1 = self._parseAddrRange(addr1)[0] |
|---|
| 90 | elif not isinstance(addr1,(int,long)): |
|---|
| 91 | raise TypeError("Invalid argument.") |
|---|
| 92 | if isinstance(addr2,str): |
|---|
| 93 | addr2 = self._parseAddrRange(addr2)[1] |
|---|
| 94 | elif not isinstance(addr2,(int,long)): |
|---|
| 95 | raise TypeError("Invalid argument.") |
|---|
| 96 | args[i] = (addr1,addr2) |
|---|
| 97 | elif not isinstance(argval,(int,long)): |
|---|
| 98 | raise TypeError("Invalid argument.") |
|---|
| 99 | |
|---|
| 100 | # Initialize the integer set. |
|---|
| 101 | super(IP4Range,self).__init__(min=self._MINIP4,max=self._MAXIP4,*args) |
|---|
| 102 | |
|---|
| 103 | # Parsing functions |
|---|
| 104 | # ----------------- |
|---|
| 105 | |
|---|
| 106 | def _parseRange(self,addr1,addr2): |
|---|
| 107 | naddr1, naddr1len = _parseAddr(addr1) |
|---|
| 108 | naddr2, naddr2len = _parseAddr(addr2) |
|---|
| 109 | if naddr2len < naddr1len: |
|---|
| 110 | naddr2 += naddr1&(((1<<((naddr1len-naddr2len)*8))-1)<< |
|---|
| 111 | (naddr2len*8)) |
|---|
| 112 | naddr2len = naddr1len |
|---|
| 113 | elif naddr2len > naddr1len: |
|---|
| 114 | raise ValueError("Range has more dots than address.") |
|---|
| 115 | naddr1 <<= (4-naddr1len)*8 |
|---|
| 116 | naddr2 <<= (4-naddr2len)*8 |
|---|
| 117 | naddr2 += (1<<((4-naddr2len)*8))-1 |
|---|
| 118 | return (naddr1,naddr2) |
|---|
| 119 | |
|---|
| 120 | def _parseMask(self,addr,mask): |
|---|
| 121 | naddr, naddrlen = _parseAddr(addr) |
|---|
| 122 | naddr <<= (4-naddrlen)*8 |
|---|
| 123 | try: |
|---|
| 124 | if not mask: |
|---|
| 125 | masklen = 0 |
|---|
| 126 | else: |
|---|
| 127 | masklen = int(mask) |
|---|
| 128 | if not 0 <= masklen <= 32: |
|---|
| 129 | raise ValueError |
|---|
| 130 | except ValueError: |
|---|
| 131 | try: |
|---|
| 132 | mask = _parseAddr(mask,False) |
|---|
| 133 | except ValueError: |
|---|
| 134 | raise ValueError("Mask isn't parseable.") |
|---|
| 135 | remaining = 0 |
|---|
| 136 | masklen = 0 |
|---|
| 137 | if not mask: |
|---|
| 138 | masklen = 0 |
|---|
| 139 | else: |
|---|
| 140 | while not (mask&1): |
|---|
| 141 | remaining += 1 |
|---|
| 142 | while (mask&1): |
|---|
| 143 | mask >>= 1 |
|---|
| 144 | masklen += 1 |
|---|
| 145 | if remaining+masklen <> 32: |
|---|
| 146 | raise ValueError("Mask isn't a proper host mask.") |
|---|
| 147 | naddr1 = naddr & (((1<<masklen)-1)<<(32-masklen)) |
|---|
| 148 | naddr2 = naddr1 + (1<<(32-masklen)) - 1 |
|---|
| 149 | return (naddr1,naddr2) |
|---|
| 150 | |
|---|
| 151 | def _parseAddrRange(self,addr): |
|---|
| 152 | naddr, naddrlen = _parseAddr(addr) |
|---|
| 153 | naddr1 = naddr<<((4-naddrlen)*8) |
|---|
| 154 | naddr2 = ( (naddr<<((4-naddrlen)*8)) + |
|---|
| 155 | (1<<((4-naddrlen)*8)) - 1 ) |
|---|
| 156 | return (naddr1,naddr2) |
|---|
| 157 | |
|---|
| 158 | # Utility functions |
|---|
| 159 | # ----------------- |
|---|
| 160 | |
|---|
| 161 | def _int2ip(self,num): |
|---|
| 162 | rv = [] |
|---|
| 163 | for i in range(4): |
|---|
| 164 | rv.append(str(num&255)) |
|---|
| 165 | num >>= 8 |
|---|
| 166 | return ".".join(reversed(rv)) |
|---|
| 167 | |
|---|
| 168 | # Iterating |
|---|
| 169 | # --------- |
|---|
| 170 | |
|---|
| 171 | def iteraddresses(self): |
|---|
| 172 | """Returns an iterator which iterates over ips in this iprange. An |
|---|
| 173 | IP is returned in string form (e.g. '1.2.3.4').""" |
|---|
| 174 | |
|---|
| 175 | for v in super(IP4Range,self).__iter__(): |
|---|
| 176 | yield self._int2ip(v) |
|---|
| 177 | |
|---|
| 178 | def iterranges(self): |
|---|
| 179 | """Returns an iterator which iterates over ip-ip ranges which build |
|---|
| 180 | this iprange if combined. An ip-ip pair is returned in string form |
|---|
| 181 | (e.g. '1.2.3.4-2.3.4.5').""" |
|---|
| 182 | |
|---|
| 183 | for r in self._ranges: |
|---|
| 184 | if r[1]-r[0] == 1: |
|---|
| 185 | yield self._int2ip(r[0]) |
|---|
| 186 | else: |
|---|
| 187 | yield '%s-%s' % (self._int2ip(r[0]),self._int2ip(r[1]-1)) |
|---|
| 188 | |
|---|
| 189 | def itermasks(self): |
|---|
| 190 | """Returns an iterator which iterates over ip/mask pairs which build |
|---|
| 191 | this iprange if combined. An IP/Mask pair is returned in string form |
|---|
| 192 | (e.g. '1.2.3.0/24').""" |
|---|
| 193 | |
|---|
| 194 | for r in self._ranges: |
|---|
| 195 | for v in self._itermasks(r): |
|---|
| 196 | yield v |
|---|
| 197 | |
|---|
| 198 | def _itermasks(self,r): |
|---|
| 199 | ranges = [r] |
|---|
| 200 | while ranges: |
|---|
| 201 | cur = ranges.pop() |
|---|
| 202 | curmask = 0 |
|---|
| 203 | while True: |
|---|
| 204 | curmasklen = 1<<(32-curmask) |
|---|
| 205 | start = (cur[0]+curmasklen-1)&(((1<<curmask)-1)<<(32-curmask)) |
|---|
| 206 | if start >= cur[0] and start+curmasklen <= cur[1]: |
|---|
| 207 | break |
|---|
| 208 | else: |
|---|
| 209 | curmask += 1 |
|---|
| 210 | yield "%s/%s" % (self._int2ip(start),curmask) |
|---|
| 211 | if cur[0] < start: |
|---|
| 212 | ranges.append((cur[0],start)) |
|---|
| 213 | if cur[1] > start+curmasklen: |
|---|
| 214 | ranges.append((start+curmasklen,cur[1])) |
|---|
| 215 | |
|---|
| 216 | __iter__ = iteraddresses |
|---|
| 217 | |
|---|
| 218 | # Printing |
|---|
| 219 | # -------- |
|---|
| 220 | |
|---|
| 221 | def __repr__(self): |
|---|
| 222 | """Returns a string which can be used to reconstruct this iprange.""" |
|---|
| 223 | |
|---|
| 224 | rv = [] |
|---|
| 225 | for start, stop in self._ranges: |
|---|
| 226 | if stop-start == 1: |
|---|
| 227 | rv.append("%r" % (self._int2ip(start),)) |
|---|
| 228 | else: |
|---|
| 229 | rv.append("(%r,%r)" % (self._int2ip(start), |
|---|
| 230 | self._int2ip(stop-1))) |
|---|
| 231 | return "%s(%s)" % (self.__class__.__name__,",".join(rv)) |
|---|
| 232 | |
|---|
| 233 | def _parseAddr(addr,lookup=True): |
|---|
| 234 | if lookup and addr.translate(IP4Range._UNITYTRANS, IP4Range._IPREMOVE): |
|---|
| 235 | try: |
|---|
| 236 | addr = socket.gethostbyname(addr) |
|---|
| 237 | except socket.error: |
|---|
| 238 | raise ValueError("Invalid Hostname as argument.") |
|---|
| 239 | naddr = 0 |
|---|
| 240 | for naddrpos, part in enumerate(addr.split(".")): |
|---|
| 241 | if naddrpos >= 4: |
|---|
| 242 | raise ValueError("Address contains more than four parts.") |
|---|
| 243 | try: |
|---|
| 244 | if not part: |
|---|
| 245 | part = 0 |
|---|
| 246 | else: |
|---|
| 247 | part = int(part) |
|---|
| 248 | if not 0 <= part < 256: |
|---|
| 249 | raise ValueError |
|---|
| 250 | except ValueError: |
|---|
| 251 | raise ValueError("Address part out of range.") |
|---|
| 252 | naddr <<= 8 |
|---|
| 253 | naddr += part |
|---|
| 254 | return naddr, naddrpos+1 |
|---|
| 255 | |
|---|
| 256 | def ip2int(addr, lookup=True): |
|---|
| 257 | return _parseAddr(addr, lookup=lookup)[0] |
|---|
| 258 | |
|---|
| 259 | if __name__ == "__main__": |
|---|
| 260 | # Little test script. |
|---|
| 261 | x = IP4Range("172.22.162.250/24") |
|---|
| 262 | y = IP4Range("172.22.162.250","172.22.163.250","172.22.163.253<->255") |
|---|
| 263 | print x |
|---|
| 264 | for val in x.itermasks(): |
|---|
| 265 | print val |
|---|
| 266 | for val in y.itermasks(): |
|---|
| 267 | print val |
|---|
| 268 | for val in (x|y).itermasks(): |
|---|
| 269 | print val |
|---|
| 270 | for val in (x^y).iterranges(): |
|---|
| 271 | print val |
|---|
| 272 | for val in x: |
|---|
| 273 | print val |
|---|