jupyter / jupyter_client

Jupyter protocol client APIs
https://jupyter-client.readthedocs.io
BSD 3-Clause "New" or "Revised" License
383 stars 283 forks source link

Psutil changes in `public_ips()` causes breaking changes #1039

Closed BwL1289 closed 1 week ago

BwL1289 commented 2 weeks ago

Introduced in 5d4888116a69b8f8edb69e51a22b8d4df3a54741 in jupyter_client/localinterfaces.py, the changes made to public_ips() method breaks existing deployments.

Previously, the following yielded the correct public ip address:

ip = public_ips()[0]
print(ip)  # 10.0.XX.XX

Now, it yields the linked local address:

ip = public_ips()[0]
print(ip)  # 169.254.XXX.X

For now, I am manually filtering out IPs that are one of: ipv4 link local ips (unicast), ipv6 ips (unicast), ipv4 multicast ips, or ipv6 multicast ips, but would love to see this get fixed upstream.

minrk commented 2 weeks ago

Thanks for reporting! I'll see if I can find a quick fix. Can you share the full public ips list for the two (feel free to obfuscate part of the ips, I mainly want to see if the lists are the same and how the order differs)? The ordering hasn't ever technically been meaningful, but it's nice when a guess is useful and consistency is important.

BwL1289 commented 2 weeks ago

Unfortunately I only have the full list of public ips for the new behavior (before we filter) but here's the list:

['169.254.XXX.X', '10.0.XX.XX']

Let me know what else I can provide!

minrk commented 2 weeks ago

Ah, sorry. You can still compute them both if you have both netifaces and psutil:

import netifaces
import psutil
from jupyter_client import localinterfaces

localinterfaces._load_ips_psutil()
print("psutil   ", localinterfaces.PUBLIC_IPS)
localinterfaces._load_ips_netifaces()
print("netifaces", localinterfaces.PUBLIC_IPS)

print("psutil   ", list(psutil.net_if_addrs()))
print("netifaces", list(netifaces.interfaces()))

which give me:

psutil    ['192.168.1.50', '192.168.69.1']
netifaces ['192.168.1.50', '192.168.69.1']
psutil    ['lo0', 'en0', 'bridge100', 'anpi0', 'anpi1', 'anpi2', 'en4', 'en5', 'en6', 'en1', 'en2', 'en3', 'bridge0', 'ap1', 'awdl0', 'llw0', 'vmenet0', 'utun0', 'utun1', 'utun2', 'utun3', 'utun4', 'utun5', 'utun6', 'utun7']
netifaces ['lo0', 'gif0', 'stf0', 'anpi0', 'anpi1', 'anpi2', 'en4', 'en5', 'en6', 'en1', 'en2', 'en3', 'bridge0', 'ap1', 'en0', 'awdl0', 'llw0', 'utun0', 'utun1', 'utun2', 'utun3', 'utun4', 'utun5', 'utun6', 'utun7', 'vmenet0', 'bridge100']

and if you can identify the interface label for each of the ipv4 addresses, e.g. with:

ifconfig | grep -B 5 'inet '

But public_ips should effectively be considered an unordered set. We can manually sort 169.254 to last.

BwL1289 commented 2 weeks ago

Got it! I can keep the filtering we have.

Manually sorting link locals to last or updating the docs that it should be considered an unordered set sounds like a good solution. Might help some users down the road.

Thanks!

minrk commented 2 weeks ago

Thinking about it, 169.254 probably should be filtered out of public_ips like 127 is. I can't think of what that change would break, but probably something.

BwL1289 commented 2 weeks ago

Makes sense to me (though not sure about the unintended breaking changes that may result). The method name public_ips makes me think this is the behavior users expect.

There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton

minrk commented 2 weeks ago

1040 removes link-local addresses from public_ips entirely. public_ips is specifically meant to be for "pick some ips that other machines on some network will be able to connect to me", and link-local isn't that, so it makes sense to exclude it just like we do for 127.

BTW, how were you using this? I've used public_ips()[0] in some jupyterhub demos, but it's never been guaranteed to work since ordering is unstable and not consistent across systems, and the right answer depends on your networking situation. Specifying an actual network interface is far more reliable (but tedious, because every environment has different networking interfaces):

import socket
import psutil

def ip_for_iface(iface="en0"):
    """Return ipv4 address for the given named network interface"""
    # select interface
    iface_addrs = psutil.net_if_addrs()[iface]
    # filter to ipv4
    iface_inet = [addr for addr in iface_addrs if addr.family == socket.AF_INET]
    # return first ip
    return iface_inet[0].address

print(ip_for_iface("en0"))
BwL1289 commented 2 weeks ago

This was a naive implementation in dev.

Deploying to prod will include specifying a network interface as you indicated above.

Thank you and appreciate the prompt patch.