Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

bpo-28009: Fix uuid.uuid1() and uuid.get_node() on AIX #8672

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

Merged
merged 43 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4ed41e9
uuid: correct the field seperator value of ethernet adapter MAC addre…
aixtools Aug 4, 2018
ba47933
Merge branch 'master' into bpo-28009-1
aixtools Aug 4, 2018
c6029a4
correct test logic, cut/paste error
aixtools Aug 4, 2018
7e1874d
specify shorter getters list per https://bugs.python.org/issue28009#m…
aixtools Aug 5, 2018
6a5bccb
write _notAIX as non-negative constant and use 'not AIX'
aixtools Aug 22, 2018
a98309b
simplify NEWS entry
aixtools Aug 22, 2018
53ef750
skip a test the right way
aixtools Aug 22, 2018
6fd5f8e
capitalize constants
aixtools Aug 22, 2018
3bb1c08
oops - restore missing assignment
aixtools Aug 22, 2018
1af8e3d
skip both tests the right way
aixtools Aug 22, 2018
d3eaab9
write _notAIX as non-negative constant and use 'not _AIX'
aixtools Aug 22, 2018
3469a01
added additional 'skip' entries for commands that do not exist on AIX
aixtools Aug 22, 2018
399e8ec
Use and set a single _NODE_GETTERS constant with platform test outsid…
aixtools Aug 24, 2018
6e2a9bf
modify the lambda function so it does not look like a bug
aixtools Sep 16, 2018
bb3a460
resolve differences in imports
aixtools Sep 16, 2018
db6767f
Switch back to using sys.platform.startswith() to resolve conflicts
aixtools Sep 16, 2018
25d3ef1
still trying to get differencs to resolve...
aixtools Sep 16, 2018
fa9b43b
changes per request. Thx for the review!
aixtools Oct 28, 2018
4a0c8f0
resolve merge conflict
aixtools Oct 28, 2018
6463707
resolve merge conflict, 2nd try
aixtools Oct 28, 2018
a0ef760
remove unused added import
taleinat Nov 12, 2018
095e221
Per requested changes
aixtools Nov 14, 2018
ec4c0e8
Merge branch 'bpo-28009-1' of github.com:aixtools/cpython into bpo-28…
aixtools Nov 14, 2018
70a45f0
Merge branch 'master' into bpo-28009-1
ncoghlan Dec 26, 2018
b1b4952
sync with master
aixtools Jun 14, 2019
8f0687a
rename find_mac_routines (in test_uuid.py)
aixtools Jun 14, 2019
4756670
fix sync issues
aixtools Jun 14, 2019
c55714a
sync with master
aixtools Jun 16, 2019
10f272e
add inline comments, correct typos
aixtools Jun 17, 2019
27b6c32
Improve blurb
aixtools Jun 21, 2019
33969b9
refactor more of getting a command's stdout into the helper func
taleinat Jun 24, 2019
7a57734
reduce scope of try/except blocks, for clarity
taleinat Jun 24, 2019
4628cea
make tests run regardless of host OS
taleinat Jun 24, 2019
d2830a2
remove no longer necessary _AIX definition in the test file
taleinat Jul 14, 2019
0688727
fix unittest import and indentation in one place
taleinat Jul 14, 2019
ff1ee20
simplify macaddr parsing and control flow in _find_mac_nextlines()
taleinat Jul 14, 2019
083e9c6
no need for extra return value check in _netstart_getnode()
taleinat Jul 14, 2019
30cd017
improve wording of NEWS entry
taleinat Jul 14, 2019
27a972b
make AIX formatted MAC address handling AIX-specific
taleinat Jul 15, 2019
543e66d
Stop reverse DNS lookups with netstat
aixtools Jul 15, 2019
588fda7
add _MAC_OMITS_LEADING_ZEROES, better function names and doc-strings
taleinat Jul 17, 2019
28f7a01
update blurb and comments in test_uuid.py
aixtools Aug 30, 2019
6fc2129
Update News blurb
aixtools Sep 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions 71 Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import unittest.mock
import unittest
from test import support
import builtins
import contextlib
Expand All @@ -15,7 +15,6 @@
py_uuid = support.import_fresh_module('uuid', blocked=['_uuid'])
c_uuid = support.import_fresh_module('uuid', fresh=['_uuid'])


def importable(name):
try:
__import__(name)
Expand Down Expand Up @@ -459,7 +458,7 @@ def test_uuid1_eui64(self):
# uuid.getnode to fall back on uuid._random_getnode, which will
# generate a valid value.
too_large_getter = lambda: 1 << 48
with unittest.mock.patch.multiple(
with mock.patch.multiple(
self.uuid,
_node=None, # Ignore any cached node value.
_GETTERS=[too_large_getter],
Expand Down Expand Up @@ -538,8 +537,8 @@ def mock_generate_time_safe(self, safe_value):
f = self.uuid._generate_time_safe
if f is None:
self.skipTest('need uuid._generate_time_safe')
with unittest.mock.patch.object(self.uuid, '_generate_time_safe',
lambda: (f()[0], safe_value)):
with mock.patch.object(self.uuid, '_generate_time_safe',
lambda: (f()[0], safe_value)):
yield

@unittest.skipUnless(os.name == 'posix', 'POSIX-only test')
Expand Down Expand Up @@ -674,27 +673,57 @@ class TestUUIDWithExtModule(BaseTestUUID, unittest.TestCase):
class BaseTestInternals:
_uuid = py_uuid

@unittest.skipUnless(os.name == 'posix', 'requires Posix')
def test_find_mac(self):

def test_find_under_heading(self):
data = '''\
Name Mtu Network Address Ipkts Ierrs Opkts Oerrs Coll
en0 1500 link#2 fe.ad.c.1.23.4 1714807956 0 711348489 0 0
01:00:5e:00:00:01
en0 1500 192.168.129 x071 1714807956 0 711348489 0 0
224.0.0.1
en0 1500 192.168.90 x071 1714807956 0 711348489 0 0
224.0.0.1
'''

def mock_get_command_stdout(command, args):
return io.BytesIO(data.encode())

# The above data is from AIX - with '.' as _MAC_DELIM and strings
# shorter than 17 bytes (no leading 0). (_MAC_OMITS_LEADING_ZEROES=True)
with mock.patch.multiple(self.uuid,
_MAC_DELIM=b'.',
_MAC_OMITS_LEADING_ZEROES=True,
_get_command_stdout=mock_get_command_stdout):
mac = self.uuid._find_mac_under_heading(
command='netstat',
args='-ian',
heading=b'Address',
)

self.assertEqual(mac, 0xfead0c012304)
aixtools marked this conversation as resolved.
Show resolved Hide resolved

def test_find_mac_near_keyword(self):
# key and value are on the same line
data = '''
fake hwaddr
fake Link encap:UNSPEC hwaddr 00-00
cscotun0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
eth0 Link encap:Ethernet HWaddr 12:34:56:78:90:ab
'''

popen = unittest.mock.MagicMock()
popen.stdout = io.BytesIO(data.encode())

with unittest.mock.patch.object(shutil, 'which',
return_value='/sbin/ifconfig'):
with unittest.mock.patch.object(subprocess, 'Popen',
return_value=popen):
mac = self.uuid._find_mac(
command='ifconfig',
args='',
hw_identifiers=[b'hwaddr'],
get_index=lambda x: x + 1,
)
def mock_get_command_stdout(command, args):
return io.BytesIO(data.encode())

# The above data will only be parsed properly on non-AIX unixes.
with mock.patch.multiple(self.uuid,
_MAC_DELIM=b':',
_MAC_OMITS_LEADING_ZEROES=False,
_get_command_stdout=mock_get_command_stdout):
mac = self.uuid._find_mac_near_keyword(
command='ifconfig',
args='',
keywords=[b'hwaddr'],
get_word_index=lambda x: x + 1,
)

self.assertEqual(mac, 0x1234567890ab)

Expand Down
195 changes: 123 additions & 72 deletions 195 Lib/uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
_LINUX = platform.system() == 'Linux'
_WINDOWS = platform.system() == 'Windows'

_MAC_DELIM = b':'
_MAC_OMITS_LEADING_ZEROES = False
if _AIX:
_MAC_DELIM = b'.'
_MAC_OMITS_LEADING_ZEROES = True

RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
'reserved for NCS compatibility', 'specified in RFC 4122',
'reserved for Microsoft compatibility', 'reserved for future definition']
Expand Down Expand Up @@ -347,24 +353,32 @@ def version(self):
if self.variant == RFC_4122:
return int((self.int >> 76) & 0xf)

def _popen(command, *args):
import os, shutil, subprocess
executable = shutil.which(command)
if executable is None:
path = os.pathsep.join(('/sbin', '/usr/sbin'))
executable = shutil.which(command, path=path)

def _get_command_stdout(command, *args):
import io, os, shutil, subprocess

try:
path_dirs = os.environ.get('PATH', os.defpath).split(os.pathsep)
path_dirs.extend(['/sbin', '/usr/sbin'])
executable = shutil.which(command, path=os.pathsep.join(path_dirs))
if executable is None:
return None
# LC_ALL=C to ensure English output, stderr=DEVNULL to prevent output
# on stderr (Note: we don't have an example where the words we search
# for are actually localized, but in theory some system could do so.)
env = dict(os.environ)
env['LC_ALL'] = 'C'
proc = subprocess.Popen((executable,) + args,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env)
return proc
# LC_ALL=C to ensure English output, stderr=DEVNULL to prevent output
# on stderr (Note: we don't have an example where the words we search
# for are actually localized, but in theory some system could do so.)
env = dict(os.environ)
env['LC_ALL'] = 'C'
proc = subprocess.Popen((executable,) + args,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env)
if not proc:
return None
stdout, stderr = proc.communicate()
return io.BytesIO(stdout)
except (OSError, subprocess.SubprocessError):
return None


# For MAC (a.k.a. IEEE 802, or EUI-48) addresses, the second least significant
# bit of the first octet signifies whether the MAC address is universally (0)
Expand All @@ -384,48 +398,109 @@ def _popen(command, *args):
def _is_universal(mac):
return not (mac & (1 << 41))

def _find_mac(command, args, hw_identifiers, get_index):

def _find_mac_near_keyword(command, args, keywords, get_word_index):
"""Searches a command's output for a MAC address near a keyword.

Each line of words in the output is case-insensitively searched for
any of the given keywords. Upon a match, get_word_index is invoked
to pick a word from the line, given the index of the match. For
example, lambda i: 0 would get the first word on the line, while
lambda i: i - 1 would get the word preceding the keyword.
"""
stdout = _get_command_stdout(command, args)
if stdout is None:
return None

first_local_mac = None
for line in stdout:
words = line.lower().rstrip().split()
for i in range(len(words)):
if words[i] in keywords:
try:
word = words[get_word_index(i)]
mac = int(word.replace(_MAC_DELIM, b''), 16)
except (ValueError, IndexError):
# Virtual interfaces, such as those provided by
# VPNs, do not have a colon-delimited MAC address
# as expected, but a 16-byte HWAddr separated by
# dashes. These should be ignored in favor of a
# real MAC address
pass
else:
if _is_universal(mac):
return mac
first_local_mac = first_local_mac or mac
return first_local_mac or None


def _find_mac_under_heading(command, args, heading):
"""Looks for a MAC address under a heading in a command's output.

The first line of words in the output is searched for the given
heading. Words at the same word index as the heading in subsequent
lines are then examined to see if they look like MAC addresses.
"""
stdout = _get_command_stdout(command, args)
if stdout is None:
return None

keywords = stdout.readline().rstrip().split()
try:
proc = _popen(command, *args.split())
if not proc:
return None
with proc:
for line in proc.stdout:
words = line.lower().rstrip().split()
for i in range(len(words)):
if words[i] in hw_identifiers:
try:
word = words[get_index(i)]
mac = int(word.replace(b':', b''), 16)
if _is_universal(mac):
return mac
first_local_mac = first_local_mac or mac
except (ValueError, IndexError):
# Virtual interfaces, such as those provided by
# VPNs, do not have a colon-delimited MAC address
# as expected, but a 16-byte HWAddr separated by
# dashes. These should be ignored in favor of a
# real MAC address
pass
except OSError:
pass
column_index = keywords.index(heading)
except ValueError:
return None

first_local_mac = None
for line in stdout:
try:
words = line.rstrip().split()
word = words[column_index]
if len(word) == 17:
mac = int(word.replace(_MAC_DELIM, b''), 16)
elif _MAC_OMITS_LEADING_ZEROES:
# (Only) on AIX the macaddr value given is not prefixed by 0, e.g.
# en0 1500 link#2 fa.bc.de.f7.62.4 110854824 0 160133733 0 0
# not
# en0 1500 link#2 fa.bc.de.f7.62.04 110854824 0 160133733 0 0
parts = word.split(_MAC_DELIM)
if len(parts) == 6 and all(0 < len(p) <= 2 for p in parts):
hexstr = b''.join(p.rjust(2, b'0') for p in parts)
mac = int(hexstr, 16)
else:
continue
else:
continue
except (ValueError, IndexError):
# Virtual interfaces, such as those provided by
# VPNs, do not have a colon-delimited MAC address
# as expected, but a 16-byte HWAddr separated by
# dashes. These should be ignored in favor of a
# real MAC address
pass
else:
if _is_universal(mac):
return mac
first_local_mac = first_local_mac or mac
return first_local_mac or None


# The following functions call external programs to 'get' a macaddr value to
# be used as basis for an uuid
def _ifconfig_getnode():
"""Get the hardware address on Unix by running ifconfig."""
# This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes.
keywords = (b'hwaddr', b'ether', b'address:', b'lladdr')
for args in ('', '-a', '-av'):
mac = _find_mac('ifconfig', args, keywords, lambda i: i+1)
mac = _find_mac_near_keyword('ifconfig', args, keywords, lambda i: i+1)
if mac:
return mac
return None

def _ip_getnode():
"""Get the hardware address on Unix by running ip."""
# This works on Linux with iproute2.
mac = _find_mac('ip', 'link', [b'link/ether'], lambda i: i+1)
mac = _find_mac_near_keyword('ip', 'link', [b'link/ether'], lambda i: i+1)
if mac:
return mac
return None
Expand All @@ -439,17 +514,17 @@ def _arp_getnode():
return None

# Try getting the MAC addr from arp based on our IP address (Solaris).
mac = _find_mac('arp', '-an', [os.fsencode(ip_addr)], lambda i: -1)
mac = _find_mac_near_keyword('arp', '-an', [os.fsencode(ip_addr)], lambda i: -1)
if mac:
return mac

# This works on OpenBSD
mac = _find_mac('arp', '-an', [os.fsencode(ip_addr)], lambda i: i+1)
mac = _find_mac_near_keyword('arp', '-an', [os.fsencode(ip_addr)], lambda i: i+1)
if mac:
return mac

# This works on Linux, FreeBSD and NetBSD
mac = _find_mac('arp', '-an', [os.fsencode('(%s)' % ip_addr)],
mac = _find_mac_near_keyword('arp', '-an', [os.fsencode('(%s)' % ip_addr)],
lambda i: i+2)
# Return None instead of 0.
if mac:
Expand All @@ -459,36 +534,12 @@ def _arp_getnode():
def _lanscan_getnode():
"""Get the hardware address on Unix by running lanscan."""
# This might work on HP-UX.
return _find_mac('lanscan', '-ai', [b'lan0'], lambda i: 0)
return _find_mac_near_keyword('lanscan', '-ai', [b'lan0'], lambda i: 0)

def _netstat_getnode():
"""Get the hardware address on Unix by running netstat."""
# This might work on AIX, Tru64 UNIX.
first_local_mac = None
try:
proc = _popen('netstat', '-ia')
if not proc:
return None
with proc:
words = proc.stdout.readline().rstrip().split()
try:
i = words.index(b'Address')
except ValueError:
return None
for line in proc.stdout:
try:
words = line.rstrip().split()
word = words[i]
if len(word) == 17 and word.count(b':') == 5:
mac = int(word.replace(b':', b''), 16)
if _is_universal(mac):
return mac
first_local_mac = first_local_mac or mac
except (ValueError, IndexError):
pass
except OSError:
pass
return first_local_mac or None
# This works on AIX and might work on Tru64 UNIX.
return _find_mac_under_heading('netstat', '-ian', b'Address')

def _ipconfig_getnode():
"""Get the hardware address on Windows by running ipconfig.exe."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix uuid.getnode() on platforms with '.' as MAC Addr delimiter as well
fix for MAC Addr format that omits a leading 0 in MAC Addr values.
Currently, AIX is the only know platform with these settings.
Patch by Michael Felt.
Morty Proxy This is a proxified and sanitized view of the page, visit original site.