Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add even more ways to populate localinterfaces #4305

Merged
merged 2 commits into from
Oct 6, 2013
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
180 changes: 172 additions & 8 deletions IPython/utils/localinterfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
machine. It will *almost* always be '127.0.0.1'

LOCAL_IPS : A list of IP addresses, loopback first, that point to this machine.
This will include LOCALHOST, PUBLIC_IPS, and aliases for all hosts,
such as '0.0.0.0'.

PUBLIC_IPS : A list of public IP addresses that point to this machine.
Use these to tell remote clients where to find you.
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2010-2011 The IPython Development Team
# Copyright (C) 2010 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
Expand All @@ -20,9 +22,13 @@
# Imports
#-----------------------------------------------------------------------------

import os
import socket

from .data import uniq_stable
from .process import get_output_error_code
from .py3compat import bytes_to_str
from .warn import warn

#-----------------------------------------------------------------------------
# Code
Expand All @@ -31,7 +37,7 @@
LOCAL_IPS = []
PUBLIC_IPS = []

LOCALHOST = '127.0.0.1'
LOCALHOST = ''

def _only_once(f):
"""decorator to only run a function once"""
Expand All @@ -51,17 +57,124 @@ def ips_loaded(*args, **kwargs):
return f(*args, **kwargs)
return ips_loaded

@_only_once
def _load_ips():
"""load the IPs that point to this machine
# subprocess-parsing ip finders
class NoIPAddresses(Exception):
pass

def _populate_from_list(addrs):
"""populate local and public IPs from flat list of all IPs"""
if not addrs:
raise NoIPAddresses

This function will only ever be called once.
global LOCALHOST
public_ips = []
local_ips = []

for ip in addrs:
local_ips.append(ip)
if not ip.startswith('127.'):
public_ips.append(ip)
elif not LOCALHOST:
LOCALHOST = ip

if not LOCALHOST:
LOCALHOST = '127.0.0.1'
local_ips.insert(0, LOCALHOST)

local_ips.extend(['0.0.0.0', ''])

LOCAL_IPS[:] = uniq_stable(local_ips)
PUBLIC_IPS[:] = uniq_stable(public_ips)

def _load_ips_ifconfig():
"""load ip addresses from `ifconfig` output (posix)"""

out, err, rc = get_output_error_code('ifconfig')
if rc:
# no ifconfig, it's usually in /sbin and /sbin is not on everyone's PATH
out, err, rc = get_output_error_code('/sbin/ifconfig')
if rc:
raise IOError("no ifconfig: %s" % err)

lines = bytes_to_str(out).splitlines()
addrs = []
for line in lines:
blocks = line.lower().split()
if blocks[0] == 'inet':
addrs.append(blocks[1])
_populate_from_list(addrs)


def _load_ips_ip():
"""load ip addresses from `ip addr` output (Linux)"""
out, err, rc = get_output_error_code('ip addr')
if rc:
raise IOError("no ip: %s" % err)

lines = bytes_to_str(out).splitlines()
addrs = []
for line in lines:
blocks = line.lower().split()
if blocks[0] == 'inet':
addrs.append(blocks[1].split('/')[0])
_populate_from_list(addrs)


def _load_ips_ipconfig():
"""load ip addresses from `ipconfig` output (Windows)"""
out, err, rc = get_output_error_code('ipconfig')
if rc:
raise IOError("no ipconfig: %s" % err)

lines = bytes_to_str(out).splitlines()
addrs = ['127.0.0.1']
for line in lines:
line = line.lower().split()
if line[:2] == ['ipv4', 'address']:
addrs.append(line.split()[-1])
_populate_from_list(addrs)


def _load_ips_netifaces():
"""load ip addresses with netifaces"""
import netifaces
global LOCALHOST
local_ips = []
public_ips = []

# list of iface names, 'lo0', 'eth0', etc.
for iface in netifaces.interfaces():
# list of ipv4 addrinfo dicts
ipv4s = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
for entry in ipv4s:
addr = entry.get('addr')
if not addr:
continue
if not (iface.startswith('lo') or addr.startswith('127.')):
public_ips.append(addr)
elif not LOCALHOST:
LOCALHOST = addr
local_ips.append(addr)
if not LOCALHOST:
# we never found a loopback interface (can this ever happen?), assume common default
LOCALHOST = '127.0.0.1'
local_ips.insert(0, LOCALHOST)
local_ips.extend(['0.0.0.0', ''])
LOCAL_IPS[:] = uniq_stable(local_ips)
PUBLIC_IPS[:] = uniq_stable(public_ips)


def _load_ips_gethostbyname():
"""load ip addresses with socket.gethostbyname_ex

This can be slow.
"""
global LOCALHOST
try:
LOCAL_IPS[:] = socket.gethostbyname_ex('localhost')[2]
except socket.error:
pass
# assume common default
LOCAL_IPS[:] = ['127.0.0.1']

try:
hostname = socket.gethostname()
Expand All @@ -74,14 +187,65 @@ def _load_ips():
finally:
PUBLIC_IPS[:] = uniq_stable(PUBLIC_IPS)
LOCAL_IPS.extend(PUBLIC_IPS)

# include all-interface aliases: 0.0.0.0 and ''
LOCAL_IPS.extend(['0.0.0.0', ''])

LOCAL_IPS[:] = uniq_stable(LOCAL_IPS)

LOCALHOST = LOCAL_IPS[0]

def _load_ips_dumb():
"""Fallback in case of unexpected failure"""
global LOCALHOST
LOCALHOST = '127.0.0.1'
LOCAL_IPS[:] = [LOCALHOST, '0.0.0.0', '']
PUBLIC_IPS[:] = []

@_only_once
def _load_ips():
"""load the IPs that point to this machine

This function will only ever be called once.

It will use netifaces to do it quickly if available.
Then it will fallback on parsing the output of ifconfig / ip addr / ipconfig, as appropriate.
Finally, it will fallback on socket.gethostbyname_ex, which can be slow.
"""

try:
# first priority, use netifaces
try:
return _load_ips_netifaces()
except ImportError:
pass

# second priority, parse subprocess output (how reliable is this?)

if os.name == 'nt':
try:
return _load_ips_ipconfig()
except (IOError, NoIPAddresses):
pass
else:
try:
return _load_ips_ifconfig()
except (IOError, NoIPAddresses):
pass
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the need of if os.name == 'nt': here if what are in the 2 cases are ~ same for the first block.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's ipconfig (Windows only) and ifconfig (non-Windows), not the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes... hard to spot.
Thanks.

return _load_ips_ip()
except (IOError, NoIPAddresses):
pass

# lowest priority, use gethostbyname

return _load_ips_gethostbyname()
except Exception as e:
# unexpected error shouldn't crash, load dumb default values instead.
warn("Unexpected error discovering local network interfaces: %s" % e)
_load_ips_dumb()


@_requires_ips
def local_ips():
"""return the IP addresses that point to this machine"""
Expand Down