diff options
Diffstat (limited to 'testing/mozbase/moznetwork')
-rw-r--r-- | testing/mozbase/moznetwork/moznetwork/__init__.py | 26 | ||||
-rw-r--r-- | testing/mozbase/moznetwork/moznetwork/moznetwork.py | 215 | ||||
-rw-r--r-- | testing/mozbase/moznetwork/setup.py | 36 | ||||
-rw-r--r-- | testing/mozbase/moznetwork/tests/manifest.toml | 4 | ||||
-rw-r--r-- | testing/mozbase/moznetwork/tests/test_moznetwork.py | 73 |
5 files changed, 354 insertions, 0 deletions
diff --git a/testing/mozbase/moznetwork/moznetwork/__init__.py b/testing/mozbase/moznetwork/moznetwork/__init__.py new file mode 100644 index 0000000000..622305d8f4 --- /dev/null +++ b/testing/mozbase/moznetwork/moznetwork/__init__.py @@ -0,0 +1,26 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +moznetwork is a very simple module designed for one task: getting the +network address of the current machine. + +Example usage: + +:: + + import moznetwork + + try: + ip = moznetwork.get_ip() + print "The external IP of your machine is '%s'" % ip + except moznetwork.NetworkError: + print "Unable to determine IP address of machine" + raise + +""" + +from .moznetwork import NetworkError, get_ip + +__all__ = ["get_ip", "NetworkError"] diff --git a/testing/mozbase/moznetwork/moznetwork/moznetwork.py b/testing/mozbase/moznetwork/moznetwork/moznetwork.py new file mode 100644 index 0000000000..ecc04d563d --- /dev/null +++ b/testing/mozbase/moznetwork/moznetwork/moznetwork.py @@ -0,0 +1,215 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import argparse +import array +import re +import socket +import struct +import subprocess +import sys + +import mozinfo +import mozlog +import six + +if mozinfo.isLinux: + import fcntl +if mozinfo.isWin: + import os + + +class NetworkError(Exception): + """Exception thrown when unable to obtain interface or IP.""" + + +def _get_logger(): + logger = mozlog.get_default_logger(component="moznetwork") + if not logger: + logger = mozlog.unstructured.getLogger("moznetwork") + return logger + + +def _get_interface_list(): + """Provides a list of available network interfaces + as a list of tuples (name, ip)""" + logger = _get_logger() + logger.debug("Gathering interface list") + max_iface = 32 # Maximum number of interfaces(arbitrary) + bytes = max_iface * 32 + is_32bit = (8 * struct.calcsize("P")) == 32 # Set Architecture + struct_size = 32 if is_32bit else 40 + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + names = array.array("B", b"\0" * bytes) + outbytes = struct.unpack( + "iL", + fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack("iL", bytes, names.buffer_info()[0]), + ), + )[0] + if six.PY3: + namestr = names.tobytes() + else: + namestr = names.tostring() + return [ + ( + six.ensure_str(namestr[i : i + 32].split(b"\0", 1)[0]), + socket.inet_ntoa(namestr[i + 20 : i + 24]), + ) + for i in range(0, outbytes, struct_size) + ] + + except IOError: + raise NetworkError("Unable to call ioctl with SIOCGIFCONF") + + +def _proc_matches(args, regex): + """Helper returns the matches of regex in the output of a process created with + the given arguments""" + output = subprocess.Popen( + args=args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ).stdout.read() + return re.findall(regex, output) + + +def _parse_ifconfig(): + """Parse the output of running ifconfig on mac in cases other methods + have failed""" + logger = _get_logger() + logger.debug("Parsing ifconfig") + + # Attempt to determine the default interface in use. + default_iface = _proc_matches( + ["route", "-n", "get", "default"], r"interface: (\w+)" + ) + + if default_iface: + addr_list = _proc_matches( + ["ifconfig", default_iface[0]], r"inet (\d+.\d+.\d+.\d+)" + ) + if addr_list: + logger.debug( + "Default interface: [%s] %s" % (default_iface[0], addr_list[0]) + ) + if not addr_list[0].startswith("127."): + return addr_list[0] + + # Iterate over plausible interfaces if we didn't find a suitable default. + for iface in ["en%s" % i for i in range(10)]: + addr_list = _proc_matches(["ifconfig", iface], r"inet (\d+.\d+.\d+.\d+)") + if addr_list: + logger.debug("Interface: [%s] %s" % (iface, addr_list[0])) + if not addr_list[0].startswith("127."): + return addr_list[0] + + # Just return any that isn't localhost. If we can't find one, we have + # failed. + addrs = _proc_matches(["ifconfig"], r"inet (\d+.\d+.\d+.\d+)") + try: + return [addr for addr in addrs if not addr.startswith("127.")][0] + except IndexError: + return None + + +def _parse_powershell(): + logger = _get_logger() + logger.debug("Parsing Get-NetIPAdress output via PowerShell") + + try: + cmd = os.path.join( + os.environ.get("SystemRoot", "C:\\WINDOWS"), + "system32", + "windowspowershell", + "v1.0", + "powershell.exe", + ) + output = subprocess.check_output( + [ + cmd, + "(Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred | Format-List -Property IPAddress)", + ] + ).decode("ascii") + ips = re.findall(r"IPAddress : (\d+.\d+.\d+.\d+)", output) + for ip in ips: + logger.debug("IPAddress: %s" % ip) + if not ip.startswith("127."): + return ip + return None + except FileNotFoundError: + return None + + +def get_ip(): + """Provides an available network interface address, for example + "192.168.1.3". + + A `NetworkError` exception is raised in case of failure.""" + logger = _get_logger() + try: + hostname = socket.gethostname() + try: + logger.debug("Retrieving IP for %s" % hostname) + ips = socket.gethostbyname_ex(hostname)[2] + except socket.gaierror: # for Mac OS X + hostname += ".local" + logger.debug("Retrieving IP for %s" % hostname) + ips = socket.gethostbyname_ex(hostname)[2] + if len(ips) == 1: + ip = ips[0] + elif len(ips) > 1: + logger.debug("Multiple addresses found: %s" % ips) + ip = None + else: + ip = None + except socket.gaierror: + # sometimes the hostname doesn't resolve to an ip address, in which + # case this will always fail + ip = None + + if ip is None or ip.startswith("127."): + if mozinfo.isLinux: + interfaces = _get_interface_list() + for ifconfig in interfaces: + logger.debug("Interface: [%s] %s" % (ifconfig[0], ifconfig[1])) + if ifconfig[0] == "lo": + continue + else: + return ifconfig[1] + elif mozinfo.isMac: + ip = _parse_ifconfig() + elif mozinfo.isWin: + ip = _parse_powershell() + + if ip is None: + raise NetworkError("Unable to obtain network address") + + return ip + + +def get_lan_ip(): + """Deprecated. Please use get_ip() instead.""" + return get_ip() + + +def cli(args=sys.argv[1:]): + parser = argparse.ArgumentParser(description="Retrieve IP address") + mozlog.commandline.add_logging_group( + parser, include_formatters=mozlog.commandline.TEXT_FORMATTERS + ) + + args = parser.parse_args() + mozlog.commandline.setup_logging("mozversion", args, {"mach": sys.stdout}) + + _get_logger().info("IP address: %s" % get_ip()) + + +if __name__ == "__main__": + cli() diff --git a/testing/mozbase/moznetwork/setup.py b/testing/mozbase/moznetwork/setup.py new file mode 100644 index 0000000000..84940e3de0 --- /dev/null +++ b/testing/mozbase/moznetwork/setup.py @@ -0,0 +1,36 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from setuptools import setup + +PACKAGE_VERSION = "1.1.0" + +deps = [ + "mozinfo", + "mozlog >= 6.0", +] + +setup( + name="moznetwork", + version=PACKAGE_VERSION, + description="Library of network utilities for use in Mozilla testing", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Development Status :: 5 - Production/Stable", + ], + # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords="mozilla", + author="Mozilla Automation and Tools team", + author_email="tools@lists.mozilla.org", + url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase", + license="MPL", + packages=["moznetwork"], + include_package_data=True, + zip_safe=False, + install_requires=deps, + entry_points={"console_scripts": ["moznetwork = moznetwork:cli"]}, +) diff --git a/testing/mozbase/moznetwork/tests/manifest.toml b/testing/mozbase/moznetwork/tests/manifest.toml new file mode 100644 index 0000000000..6a2d385c92 --- /dev/null +++ b/testing/mozbase/moznetwork/tests/manifest.toml @@ -0,0 +1,4 @@ +[DEFAULT] +subsuite = "mozbase" + +["test_moznetwork.py"] diff --git a/testing/mozbase/moznetwork/tests/test_moznetwork.py b/testing/mozbase/moznetwork/tests/test_moznetwork.py new file mode 100644 index 0000000000..8a1ee31a2e --- /dev/null +++ b/testing/mozbase/moznetwork/tests/test_moznetwork.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +""" +Unit-Tests for moznetwork +""" + +import re +import subprocess +from distutils.spawn import find_executable +from unittest import mock + +import mozinfo +import moznetwork +import mozunit +import pytest + + +@pytest.fixture(scope="session") +def ip_addresses(): + """List of IP addresses associated with the host.""" + + # Regex to match IPv4 addresses. + # 0-255.0-255.0-255.0-255, note order is important here. + regexip = re.compile( + "((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}" + "(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)" + ) + + commands = ( + ["ip", "addr", "show"], + ["ifconfig"], + ["ipconfig"], + # Explicitly search '/sbin' because it doesn't always appear + # to be on the $PATH of all systems + ["/sbin/ip", "addr", "show"], + ["/sbin/ifconfig"], + ) + + cmd = None + for command in commands: + if find_executable(command[0]): + cmd = command + break + else: + raise OSError( + "No program for detecting ip address found! Ensure one of 'ip', " + "'ifconfig' or 'ipconfig' exists on your $PATH." + ) + + ps = subprocess.Popen(cmd, stdout=subprocess.PIPE) + standardoutput, _ = ps.communicate() + + # Generate a list of IPs by parsing the output of ip/ifconfig + return [x.group() for x in re.finditer(regexip, standardoutput.decode("UTF-8"))] + + +def test_get_ip(ip_addresses): + """Attempt to test the IP address returned by + moznetwork.get_ip() is valid""" + assert moznetwork.get_ip() in ip_addresses + + +@pytest.mark.skipif(mozinfo.isWin, reason="Test is not supported in Windows") +def test_get_ip_using_get_interface(ip_addresses): + """Test that the control flow path for get_ip() using + _get_interface_list() is works""" + with mock.patch("socket.gethostbyname") as byname: + # Force socket.gethostbyname to return None + byname.return_value = None + assert moznetwork.get_ip() in ip_addresses + + +if __name__ == "__main__": + mozunit.main() |