├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.rst ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── src └── ifcfg │ ├── __init__.py │ ├── cli.py │ ├── parser.py │ └── tools.py ├── tests ├── __init__.py ├── base.py ├── cli_tests.py ├── ifconfig_out.py ├── ifconfig_tests.py ├── ip_out.py ├── ip_tests.py ├── ipconfig_out.py ├── ipconfig_tests.py ├── no_mock_tests.py └── tools_tests.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .coverage.* 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | htmlcov 30 | coverage_html_report 31 | test.py 32 | .venv 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: check-yaml 7 | - id: check-added-large-files 8 | - id: debug-statements 9 | - id: end-of-file-fixer 10 | - repo: https://github.com/pycqa/isort 11 | rev: 5.12.0 12 | hooks: 13 | - id: isort 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | matrix: 6 | include: 7 | - python: "2.7" 8 | env: 9 | - TOX_ENV=py2.7 10 | - python: "2.7" 11 | env: 12 | - TOX_ENV=lint 13 | - python: "3.4" 14 | - python: "3.5" 15 | - python: "3.6" 16 | - python: "3.7" 17 | - python: "3.8" 18 | dist: bionic 19 | 20 | before_install: 21 | - sudo apt-get install -y net-tools 22 | 23 | install: 24 | - pip install -U pip codecov tox tox-travis 25 | 26 | script: 27 | - tox 28 | 29 | after_success: 30 | - codecov 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2012, BJ Dierkes 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of BJ Dierkes nor the names of his contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | install: clean 4 | python setup.py install 5 | 6 | build: clean 7 | python setup.py build 8 | 9 | clean: clean-build clean-pyc clean-test 10 | 11 | clean-build: ## remove build artifacts 12 | rm -fr build/ 13 | rm -fr dist/ 14 | rm -fr .eggs/ 15 | find . -name '*.egg-info' -exec rm -fr {} + 16 | find . -name '*.egg' -exec rm -f {} + 17 | 18 | clean-pyc: ## remove Python file artifacts 19 | find . -name '*.pyc' -exec rm -f {} + 20 | find . -name '*.pyo' -exec rm -f {} + 21 | find . -name '*~' -exec rm -f {} + 22 | find . -name '__pycache__' -exec rm -fr {} + 23 | 24 | clean-test: ## remove test and coverage artifacts 25 | rm -fr .tox/ 26 | rm -f .coverage 27 | rm -fr htmlcov/ 28 | 29 | lint: ## Check python code conventions 30 | tox -e lint 31 | 32 | test: ## Run automated test suite 33 | nosetests 34 | 35 | test-all: ## Run tests on all supported Python environments 36 | tox 37 | 38 | coverage: ## Generate test coverage report 39 | coverage run --source src/ifcfg `which nosetests` 40 | coverage report -m 41 | 42 | release: dist ## Generate and upload release to PyPi 43 | @echo "" 44 | @echo "Release check list:" 45 | @echo "" 46 | @echo "1. Release notes?" 47 | @echo "2. Did you do a signed commit and push to Github?" 48 | @echo "3. Check that the .whl and .tar.gz dists work - e.g. that MANIFEST.in is updated." 49 | @echo "" 50 | @read -p "CTRL+C or ENTER" dummy 51 | twine upload -s dist/* 52 | 53 | dist: clean ## Generate wheels distribution 54 | python setup.py bdist_wheel 55 | python setup.py sdist 56 | ls -l dist 57 | 58 | req: 59 | pip install --requirement requirements_dev.txt 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python cross-platform network interface discovery 2 | ================================================= 3 | 4 | .. image:: https://badge.fury.io/py/ifcfg.svg 5 | :target: https://pypi.python.org/pypi/ifcfg/ 6 | .. image:: https://travis-ci.org/ftao/python-ifcfg.svg 7 | :target: https://travis-ci.org/ftao/python-ifcfg 8 | .. image:: http://codecov.io/github/ftao/python-ifcfg/coverage.svg?branch=master 9 | :target: http://codecov.io/github/ftao/python-ifcfg?branch=master 10 | 11 | Ifcfg is a cross-platform (Windows/Unix) library for parsing ``ifconfig`` and 12 | ``ipconfig`` output in Python. It is useful for pulling information such as IP, 13 | Netmask, MAC Address, Hostname, etc. 14 | 15 | A fallbacks to ``ip`` is included for newer Unix systems w/o ``ifconfig``. Windows 16 | systems are supported (in English) through ``ipconfig``. 17 | 18 | Usage 19 | ----- 20 | 21 | :: 22 | 23 | import ifcfg 24 | import json 25 | 26 | for name, interface in ifcfg.interfaces().items(): 27 | # do something with interface 28 | print interface['device'] # Device name 29 | print interface['inet'] # First IPv4 found 30 | print interface['inet4'] # List of ips 31 | print interface['inet6'] # List of ips 32 | print interface['netmask'] # Backwards compat: First netmask 33 | print interface['netmasks'] # List of netmasks 34 | print interface['broadcast'] # Backwards compat: First broadcast 35 | print interface['broadcasts'] # List of broadcast 36 | 37 | default = ifcfg.default_interface() 38 | 39 | The output of 'ifcfg.interfaces()' dumped to JSON looks something like the 40 | following: 41 | 42 | :: 43 | 44 | $ python -m ifcfg.cli | python -mjson.tool 45 | { 46 | "docker0": { 47 | "inet": "172.17.0.1", 48 | "inet4": [ 49 | "172.17.0.1" 50 | ], 51 | "ether": "01:02:9d:04:07:e3", 52 | "inet6": [], 53 | "netmask": "255.255.0.0", 54 | "netmasks": [ 55 | "255.255.0.0" 56 | ], 57 | "broadcast": "172.17.255.255", 58 | "broadcasts": [ 59 | "172.17.255.255" 60 | ], 61 | "prefixlens": [], 62 | "device": "docker0", 63 | "flags": "4099 ", 64 | "mtu": "1500" 65 | }, 66 | "enp0s25": { 67 | "inet": null, 68 | "inet4": [], 69 | "ether": "a0:88:b4:3d:67:7b", 70 | "inet6": [], 71 | "netmask": null, 72 | "netmasks": [], 73 | "broadcast": null, 74 | "broadcasts": [], 75 | "prefixlens": [], 76 | "device": "enp0s25", 77 | "flags": "4099 ", 78 | "mtu": "1500" 79 | }, 80 | "lo": { 81 | "inet": "127.0.0.1", 82 | "inet4": [ 83 | "127.0.0.1" 84 | ], 85 | "ether": null, 86 | "inet6": [ 87 | "::1" 88 | ], 89 | "netmask": "255.0.0.0", 90 | "netmasks": [ 91 | "255.0.0.0" 92 | ], 93 | "broadcast": null, 94 | "broadcasts": [ 95 | null 96 | ], 97 | "prefixlens": [ 98 | "128" 99 | ], 100 | "device": "lo", 101 | "flags": "73 ", 102 | "mtu": "65536" 103 | }, 104 | } 105 | 106 | 107 | Development 108 | ----------- 109 | 110 | To bootstrap development, use a Python virtual environment, and install the dev requirements:: 111 | 112 | # Install dev dependencies 113 | pip install -r requirements_dev.txt 114 | # Run tests locally 115 | make test 116 | 117 | You can also install tox and run the tests in a specific environment:: 118 | 119 | pip install tox 120 | tox -e py27 121 | 122 | Before commiting and opening PRs, ensure that you have pre-commit hooks running:: 123 | 124 | pip install pre-commit 125 | pre-commit install 126 | 127 | 128 | Release notes 129 | ------------- 130 | 131 | 0.24 132 | ---- 133 | 134 | * add support for arrow notation inet #71 135 | 136 | 0.23 137 | ____ 138 | 139 | * Add support for multiple netmasks, broadcast addresses, as well as ipv6 prefix lengths #67 140 | 141 | 0.22 142 | ____ 143 | 144 | * Python 3.7 and 3.8 support #51 #53 145 | * Default interface detection on Windows #25 #56 146 | * New flags for unix `ip` command #61 147 | 148 | 0.22 149 | ____ 150 | 151 | * Python 3.7 and 3.8 support #51 #53 152 | * Default interface detection on Windows #25 #56 153 | * New flags for unix `ip` command #61 154 | 155 | 0.21 156 | ____ 157 | 158 | * Force `C` as locale for running commands, to ensure consistent regex patterns #47 159 | 160 | 0.20 161 | ____ 162 | 163 | * Throw an exception when neither `ip` nor `ifconfig` commands exist #45 164 | 165 | 0.19 166 | ____ 167 | 168 | * Adds support for interfaces with VLAN notation, e.g. `eth2.2` #40 169 | * Fetch MTU values from `ip` command results #39 170 | 171 | 0.18 172 | ____ 173 | 174 | * Adds support for interfaces with non-alphanumeric characters, e.g. `eth-int` #35 and #36 175 | 176 | 0.17 177 | ____ 178 | 179 | * Restore ``ip`` after regressions + add tests 180 | * Add MacOSX support for ``ip`` command 181 | 182 | 0.16 183 | ____ 184 | 185 | * Support for multiple IPv4 addresses in the new 'inet4' field 186 | * Packaging cleanup 187 | 188 | 0.15 189 | ____ 190 | 191 | * Support for bridged interface names #24 192 | 193 | 194 | 0.14 195 | ____ 196 | 197 | * Replace Python 2 syntax #21 198 | 199 | 200 | 0.13 201 | ____ 202 | 203 | * Further crashes on non-English Windows systems #17 204 | * Known issue: Localized non-English Windows parsing does not work #18 205 | 206 | 207 | 0.12 208 | ____ 209 | 210 | * Fix encoding crashes on non-English Windows systems 211 | 212 | 213 | 0.11 214 | ____ 215 | 216 | After 6 beta releases, we move on from an idea that this is beta software and instead consider 217 | it to be stable -- we will probably never actually keep up with all the various ways of detecting 218 | network properties for different systems. Anything that is incorrectly detected and can be updated, 219 | can also be implemented and shipped as a new patch release. 220 | 221 | So let's **ship early, ship often** instead. 222 | 223 | This release seeks to clean up the codebase (sparingly!) and introduce 224 | Windows compatibility. 225 | 226 | * Add Windows compatible parsing of ``ipconfig`` output 227 | * Handle non-unicode terminals (Windows+Mac especially) 228 | * Removing ill-defined ``encoding`` keyword arg from ``ifcfg.get_parser`` 229 | * Removed no-op Linux Kernel 2.x parsing and ``kernel`` keyword arg 230 | * Removed class ``ifcfg.IfcfgParser``, use ``UnixParser`` instead 231 | * All strings are UTF-8, also in Py 2.7 232 | * Only cross-platform features are now guaranteed to be in the result set: 233 | ``['inet', 'ether', 'inet6', 'netmask']`` 234 | * IPv6 addresses are now stored in a list. 235 | * Removed prefixlen and scopeid, as they should be added for each IPv6 address, not the 236 | interface 237 | * Allow ``ifcfg`` to be imported despite whether or not the OS system is 238 | recognized. 239 | * Remove ``ifcfg.exc`` module 240 | * Fix some interface names containing `:_-` characters on Linux (Sergej Vasiljev) 241 | 242 | 243 | 0.10.1 244 | ______ 245 | 246 | * Fixed encoding issues, preventing ``default_interface`` to be detected 247 | 248 | 249 | 0.10 250 | ____ 251 | 252 | * Support for Unix systems w/o ``ifconfig``, for instance newer Ubuntu/Debian 253 | * Refactored to use ``src/`` hierarchy 254 | 255 | 256 | 257 | License 258 | ------- 259 | 260 | The Ifcfg library is Open Source and is distributed under the BSD 261 | License (three clause). Please see the LICENSE file included with this 262 | software. 263 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | mock 2 | nose 3 | pre-commit 4 | tox 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = E226,E302,E303,E41,E501 6 | max-line-length = 160 7 | max-complexity = 10 8 | exclude = */*migrations 9 | 10 | [metadata] 11 | description-file = README.rst 12 | 13 | [isort] 14 | atomic = true 15 | multi_line_output = 5 16 | line_length = 160 17 | indent = ' ' 18 | combine_as_imports = true 19 | skip = setup.py,.eggs,build 20 | 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import find_packages, setup 5 | 6 | sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) 7 | 8 | import ifcfg # noqa 9 | 10 | 11 | setup( 12 | name='ifcfg', 13 | version=ifcfg.__version__, 14 | description="Python ifconfig wrapper for Unix/Linux/MacOSX + ipconfig for Windows", 15 | long_description=open("README.rst").read(), 16 | keywords='', 17 | author='Original author: BJ Dierkes', 18 | author_email='info@learningequality.org', 19 | url='https://github.com/ftao/python-ifcfg', 20 | license='BSD', 21 | packages=find_packages('src'), 22 | package_dir={'': 'src'}, 23 | include_package_data=True, 24 | zip_safe=False, 25 | test_suite='nose.collector', 26 | install_requires=[], 27 | setup_requires=[], 28 | entry_points=""" 29 | """, 30 | namespace_packages=[], 31 | classifiers=[ 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: BSD License', 34 | 'Natural Language :: English', 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: 3.5', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Programming Language :: Python :: 3.9', 46 | 'Programming Language :: Python :: 3.10', 47 | 'Programming Language :: Python :: 3.11', 48 | 'Programming Language :: Python :: Implementation :: PyPy', 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /src/ifcfg/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import platform 6 | 7 | from . import parser, tools 8 | 9 | __version__ = "0.24" 10 | 11 | Log = tools.minimal_logger(__name__) 12 | 13 | 14 | #: Module instance properties, can be mocked for testing 15 | distro = platform.system() 16 | 17 | 18 | def get_parser_class(): 19 | """ 20 | Returns the parser according to the system platform 21 | """ 22 | global distro 23 | if distro == 'Linux': 24 | Parser = parser.LinuxParser 25 | if not os.path.exists(Parser.get_command().split(' ')[0]): 26 | Parser = parser.UnixIPParser 27 | if not os.path.exists(Parser.get_command().split(' ')[0]): 28 | Log.warning("Neither `ifconfig` (`%s`) nor `ip` (`%s`) commands are available, listing network interfaces is likely to fail", 29 | parser.LinuxParser.get_command(), 30 | parser.UnixIPParser.get_command()) 31 | elif distro in ['Darwin', 'MacOSX']: 32 | Parser = parser.MacOSXParser 33 | elif distro == 'Windows': 34 | # For some strange reason, Windows will always be win32, see: 35 | # https://stackoverflow.com/a/2145582/405682 36 | Parser = parser.WindowsParser 37 | else: 38 | Parser = parser.NullParser 39 | Log.error("Unknown distro type '%s'." % distro) 40 | Log.debug("Distro detected as '%s'" % distro) 41 | Log.debug("Using '%s'" % Parser) 42 | 43 | return Parser 44 | 45 | 46 | #: Module instance properties, can be mocked for testing 47 | Parser = get_parser_class() 48 | 49 | 50 | def get_parser(ifconfig=None): 51 | """ 52 | Detect the proper parser class, and return it instantiated. 53 | 54 | Optional Arguments: 55 | 56 | ifconfig 57 | The ifconfig (stdout) to pass to the parser (used for testing). 58 | 59 | """ 60 | global Parser 61 | return Parser(ifconfig=ifconfig) 62 | 63 | 64 | def interfaces(ifconfig=None): 65 | """ 66 | Return just the parsed interfaces dictionary from the proper parser. 67 | 68 | """ 69 | global Parser 70 | return Parser(ifconfig=ifconfig).interfaces 71 | 72 | 73 | def default_interface(ifconfig=None, route_output=None): 74 | """ 75 | Return just the default interface device dictionary. 76 | 77 | :param ifconfig: For mocking actual command output 78 | :param route_output: For mocking actual command output 79 | """ 80 | global Parser 81 | return Parser(ifconfig=ifconfig)._default_interface(route_output=route_output) 82 | -------------------------------------------------------------------------------- /src/ifcfg/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import json 4 | 5 | import ifcfg 6 | 7 | 8 | def main(): 9 | print(json.dumps(ifcfg.interfaces(), indent=2)) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /src/ifcfg/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import re 6 | import socket 7 | 8 | from .tools import exec_cmd, hex2dotted, minimal_logger 9 | 10 | Log = minimal_logger(__name__) 11 | 12 | 13 | #: These will always be present for each device (even if they are None) 14 | DEVICE_PROPERTY_DEFAULTS = { 15 | 'inet': None, 16 | 'inet4': 'LIST', 17 | 'ether': None, 18 | 'inet6': 'LIST', # Because lists are mutable 19 | 'netmask': None, 20 | 'netmasks': 'LIST', 21 | 'broadcast': None, 22 | 'broadcasts': 'LIST', 23 | 'prefixlens': 'LIST', 24 | } 25 | 26 | 27 | class Parser(object): 28 | """ 29 | Main parser class interface 30 | """ 31 | def __init__(self, ifconfig=None): 32 | self._interfaces = {} 33 | self.ifconfig_data = ifconfig 34 | self.parse(self.ifconfig_data) 35 | 36 | def add_device(self, device_name): 37 | if device_name in self._interfaces: 38 | raise RuntimeError("Device {} already added".format(device_name)) 39 | self._interfaces[device_name] = {} 40 | for key, value in DEVICE_PROPERTY_DEFAULTS.items(): 41 | if value == "LIST": 42 | self._interfaces[device_name][key] = [] 43 | else: 44 | self._interfaces[device_name][key] = value 45 | 46 | def parse(self, ifconfig=None): # noqa: max-complexity=12 47 | """ 48 | Parse ifconfig output into self._interfaces. 49 | Optional Arguments: 50 | ifconfig 51 | The data (stdout) from the ifconfig command. Default is to 52 | call exec_cmd(self.get_command()). 53 | """ 54 | if not ifconfig: 55 | ifconfig, errors, return_code = exec_cmd(self.get_command()) 56 | if return_code: 57 | Log.error(errors) 58 | raise ValueError("Non-zero return code, reporting error-code '{}'".format(errors)) 59 | self.ifconfig_data = ifconfig 60 | cur = None 61 | patterns = self.get_patterns() 62 | for line in self.ifconfig_data.splitlines(): 63 | for pattern in patterns: 64 | m = re.match(pattern, line) 65 | if not m: 66 | continue 67 | groupdict = m.groupdict() 68 | # Special treatment to trigger which interface we're 69 | # setting for if 'device' is in the line. Presumably the 70 | # device of the interface is within the first line of the 71 | # device block. 72 | if 'device' in groupdict: 73 | cur = groupdict['device'] 74 | self.add_device(cur) 75 | elif cur is None: 76 | raise RuntimeError( 77 | "Got results that don't belong to a device" 78 | ) 79 | 80 | for k, v in groupdict.items(): 81 | if k in self._interfaces[cur]: 82 | if self._interfaces[cur][k] is None: 83 | self._interfaces[cur][k] = v 84 | elif hasattr(self._interfaces[cur][k], 'append'): 85 | self._interfaces[cur][k].append(v) 86 | elif self._interfaces[cur][k] == v: 87 | # Silently ignore if the it's the same value as last. Example: Multiple 88 | # inet4 addresses, result in multiple netmasks. Cardinality mismatch 89 | continue 90 | else: 91 | raise RuntimeError( 92 | "Tried to add {}={} multiple times to {}, it was already: {}".format( 93 | k, 94 | v, 95 | cur, 96 | self._interfaces[cur][k] 97 | ) 98 | ) 99 | else: 100 | self._interfaces[cur][k] = v 101 | 102 | # Copy the first 'inet4' ip address to 'inet' for backwards compatibility 103 | for device, device_dict in self._interfaces.items(): 104 | if len(device_dict['inet4']) > 0: 105 | device_dict['inet'] = device_dict['inet4'][0] 106 | 107 | # Copy the first 'netmasks' mask to 'netmask' for backwards compatibility 108 | for device, device_dict in self._interfaces.items(): 109 | if len(device_dict['netmasks']) > 0: 110 | device_dict['netmask'] = device_dict['netmasks'][0] 111 | 112 | # Copy the first 'broadcast' ip address to 'broadcast' for backwards compatibility 113 | for device, device_dict in self._interfaces.items(): 114 | if len(device_dict['broadcasts']) > 0: 115 | device_dict['broadcast'] = device_dict['broadcasts'][0] 116 | 117 | # fix it up 118 | self._interfaces = self.alter(self._interfaces) 119 | 120 | def alter(self, interfaces): 121 | """ 122 | Used to provide the ability to alter the interfaces dictionary before 123 | it is returned from self.parse(). 124 | Required Arguments: 125 | interfaces 126 | The interfaces dictionary. 127 | Returns: interfaces dict 128 | """ 129 | # fixup some things 130 | for device, device_dict in interfaces.items(): 131 | if len(device_dict['inet4']) > 0: 132 | device_dict['inet'] = device_dict['inet4'][0] 133 | if 'inet' in device_dict and device_dict['inet'] is not None: 134 | try: 135 | host = socket.gethostbyaddr(device_dict['inet'])[0] 136 | interfaces[device]['hostname'] = host 137 | except (socket.herror, socket.gaierror): 138 | interfaces[device]['hostname'] = None 139 | 140 | # To be sure that hex values and similar are always consistent, we 141 | # return everything in lowercase. For instance, Windows writes 142 | # MACs in upper-case. 143 | for key, device_item in device_dict.items(): 144 | if hasattr(device_item, 'lower'): 145 | interfaces[device][key] = device_dict[key].lower() 146 | 147 | return interfaces 148 | 149 | @classmethod 150 | def get_command(cls): 151 | raise NotImplementedError() 152 | 153 | @classmethod 154 | def get_patterns(cls): 155 | raise NotImplementedError() 156 | 157 | @property 158 | def interfaces(self): 159 | raise NotImplementedError() 160 | 161 | @property 162 | def default_interface(self): 163 | raise NotImplementedError() 164 | 165 | 166 | class NullParser(Parser): 167 | """ 168 | Doesn't do anything, useful to maintain internal interfaces in case we 169 | don't want to do anything (because we haven't determined the OS) 170 | """ 171 | 172 | def __init__(self, ifconfig=None): 173 | self._interfaces = {} 174 | self.ifconfig_data = ifconfig 175 | 176 | def parse(self, ifconfig=None): 177 | raise NotImplementedError() 178 | 179 | @property 180 | def interfaces(self): 181 | return [] 182 | 183 | @property 184 | def default_interface(self): 185 | return None 186 | 187 | 188 | class WindowsParser(Parser): 189 | 190 | @classmethod 191 | def get_command(cls): 192 | return 'ipconfig /all' 193 | 194 | @classmethod 195 | def get_patterns(cls): 196 | return [ 197 | r"^(?P\w.+):", 198 | r"^ Physical Address. . . . . . . . . : (?P[ABCDEFabcdef\d-]+)", 199 | r"^ IPv4 Address. . . . . . . . . . . : (?P[^\s\(]+)", 200 | r"^ IPv6 Address. . . . . . . . . . . : (?P[ABCDEFabcdef\d\:\%]+)", 201 | r"^\s+Default Gateway . . . . . . . . . : (?P[^\s\(]+)", 202 | ] 203 | 204 | @property 205 | def interfaces(self): 206 | """ 207 | Returns the full interfaces dictionary. 208 | """ 209 | return self._interfaces 210 | 211 | def alter(self, interfaces): 212 | interfaces = Parser.alter(self, interfaces) 213 | # fixup some things 214 | for device, device_dict in interfaces.items(): 215 | if 'ether' in device_dict and interfaces[device]['ether']: 216 | interfaces[device]['ether'] = device_dict['ether'].replace('-', ':') 217 | return interfaces 218 | 219 | @property 220 | def default_interface(self): 221 | """ 222 | Returns the default interface device. 223 | """ 224 | for _ifn, interface in self.interfaces.items(): 225 | gateway = interface.get('default_gateway', '').strip() 226 | if gateway and gateway != '::' and not gateway.startswith('127'): 227 | return interface 228 | 229 | class UnixParser(Parser): 230 | 231 | @classmethod 232 | def get_command(cls): 233 | ifconfig_cmd = 'ifconfig' 234 | for path in ['/sbin', '/usr/sbin', '/bin', '/usr/bin']: 235 | if os.path.exists(os.path.join(path, ifconfig_cmd)): 236 | ifconfig_cmd = os.path.join(path, ifconfig_cmd) 237 | break 238 | ifconfig_cmd = ifconfig_cmd + " -a" 239 | return ifconfig_cmd 240 | 241 | @classmethod 242 | def get_patterns(cls): 243 | return [ 244 | r'(?P^[-a-zA-Z0-9:_\.]+): flags=(?P.*) mtu (?P\d+)', 245 | r'.*inet\s+(?P[\d\.]+)(\s+-->\s+(?P<_inet4>[\d\.]+))?((\s+netmask\s)|\/)(?P[\w.]+)((\s+(brd|broadcast)\s(?P[^\s]*))?.*)?', 246 | r'.*inet6\s+(?P[\d\:abcdef]+)(%\w+)?((\s+prefixlen\s+)|\/)(?P\d+)', 247 | r'.*ether (?P[^\s]*).*', 248 | ] 249 | 250 | @property 251 | def interfaces(self): 252 | """ 253 | Returns the full interfaces dictionary. 254 | """ 255 | return self._interfaces 256 | 257 | def _default_interface(self, route_output=None): 258 | """ 259 | :param route_output: For mocking actual output 260 | """ 261 | if not route_output: 262 | out, __, __ = exec_cmd('/sbin/route -n') 263 | lines = out.splitlines() 264 | else: 265 | lines = route_output.split("\n") 266 | 267 | for line in lines[2:]: 268 | line = line.split() 269 | if '0.0.0.0' in line and \ 270 | 'UG' in line: 271 | iface = line[-1] 272 | return self.interfaces.get(iface, None) 273 | 274 | @property 275 | def default_interface(self): 276 | """ 277 | Returns the default interface device. 278 | """ 279 | return self._default_interface() 280 | 281 | 282 | class LinuxParser(UnixParser): 283 | """ 284 | A parser for certain ifconfig versions. 285 | """ 286 | 287 | @classmethod 288 | def get_patterns(cls): 289 | return super(LinuxParser, cls).get_patterns() + [ 290 | r'(?P^[a-zA-Z0-9:_\-\.]+)(.*)Link encap:(.*).*', 291 | r'(.*)Link encap:(.*)(HWaddr )(?P[^\s]*).*', 292 | r'.*(inet addr:\s*)(?P[^\s]+)\s+Bcast:(?P[^\s]*)\s+Mask:(?P[^\s]*).*', 293 | r'.*(inet6 addr:\s*)(?P[^\s\/]+)\/(?P\d+)', 294 | r'.*(MTU:\s*)(?P\d+)', 295 | r'.*(P-t-P:)(?P[^\s]*).*', 296 | r'.*(RX bytes:)(?P\d+).*', 297 | r'.*(TX bytes:)(?P\d+).*', 298 | ] 299 | 300 | def alter(self, interfaces): 301 | return interfaces 302 | 303 | 304 | class UnixIPParser(UnixParser): 305 | """ 306 | Because ifconfig is getting deprecated, we can use ip address instead 307 | """ 308 | 309 | @classmethod 310 | def get_command(cls): 311 | ifconfig_cmd = 'ip' 312 | for path in ['/sbin', '/usr/sbin', '/bin', '/usr/bin']: 313 | if os.path.exists(os.path.join(path, ifconfig_cmd)): 314 | ifconfig_cmd = os.path.join(path, ifconfig_cmd) 315 | break 316 | ifconfig_cmd = ifconfig_cmd + " address show" 317 | return ifconfig_cmd 318 | 319 | @classmethod 320 | def get_patterns(cls): 321 | return [ 322 | r'\s*[0-9]+:\s+(?P[^@:]+)[^:]*:(?P.*) mtu (?P\d+)', 323 | r'.*(inet\s)(?P[\d\.]+)', 324 | r'.*(inet6 )(?P[^/]*).*', 325 | r'.*(ether )(?P[^\s]*).*', 326 | r'.*inet\s.*(brd )(?P[^\s]*).*', 327 | r'.*(inet )[^/]+(?P[/][0-9]+).*', 328 | ] 329 | 330 | def _default_interface(self, route_output=None): 331 | """ 332 | :param route_output: For mocking actual output 333 | """ 334 | if not route_output: 335 | out, __, __ = exec_cmd('/sbin/ip route') 336 | lines = out.splitlines() 337 | else: 338 | lines = route_output.split("\n") 339 | 340 | for line in lines: 341 | line = line.split() 342 | if 'default' in line: 343 | iface = line[4] 344 | return self.interfaces.get(iface, None) 345 | 346 | @property 347 | def default_interface(self): 348 | """ 349 | Returns the default interface device. 350 | """ 351 | return self._default_interface() 352 | 353 | def alter(self, interfaces): 354 | return interfaces 355 | 356 | 357 | class MacOSXParser(UnixParser): 358 | 359 | @classmethod 360 | def get_patterns(cls): 361 | return super(MacOSXParser, cls).get_patterns() + [ 362 | r'.*(status: )(?P[^\s]*).*', 363 | r'.*(media: )(?P.*)', 364 | ] 365 | 366 | def alter(self, interfaces): 367 | interfaces = super(MacOSXParser, self).alter(interfaces) 368 | # fix up netmask address for mac 369 | for device, device_dict in interfaces.items(): 370 | if device_dict['netmask'] is not None: 371 | interfaces[device]['netmask'] = hex2dotted(device_dict['netmask']) 372 | if device_dict['netmasks'] is not None: 373 | interfaces[device]['netmasks'] = [hex2dotted(mask) for mask in device_dict['netmasks']] 374 | return interfaces 375 | 376 | def _default_interface(self, route_output=None): 377 | """ 378 | :param route_output: For mocking actual output 379 | """ 380 | if not route_output: 381 | out, __, __ = exec_cmd('/usr/sbin/netstat -rn -f inet') 382 | lines = out.splitlines() 383 | else: 384 | lines = route_output.split("\n") 385 | 386 | for line in lines: 387 | line = line.split() 388 | if 'default' in line: 389 | iface = line[-1] 390 | return self.interfaces.get(iface, None) 391 | 392 | @property 393 | def default_interface(self): 394 | """ 395 | Returns the default interface device. 396 | """ 397 | return self._default_interface() 398 | -------------------------------------------------------------------------------- /src/ifcfg/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import locale 5 | import logging 6 | import os 7 | import socket 8 | import struct 9 | from subprocess import PIPE, Popen 10 | 11 | system_encoding = locale.getpreferredencoding() 12 | 13 | 14 | def minimal_logger(name): 15 | log = logging.getLogger(name) 16 | formatter = logging.Formatter( 17 | "%(asctime)s IFCFG DEBUG : %(name)s : %(message)s" 18 | ) 19 | console = logging.StreamHandler() 20 | console.setFormatter(formatter) 21 | console.setLevel(logging.DEBUG) 22 | 23 | if 'IFCFG_DEBUG' in os.environ.keys() and os.environ['IFCFG_DEBUG'] == '1': 24 | log.setLevel(logging.DEBUG) 25 | log.addHandler(console) 26 | return log 27 | 28 | def exec_cmd(cmd_args): 29 | # Using `shell=True` because commands may be scripts 30 | # Using `universal_newlines=False` so we do not choke on implicit decode 31 | # errors of bytes that are invalid (happens on Windows) 32 | # NB! Python 2 returns a string, Python 3 returns a bytestring 33 | # https://github.com/ftao/python-ifcfg/issues/17 34 | env = os.environ.copy() 35 | env.update({"LANG": "C"}) 36 | proc = Popen(cmd_args, stdout=PIPE, stderr=PIPE, universal_newlines=False, shell=True, env=env) 37 | stdout, stderr = proc.communicate() 38 | proc.wait() 39 | 40 | # Decode returned strings according to system encoding 41 | stdout = stdout.decode(system_encoding, errors='replace') 42 | stderr = stderr.decode(system_encoding, errors='replace') 43 | 44 | return (stdout, stderr, proc.returncode) 45 | 46 | def hex2dotted(hex_num): 47 | return socket.inet_ntoa(struct.pack('>L',int(hex_num, base=0))) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftao/python-ifcfg/af30533c5767876cdb4050b53b0bd9dc942eb18b/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import ifcfg 4 | 5 | 6 | class IfcfgTestCase(unittest.TestCase): 7 | """ 8 | Basic test class, just makes sure to restore the original module-wide 9 | properties of ifcfg 10 | """ 11 | 12 | def setUp(self): 13 | self.old_platform = ifcfg.platform 14 | self.old_parser_class = ifcfg.Parser 15 | 16 | def tearDown(self): 17 | ifcfg.platform = self.old_platform 18 | ifcfg.Parser = self.old_parser_class 19 | -------------------------------------------------------------------------------- /tests/cli_tests.py: -------------------------------------------------------------------------------- 1 | from ifcfg import cli 2 | 3 | 4 | def test_cli(): 5 | cli.main() 6 | -------------------------------------------------------------------------------- /tests/ifconfig_out.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | LINUX2 = """ 5 | eth0 Link encap:Ethernet HWaddr 1a:2b:3c:4d:5e:6f 6 | inet addr:192.168.0.1 Bcast:192.168.0.255 Mask:255.255.255.0 7 | inet6 addr: fe80::4240:36ff:fe38:a121/64 Scope:Link 8 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 9 | RX packets:4041381 errors:0 dropped:0 overruns:0 frame:0 10 | TX packets:3851783 errors:0 dropped:0 overruns:0 carrier:0 11 | collisions:0 txqueuelen:1000 12 | RX bytes:1123058554 (1.0 GiB) TX bytes:737462074 (703.2 MiB) 13 | Interrupt:24 14 | 15 | lo Link encap:Local Loopback 16 | inet addr:127.0.0.1 Mask:255.0.0.0 17 | inet6 addr: ::1/128 Scope:Host 18 | UP LOOPBACK RUNNING MTU:16436 Metric:1 19 | RX packets:126491 errors:0 dropped:0 overruns:0 frame:0 20 | TX packets:126491 errors:0 dropped:0 overruns:0 carrier:0 21 | collisions:0 txqueuelen:0 22 | RX bytes:43170341 (41.1 MiB) TX bytes:43170341 (41.1 MiB) 23 | """ 24 | 25 | LINUX3 = """ 26 | eth0: flags=4163 mtu 1500 27 | inet 192.168.0.1 netmask 255.255.255.0 broadcast 192.168.0.255 28 | inet6 fe80::4240:36ff:fe38:a121 prefixlen 64 scopeid 0x20 29 | ether 1a:2b:3c:4d:5e:6f txqueuelen 1000 (Ethernet) 30 | RX packets 251031 bytes 146644544 (139.8 MiB) 31 | RX errors 0 dropped 0 overruns 0 frame 0 32 | TX packets 141266 bytes 28028187 (26.7 MiB) 33 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 34 | 35 | lo: flags=73 mtu 16436 36 | inet 127.0.0.1 netmask 255.0.0.0 37 | inet6 ::1 prefixlen 128 scopeid 0x10 38 | loop txqueuelen 0 (Local Loopback) 39 | RX packets 1462 bytes 112866 (110.2 KiB) 40 | RX errors 0 dropped 0 overruns 0 frame 0 41 | TX packets 1462 bytes 112866 (110.2 KiB) 42 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 43 | """ 44 | 45 | LINUXDOCKER = """ 46 | br-736aa253dd57: flags=4099 mtu 1500 47 | inet 172.19.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 48 | inet6 fe80::42:9cff:fefe:60db prefixlen 64 scopeid 0x20 49 | ether 02:42:9c:fe:60:db txqueuelen 0 (Ethernet) 50 | RX packets 120327 bytes 1571313930 (1.5 GB) 51 | RX errors 0 dropped 0 overruns 0 frame 0 52 | TX packets 121053 bytes 23186380 (23.1 MB) 53 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 54 | 55 | br-c4d6dd0473e5: flags=4099 mtu 1500 56 | inet 172.18.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 57 | ether 02:42:a4:85:da:bd txqueuelen 0 (Ethernet) 58 | RX packets 0 bytes 0 (0.0 B) 59 | RX errors 0 dropped 0 overruns 0 frame 0 60 | TX packets 0 bytes 0 (0.0 B) 61 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 62 | 63 | br-c75cf3b1db15: flags=4099 mtu 1500 64 | inet 172.20.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 65 | ether 02:42:97:dd:9a:dd txqueuelen 0 (Ethernet) 66 | RX packets 0 bytes 0 (0.0 B) 67 | RX errors 0 dropped 0 overruns 0 frame 0 68 | TX packets 0 bytes 0 (0.0 B) 69 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 70 | 71 | docker0: flags=4099 mtu 1500 72 | inet 172.17.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 73 | ether 02:42:2a:ec:97:79 txqueuelen 0 (Ethernet) 74 | RX packets 0 bytes 0 (0.0 B) 75 | RX errors 0 dropped 0 overruns 0 frame 0 76 | TX packets 0 bytes 0 (0.0 B) 77 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 78 | 79 | enp0s31f6: flags=4163 mtu 1500 80 | inet 192.168.1.94 netmask 255.255.255.0 broadcast 192.168.1.255 81 | inet6 2a01:cb19:810a:da00:59d7:a6e1:8596:12f3 prefixlen 64 scopeid 0x0 82 | inet6 2a01:cb19:810a:da00:1e2b:9b33:6f04:df10 prefixlen 64 scopeid 0x0 83 | inet6 fe80::1487:95e4:d852:11aa prefixlen 64 scopeid 0x20 84 | ether 54:e1:ad:76:c8:cb txqueuelen 1000 (Ethernet) 85 | RX packets 37234442 bytes 45411193577 (45.4 GB) 86 | RX errors 0 dropped 0 overruns 0 frame 0 87 | TX packets 17430466 bytes 12618211757 (12.6 GB) 88 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 89 | device interrupt 16 memory 0xec200000-ec220000 90 | 91 | lo: flags=73 mtu 65536 92 | inet 127.0.0.1 netmask 255.0.0.0 93 | inet6 ::1 prefixlen 128 scopeid 0x10 94 | loop txqueuelen 1000 (Local Loopback) 95 | RX packets 2385680 bytes 1832529401 (1.8 GB) 96 | RX errors 0 dropped 0 overruns 0 frame 0 97 | TX packets 2385680 bytes 1832529401 (1.8 GB) 98 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 99 | 100 | virbr0: flags=4099 mtu 1500 101 | inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255 102 | ether 52:54:00:e2:85:a9 txqueuelen 1000 (Ethernet) 103 | RX packets 0 bytes 0 (0.0 B) 104 | RX errors 0 dropped 0 overruns 0 frame 0 105 | TX packets 0 bytes 0 (0.0 B) 106 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 107 | """ 108 | 109 | LINUX = LINUX3 110 | 111 | 112 | # Something out of order that we cannot parse 113 | ILLEGAL_OUTPUT = """ 114 | inet 192.168.0.1 netmask 255.255.255.0 broadcast 192.168.0.255 115 | eth0: flags=4163 mtu 1500 116 | inet 192.168.0.1 netmask 255.255.255.0 broadcast 192.168.0.255 117 | """ 118 | 119 | MACOSX = """ 120 | en0: flags=8863 mtu 1500 121 | options=2b 122 | ether 1a:2b:3c:4d:5e:6f 123 | inet6 fe80::4240:36ff:fe38:a121%en0 prefixlen 64 scopeid 0x5 124 | inet 192.168.0.1 netmask 0xffffff00 broadcast 192.168.0.255 125 | media: autoselect (100baseTX ) 126 | status: active 127 | lo0: flags=8049 mtu 16384 128 | options=3 129 | inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 130 | inet 127.0.0.1 netmask 0xff000000 131 | inet6 ::1 prefixlen 128 132 | """ # noqa 133 | 134 | 135 | MACOSX2 = """ 136 | lo0: flags=8049 mtu 16384 137 | options=1203 138 | inet 127.0.0.1 netmask 0xff000000 139 | inet6 ::1 prefixlen 128 140 | inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 141 | inet 127.0.1.99 netmask 0xff000000 142 | nd6 options=201 143 | en0: flags=8863 mtu 1500 144 | ether 8c:85:90:00:aa:bb 145 | inet6 fe80::1c8b:e7cc:c695:a6de%en0 prefixlen 64 secured scopeid 0x6 146 | inet 10.0.1.12 netmask 0xffff0000 broadcast 10.0.255.255 147 | inet6 2601:980:c000:7c5a:da:6a59:1e94:b03 prefixlen 64 autoconf secured 148 | inet6 2601:980:c000:7c5a:fdb3:b90c:80d6:abcd prefixlen 64 deprecated autoconf temporary 149 | inet6 2601:980:c000:7c5a:34b0:676d:93f7:0123 prefixlen 64 autoconf temporary 150 | nd6 options=201 151 | media: autoselect 152 | status: active 153 | en1: flags=963 mtu 1500 154 | options=60 155 | ether 7a:00:48:a1:b2:00 156 | media: autoselect 157 | status: inactive 158 | p2p0: flags=8843 mtu 2304 159 | ether 0e:85:90:0f:ab:cd 160 | media: autoselect 161 | status: inactive 162 | awdl0: flags=8943 mtu 1484 163 | ether 2e:51:48:37:99:0a 164 | inet6 fe80::2c51:48ff:fe37:86f8%awdl0 prefixlen 64 scopeid 0xc 165 | nd6 options=201 166 | media: autoselect 167 | status: active 168 | bridge0: flags=8863 mtu 1500 169 | options=63 170 | ether 7a:00:48:c8:aa:bd 171 | Configuration: 172 | id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 173 | maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 174 | root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 175 | ipfilter disabled flags 0x2 176 | member: en1 flags=3 177 | ifmaxaddr 0 port 7 priority 0 path cost 0 178 | member: en2 flags=3 179 | ifmaxaddr 0 port 8 priority 0 path cost 0 180 | member: en3 flags=3 181 | ifmaxaddr 0 port 9 priority 0 path cost 0 182 | member: en4 flags=3 183 | ifmaxaddr 0 port 10 priority 0 path cost 0 184 | nd6 options=201 185 | media: 186 | status: inactive 187 | utun0: flags=8051 mtu 2000 188 | inet6 fe80::ebb7:7d4a:49e8:e4fc%utun0 prefixlen 64 scopeid 0xe 189 | nd6 options=201 190 | vboxnet0: flags=8842 mtu 1500 191 | ether 0a:00:27:00:00:00 192 | en5: flags=8863 mtu 1500 193 | ether ac:de:48:00:22:11 194 | inet6 fe80::aede:48ff:fe00:2211%en5 prefixlen 64 scopeid 0x5 195 | nd6 options=281 196 | media: autoselect 197 | status: active 198 | utun3: flags=8051 mtu 1400 199 | inet 10.63.73.73 --> 10.63.73.73 netmask 0xffffffff 200 | """ 201 | 202 | # See: https://github.com/ftao/python-ifcfg/issues/40 203 | LINUX_VLAN = """ 204 | br2 Link encap:Ethernet HWaddr 08:00:27:7c:6d:9d 205 | inet addr:10.0.0.1 Bcast:10.0.0.255 Mask:255.255.255.0 206 | inet6 addr: fe80::a00:27ff:fe7c:6d9d/64 Scope:Link 207 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 208 | RX packets:0 errors:0 dropped:0 overruns:0 frame:0 209 | TX packets:20 errors:0 dropped:0 overruns:0 carrier:0 210 | collisions:0 txqueuelen:1000 211 | RX bytes:0 (0.0 B) TX bytes:1528 (1.4 KiB) 212 | 213 | br4 Link encap:Ethernet HWaddr 08:00:27:7c:6d:9d 214 | inet addr:10.0.2.1 Bcast:10.0.2.255 Mask:255.255.255.0 215 | inet6 addr: fe80::a00:27ff:fe7c:6d9d/64 Scope:Link 216 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 217 | RX packets:727 errors:0 dropped:0 overruns:0 frame:0 218 | TX packets:643 errors:0 dropped:0 overruns:0 carrier:0 219 | collisions:0 txqueuelen:1000 220 | RX bytes:88547 (86.4 KiB) TX bytes:890348 (869.4 KiB) 221 | 222 | brlan Link encap:Ethernet HWaddr 08:00:27:d8:78:41 223 | inet addr:192.168.0.1 Bcast:192.168.0.255 Mask:255.255.255.0 224 | inet6 addr: fe80::a00:27ff:fed8:7841/64 Scope:Link 225 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 226 | RX packets:6905 errors:0 dropped:0 overruns:0 frame:0 227 | TX packets:20 errors:0 dropped:0 overruns:0 carrier:0 228 | collisions:0 txqueuelen:1000 229 | RX bytes:351150 (342.9 KiB) TX bytes:1568 (1.5 KiB) 230 | 231 | eth0 Link encap:Ethernet HWaddr 08:00:27:d8:78:41 232 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 233 | RX packets:7353 errors:0 dropped:433 overruns:0 frame:0 234 | TX packets:20 errors:0 dropped:0 overruns:0 carrier:0 235 | collisions:0 txqueuelen:1000 236 | RX bytes:482610 (471.2 KiB) TX bytes:1568 (1.5 KiB) 237 | 238 | eth1 Link encap:Ethernet HWaddr 08:00:27:f2:d0:cb 239 | inet addr:172.16.25.205 Bcast:172.16.25.255 Mask:255.255.255.0 240 | inet6 addr: fe80::a00:27ff:fef2:d0cb/64 Scope:Link 241 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 242 | RX packets:65901 errors:0 dropped:0 overruns:0 frame:0 243 | TX packets:36638 errors:0 dropped:0 overruns:0 carrier:0 244 | collisions:0 txqueuelen:1000 245 | RX bytes:80272939 (76.5 MiB) TX bytes:5598473 (5.3 MiB) 246 | 247 | eth2 Link encap:Ethernet HWaddr 08:00:27:7c:6d:9d 248 | inet6 addr: fe80::a00:27ff:fe7c:6d9d/64 Scope:Link 249 | UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 250 | RX packets:7200 errors:0 dropped:431 overruns:0 frame:0 251 | TX packets:111 errors:0 dropped:0 overruns:0 carrier:0 252 | collisions:0 txqueuelen:1000 253 | RX bytes:471382 (460.3 KiB) TX bytes:8368 (8.1 KiB) 254 | 255 | eth2.2 Link encap:Ethernet HWaddr 08:00:27:7c:6d:9d 256 | inet6 addr: fe80::a00:27ff:fe7c:6d9d/64 Scope:Link 257 | UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 258 | RX packets:0 errors:0 dropped:0 overruns:0 frame:0 259 | TX packets:31 errors:0 dropped:0 overruns:0 carrier:0 260 | collisions:0 txqueuelen:1000 261 | RX bytes:0 (0.0 B) TX bytes:2278 (2.2 KiB) 262 | 263 | eth2.4 Link encap:Ethernet HWaddr 08:00:27:7c:6d:9d 264 | inet6 addr: fe80::a00:27ff:fe7c:6d9d/64 Scope:Link 265 | UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1 266 | RX packets:0 errors:0 dropped:0 overruns:0 frame:0 267 | TX packets:52 errors:0 dropped:0 overruns:0 carrier:0 268 | collisions:0 txqueuelen:1000 269 | RX bytes:0 (0.0 B) TX bytes:3704 (3.6 KiB) 270 | 271 | lo Link encap:Local Loopback 272 | inet addr:127.0.0.1 Mask:255.0.0.0 273 | inet6 addr: ::1/128 Scope:Host 274 | UP LOOPBACK RUNNING MTU:65536 Metric:1 275 | RX packets:12786 errors:0 dropped:0 overruns:0 frame:0 276 | TX packets:12786 errors:0 dropped:0 overruns:0 carrier:0 277 | collisions:0 txqueuelen:1 278 | RX bytes:1853178 (1.7 MiB) TX bytes:1853178 (1.7 MiB) 279 | """ 280 | 281 | 282 | ROUTE_OUTPUT = """ 283 | Kernel IP routing table 284 | Destination Gateway Genmask Flags Metric Ref Use Iface 285 | 0.0.0.0 192.168.1.1 0.0.0.0 UG 600 0 0 eth0 286 | 169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 eth0 287 | 192.168.1.0 0.0.0.0 255.255.255.0 U 600 0 0 eth0 288 | """ 289 | -------------------------------------------------------------------------------- /tests/ifconfig_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from nose.tools import eq_, ok_, raises 5 | 6 | import ifcfg 7 | from ifcfg.parser import LinuxParser, NullParser 8 | 9 | from . import ifconfig_out 10 | from .base import IfcfgTestCase 11 | 12 | 13 | class IfcfgTestCase(IfcfgTestCase): 14 | 15 | def test_ifcfg(self): 16 | ifcfg.distro = 'Linux' 17 | ifcfg.Parser = LinuxParser 18 | interfaces = ifcfg.interfaces(ifconfig=ifconfig_out.LINUX) 19 | res = len(interfaces) > 0 20 | ok_(res) 21 | 22 | def test_unknown(self): 23 | ifcfg.distro = 'Bogus' 24 | ifcfg.Parser = ifcfg.get_parser_class() 25 | self.assertTrue(issubclass(ifcfg.Parser, NullParser)) 26 | 27 | @raises(RuntimeError) 28 | def test_illegal(self): 29 | ifcfg.distro = 'Linux' 30 | ifcfg.Parser = LinuxParser 31 | ifcfg.get_parser(ifconfig=ifconfig_out.ILLEGAL_OUTPUT) 32 | 33 | def test_linux(self): 34 | ifcfg.distro = 'Linux' 35 | ifcfg.Parser = LinuxParser 36 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.LINUX) 37 | interfaces = parser.interfaces 38 | self.assertEqual(len(interfaces.keys()), 2) 39 | eq_(interfaces['eth0']['ether'], '1a:2b:3c:4d:5e:6f') 40 | eq_(interfaces['eth0']['inet'], '192.168.0.1') 41 | eq_(interfaces['eth0']['broadcast'], '192.168.0.255') 42 | eq_(interfaces['eth0']['netmask'], '255.255.255.0') 43 | 44 | def test_linux2(self): 45 | ifcfg.distro = 'Linux' 46 | ifcfg.Parser = LinuxParser 47 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.LINUX2) 48 | interfaces = parser.interfaces 49 | self.assertEqual(len(interfaces.keys()), 2) 50 | eq_(interfaces['eth0']['ether'], '1a:2b:3c:4d:5e:6f') 51 | eq_(interfaces['eth0']['inet'], '192.168.0.1') 52 | eq_(interfaces['eth0']['broadcast'], '192.168.0.255') 53 | eq_(interfaces['eth0']['netmask'], '255.255.255.0') 54 | 55 | def test_linux3(self): 56 | ifcfg.distro = 'Linux' 57 | ifcfg.Parser = LinuxParser 58 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.LINUX3) 59 | interfaces = parser.interfaces 60 | self.assertEqual(len(interfaces.keys()), 2) 61 | eq_(interfaces['eth0']['ether'], '1a:2b:3c:4d:5e:6f') 62 | eq_(interfaces['eth0']['inet'], '192.168.0.1') 63 | eq_(interfaces['eth0']['broadcast'], '192.168.0.255') 64 | eq_(interfaces['eth0']['netmask'], '255.255.255.0') 65 | 66 | def test_linuxdocker(self): 67 | ifcfg.distro = 'Linux' 68 | ifcfg.Parser = LinuxParser 69 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.LINUXDOCKER) 70 | interfaces = parser.interfaces 71 | self.assertEqual(len(interfaces.keys()), 7) 72 | eq_(interfaces['enp0s31f6']['ether'], '54:e1:ad:76:c8:cb') 73 | eq_(interfaces['enp0s31f6']['inet'], '192.168.1.94') 74 | eq_(interfaces['enp0s31f6']['broadcast'], '192.168.1.255') 75 | eq_(interfaces['enp0s31f6']['netmask'], '255.255.255.0') 76 | eq_(interfaces['br-736aa253dd57']['ether'], '02:42:9c:fe:60:db') 77 | eq_(interfaces['br-736aa253dd57']['inet'], '172.19.0.1') 78 | eq_(interfaces['br-736aa253dd57']['broadcast'], '0.0.0.0') 79 | eq_(interfaces['br-736aa253dd57']['netmask'], '255.255.0.0') 80 | 81 | def test_vlan(self): 82 | """ 83 | Regression test for: https://github.com/ftao/python-ifcfg/issues/40 84 | """ 85 | ifcfg.distro = 'Linux' 86 | ifcfg.Parser = LinuxParser 87 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.LINUX_VLAN) 88 | interfaces = parser.interfaces 89 | self.assertEqual(len(interfaces.keys()), 9) 90 | eq_(interfaces['eth2.2']['ether'], '08:00:27:7c:6d:9d') 91 | 92 | def test_macosx(self): 93 | ifcfg.distro = 'MacOSX' 94 | ifcfg.Parser = ifcfg.get_parser_class() 95 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.MACOSX) 96 | interfaces = parser.interfaces 97 | self.assertEqual(len(interfaces.keys()), 2) 98 | eq_(interfaces['en0']['ether'], '1a:2b:3c:4d:5e:6f') 99 | eq_(interfaces['en0']['inet'], '192.168.0.1') 100 | eq_(interfaces['en0']['broadcasts'], ['192.168.0.255']) 101 | eq_(interfaces['en0']['broadcast'], '192.168.0.255') 102 | eq_(interfaces['en0']['netmask'], '255.255.255.0') 103 | eq_(interfaces['lo0']['netmasks'], ['255.0.0.0']) 104 | eq_(interfaces['lo0']['prefixlens'], ['64', '128']) 105 | 106 | def test_macosx2(self): 107 | ifcfg.distro = 'MacOSX' 108 | ifcfg.Parser = ifcfg.get_parser_class() 109 | parser = ifcfg.get_parser(ifconfig=ifconfig_out.MACOSX2) 110 | interfaces = parser.interfaces 111 | self.assertEqual(len(interfaces.keys()), 10) 112 | eq_(interfaces['lo0']['inet'], '127.0.0.1') 113 | eq_(interfaces['lo0']['inet4'], ['127.0.0.1', '127.0.1.99']) 114 | eq_(interfaces['lo0']['netmask'], '255.0.0.0') 115 | eq_(interfaces['lo0']['netmasks'], ['255.0.0.0', '255.0.0.0']) 116 | eq_(interfaces['utun3']['inet'], '10.63.73.73') 117 | eq_(interfaces['utun3']['netmask'], '255.255.255.255') 118 | 119 | def test_default_interface(self): 120 | ifcfg.distro = 'Linux' 121 | ifcfg.Parser = LinuxParser 122 | route_output = ifconfig_out.ROUTE_OUTPUT 123 | res = ifcfg.default_interface( 124 | ifconfig=ifconfig_out.LINUX3, route_output=route_output 125 | ) 126 | ok_(res) 127 | -------------------------------------------------------------------------------- /tests/ip_out.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | # Output from Ubuntu 16.04 5 | 6 | LINUX = """1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 7 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 8 | inet 127.0.0.1/8 scope host lo 9 | valid_lft forever preferred_lft forever 10 | inet6 ::1/128 scope host 11 | valid_lft forever preferred_lft forever 12 | 2: enp0s25: mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 13 | link/ether a0:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff 14 | 3: wlp3s0: mtu 1500 qdisc mq state UP group default qlen 1000 15 | link/ether a0:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff 16 | inet 192.168.12.34/24 brd 192.168.12.255 scope global dynamic wlp3s0 17 | valid_lft 30124sec preferred_lft 30124sec 18 | inet6 fd37:a521:ada9::869/128 scope global 19 | valid_lft forever preferred_lft forever 20 | inet6 fd37:a521:ada9:0:b9f7:44f8:bb19:c78c/64 scope global temporary dynamic 21 | valid_lft 7188sec preferred_lft 1788sec 22 | inet6 fd37:a521:ada9:0:9073:a91:d14f:8087/64 scope global mngtmpaddr noprefixroute dynamic 23 | valid_lft 7188sec preferred_lft 1788sec 24 | inet6 fe80::205f:5d09:d0da:7aed/64 scope link 25 | valid_lft forever preferred_lft forever 26 | 6: wwp0s29u1u4i6: mtu 1500 qdisc noop state DOWN group default qlen 1000 27 | link/ether a0:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff 28 | 8: enp6s0.2@enp6s0: mtu 1500 qdisc noop state DOWN group default qlen 1000 29 | link/ether 00:73:00:5c:09:9a brd ff:ff:ff:ff:ff:ff 30 | inet 10.2.2.253/24 scope global enp6s0.2 31 | valid_lft forever preferred_lft forever 32 | inet 10.1.1.253/24 scope global enp6s0.2 33 | valid_lft forever preferred_lft forever 34 | """ 35 | 36 | LINUX_MULTI_IPV4 = """ 37 | 2: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 38 | link/ether b8:27:eb:50:39:69 brd ff:ff:ff:ff:ff:ff 39 | inet 192.168.13.1/24 brd 192.168.13.255 scope global eth0 40 | valid_lft forever preferred_lft forever 41 | inet 192.168.10.3/24 scope global eth0 42 | valid_lft forever preferred_lft forever 43 | inet6 fe80::ba27:ebff:fe50:3969/64 scope link 44 | valid_lft forever preferred_lft forever 45 | """ 46 | 47 | 48 | ROUTE_OUTPUT = """ 49 | Kernel IP routing table 50 | Destination Gateway Genmask Flags Metric Ref Use Iface 51 | 0.0.0.0 192.168.1.1 0.0.0.0 UG 600 0 0 wlp3s0 52 | 169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 wlp3s0 53 | 192.168.12.0 0.0.0.0 255.255.255.0 U 600 0 0 wlp3s0 54 | """ 55 | 56 | ROUTE_OUTPUT_IPROUTE = """ 57 | default via 192.168.100.1 dev enp0s25 proto dhcp metric 100 58 | 10.8.0.0/24 via 10.8.0.2 dev tun0 59 | 10.8.0.2 dev tun0 proto kernel scope link src 10.8.0.1 60 | 169.254.0.0/16 dev enp0s25 scope link metric 1000 61 | 192.168.100.0/24 dev enp0s25 proto kernel scope link src 192.168.100.252 metric 100 62 | """ 63 | -------------------------------------------------------------------------------- /tests/ip_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import ifcfg 5 | from ifcfg.parser import UnixIPParser 6 | from nose.tools import eq_, ok_ 7 | 8 | from . import ip_out 9 | from .base import IfcfgTestCase 10 | 11 | 12 | class IpTestCase(IfcfgTestCase): 13 | 14 | def test_ifcfg(self): 15 | ifcfg.distro = 'Linux' 16 | ifcfg.Parser = UnixIPParser 17 | interfaces = ifcfg.interfaces(ifconfig=ip_out.LINUX) 18 | res = len(interfaces) > 0 19 | ok_(res) 20 | 21 | def test_linux(self): 22 | ifcfg.Parser = UnixIPParser 23 | parser = ifcfg.get_parser(ifconfig=ip_out.LINUX) 24 | interfaces = parser.interfaces 25 | # Unconnected interface 26 | eq_(interfaces['enp0s25']['ether'], 'a0:00:00:00:00:00') 27 | eq_(interfaces['enp0s25']['inet'], None) 28 | # Connected interface 29 | eq_(interfaces['wlp3s0']['ether'], 'a0:00:00:00:00:00') 30 | eq_(interfaces['wlp3s0']['inet'], '192.168.12.34') 31 | eq_(interfaces['wlp3s0']['inet4'], ['192.168.12.34']) 32 | eq_(interfaces['wlp3s0']['inet6'], ['fd37:a521:ada9::869', 'fd37:a521:ada9:0:b9f7:44f8:bb19:c78c', 'fd37:a521:ada9:0:9073:a91:d14f:8087', 'fe80::205f:5d09:d0da:7aed']) 33 | eq_(interfaces['wlp3s0']['broadcast'], '192.168.12.255') 34 | eq_(interfaces['wlp3s0']['netmask'], '/24') 35 | eq_(interfaces['wlp3s0']['mtu'], '1500') 36 | # Connected interface 37 | eq_(interfaces['enp6s0.2']['ether'], '00:73:00:5c:09:9a') 38 | eq_(interfaces['enp6s0.2']['inet'], '10.2.2.253') 39 | eq_(interfaces['enp6s0.2']['mtu'], '1500') 40 | eq_(interfaces['enp6s0.2']['inet4'], ['10.2.2.253', '10.1.1.253']) 41 | eq_(interfaces['enp6s0.2']['inet6'], []) 42 | 43 | def test_linux_multi_inet4(self): 44 | ifcfg.Parser = UnixIPParser 45 | parser = ifcfg.get_parser(ifconfig=ip_out.LINUX_MULTI_IPV4) 46 | interfaces = parser.interfaces 47 | # Connected interface 48 | eq_(interfaces['eth0']['ether'], 'b8:27:eb:50:39:69') 49 | eq_(interfaces['eth0']['inet'], '192.168.13.1') 50 | eq_(interfaces['eth0']['inet4'], ['192.168.13.1', '192.168.10.3']) 51 | eq_(interfaces['eth0']['broadcast'], '192.168.13.255') 52 | eq_(interfaces['eth0']['netmask'], '/24') 53 | eq_(interfaces['eth0']['mtu'], '1500') 54 | 55 | def test_default_interface(self): 56 | ifcfg.distro = 'Linux' 57 | ifcfg.Parser = UnixIPParser 58 | res = ifcfg.default_interface( 59 | ifconfig=ip_out.LINUX, 60 | route_output=ip_out.ROUTE_OUTPUT_IPROUTE 61 | ) 62 | ok_(res) 63 | -------------------------------------------------------------------------------- /tests/ipconfig_out.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | # This example contains a bunch of virtual devices and one configured ethernet 5 | # device. The unconfigured devices should not figure in parsed results. 6 | WINDOWS_10_ETH = """ 7 | Windows IP Configuration 8 | 9 | Host Name . . . . . . . . . . . . : TEST-COMPUTER 10 | Primary Dns Suffix . . . . . . . : 11 | Node Type . . . . . . . . . . . . : Hybrid 12 | IP Routing Enabled. . . . . . . . : No 13 | WINS Proxy Enabled. . . . . . . . : No 14 | DNS Suffix Search List. . . . . . : lan 15 | 16 | Ethernet adapter Ethernet: 17 | 18 | Connection-specific DNS Suffix . : lan 19 | Description . . . . . . . . . . . : Realtek PCIe GBE Family Controller 20 | Physical Address. . . . . . . . . : 11-11-11-11-A1-FA 21 | DHCP Enabled. . . . . . . . . . . : Yes 22 | Autoconfiguration Enabled . . . . : Yes 23 | IPv6 Address. . . . . . . . . . . : abcd:1234:a123::123(Preferred) 24 | Lease Obtained. . . . . . . . . . : Thursday, July 13, 2017 7:14:10 PM 25 | Lease Expires . . . . . . . . . . : Sunday, September 2, 2153 11:24:49 PM 26 | IPv6 Address. . . . . . . . . . . : abcd:1234:1234::1:abcd:1234:abcd:123(Preferred) 27 | Temporary IPv6 Address. . . . . . : abcd:1234:1234::1:abcd:1234:abcd:123(Preferred) 28 | Link-local IPv6 Address . . . . . : abcd::abcd:1234:a123:123%6(Preferred) 29 | IPv4 Address. . . . . . . . . . . : 192.168.1.2(Preferred) 30 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 31 | Lease Obtained. . . . . . . . . . : Thursday, July 27, 2017 11:53:27 AM 32 | Lease Expires . . . . . . . . . . : Thursday, July 27, 2017 11:53:25 PM 33 | Default Gateway . . . . . . . . . : 192.168.1.1 34 | DHCP Server . . . . . . . . . . . : 192.168.1.1 35 | DHCPv6 IAID . . . . . . . . . . . : 160443188 36 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1F-2D-93-7D-90-2B-34-D1-AC-F7 37 | DNS Servers . . . . . . . . . . . : abcd:1234:1234::1 38 | 192.168.1.1 39 | NetBIOS over Tcpip. . . . . . . . : Enabled 40 | Connection-specific DNS Suffix Search List : 41 | lan 42 | 43 | Wireless LAN adapter Local Area Connection* 2: 44 | 45 | Media State . . . . . . . . . . . : Media disconnected 46 | Connection-specific DNS Suffix . : 47 | Description . . . . . . . . . . . : Microsoft Wi-Fi Direct Virtual Adapter 48 | Physical Address. . . . . . . . . : AB-12-AB-12-AB-12 49 | DHCP Enabled. . . . . . . . . . . : Yes 50 | Autoconfiguration Enabled . . . . : Yes 51 | 52 | Wireless LAN adapter Local Area Connection* 3: 53 | 54 | Media State . . . . . . . . . . . : Media disconnected 55 | Connection-specific DNS Suffix . : 56 | Description . . . . . . . . . . . : Microsoft Hosted Network Virtual Adapter 57 | Physical Address. . . . . . . . . : AB-12-AB-12-AB-12 58 | DHCP Enabled. . . . . . . . . . . : Yes 59 | Autoconfiguration Enabled . . . . : Yes 60 | 61 | Wireless LAN adapter Wi-Fi: 62 | 63 | Media State . . . . . . . . . . . : Media disconnected 64 | Connection-specific DNS Suffix . : lan 65 | Description . . . . . . . . . . . : ASUS PCE-N15 11n Wireless LAN PCI-E Card 66 | Physical Address. . . . . . . . . : AB-12-AB-12-AB-12 67 | DHCP Enabled. . . . . . . . . . . : Yes 68 | Autoconfiguration Enabled . . . . : Yes 69 | 70 | Tunnel adapter isatap.lan: 71 | 72 | Media State . . . . . . . . . . . : Media disconnected 73 | Connection-specific DNS Suffix . : lan 74 | Description . . . . . . . . . . . : Microsoft ISATAP Adapter 75 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 76 | DHCP Enabled. . . . . . . . . . . : No 77 | Autoconfiguration Enabled . . . . : Yes 78 | 79 | Tunnel adapter Teredo Tunneling Pseudo-Interface: 80 | 81 | Connection-specific DNS Suffix . : 82 | Description . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface 83 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 84 | DHCP Enabled. . . . . . . . . . . : No 85 | Autoconfiguration Enabled . . . . : Yes 86 | IPv6 Address. . . . . . . . . . . : 2001:0:5ef5:79fb:102f:fbfe:678c:7875(Preferred) 87 | Link-local IPv6 Address . . . . . : fe80::102f:fbfe:678c:7875%13(Preferred) 88 | Default Gateway . . . . . . . . . : 89 | DHCPv6 IAID . . . . . . . . . . . : 503316480 90 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1F-2D-93-7D-90-2B-34-D1-AC-F7 91 | NetBIOS over Tcpip. . . . . . . . : Disabled""" 92 | 93 | 94 | WINDOWS_10_WLAN = """ 95 | Windows IP Configuration 96 | 97 | Host Name . . . . . . . . . . . . : TEST-PC 98 | Primary Dns Suffix . . . . . . . : 99 | Node Type . . . . . . . . . . . . : Hybrid 100 | IP Routing Enabled. . . . . . . . : No 101 | WINS Proxy Enabled. . . . . . . . : No 102 | DNS Suffix Search List. . . . . . : lan 103 | 104 | Ethernet adapter Ethernet: 105 | 106 | Media State . . . . . . . . . . . : Media disconnected 107 | Connection-specific DNS Suffix . : some_old_cached_result.com 108 | Description . . . . . . . . . . . : Controller i Realtek PCIe FE-serien 109 | Physical Address. . . . . . . . . : 12-34-56-78-ab-cd 110 | DHCP Enabled. . . . . . . . . . . : Yes 111 | Autoconfiguration Enabled . . . . : Yes 112 | 113 | Wireless LAN adapter LAN-forbindelse* 11: 114 | 115 | Media State . . . . . . . . . . . : Media disconnected 116 | Connection-specific DNS Suffix . : 117 | Description . . . . . . . . . . . : Virtuelt kort til Microsoft Wi-Fi Direct 118 | Physical Address. . . . . . . . . : 23-45-67-89-ab-cd 119 | DHCP Enabled. . . . . . . . . . . : Yes 120 | Autoconfiguration Enabled . . . . : Yes 121 | 122 | Wireless LAN adapter Wi-Fi: 123 | 124 | Connection-specific DNS Suffix . : lan 125 | Description . . . . . . . . . . . : Broadcom 802.11 netværksadapter 126 | Physical Address. . . . . . . . . : 23-45-67-89-ab-cd 127 | DHCP Enabled. . . . . . . . . . . : Yes 128 | Autoconfiguration Enabled . . . . : Yes 129 | IPv6 Address. . . . . . . . . . . : fd37:a521:ada9::c69(Preferred) 130 | Lease Obtained. . . . . . . . . . : 27. juli 2017 13:31:59 131 | Lease Expires . . . . . . . . . . : 2. september 2153 23:39:45 132 | IPv6 Address. . . . . . . . . . . : fd37:a521:ada9:0:959f:2086:5992:3d86(Preferred) 133 | Temporary IPv6 Address. . . . . . : fd37:a521:ada9:0:e4b6:6195:75af:f121(Preferred) 134 | Link-local IPv6 Address . . . . . : fe80::959f:2086:5992:3d86%20(Preferred) 135 | IPv4 Address. . . . . . . . . . . : 192.168.40.219(Preferred) 136 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 137 | Lease Obtained. . . . . . . . . . : 27. juli 2017 13:32:00 138 | Lease Expires . . . . . . . . . . : 28. juli 2017 01:32:00 139 | Default Gateway . . . . . . . . . : 192.168.40.1 140 | DHCP Server . . . . . . . . . . . : 192.168.40.1 141 | DHCPv6 IAID . . . . . . . . . . . : 267696098 142 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-19-D8-06-36-F4-B7-E2-F4-9E-1F 143 | DNS Servers . . . . . . . . . . . : fd37:a521:ada9::1 144 | 192.168.40.1 145 | NetBIOS over Tcpip. . . . . . . . : Enabled 146 | Connection-specific DNS Suffix Search List : 147 | lan 148 | 149 | Ethernet adapter Bluetooth-netværksforbindelse: 150 | 151 | Media State . . . . . . . . . . . : Media disconnected 152 | Connection-specific DNS Suffix . : 153 | Description . . . . . . . . . . . : Bluetooth-enhed (Personal Area Network) 154 | Physical Address. . . . . . . . . : F4-B7-E2-F4-9E-20 155 | DHCP Enabled. . . . . . . . . . . : Yes 156 | Autoconfiguration Enabled . . . . : Yes 157 | 158 | Tunnel adapter isatap.lan: 159 | 160 | Media State . . . . . . . . . . . : Media disconnected 161 | Connection-specific DNS Suffix . : lan 162 | Description . . . . . . . . . . . : Microsoft ISATAP Adapter 163 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 164 | DHCP Enabled. . . . . . . . . . . : No 165 | Autoconfiguration Enabled . . . . : Yes 166 | 167 | Tunnel adapter LAN-forbindelse* 13: 168 | 169 | Connection-specific DNS Suffix . : 170 | Description . . . . . . . . . . . : Microsoft Teredo Tunneling Adapter 171 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 172 | DHCP Enabled. . . . . . . . . . . : No 173 | Autoconfiguration Enabled . . . . : Yes 174 | IPv6 Address. . . . . . . . . . . : 2001:0:5ef5:79fb:2c7a:b7e:678c:7875(Preferred) 175 | Link-local IPv6 Address . . . . . : fe80::2c7a:b7e:678c:7875%6(Preferred) 176 | Default Gateway . . . . . . . . . : 177 | DHCPv6 IAID . . . . . . . . . . . : 553648128 178 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-19-D8-06-36-F4-B7-E2-F4-9E-1F 179 | NetBIOS over Tcpip. . . . . . . . : Disabled 180 | 181 | """ 182 | 183 | 184 | WINDOWS_7_VM = """ 185 | Windows IP Configuration 186 | 187 | 188 | Ethernet adapter Local Area Connection 2: 189 | 190 | Connection-specific DNS Suffix . : lan 191 | Link-local IPv6 Address . . . . . : fe80::cad:9321:168e:83e3%13 192 | IPv4 Address. . . . . . . . . . . : 10.0.2.15 193 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 194 | Default Gateway . . . . . . . . . : 10.0.2.2 195 | 196 | Tunnel adapter isatap.lan: 197 | 198 | Media State . . . . . . . . . . . : Media disconnected 199 | Connection-specific DNS Suffix . : lan 200 | 201 | Tunnel adapter Teredo Tunneling Pseudo-Interface: 202 | 203 | Connection-specific DNS Suffix . : 204 | IPv6 Address. . . . . . . . . . . : 2001:0:5ef5:79fb:2002:2dbb:f5ff:fdf0 205 | Link-local IPv6 Address . . . . . : fe80::2002:2dbb:f5ff:fdf0%14 206 | Default Gateway . . . . . . . . . : :: 207 | 208 | """ 209 | 210 | 211 | WINDOWS_10_WITH_2_ETHERNETS = """ 212 | Windows IP Configuration 213 | 214 | Host Name . . . . . . . . . . . . : MSEDGEWIN10 215 | Primary Dns Suffix . . . . . . . : 216 | Node Type . . . . . . . . . . . . : Hybrid 217 | IP Routing Enabled. . . . . . . . : No 218 | WINS Proxy Enabled. . . . . . . . : No 219 | 220 | Ethernet adapter Ethernet: 221 | 222 | Connection-specific DNS Suffix . : 223 | Description . . . . . . . . . . . : Intel(R) PRO/1000 MT Desktop Adapter 224 | Physical Address. . . . . . . . . : 08-00-27-CC-BE-AF 225 | DHCP Enabled. . . . . . . . . . . : Yes 226 | Autoconfiguration Enabled . . . . : Yes 227 | Link-local IPv6 Address . . . . . : fe80::e59b:7afd:78a6:d073%4(Preferred) 228 | IPv4 Address. . . . . . . . . . . : 10.0.2.15(Preferred) 229 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 230 | Lease Obtained. . . . . . . . . . : jueves, 3 de agosto de 2017 1:00:44 231 | Lease Expires . . . . . . . . . . : viernes, 4 de agosto de 2017 1:00:59 232 | Default Gateway . . . . . . . . . : 10.0.2.2 233 | DHCP Server . . . . . . . . . . . : 10.0.2.2 234 | DHCPv6 IAID . . . . . . . . . . . : 34078759 235 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1F-22-AC-9F-08-00-27-CC-BE-AF 236 | DNS Servers . . . . . . . . . . . : 89.150.129.22 237 | 89.150.129.10 238 | NetBIOS over Tcpip. . . . . . . . : Enabled 239 | 240 | Ethernet adapter Ethernet 2: 241 | 242 | Connection-specific DNS Suffix . : 243 | Description . . . . . . . . . . . : Intel(R) PRO/1000 MT Desktop Adapter #2 244 | Physical Address. . . . . . . . . : 08-00-27-0D-9A-0B 245 | DHCP Enabled. . . . . . . . . . . : Yes 246 | Autoconfiguration Enabled . . . . : Yes 247 | Link-local IPv6 Address . . . . . : fe80::99e0:2790:ba60:20e1%5(Preferred) 248 | IPv4 Address. . . . . . . . . . . : 192.168.56.101(Preferred) 249 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 250 | Lease Obtained. . . . . . . . . . : jueves, 3 de agosto de 2017 1:00:44 251 | Lease Expires . . . . . . . . . . : jueves, 3 de agosto de 2017 1:20:59 252 | Default Gateway . . . . . . . . . : 253 | DHCP Server . . . . . . . . . . . : 192.168.56.100 254 | DHCPv6 IAID . . . . . . . . . . . : 151519271 255 | DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1F-22-AC-9F-08-00-27-CC-BE-AF 256 | DNS Servers . . . . . . . . . . . : fec0:0:0:ffff::1%1 257 | fec0:0:0:ffff::2%1 258 | fec0:0:0:ffff::3%1 259 | NetBIOS over Tcpip. . . . . . . . : Enabled 260 | 261 | Tunnel adapter isatap.{ADDEF65B-69BD-40F2-A0E6-4B67361ECE85}: 262 | 263 | Media State . . . . . . . . . . . : Media disconnected 264 | Connection-specific DNS Suffix . : 265 | Description . . . . . . . . . . . : Microsoft ISATAP Adapter 266 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 267 | DHCP Enabled. . . . . . . . . . . : No 268 | Autoconfiguration Enabled . . . . : Yes 269 | 270 | Tunnel adapter isatap.{BFDBE788-14F2-499A-9D83-CDCD34CDE463}: 271 | 272 | Media State . . . . . . . . . . . : Media disconnected 273 | Connection-specific DNS Suffix . : 274 | Description . . . . . . . . . . . : Microsoft ISATAP Adapter #2 275 | Physical Address. . . . . . . . . : 00-00-00-00-00-00-00-E0 276 | DHCP Enabled. . . . . . . . . . . : No 277 | Autoconfiguration Enabled . . . . : Yes 278 | """ 279 | -------------------------------------------------------------------------------- /tests/ipconfig_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import binascii 5 | import subprocess 6 | 7 | import ifcfg 8 | import mock 9 | from ifcfg import tools 10 | from ifcfg.parser import WindowsParser 11 | from nose.tools import eq_, ok_ 12 | 13 | from . import ipconfig_out 14 | from .base import IfcfgTestCase 15 | 16 | 17 | class WindowsTestCase(IfcfgTestCase): 18 | 19 | def test_interfaces(self): 20 | ifcfg.Parser = WindowsParser 21 | interfaces = ifcfg.interfaces(ifconfig=ipconfig_out.WINDOWS_10_ETH) 22 | res = len(interfaces) > 0 23 | ok_(res) 24 | 25 | def test_windows10(self): 26 | ifcfg.distro = "Windows" 27 | ifcfg.Parser = ifcfg.get_parser_class() 28 | 29 | self.assertTrue(issubclass(ifcfg.Parser, WindowsParser)) 30 | 31 | parser = ifcfg.get_parser(ifconfig=ipconfig_out.WINDOWS_10_ETH) 32 | interfaces = parser.interfaces 33 | 34 | self.assertIn("Ethernet adapter Ethernet", interfaces.keys()) 35 | self.assertIn("Wireless LAN adapter Local Area Connection* 2", interfaces.keys()) 36 | self.assertIn("Wireless LAN adapter Local Area Connection* 3", interfaces.keys()) 37 | self.assertIn("Wireless LAN adapter Wi-Fi", interfaces.keys()) 38 | self.assertIn("Tunnel adapter isatap.lan", interfaces.keys()) 39 | self.assertIn("Tunnel adapter Teredo Tunneling Pseudo-Interface", interfaces.keys()) 40 | 41 | self.assertEqual(len(interfaces.keys()), 6) 42 | 43 | eq_(interfaces['Ethernet adapter Ethernet']['inet'], '192.168.1.2') 44 | eq_(interfaces['Ethernet adapter Ethernet']['ether'], '11:11:11:11:a1:fa') 45 | 46 | def test_default_interface_windows10(self): 47 | ifcfg.distro = 'Windows' 48 | ifcfg.Parser = ifcfg.get_parser_class() 49 | 50 | parser = ifcfg.get_parser(ifconfig=ipconfig_out.WINDOWS_10_ETH) 51 | default = parser.default_interface 52 | ok_(default) 53 | eq_(default['inet'], '192.168.1.2') 54 | eq_(default['default_gateway'], '192.168.1.1') 55 | 56 | def test_windows10_w_2_ethernets(self): 57 | ifcfg.distro = "Windows" 58 | ifcfg.Parser = ifcfg.get_parser_class() 59 | 60 | self.assertTrue(issubclass(ifcfg.Parser, WindowsParser)) 61 | 62 | parser = ifcfg.get_parser( 63 | ifconfig=ipconfig_out.WINDOWS_10_WITH_2_ETHERNETS 64 | ) 65 | interfaces = parser.interfaces 66 | 67 | # Check that the first adapter is present 68 | self.assertIn("Ethernet adapter Ethernet", interfaces.keys()) 69 | 70 | # Check that the second adapter is present 71 | self.assertIn("Ethernet adapter Ethernet 2", interfaces.keys()) 72 | 73 | self.assertEqual(len(interfaces.keys()), 4) 74 | 75 | # Check that properties of both adapters are correct 76 | 77 | # Adapter 1 78 | eq_(interfaces['Ethernet adapter Ethernet']['inet'], '10.0.2.15') 79 | eq_(interfaces['Ethernet adapter Ethernet']['ether'], '08:00:27:cc:be:af') 80 | eq_(interfaces['Ethernet adapter Ethernet']['inet6'], []) 81 | 82 | # Adapter 2 83 | eq_(interfaces['Ethernet adapter Ethernet 2']['inet'], '192.168.56.101') 84 | eq_(interfaces['Ethernet adapter Ethernet 2']['ether'], '08:00:27:0d:9a:0b') 85 | eq_(interfaces['Ethernet adapter Ethernet 2']['inet6'], []) 86 | 87 | def test_default_interface_windows10_w_2_ethernets(self): 88 | ifcfg.distro = 'Windows' 89 | ifcfg.Parser = ifcfg.get_parser_class() 90 | 91 | parser = ifcfg.get_parser( 92 | ifconfig=ipconfig_out.WINDOWS_10_WITH_2_ETHERNETS 93 | ) 94 | default = parser.default_interface 95 | ok_(default) 96 | eq_(default['inet'], '10.0.2.15') 97 | eq_(default['default_gateway'], '10.0.2.2') 98 | 99 | def test_windows7vm(self): 100 | ifcfg.distro = "Windows" 101 | ifcfg.Parser = ifcfg.get_parser_class() 102 | 103 | self.assertTrue(issubclass(ifcfg.Parser, WindowsParser)) 104 | 105 | parser = ifcfg.get_parser(ifconfig=ipconfig_out.WINDOWS_7_VM) 106 | interfaces = parser.interfaces 107 | 108 | self.assertIn("Ethernet adapter Local Area Connection 2", interfaces.keys()) 109 | self.assertIn("Tunnel adapter isatap.lan", interfaces.keys()) 110 | self.assertIn("Tunnel adapter Teredo Tunneling Pseudo-Interface", interfaces.keys()) 111 | 112 | self.assertEqual(len(interfaces.keys()), 3) 113 | 114 | eq_(interfaces['Ethernet adapter Local Area Connection 2']['inet'], '10.0.2.15') 115 | self.assertEqual( 116 | len(interfaces['Ethernet adapter Local Area Connection 2']['inet6']), 117 | 0 118 | ) 119 | 120 | def test_default_interface_windows7vm(self): 121 | ifcfg.distro = 'Windows' 122 | ifcfg.Parser = ifcfg.get_parser_class() 123 | 124 | parser = ifcfg.get_parser(ifconfig=ipconfig_out.WINDOWS_7_VM) 125 | default = parser.default_interface 126 | ok_(default) 127 | eq_(default['inet'], '10.0.2.15') 128 | eq_(default['default_gateway'], '10.0.2.2') 129 | 130 | def test_default_interface_windows10_wlan(self): 131 | ifcfg.distro = 'Windows' 132 | ifcfg.Parser = ifcfg.get_parser_class() 133 | 134 | parser = ifcfg.get_parser(ifconfig=ipconfig_out.WINDOWS_10_WLAN) 135 | default = parser.default_interface 136 | ok_(default) 137 | eq_(default['inet'], '192.168.40.219') 138 | eq_(default['default_gateway'], '192.168.40.1') 139 | 140 | # Add a character that's known to fail in cp1252 encoding 141 | # Patching `wait` is needed because on CI, these process (for Windows) don't really exist and cannot be executed 142 | # `wait` sets the returncode which must be 0 for the parser to run 143 | @mock.patch.object(subprocess.Popen, 'wait', lambda __: 0) 144 | @mock.patch.object(subprocess.Popen, 'communicate', lambda __: [ipconfig_out.WINDOWS_7_VM.encode('cp1252') + binascii.unhexlify("81"), "".encode('cp1252')]) 145 | @mock.patch.object(tools, 'system_encoding', "cp1252") 146 | def test_cp1252_encoding(self): 147 | """ 148 | Tests that things are still working when using this bizarre encoding 149 | """ 150 | 151 | ifcfg.distro = "Windows" 152 | ifcfg.Parser = ifcfg.get_parser_class() 153 | 154 | self.assertTrue(issubclass(ifcfg.Parser, WindowsParser)) 155 | 156 | parser = ifcfg.get_parser() 157 | interfaces = parser.interfaces 158 | 159 | self.assertIn("Ethernet adapter Local Area Connection 2", interfaces.keys()) 160 | self.assertIn("Tunnel adapter isatap.lan", interfaces.keys()) 161 | self.assertIn("Tunnel adapter Teredo Tunneling Pseudo-Interface", interfaces.keys()) 162 | 163 | self.assertEqual(len(interfaces.keys()), 3) 164 | 165 | eq_(interfaces['Ethernet adapter Local Area Connection 2']['inet'], '10.0.2.15') 166 | self.assertEqual( 167 | len(interfaces['Ethernet adapter Local Area Connection 2']['inet6']), 168 | 0 169 | ) 170 | 171 | # Add a character that's known to fail in unicode encoding 172 | # https://github.com/ftao/python-ifcfg/issues/17 173 | @mock.patch.object(subprocess.Popen, 'wait', lambda __: 0) # See test_cp1252_encoding's mocks as well 174 | @mock.patch.object(subprocess.Popen, 'communicate', lambda __: [ipconfig_out.WINDOWS_7_VM.encode('cp1252') + binascii.unhexlify("84"), "".encode('cp1252')]) 175 | @mock.patch.object(tools, 'system_encoding', "cp1252") 176 | def test_cp1252_non_utf8_byte(self): 177 | """ 178 | Tests that things are still working when using this bizarre encoding 179 | """ 180 | 181 | ifcfg.distro = "Windows" 182 | ifcfg.Parser = ifcfg.get_parser_class() 183 | 184 | self.assertTrue(issubclass(ifcfg.Parser, WindowsParser)) 185 | 186 | parser = ifcfg.get_parser() 187 | interfaces = parser.interfaces 188 | 189 | self.assertIn("Ethernet adapter Local Area Connection 2", interfaces.keys()) 190 | self.assertIn("Tunnel adapter isatap.lan", interfaces.keys()) 191 | self.assertIn("Tunnel adapter Teredo Tunneling Pseudo-Interface", interfaces.keys()) 192 | 193 | self.assertEqual(len(interfaces.keys()), 3) 194 | 195 | eq_(interfaces['Ethernet adapter Local Area Connection 2']['inet'], '10.0.2.15') 196 | self.assertEqual( 197 | len(interfaces['Ethernet adapter Local Area Connection 2']['inet6']), 198 | 0 199 | ) 200 | -------------------------------------------------------------------------------- /tests/no_mock_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Tests that don't mock anything, just run on the host system. 4 | """ 5 | from __future__ import unicode_literals 6 | 7 | import ifcfg 8 | 9 | from .base import IfcfgTestCase 10 | 11 | 12 | class IfcfgTestCase(IfcfgTestCase): 13 | 14 | def test_ifcfg(self): 15 | for interface in ifcfg.interfaces(): 16 | print(interface) 17 | -------------------------------------------------------------------------------- /tests/tools_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for ifcfg.tools.""" 2 | 3 | import locale 4 | import logging 5 | import os 6 | import sys 7 | import unittest 8 | 9 | import ifcfg 10 | from ifcfg.tools import exec_cmd 11 | from nose.tools import eq_ 12 | 13 | 14 | class IfcfgToolsTestCase(unittest.TestCase): 15 | 16 | def test_minimal_logger(self): 17 | os.environ['IFCFG_DEBUG'] = '1' 18 | log = ifcfg.tools.minimal_logger(__name__) 19 | eq_(log.level, logging.DEBUG) 20 | os.environ['IFCFG_DEBUG'] = '0' 21 | 22 | def test_command(self): 23 | if sys.platform == "darwin": 24 | cmd = "/bin/echo -n 'this is a test'" 25 | else: 26 | cmd = "echo -n 'this is a test'" 27 | output, __, __ = exec_cmd(cmd) 28 | self.assertEqual(output, "this is a test") 29 | 30 | 31 | @unittest.skipIf(sys.version[0] != '2', 32 | "Python 2 only supports non-unicode stuff") 33 | def test_command_non_unicode(self): 34 | getpreferredencoding_orig = locale.getpreferredencoding 35 | locale.getpreferredencoding = lambda: "ISO-8859-1" 36 | output, __, __ = exec_cmd("echo -n 'this is a test'") 37 | self.assertEqual(output, "this is a test") 38 | locale.getpreferredencoding = getpreferredencoding_orig 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Ensure you add to .travis.yml if you add here, using `tox -l` 3 | envlist = py27,py34,py35,py36,py37,py38,py39,py310,py311,lint 4 | 5 | [travis] 6 | python = 7 | 2.7: py27,lint 8 | 3.4: py34 9 | 3.5: py35 10 | 3.6: py36 11 | 3.7: py37 12 | 3.8: py38 13 | 3.9: py39 14 | 3.10: py310 15 | 3.11: py311 16 | 17 | [testenv] 18 | 19 | basepython = 20 | py27: python2.7 21 | py34: python3.4 22 | py35: python3.5 23 | py36: python3.6 24 | py37: python3.7 25 | py38: python3.8 26 | py39: python3.9 27 | py310: python3.10 28 | py311: python3.11 29 | 30 | commands = 31 | python --version 32 | nosetests {posargs:--with-coverage} --cover-package=ifcfg 33 | 34 | usedevelop = True 35 | 36 | deps = 37 | coverage 38 | nose 39 | mock 40 | 41 | [testenv:lint] 42 | basepython = python2.7 43 | skip_install = true 44 | deps = flake8 45 | commands = 46 | flake8 src/ifcfg 47 | flake8 tests/ 48 | --------------------------------------------------------------------------------