├── .gitignore ├── .landscape.yaml ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── README.md ├── doc ├── Makefile └── source │ ├── conf.py │ └── index.txt ├── ipcalc.py ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | doc/build/ 3 | build/ 4 | dist/ 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: yes 2 | test-warnings: yes 3 | strictness: high 4 | max-line-length: 120 5 | autodetect: yes 6 | requirements: 7 | - requirements.txt 8 | ignore-paths: 9 | - doc 10 | 11 | # Test utilities 12 | pep8: 13 | run: true 14 | pep257: 15 | run: true 16 | pyflakes: 17 | run: true 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | # command to run tests 10 | script: 11 | - python -v ipcalc.py 12 | - python -m doctest -v ipcalc.py 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Wijnand Modderman-Lenstra et al 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py *.md 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: docs clean 2 | 3 | docs: 4 | make -C doc/ html 5 | 6 | clean: 7 | find . -name \*.pyc -exec rm {} \; 8 | 9 | push: docs 10 | hg push /home/hg/projects/ipcalc 11 | hg push ssh://wijnand@10.23.5.1/code0/trac/data/ipcalc/ 12 | rsync -avP doc/build/html/ wijnand@10.23.5.1:code0/trac/docs/ipcalc/ 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | This module allows you to perform IP subnet calculations, there is support for 5 | both IPv4 and IPv6 CIDR notation. 6 | 7 | Example Usage 8 | ============= 9 | 10 | ```python 11 | 12 | >>> import ipcalc 13 | >>> for x in ipcalc.Network('172.16.42.0/30'): 14 | ... print str(x) 15 | ... 16 | 172.16.42.1 17 | 172.16.42.2 18 | >>> subnet = ipcalc.Network('2001:beef:babe::/48') 19 | >>> print(str(subnet.network())) 20 | 2001:beef:babe:0000:0000:0000:0000:0000 21 | >>> print(str(subnet.netmask())) 22 | ffff:ffff:ffff:0000:0000:0000:0000:0000 23 | >>> '192.168.42.23' in Network('192.168.42.0/24') 24 | True 25 | >>> int(IP('fe80::213:ceff:fee8:c937')) 26 | 338288524927261089654168587652869703991 27 | ``` 28 | 29 | Bugs/Features 30 | ============= 31 | 32 | You can issue a ticket in GitHub: https://github.com/tehmaze/ipcalc/issues 33 | 34 | [![Build Status](https://travis-ci.org/tehmaze/ipcalc.svg?branch=master)](https://travis-ci.org/tehmaze/ipcalc) 35 | [![Code Health](https://landscape.io/github/tehmaze/ipcalc/master/landscape.svg)](https://landscape.io/github/tehmaze/ipcalc/master) 36 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in build/qthelp, like this:" 63 | @echo "# qcollectiongenerator build/qthelp/ipcalc.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile build/qthelp/ipcalc.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes 76 | @echo 77 | @echo "The overview file is in build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ipcalc documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Apr 11 00:06:38 2009. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | dirname = os.path.dirname 20 | sys.path.append(os.path.join(dirname(dirname(dirname(__file__))), 'src')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.txt' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'ipcalc' 42 | copyright = u'2009, Wijnand Modderman-Lenstra' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '2.0' 50 | # The full version, including alpha/beta/rc tags. 51 | release = '1.99.0' 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of documents that shouldn't be included in the build. 64 | #unused_docs = [] 65 | 66 | # List of directories, relative to source directory, that shouldn't be searched 67 | # for source files. 68 | exclude_trees = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. Major themes that come with 94 | # Sphinx are currently 'default' and 'sphinxdoc'. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_use_modindex = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, an OpenSearch description file will be output, and all pages will 154 | # contain a tag referring to it. The value of this option must be the 155 | # base URL from which the finished HTML is served. 156 | #html_use_opensearch = '' 157 | 158 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 159 | #html_file_suffix = '' 160 | 161 | # Output file base name for HTML help builder. 162 | htmlhelp_basename = 'ipcalcdoc' 163 | 164 | 165 | # -- Options for LaTeX output -------------------------------------------------- 166 | 167 | # The paper size ('letter' or 'a4'). 168 | #latex_paper_size = 'letter' 169 | 170 | # The font size ('10pt', '11pt' or '12pt'). 171 | #latex_font_size = '10pt' 172 | 173 | # Grouping the document tree into LaTeX files. List of tuples 174 | # (source start file, target name, title, author, documentclass [howto/manual]). 175 | latex_documents = [ 176 | ('index', 'ipcalc.tex', u'ipcalc Documentation', 177 | u'Wijnand Modderman-Lenstra', 'manual'), 178 | ] 179 | 180 | # The name of an image file (relative to this directory) to place at the top of 181 | # the title page. 182 | #latex_logo = None 183 | 184 | # For "manual" documents, if this is true, then toplevel headings are parts, 185 | # not chapters. 186 | #latex_use_parts = False 187 | 188 | # Additional stuff for the LaTeX preamble. 189 | #latex_preamble = '' 190 | 191 | # Documents to append as an appendix to all manuals. 192 | #latex_appendices = [] 193 | 194 | # If false, no module index is generated. 195 | #latex_use_modindex = True 196 | 197 | 198 | # Example configuration for intersphinx: refer to the Python standard library. 199 | intersphinx_mapping = {'http://docs.python.org/': None} 200 | -------------------------------------------------------------------------------- /doc/source/index.txt: -------------------------------------------------------------------------------- 1 | .. ipcalc documentation master file, created by 2 | sphinx-quickstart on Sat Apr 11 00:06:38 2009. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Ipcalc 7 | ====== 8 | 9 | This module allows you to perform IP subnet calculations, there is support for 10 | both IPv4 and IPv6 CIDR notation. 11 | 12 | Example Usage 13 | ------------- 14 | 15 | :: 16 | 17 | >>> import ipcalc 18 | >>> for x in ipcalc.Network('172.16.42.0/30'): 19 | ... print str(x) 20 | ... 21 | 172.16.42.1 22 | 172.16.42.2 23 | >>> subnet = ipcalc.Network('2001:beef:babe::/48') 24 | >>> print str(subnet.network()) 25 | 2001:beef:babe:0000:0000:0000:0000:0000 26 | >>> print str(subnet.netmask()) 27 | ffff:ffff:ffff:0000:0000:0000:0000:0000 28 | >>> '192.168.42.23' in Network('192.168.42.0/24') 29 | True 30 | >>> long(IP('fe80::213:ceff:fee8:c937')) 31 | 338288524927261089654168587652869703991L 32 | 33 | Bugs/Features 34 | ------------- 35 | 36 | You can issue a ticket in GitHub: https://github.com/tehmaze/ipcalc/issues 37 | 38 | Download 39 | -------- 40 | 41 | Get your copy of ipcalc from pypi: http://pypi.python.org/pypi/ipcalc 42 | 43 | .. automodule:: ipcalc 44 | :members: 45 | 46 | Indices and tables 47 | ================== 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | 53 | -------------------------------------------------------------------------------- /ipcalc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pep8-ignore: E501, E241 3 | # pylint: disable=invalid-name 4 | 5 | """ 6 | IP subnet calculator. 7 | 8 | .. moduleauthor:: Wijnand Modderman-Lenstra 9 | .. note:: BSD License 10 | 11 | About 12 | ===== 13 | 14 | This module allows you to perform network calculations. 15 | 16 | References 17 | ========== 18 | 19 | References: 20 | * http://www.estoile.com/links/ipv6.pdf 21 | * http://www.iana.org/assignments/ipv4-address-space 22 | * http://www.iana.org/assignments/multicast-addresses 23 | * http://www.iana.org/assignments/ipv6-address-space 24 | * http://www.iana.org/assignments/ipv6-tla-assignments 25 | * http://www.iana.org/assignments/ipv6-multicast-addresses 26 | * http://www.iana.org/assignments/ipv6-anycast-addresses 27 | 28 | Thanks 29 | ====== 30 | 31 | Thanks to all who have contributed: 32 | 33 | https://github.com/tehmaze/ipcalc/graphs/contributors 34 | """ 35 | 36 | from __future__ import print_function 37 | 38 | __version__ = '1.99.0' 39 | 40 | 41 | import re 42 | import six 43 | 44 | 45 | MAX_IPV6 = (1 << 128) - 1 46 | MAX_IPV4 = (1 << 32) - 1 47 | BASE_6TO4 = (0x2002 << 112) 48 | 49 | 50 | class IP(object): 51 | 52 | """ 53 | Represent a single IP address. 54 | 55 | :param ip: the ip address 56 | :type ip: :class:`IP` or str or long or int 57 | 58 | >>> localhost = IP("127.0.0.1") 59 | >>> print(localhost) 60 | 127.0.0.1 61 | >>> localhost6 = IP("::1") 62 | >>> print(localhost6) 63 | 0000:0000:0000:0000:0000:0000:0000:0001 64 | """ 65 | 66 | # Hex-to-Bin conversion masks 67 | _bitmask = { 68 | '0': '0000', '1': '0001', '2': '0010', '3': '0011', 69 | '4': '0100', '5': '0101', '6': '0110', '7': '0111', 70 | '8': '1000', '9': '1001', 'a': '1010', 'b': '1011', 71 | 'c': '1100', 'd': '1101', 'e': '1110', 'f': '1111' 72 | } 73 | 74 | # IP range specific information, see IANA allocations. 75 | _range = { 76 | # http://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml 77 | 4: { 78 | '00000000': 'THIS HOST', # 0/8 79 | '00001010': 'PRIVATE', # 10/8 80 | '0110010001': 'SHARED ADDRESS SPACE', # 100.64/10 81 | '01111111': 'LOOPBACK', # 127/8 82 | '101011000001': 'PRIVATE', # 172.16/12 83 | '110000000000000000000000': 'IETF PROTOCOL', # 192/24 84 | '110000000000000000000010': 'TEST-NET-1', # 192.0.2/24 85 | '110000000101100001100011': '6TO4-RELAY ANYCAST', # 192.88.99/24 86 | '1100000010101000': 'PRIVATE', # 192.168/16 87 | '110001100001001': 'BENCHMARKING', # 198.18/15 88 | '110001100011001': 'TEST-NET-2', # 198.51.100/24 89 | '110010110000000': 'TEST-NET-3', # 203.0.113/24 90 | '1111': 'RESERVED', # 240/4 91 | 92 | }, 93 | # http://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml 94 | 6: { 95 | '0' * 128: 'UNSPECIFIED', # ::/128 96 | '0' * 127 + '1': 'LOOPBACK', # ::1/128 97 | '0' * 96: 'IPV4COMP', # ::/96 98 | '0' * 80 + '1' * 16: 'IPV4MAP', # ::ffff:0:0/96 99 | # 64:ff9b::/96 100 | '00000000011001001111111110011011' + 64 * '0': 'IPV4-IPV6', 101 | '00000001' + 56 * '0': 'DISCARD-ONLY', # 100::/64 102 | '0010000000000001' + 7 * '0': 'IETF PROTOCOL', # 2001::/23 103 | '0010000000000001' + 16 * '0': 'TEREDO', # 2001::/32 104 | # 2001:2::/48 105 | '00100000000000010000000000000010000000000000000': 'BENCHMARKING', 106 | '00100000000000010000110110111000': 'DOCUMENTATION', # 2001:db8::/32 107 | '0010000000000001000000000001': 'DEPRECATED', # 2001:10::/28 108 | '0010000000000001000000000010': 'ORCHIDv2', # 2001:20::/28 109 | '0010000000000010': '6TO4', # 2002::/16 110 | '11111100000000000': 'UNIQUE-LOCAL', # fc00::/7 111 | '1111111010': 'LINK-LOCAL', # fe80::/10 112 | } 113 | } 114 | 115 | def __init__(self, ip, mask=None, version=0): 116 | """Initialize a new IPv4 or IPv6 address.""" 117 | self.mask = mask 118 | self.v = 0 119 | # Parse input 120 | if ip is None: 121 | raise ValueError('Can not pass None') 122 | elif isinstance(ip, IP): 123 | self.ip = ip.ip 124 | self.dq = ip.dq 125 | self.v = ip.v 126 | self.mask = ip.mask 127 | elif isinstance(ip, six.integer_types): 128 | self.ip = int(ip) 129 | if self.ip <= MAX_IPV4: 130 | self.v = version or 4 131 | self.dq = self._itodq(ip) 132 | else: 133 | self.v = version or 6 134 | self.dq = self._itodq(ip) 135 | else: 136 | # network identifier 137 | if '%' in ip: 138 | ip = ip.split('%', 1)[0] 139 | # If string is in CIDR or netmask notation 140 | if '/' in ip: 141 | ip, mask = ip.split('/', 1) 142 | self.mask = mask 143 | self.v = version or 0 144 | self.dq = ip 145 | self.ip = self._dqtoi(ip) 146 | assert self.v != 0, 'Could not parse input' 147 | # Netmask defaults to one ip 148 | if self.mask is None: 149 | self.mask = {4: 32, 6: 128}[self.v] 150 | # Netmask is numeric CIDR subnet 151 | elif isinstance(self.mask, six.integer_types) or self.mask.isdigit(): 152 | self.mask = int(self.mask) 153 | # Netmask is in subnet notation 154 | elif isinstance(self.mask, six.string_types): 155 | limit = [32, 128][':' in self.mask] 156 | inverted = ~self._dqtoi(self.mask) 157 | if inverted == -1: 158 | self.mask = 0 159 | else: 160 | count = 0 161 | while inverted & pow(2, count): 162 | count += 1 163 | self.mask = (limit - count) 164 | else: 165 | raise ValueError('Invalid netmask') 166 | # Validate subnet size 167 | if self.v == 6: 168 | self.dq = self._itodq(self.ip) 169 | if not 0 <= self.mask <= 128: 170 | raise ValueError('IPv6 subnet size must be between 0 and 128') 171 | elif self.v == 4: 172 | if not 0 <= self.mask <= 32: 173 | raise ValueError('IPv4 subnet size must be between 0 and 32') 174 | 175 | def bin(self): 176 | """Full-length binary representation of the IP address. 177 | 178 | >>> ip = IP("127.0.0.1") 179 | >>> print(ip.bin()) 180 | 01111111000000000000000000000001 181 | """ 182 | bits = self.v == 4 and 32 or 128 183 | return bin(self.ip).split('b')[1].rjust(bits, '0') 184 | 185 | def hex(self): 186 | """Full-length hexadecimal representation of the IP address. 187 | 188 | >>> ip = IP("127.0.0.1") 189 | >>> print(ip.hex()) 190 | 7f000001 191 | """ 192 | if self.v == 4: 193 | return '%08x' % self.ip 194 | else: 195 | return '%032x' % self.ip 196 | 197 | def subnet(self): 198 | """CIDR subnet size.""" 199 | return self.mask 200 | 201 | def version(self): 202 | """IP version. 203 | 204 | >>> ip = IP("127.0.0.1") 205 | >>> print(ip.version()) 206 | 4 207 | """ 208 | return self.v 209 | 210 | def info(self): 211 | """Show IANA allocation information for the current IP address. 212 | 213 | >>> ip = IP("127.0.0.1") 214 | >>> print(ip.info()) 215 | LOOPBACK 216 | """ 217 | b = self.bin() 218 | for i in range(len(b), 0, -1): 219 | if b[:i] in self._range[self.v]: 220 | return self._range[self.v][b[:i]] 221 | return 'UNKNOWN' 222 | 223 | def _dqtoi(self, dq): 224 | """Convert dotquad or hextet to long.""" 225 | # hex notation 226 | if dq.startswith('0x'): 227 | return self._dqtoi_hex(dq) 228 | 229 | # IPv6 230 | if ':' in dq: 231 | return self._dqtoi_ipv6(dq) 232 | elif len(dq) == 32: 233 | # Assume full heximal notation 234 | self.v = 6 235 | return int(dq, 16) 236 | 237 | # IPv4 238 | if '.' in dq: 239 | return self._dqtoi_ipv4(dq) 240 | 241 | raise ValueError('Invalid address input') 242 | 243 | def _dqtoi_hex(self, dq): 244 | ip = int(dq[2:], 16) 245 | if ip > MAX_IPV6: 246 | raise ValueError('%s: IP address is bigger than 2^128' % dq) 247 | if ip <= MAX_IPV4: 248 | self.v = 4 249 | else: 250 | self.v = 6 251 | return ip 252 | 253 | def _dqtoi_ipv4(self, dq): 254 | q = dq.split('.') 255 | q.reverse() 256 | if len(q) > 4: 257 | raise ValueError('%s: IPv4 address invalid: ' 258 | 'more than 4 bytes' % dq) 259 | for x in q: 260 | if not 0 <= int(x) <= 255: 261 | raise ValueError('%s: IPv4 address invalid: ' 262 | 'bytes should be between 0 and 255' % dq) 263 | while len(q) < 4: 264 | q.insert(1, '0') 265 | self.v = 4 266 | return sum(int(byte) << 8 * index for index, byte in enumerate(q)) 267 | 268 | def _dqtoi_ipv6(self, dq): 269 | # Split hextets 270 | hx = dq.split(':') 271 | if ':::' in dq: 272 | raise ValueError("%s: IPv6 address can't contain :::" % dq) 273 | # Mixed address (or 4-in-6), ::ffff:192.0.2.42 274 | if '.' in dq: 275 | col_ind = dq.rfind(":") 276 | ipv6part = dq[:col_ind] + ":0:0" 277 | return self._dqtoi_ipv6(ipv6part) + self._dqtoi(hx[-1]) 278 | if len(hx) > 8: 279 | raise ValueError('%s: IPv6 address with more than 8 hexlets' % dq) 280 | elif len(hx) < 8: 281 | # No :: in address 282 | if '' not in hx: 283 | raise ValueError('%s: IPv6 address invalid: ' 284 | 'compressed format malformed' % dq) 285 | elif not (dq.startswith('::') or dq.endswith('::')) and len([x for x in hx if x == '']) > 1: 286 | raise ValueError('%s: IPv6 address invalid: ' 287 | 'compressed format malformed' % dq) 288 | ix = hx.index('') 289 | px = len(hx[ix + 1:]) 290 | for x in range(ix + px + 1, 8): 291 | hx.insert(ix, '0') 292 | elif dq.endswith('::'): 293 | pass 294 | elif '' in hx: 295 | raise ValueError('%s: IPv6 address invalid: ' 296 | 'compressed format detected in full notation' % dq) 297 | ip = '' 298 | hx = [x == '' and '0' or x for x in hx] 299 | for h in hx: 300 | if len(h) < 4: 301 | h = '%04x' % int(h, 16) 302 | if not 0 <= int(h, 16) <= 0xffff: 303 | raise ValueError('%r: IPv6 address invalid: ' 304 | 'hexlets should be between 0x0000 and 0xffff' % dq) 305 | ip += h 306 | self.v = 6 307 | return int(ip, 16) 308 | 309 | def _itodq(self, n): 310 | """Convert long to dotquad or hextet.""" 311 | if self.v == 4: 312 | return '.'.join(map(str, [ 313 | (n >> 24) & 0xff, 314 | (n >> 16) & 0xff, 315 | (n >> 8) & 0xff, 316 | n & 0xff, 317 | ])) 318 | else: 319 | n = '%032x' % n 320 | return ':'.join(n[4 * x:4 * x + 4] for x in range(0, 8)) 321 | 322 | def __str__(self): 323 | """Return dotquad representation of the IP. 324 | 325 | >>> ip = IP("::1") 326 | >>> print(str(ip)) 327 | 0000:0000:0000:0000:0000:0000:0000:0001 328 | """ 329 | return self.dq 330 | 331 | def __repr__(self): 332 | """Return canonical representation of the IP. 333 | 334 | >>> repr(IP("::1")) 335 | "IP('::1')" 336 | >>> repr(IP("fe80:0000:0000:0000:abde:3eff:ffab:0012/64")) 337 | "IP('fe80::abde:3eff:ffab:12/64')" 338 | >>> repr(IP("1.2.3.4/29")) 339 | "IP('1.2.3.4/29')" 340 | >>> repr(IP("127.0.0.1/8")) 341 | "IP('127.0.0.1/8')" 342 | """ 343 | dq = self.dq if self.v == 4 else self.to_compressed() 344 | args = (self.__class__.__name__, dq, self.mask) 345 | if (self.version(), self.mask) in [(4, 32), (6, 128)]: 346 | fmt = "{0}('{1}')" 347 | else: 348 | fmt = "{0}('{1}/{2}')" 349 | return fmt.format(*args) 350 | 351 | def __hash__(self): 352 | """Hash for collection operations and py:`hash()`.""" 353 | return hash(self.to_tuple()) 354 | 355 | hash = __hash__ 356 | 357 | def __int__(self): 358 | """Convert to int.""" 359 | return int(self.ip) 360 | 361 | def __long__(self): 362 | """Convert to long.""" 363 | return self.ip 364 | 365 | def __lt__(self, other): 366 | """Less than other test.""" 367 | return int(self) < int(IP(other)) 368 | 369 | def __le__(self, other): 370 | """Less than or equal to other test.""" 371 | return int(self) <= int(IP(other)) 372 | 373 | def __ge__(self, other): 374 | """Greater than or equal to other test.""" 375 | return int(self) >= int(IP(other)) 376 | 377 | def __gt__(self, other): 378 | """Greater than other.""" 379 | return int(self) > int(IP(other)) 380 | 381 | def __eq__(self, other): 382 | """Test if other is address is equal to the current address.""" 383 | return int(self) == int(IP(other)) 384 | 385 | def __ne__(self, other): 386 | """Test if other is address is not equal to the current address.""" 387 | return int(self) != int(IP(other)) 388 | 389 | def __add__(self, offset): 390 | """Add numeric offset to the IP.""" 391 | if not isinstance(offset, six.integer_types): 392 | return ValueError('Value is not numeric') 393 | return self.__class__(self.ip + offset, mask=self.mask, version=self.v) 394 | 395 | def __sub__(self, offset): 396 | """Substract numeric offset from the IP.""" 397 | if not isinstance(offset, six.integer_types): 398 | return ValueError('Value is not numeric') 399 | return self.__class__(self.ip - offset, mask=self.mask, version=self.v) 400 | 401 | @staticmethod 402 | def size(): 403 | """Return network size.""" 404 | return 1 405 | 406 | def clone(self): 407 | """ 408 | Return a new object with a copy of this one. 409 | 410 | >>> ip = IP('127.0.0.1') 411 | >>> ip2 = ip.clone() 412 | >>> ip2 413 | IP('127.0.0.1') 414 | >>> ip is ip2 415 | False 416 | >>> ip == ip2 417 | True 418 | >>> ip.mask = 24 419 | >>> ip2.mask 420 | 32 421 | """ 422 | return IP(self) 423 | 424 | def to_compressed(self): 425 | """ 426 | Compress an IP address to its shortest possible compressed form. 427 | 428 | >>> print(IP('127.0.0.1').to_compressed()) 429 | 127.1 430 | >>> print(IP('127.1.0.1').to_compressed()) 431 | 127.1.1 432 | >>> print(IP('127.0.1.1').to_compressed()) 433 | 127.0.1.1 434 | >>> print(IP('2001:1234:0000:0000:0000:0000:0000:5678').to_compressed()) 435 | 2001:1234::5678 436 | >>> print(IP('1234:0000:0000:beef:0000:0000:0000:5678').to_compressed()) 437 | 1234:0:0:beef::5678 438 | >>> print(IP('0000:0000:0000:0000:0000:0000:0000:0001').to_compressed()) 439 | ::1 440 | >>> print(IP('fe80:0000:0000:0000:0000:0000:0000:0000').to_compressed()) 441 | fe80:: 442 | """ 443 | if self.v == 4: 444 | quads = self.dq.split('.') 445 | try: 446 | zero = quads.index('0') 447 | if zero == 1 and quads.index('0', zero + 1): 448 | quads.pop(zero) 449 | quads.pop(zero) 450 | return '.'.join(quads) 451 | elif zero == 2: 452 | quads.pop(zero) 453 | return '.'.join(quads) 454 | except ValueError: # No zeroes 455 | pass 456 | 457 | return self.dq 458 | else: 459 | quads = map(lambda q: '%x' % (int(q, 16)), self.dq.split(':')) 460 | quadc = ':%s:' % (':'.join(quads),) 461 | zeros = [0, -1] 462 | 463 | # Find the largest group of zeros 464 | for match in re.finditer(r'(:[:0]+)', quadc): 465 | count = len(match.group(1)) - 1 466 | if count > zeros[0]: 467 | zeros = [count, match.start(1)] 468 | 469 | count, where = zeros 470 | if count: 471 | quadc = quadc[:where] + ':' + quadc[where + count:] 472 | 473 | quadc = re.sub(r'((^:)|(:$))', '', quadc) 474 | quadc = re.sub(r'((^:)|(:$))', '::', quadc) 475 | 476 | return quadc 477 | 478 | def to_ipv4(self): 479 | """ 480 | Convert (an IPv6) IP address to an IPv4 address, if possible. 481 | 482 | Only works for IPv4-compat (::/96), IPv4-mapped (::ffff/96), and 6-to-4 483 | (2002::/16) addresses. 484 | 485 | >>> ip = IP('2002:c000:022a::') 486 | >>> print(ip.to_ipv4()) 487 | 192.0.2.42 488 | """ 489 | if self.v == 4: 490 | return self 491 | else: 492 | if self.bin().startswith('0' * 96): 493 | return IP(int(self), version=4) 494 | elif self.bin().startswith('0' * 80 + '1' * 16): 495 | return IP(int(self) & MAX_IPV4, version=4) 496 | elif int(self) & BASE_6TO4: 497 | return IP((int(self) - BASE_6TO4) >> 80, version=4) 498 | else: 499 | return ValueError('%s: IPv6 address is not IPv4 compatible or mapped, ' 500 | 'nor an 6-to-4 IP' % self.dq) 501 | 502 | @classmethod 503 | def from_bin(cls, value): 504 | """Initialize a new network from binary notation.""" 505 | value = value.lstrip('b') 506 | if len(value) == 32: 507 | return cls(int(value, 2)) 508 | elif len(value) == 128: 509 | return cls(int(value, 2)) 510 | else: 511 | return ValueError('%r: invalid binary notation' % (value,)) 512 | 513 | @classmethod 514 | def from_hex(cls, value): 515 | """Initialize a new network from hexadecimal notation.""" 516 | if len(value) == 8: 517 | return cls(int(value, 16)) 518 | elif len(value) == 32: 519 | return cls(int(value, 16)) 520 | else: 521 | raise ValueError('%r: invalid hexadecimal notation' % (value,)) 522 | 523 | def to_ipv6(self, ip_type='6-to-4'): 524 | """ 525 | Convert (an IPv4) IP address to an IPv6 address. 526 | 527 | >>> ip = IP('192.0.2.42') 528 | >>> print(ip.to_ipv6()) 529 | 2002:c000:022a:0000:0000:0000:0000:0000 530 | 531 | >>> print(ip.to_ipv6('compat')) 532 | 0000:0000:0000:0000:0000:0000:c000:022a 533 | 534 | >>> print(ip.to_ipv6('mapped')) 535 | 0000:0000:0000:0000:0000:ffff:c000:022a 536 | """ 537 | assert ip_type in ['6-to-4', 'compat', 'mapped'], 'Conversion ip_type not supported' 538 | if self.v == 4: 539 | if ip_type == '6-to-4': 540 | return IP(BASE_6TO4 | int(self) << 80, version=6) 541 | elif ip_type == 'compat': 542 | return IP(int(self), version=6) 543 | elif ip_type == 'mapped': 544 | return IP(0xffff << 32 | int(self), version=6) 545 | else: 546 | return self 547 | 548 | def to_reverse(self): 549 | """Convert the IP address to a PTR record. 550 | 551 | Using the .in-addr.arpa zone for IPv4 and .ip6.arpa for IPv6 addresses. 552 | 553 | >>> ip = IP('192.0.2.42') 554 | >>> print(ip.to_reverse()) 555 | 42.2.0.192.in-addr.arpa 556 | >>> print(ip.to_ipv6().to_reverse()) 557 | 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.a.2.2.0.0.0.0.c.2.0.0.2.ip6.arpa 558 | """ 559 | if self.v == 4: 560 | return '.'.join(list(self.dq.split('.')[::-1]) + ['in-addr', 'arpa']) 561 | else: 562 | return '.'.join(list(self.hex())[::-1] + ['ip6', 'arpa']) 563 | 564 | def to_tuple(self): 565 | """Used for comparisons.""" 566 | return (self.dq, self.mask) 567 | 568 | def guess_network(self): 569 | netmask = 0x100000000 - 2**(32-self.mask) 570 | return Network(netmask & self.ip, mask=self.mask) 571 | 572 | 573 | class Network(IP): 574 | 575 | """ 576 | Network slice calculations. 577 | 578 | :param ip: network address 579 | :type ip: :class:`IP` or str or long or int 580 | :param mask: netmask 581 | :type mask: int or str 582 | 583 | 584 | >>> localnet = Network('127.0.0.1/8') 585 | >>> print(localnet) 586 | 127.0.0.1/8 587 | """ 588 | 589 | def netmask(self): 590 | """ 591 | Network netmask derived from subnet size, as IP object. 592 | 593 | >>> localnet = Network('127.0.0.1/8') 594 | >>> print(localnet.netmask()) 595 | 255.0.0.0 596 | """ 597 | return IP(self.netmask_long(), version=self.version()) 598 | 599 | def netmask_long(self): 600 | """ 601 | Network netmask derived from subnet size, as long. 602 | 603 | >>> localnet = Network('127.0.0.1/8') 604 | >>> print(localnet.netmask_long()) 605 | 4278190080 606 | """ 607 | if self.version() == 4: 608 | return (MAX_IPV4 >> (32 - self.mask)) << (32 - self.mask) 609 | else: 610 | return (MAX_IPV6 >> (128 - self.mask)) << (128 - self.mask) 611 | 612 | def network(self): 613 | """ 614 | Network address, as IP object. 615 | 616 | >>> localnet = Network('127.128.99.3/8') 617 | >>> print(localnet.network()) 618 | 127.0.0.0 619 | """ 620 | return IP(self.network_long(), version=self.version()) 621 | 622 | def network_long(self): 623 | """ 624 | Network address, as long. 625 | 626 | >>> localnet = Network('127.128.99.3/8') 627 | >>> print(localnet.network_long()) 628 | 2130706432 629 | """ 630 | return self.ip & self.netmask_long() 631 | 632 | def broadcast(self): 633 | """ 634 | Broadcast address, as IP object. 635 | 636 | >>> localnet = Network('127.0.0.1/8') 637 | >>> print(localnet.broadcast()) 638 | 127.255.255.255 639 | """ 640 | # XXX: IPv6 doesn't have a broadcast address, but it's used for other 641 | # calculations such as 642 | return IP(self.broadcast_long(), version=self.version()) 643 | 644 | def broadcast_long(self): 645 | """ 646 | Broadcast address, as long. 647 | 648 | >>> localnet = Network('127.0.0.1/8') 649 | >>> print(localnet.broadcast_long()) 650 | 2147483647 651 | """ 652 | if self.version() == 4: 653 | return self.network_long() | (MAX_IPV4 - self.netmask_long()) 654 | else: 655 | return self.network_long() \ 656 | | (MAX_IPV6 - self.netmask_long()) 657 | 658 | def host_first(self): 659 | """First available host in this subnet.""" 660 | if (self.version() == 4 and self.mask > 30) or \ 661 | (self.version() == 6 and self.mask > 126): 662 | return self 663 | else: 664 | return IP(self.network_long() + 1, version=self.version()) 665 | 666 | def host_last(self): 667 | """Last available host in this subnet.""" 668 | if (self.version() == 4 and self.mask == 32) or \ 669 | (self.version() == 6 and self.mask == 128): 670 | return self 671 | elif (self.version() == 4 and self.mask == 31) or \ 672 | (self.version() == 6 and self.mask == 127): 673 | return IP(int(self) + 1, version=self.version()) 674 | else: 675 | return IP(self.broadcast_long() - 1, version=self.version()) 676 | 677 | def check_collision(self, other): 678 | """Check another network against the given network.""" 679 | other = Network(other) 680 | return self.network_long() <= other.network_long() <= self.broadcast_long() or \ 681 | other.network_long() <= self.network_long() <= other.broadcast_long() 682 | 683 | def __str__(self): 684 | """ 685 | Return CIDR representation of the network. 686 | 687 | >>> net = Network("::1/64") 688 | >>> print(str(net)) 689 | 0000:0000:0000:0000:0000:0000:0000:0001/64 690 | """ 691 | return "%s/%d" % (self.dq, self.mask) 692 | 693 | def __contains__(self, ip): 694 | """ 695 | Check if the given ip is part of the network. 696 | 697 | >>> '192.0.2.42' in Network('192.0.2.0/24') 698 | True 699 | >>> '192.168.2.42' in Network('192.0.2.0/24') 700 | False 701 | """ 702 | return self.check_collision(ip) 703 | 704 | def __lt__(self, other): 705 | """Compare less than.""" 706 | return self.size() < Network(other).size() 707 | 708 | def __le__(self, other): 709 | """Compare less than or equal to.""" 710 | return self.size() <= Network(other).size() 711 | 712 | def __gt__(self, other): 713 | """Compare greater than.""" 714 | return self.size() > Network(other).size() 715 | 716 | def __ge__(self, other): 717 | """Compare greater than or equal to.""" 718 | return self.size() >= Network(other).size() 719 | 720 | def __eq__(self, other): 721 | """Compare equal.""" 722 | other = Network(other) 723 | return int(self) == int(other) and self.size() == other.size() 724 | 725 | def __ne__(self, other): 726 | """Compare not equal.""" 727 | other = Network(other) 728 | return int(self) != int(other) or self.size() != other.size() 729 | 730 | def __hash__(self, other): 731 | """Hash the current network.""" 732 | return hash(int(self)) 733 | 734 | def __getitem__(self, key): 735 | """Get the nth item or slice of the network.""" 736 | if isinstance(key, slice): 737 | # Work-around IPv6 subnets being huge. Slice indices don't like 738 | # long int. 739 | x = key.start or 0 740 | slice_stop = (key.stop or self.size()) - 1 741 | slice_step = key.step or 1 742 | arr = list() 743 | while x < slice_stop: 744 | arr.append(IP(int(self) + x, mask=self.subnet())) 745 | x += slice_step 746 | return tuple(arr) 747 | else: 748 | if key >= self.size(): 749 | raise IndexError("Index out of range: %d > %d" % (key, self.size()-1)) 750 | return IP(int(self) + (key + self.size()) % self.size(), mask=self.subnet()) 751 | 752 | def __iter__(self): 753 | """Generate a range of usable host IP addresses within the network. 754 | 755 | >>> for ip in Network('192.168.114.0/30'): 756 | ... print(str(ip)) 757 | ... 758 | 192.168.114.1 759 | 192.168.114.2 760 | """ 761 | curr = int(self.host_first()) 762 | stop = int(self.host_last()) 763 | while curr <= stop: 764 | yield IP(curr) 765 | curr += 1 766 | 767 | def has_key(self, ip): 768 | """ 769 | Check if the given ip is part of the network. 770 | 771 | :param ip: the ip address 772 | :type ip: :class:`IP` or str or long or int 773 | 774 | >>> net = Network('192.0.2.0/24') 775 | >>> net.has_key('192.168.2.0') 776 | False 777 | >>> net.has_key('192.0.2.42') 778 | True 779 | """ 780 | return self.__contains__(ip) 781 | 782 | def size(self): 783 | """ 784 | Number of ip's within the network. 785 | 786 | >>> net = Network('192.0.2.0/24') 787 | >>> print(net.size()) 788 | 256 789 | """ 790 | return 2 ** ({4: 32, 6: 128}[self.version()] - self.mask) 791 | 792 | def __len__(self): 793 | return self.size() 794 | 795 | 796 | if __name__ == '__main__': 797 | tests = [ 798 | ('192.168.114.42', 23, ['192.168.0.1', '192.168.114.128', '10.0.0.1']), 799 | ('123::', 128, ['123:456::', '::1', '123::456']), 800 | ('::42', 64, ['::1', '1::']), 801 | ('2001:dead:beef:1:c01d:c01a::', 48, ['2001:dead:beef:babe::']), 802 | ('10.10.0.0', '255.255.255.0', ['10.10.0.20', '10.10.10.20']), 803 | ('2001:dead:beef:1:c01d:c01a::', 'ffff:ffff:ffff::', ['2001:dead:beef:babe::']), 804 | ('10.10.0.0/255.255.240.0', None, ['10.10.0.20', '10.10.250.0']), 805 | ] 806 | # 807 | for address, netmask, test_ips in tests: 808 | net = Network(address, netmask) 809 | print('===========') 810 | print('ip address: {0}'.format(net)) 811 | print('to ipv6...: {0}'.format(net.to_ipv6())) 812 | print('ip version: {0}'.format(net.version())) 813 | print('ip info...: {0}'.format(net.info())) 814 | print('subnet....: {0}'.format(net.subnet())) 815 | print('num ip\'s.. {0}:'.format(net.size())) 816 | print('integer...: {0}'.format(int(net))) 817 | print('hex.......: {0}'.format(net.hex())) 818 | print('netmask...: {0}'.format(net.netmask())) 819 | # Not implemented in IPv6 820 | if net.version() == 4: 821 | print('network...: {0}'.format(net.network())) 822 | print('broadcast.: {0}'.format(net.broadcast())) 823 | print('first host: {0}'.format(net.host_first())) 824 | print('reverse...: {0}'.format(net.host_first().to_reverse())) 825 | print('last host.: {0}'.format(net.host_last())) 826 | print('reverse...: {0}'.format(net.host_last().to_reverse())) 827 | for test_ip in test_ips: 828 | print('{0} in network: {1}'.format(test_ip, test_ip in net)) 829 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='ipcalc', 6 | version='1.99.0', 7 | description='IP subnet calculator', 8 | long_description=''' 9 | About 10 | ===== 11 | 12 | This module allows you to perform IP subnet calculations, there is support for 13 | both IPv4 and IPv6 CIDR notation. 14 | 15 | Example Usage 16 | ============= 17 | 18 | :: 19 | 20 | >>> import ipcalc 21 | >>> for x in ipcalc.Network('172.16.42.0/30'): 22 | ... print str(x) 23 | ... 24 | 172.16.42.1 25 | 172.16.42.2 26 | >>> subnet = ipcalc.Network('2001:beef:babe::/48') 27 | >>> print str(subnet.network()) 28 | 2001:beef:babe:0000:0000:0000:0000:0000 29 | >>> print str(subnet.netmask()) 30 | ffff:ffff:ffff:0000:0000:0000:0000:0000 31 | >>> '192.168.42.23' in Network('192.168.42.0/24') 32 | True 33 | >>> long(IP('fe80::213:ceff:fee8:c937')) 34 | 338288524927261089654168587652869703991L 35 | 36 | Bugs/Features 37 | ============= 38 | 39 | You can issue a ticket in GitHub: https://github.com/tehmaze/ipcalc/issues 40 | 41 | Documentation 42 | ============= 43 | 44 | Documentation is available from http://ipcalc.rtfd.org/ 45 | ''', 46 | author='Wijnand Modderman-Lenstra', 47 | author_email='maze@pyth0n.org', 48 | url='https://github.com/tehmaze/ipcalc/', 49 | py_modules=['ipcalc'], 50 | install_requires=['six'], 51 | ) 52 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ipcalc import IP, Network 4 | 5 | class TestSuite(unittest.TestCase): 6 | """Tests.""" 7 | 8 | def test_ipv4_1(self): 9 | net = Network('192.168.114.42', 23) 10 | self.assertTrue(str(net) == '192.168.114.42/23') 11 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '2002:c0a8:722a::') 12 | self.assertTrue(net.info() == 'PRIVATE') 13 | self.assertTrue(net.subnet() == 23) 14 | self.assertTrue(net.size() == 1 << (32 - 23)) 15 | self.assertTrue(int(net) == 0xc0a8722a) 16 | self.assertTrue(net.hex().lower() == 'c0a8722a') 17 | self.assertTrue(str(net.netmask()) == '255.255.254.0') 18 | self.assertTrue(net.version() == 4) 19 | self.assertTrue(str(net.network()) == '192.168.114.0') 20 | self.assertTrue(str(net.broadcast()) == '192.168.115.255') 21 | self.assertFalse('192.168.0.1' in net) 22 | self.assertTrue('192.168.114.128' in net) 23 | self.assertFalse('10.0.0.1' in net) 24 | self.assertTrue(str(net + 6) == '192.168.114.48/23') 25 | self.assertTrue((net + 6) in net) 26 | self.assertTrue(str(net - 6) == '192.168.114.36/23') 27 | self.assertTrue((net - 6) in net) 28 | 29 | def test_ipv4_2(self): 30 | net = Network('10.10.0.0', '255.255.255.0') 31 | self.assertTrue(str(net) == '10.10.0.0/24') 32 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '2002:a0a::') 33 | self.assertTrue(net.info() == 'PRIVATE') 34 | self.assertTrue(net.subnet() == 24) 35 | self.assertTrue(net.size() == 1 << (32 - 24)) 36 | self.assertTrue(int(net) == 0x0a0a0000) 37 | self.assertTrue(net.hex().lower() == '0a0a0000') 38 | self.assertTrue(str(net.netmask()) == '255.255.255.0') 39 | self.assertTrue(net.version() == 4) 40 | self.assertTrue(str(net.network()) == '10.10.0.0') 41 | self.assertTrue(str(net.broadcast()) == '10.10.0.255') 42 | self.assertFalse('192.168.0.1' in net) 43 | self.assertFalse('192.168.114.128' in net) 44 | self.assertFalse('10.0.0.1' in net) 45 | self.assertTrue('10.10.0.254' in net) 46 | self.assertTrue('10.10.0.100' in net) 47 | self.assertTrue(str(net + 6) == '10.10.0.6/24') 48 | self.assertTrue(str(net + 6) in net) 49 | self.assertTrue(str(net - 6) == '10.9.255.250/24') # note, result is not in subnet 50 | self.assertFalse(str(net -6) in net) 51 | 52 | def test_ipv4_3(self): 53 | net = Network('10.10.0.0/255.255.255.0') 54 | self.assertTrue(str(net) == '10.10.0.0/24') 55 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '2002:a0a::') 56 | self.assertTrue(net.info() == 'PRIVATE') 57 | self.assertTrue(net.subnet() == 24) 58 | self.assertTrue(net.size() == 1 << (32 - 24)) 59 | self.assertTrue(int(net) == 0x0a0a0000) 60 | self.assertTrue(net.hex().lower() == '0a0a0000') 61 | self.assertTrue(str(net.netmask()) == '255.255.255.0') 62 | self.assertTrue(net.version() == 4) 63 | self.assertTrue(str(net.network()) == '10.10.0.0') 64 | self.assertTrue(str(net.broadcast()) == '10.10.0.255') 65 | self.assertFalse('192.168.0.1' in net) 66 | self.assertFalse('192.168.114.128' in net) 67 | self.assertFalse('10.0.0.1' in net) 68 | self.assertTrue('10.10.0.254' in net) 69 | self.assertTrue('10.10.0.100' in net) 70 | 71 | def test_ipv6_1(self): 72 | net = Network('123::', 128) 73 | self.assertTrue(str(net) == '0123:0000:0000:0000:0000:0000:0000:0000/128') 74 | self.assertTrue(str(net.to_compressed()) == '123::') 75 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '123::') 76 | self.assertTrue(net.info() == 'UNKNOWN') 77 | self.assertTrue(net.subnet() == 128) 78 | self.assertTrue(net.size() == 1 << (128 - 128)) 79 | self.assertTrue(int(net) == (0x123<<112)) 80 | self.assertTrue(net.hex().lower() == '01230000000000000000000000000000') 81 | self.assertTrue(str(net.netmask()) == 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff') 82 | self.assertTrue(net.version() == 6) 83 | self.assertTrue(str(net.network()) == '0123:0000:0000:0000:0000:0000:0000:0000') 84 | self.assertTrue(str(net.broadcast()) == '0123:0000:0000:0000:0000:0000:0000:0000') 85 | self.assertFalse('123:456::' in net) 86 | self.assertTrue('123::' in net) 87 | self.assertFalse('::1' in net) 88 | self.assertFalse('123::456' in net) 89 | self.assertTrue(str((net + 6).to_compressed()).lower() == '123::6') 90 | self.assertFalse((net + 6) in net) 91 | self.assertTrue(str((net - 6).to_compressed()).lower() == '122:ffff:ffff:ffff:ffff:ffff:ffff:fffa') 92 | self.assertFalse((net - 6) in net) 93 | 94 | def test_ipv6_2(self): 95 | net = Network('::42', 64) 96 | self.assertTrue(str(net) == '0000:0000:0000:0000:0000:0000:0000:0042/64') 97 | self.assertTrue(str(net.to_compressed()) == '::42') 98 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '::42') 99 | self.assertTrue(net.info() == 'IPV4COMP') 100 | self.assertTrue(net.subnet() == 64) 101 | self.assertTrue(net.size() == 1 << (128 - 64)) 102 | self.assertTrue(int(net) == 0x42) 103 | self.assertTrue(net.hex().lower() == '00000000000000000000000000000042') 104 | self.assertTrue(str(net.netmask()) == 'ffff:ffff:ffff:ffff:0000:0000:0000:0000') 105 | self.assertTrue(net.version() == 6) 106 | self.assertTrue(str(net.network()) == '0000:0000:0000:0000:0000:0000:0000:0000') 107 | self.assertTrue(str(net.broadcast()) == '0000:0000:0000:0000:ffff:ffff:ffff:ffff') 108 | self.assertFalse('123:456::' in net) 109 | self.assertTrue('::aaaa:bbbb:cccc:dddd' in net) 110 | self.assertTrue('::dddd' in net) 111 | self.assertTrue('::1' in net) 112 | self.assertFalse('123::456' in net) 113 | self.assertTrue(str((net + 6).to_compressed()).lower() == '::48') 114 | self.assertTrue((net + 6) in net) 115 | self.assertTrue(str((net - 6).to_compressed()).lower() == '::3c') 116 | self.assertTrue((net - 6) in net) 117 | 118 | def test_ipv6_3(self): 119 | net = Network('2001:dead:beef:1:c01d:c01a::', 48) 120 | self.assertTrue(str(net) == '2001:dead:beef:0001:c01d:c01a:0000:0000/48') 121 | self.assertTrue(str(net.to_compressed()) == '2001:dead:beef:1:c01d:c01a::') 122 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '2001:dead:beef:1:c01d:c01a::') 123 | self.assertTrue(net.info() == 'UNKNOWN') 124 | self.assertTrue(net.subnet() == 48) 125 | self.assertTrue(net.size() == 1 << (128 - 48)) 126 | self.assertTrue(int(net) == 0x2001deadbeef0001c01dc01a00000000) 127 | self.assertTrue(net.hex().lower() == '2001deadbeef0001c01dc01a00000000') 128 | self.assertTrue(str(net.netmask()) == 'ffff:ffff:ffff:0000:0000:0000:0000:0000') 129 | self.assertTrue(net.version() == 6) 130 | self.assertTrue(str(net.network()) == '2001:dead:beef:0000:0000:0000:0000:0000') 131 | self.assertTrue(str(net.broadcast()) == '2001:dead:beef:ffff:ffff:ffff:ffff:ffff') 132 | self.assertFalse('123:456::' in net) 133 | self.assertFalse('::aaaa:bbbb:cccc:dddd' in net) 134 | self.assertFalse('::dddd' in net) 135 | self.assertFalse('::1' in net) 136 | self.assertFalse('123::456' in net) 137 | self.assertTrue('2001:dead:beef:babe::1234' in net) 138 | 139 | def test_ipv6_4(self): 140 | net = Network('2001:dead:beef:1:c01d:c01a::', 'ffff:ffff:ffff::') 141 | self.assertTrue(str(net) == '2001:dead:beef:0001:c01d:c01a:0000:0000/48') 142 | self.assertTrue(str(net.to_compressed()) == '2001:dead:beef:1:c01d:c01a::') 143 | self.assertTrue(str(net.to_ipv6().to_compressed()) == '2001:dead:beef:1:c01d:c01a::') 144 | self.assertTrue(net.info() == 'UNKNOWN') 145 | self.assertTrue(net.subnet() == 48) 146 | self.assertTrue(net.size() == 1 << (128 - 48)) 147 | self.assertTrue(int(net) == 0x2001deadbeef0001c01dc01a00000000) 148 | self.assertTrue(net.hex().lower() == '2001deadbeef0001c01dc01a00000000') 149 | self.assertTrue(str(net.netmask()) == 'ffff:ffff:ffff:0000:0000:0000:0000:0000') 150 | self.assertTrue(net.version() == 6) 151 | self.assertTrue(str(net.network()) == '2001:dead:beef:0000:0000:0000:0000:0000') 152 | self.assertTrue(str(net.broadcast()) == '2001:dead:beef:ffff:ffff:ffff:ffff:ffff') 153 | self.assertFalse('123:456::' in net) 154 | self.assertFalse('::aaaa:bbbb:cccc:dddd' in net) 155 | self.assertFalse('::dddd' in net) 156 | self.assertFalse('::1' in net) 157 | self.assertFalse('123::456' in net) 158 | self.assertTrue('2001:dead:beef:babe::1234' in net) 159 | 160 | def test_ipv6_5(self): 161 | # test parsing of 4-in-6 IPv6 address 162 | ip = IP('8000::0.0.0.1') 163 | self.assertTrue(ip.ip == ((2**127) + 1)) 164 | ip = IP('8000:8000::0.0.0.1') 165 | self.assertTrue(ip.ip == ((2**127) + (2**111) + 1)) 166 | 167 | 168 | class TestIP(unittest.TestCase): 169 | 170 | """Tests for IP.""" 171 | 172 | def test_eq_le_gt(self): 173 | self.assertEqual(IP('192.168.11.0'), IP('192.168.11.0')) 174 | self.assertNotEqual(IP('192.168.1.0'), IP('192.168.11.0')) 175 | 176 | def test_guesstimation(self): 177 | self.assertEqual(IP('192.168.0.1', mask=28).guess_network(), Network('192.168.0.0/28')) 178 | self.assertEqual(IP('192.168.0.1/24').guess_network(), Network('192.168.0.0/24')) 179 | self.assertEqual(IP('192.168.0.1/255.255.255.0', mask=28).guess_network(), Network('192.168.0.0/24')) 180 | self.assertEqual(IP('192.168.0.56', mask=26).guess_network(), Network('192.168.0.0/26')) 181 | self.assertEqual(IP('192.168.0.1').guess_network(), Network('192.168.0.1/32')) 182 | 183 | 184 | class TestNetwork(unittest.TestCase): 185 | 186 | """Tests for Network.""" 187 | 188 | def setUp(self): 189 | self.network = Network('192.168.11.0/255.255.255.0') 190 | 191 | def test_calculation(self): 192 | self.assertEqual(self.network[1].subnet(), 24) 193 | 194 | a = Network('192.168.0.100/28') 195 | self.assertEqual(str(a), '192.168.0.100/28') 196 | self.assertEqual(a.size(), 16) 197 | self.assertEqual(a.size(), len(a)) 198 | self.assertEqual(int(a), 0xC0A80064) 199 | for i in range(a.size()): 200 | self.assertEqual(int(a[i]), i + 0xC0A80064) 201 | 202 | self.assertRaises(IndexError, lambda: a[a.size()]) 203 | 204 | def test_indexers(self): 205 | expected = range(int(0xC0A80B00), int(0xC0A80C00)) 206 | self.assertEqual(self.network.size(), len(expected)) 207 | for i in range(self.network.size()): 208 | self.assertEqual(int(self.network[i]), expected[i]) 209 | self.assertEqual(int(self.network[-1]), expected[-1]) 210 | 211 | def test_contains(self): 212 | self.assertTrue(IP('192.168.11.0') in self.network) 213 | self.assertTrue(IP('192.168.11.1') in self.network) 214 | self.assertTrue(IP('192.168.11.255') in self.network) 215 | 216 | def test_eq_le_gt(self): 217 | self.assertEqual(Network('192.168.11.0'), Network('192.168.11.0')) 218 | self.assertEqual(Network('192.168.11.0/32'), Network('192.168.11.0')) 219 | self.assertEqual(Network('192.168.11.0'), IP('192.168.11.0')) 220 | self.assertEqual(Network('192.168.11.0/32'), IP('192.168.11.0')) 221 | 222 | self.assertNotEqual(Network('192.168.11.0/28'), Network('192.168.11.0/24')) 223 | self.assertNotEqual(Network('192.168.11.0'), Network('192.168.11.1')) 224 | self.assertNotEqual(Network('192.168.11.0'), Network('192.168.2.1')) 225 | self.assertNotEqual(Network('192.168.11.0/30'), IP('192.168.11.0')) 226 | self.assertNotEqual(Network('192.168.1.0'), IP('192.168.11.0')) 227 | 228 | self.assertTrue(Network('192.168.1.0/30') < Network('192.168.1.0/29')) 229 | self.assertTrue(Network('192.168.1.0/30') <= Network('192.168.1.0/29')) 230 | self.assertTrue(Network('192.168.1.0/30') <= Network('192.168.1.0/30')) 231 | 232 | self.assertTrue(Network('192.168.1.0/28') > Network('192.168.1.0/29')) 233 | self.assertTrue(Network('192.168.1.0/28') >= Network('192.168.1.0/29')) 234 | self.assertTrue(Network('192.168.1.0/28') >= Network('192.168.1.0/28')) 235 | 236 | 237 | if __name__ == '__main__': 238 | unittest.main() 239 | --------------------------------------------------------------------------------