Skip to content

Commit 0bcbfa4

Browse files
aixtoolstaleinat
authored andcommitted
bpo-28009: Fix uuid.uuid1() and uuid.get_node() on AIX (GH-8672)
1 parent 9f77268 commit 0bcbfa4

File tree

3 files changed

+177
-93
lines changed

3 files changed

+177
-93
lines changed

Lib/test/test_uuid.py

+50-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import unittest.mock
1+
import unittest
22
from test import support
33
import builtins
44
import contextlib
@@ -15,7 +15,6 @@
1515
py_uuid = support.import_fresh_module('uuid', blocked=['_uuid'])
1616
c_uuid = support.import_fresh_module('uuid', fresh=['_uuid'])
1717

18-
1918
def importable(name):
2019
try:
2120
__import__(name)
@@ -459,7 +458,7 @@ def test_uuid1_eui64(self):
459458
# uuid.getnode to fall back on uuid._random_getnode, which will
460459
# generate a valid value.
461460
too_large_getter = lambda: 1 << 48
462-
with unittest.mock.patch.multiple(
461+
with mock.patch.multiple(
463462
self.uuid,
464463
_node=None, # Ignore any cached node value.
465464
_GETTERS=[too_large_getter],
@@ -538,8 +537,8 @@ def mock_generate_time_safe(self, safe_value):
538537
f = self.uuid._generate_time_safe
539538
if f is None:
540539
self.skipTest('need uuid._generate_time_safe')
541-
with unittest.mock.patch.object(self.uuid, '_generate_time_safe',
542-
lambda: (f()[0], safe_value)):
540+
with mock.patch.object(self.uuid, '_generate_time_safe',
541+
lambda: (f()[0], safe_value)):
543542
yield
544543

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

677-
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
678-
def test_find_mac(self):
676+
677+
def test_find_under_heading(self):
678+
data = '''\
679+
Name Mtu Network Address Ipkts Ierrs Opkts Oerrs Coll
680+
en0 1500 link#2 fe.ad.c.1.23.4 1714807956 0 711348489 0 0
681+
01:00:5e:00:00:01
682+
en0 1500 192.168.129 x071 1714807956 0 711348489 0 0
683+
224.0.0.1
684+
en0 1500 192.168.90 x071 1714807956 0 711348489 0 0
685+
224.0.0.1
686+
'''
687+
688+
def mock_get_command_stdout(command, args):
689+
return io.BytesIO(data.encode())
690+
691+
# The above data is from AIX - with '.' as _MAC_DELIM and strings
692+
# shorter than 17 bytes (no leading 0). (_MAC_OMITS_LEADING_ZEROES=True)
693+
with mock.patch.multiple(self.uuid,
694+
_MAC_DELIM=b'.',
695+
_MAC_OMITS_LEADING_ZEROES=True,
696+
_get_command_stdout=mock_get_command_stdout):
697+
mac = self.uuid._find_mac_under_heading(
698+
command='netstat',
699+
args='-ian',
700+
heading=b'Address',
701+
)
702+
703+
self.assertEqual(mac, 0xfead0c012304)
704+
705+
def test_find_mac_near_keyword(self):
706+
# key and value are on the same line
679707
data = '''
680-
fake hwaddr
708+
fake Link encap:UNSPEC hwaddr 00-00
681709
cscotun0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
682710
eth0 Link encap:Ethernet HWaddr 12:34:56:78:90:ab
683711
'''
684712

685-
popen = unittest.mock.MagicMock()
686-
popen.stdout = io.BytesIO(data.encode())
687-
688-
with unittest.mock.patch.object(shutil, 'which',
689-
return_value='/sbin/ifconfig'):
690-
with unittest.mock.patch.object(subprocess, 'Popen',
691-
return_value=popen):
692-
mac = self.uuid._find_mac(
693-
command='ifconfig',
694-
args='',
695-
hw_identifiers=[b'hwaddr'],
696-
get_index=lambda x: x + 1,
697-
)
713+
def mock_get_command_stdout(command, args):
714+
return io.BytesIO(data.encode())
715+
716+
# The above data will only be parsed properly on non-AIX unixes.
717+
with mock.patch.multiple(self.uuid,
718+
_MAC_DELIM=b':',
719+
_MAC_OMITS_LEADING_ZEROES=False,
720+
_get_command_stdout=mock_get_command_stdout):
721+
mac = self.uuid._find_mac_near_keyword(
722+
command='ifconfig',
723+
args='',
724+
keywords=[b'hwaddr'],
725+
get_word_index=lambda x: x + 1,
726+
)
698727

699728
self.assertEqual(mac, 0x1234567890ab)
700729

Lib/uuid.py

+123-72
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
_LINUX = platform.system() == 'Linux'
6060
_WINDOWS = platform.system() == 'Windows'
6161

62+
_MAC_DELIM = b':'
63+
_MAC_OMITS_LEADING_ZEROES = False
64+
if _AIX:
65+
_MAC_DELIM = b'.'
66+
_MAC_OMITS_LEADING_ZEROES = True
67+
6268
RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
6369
'reserved for NCS compatibility', 'specified in RFC 4122',
6470
'reserved for Microsoft compatibility', 'reserved for future definition']
@@ -347,24 +353,32 @@ def version(self):
347353
if self.variant == RFC_4122:
348354
return int((self.int >> 76) & 0xf)
349355

350-
def _popen(command, *args):
351-
import os, shutil, subprocess
352-
executable = shutil.which(command)
353-
if executable is None:
354-
path = os.pathsep.join(('/sbin', '/usr/sbin'))
355-
executable = shutil.which(command, path=path)
356+
357+
def _get_command_stdout(command, *args):
358+
import io, os, shutil, subprocess
359+
360+
try:
361+
path_dirs = os.environ.get('PATH', os.defpath).split(os.pathsep)
362+
path_dirs.extend(['/sbin', '/usr/sbin'])
363+
executable = shutil.which(command, path=os.pathsep.join(path_dirs))
356364
if executable is None:
357365
return None
358-
# LC_ALL=C to ensure English output, stderr=DEVNULL to prevent output
359-
# on stderr (Note: we don't have an example where the words we search
360-
# for are actually localized, but in theory some system could do so.)
361-
env = dict(os.environ)
362-
env['LC_ALL'] = 'C'
363-
proc = subprocess.Popen((executable,) + args,
364-
stdout=subprocess.PIPE,
365-
stderr=subprocess.DEVNULL,
366-
env=env)
367-
return proc
366+
# LC_ALL=C to ensure English output, stderr=DEVNULL to prevent output
367+
# on stderr (Note: we don't have an example where the words we search
368+
# for are actually localized, but in theory some system could do so.)
369+
env = dict(os.environ)
370+
env['LC_ALL'] = 'C'
371+
proc = subprocess.Popen((executable,) + args,
372+
stdout=subprocess.PIPE,
373+
stderr=subprocess.DEVNULL,
374+
env=env)
375+
if not proc:
376+
return None
377+
stdout, stderr = proc.communicate()
378+
return io.BytesIO(stdout)
379+
except (OSError, subprocess.SubprocessError):
380+
return None
381+
368382

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

387-
def _find_mac(command, args, hw_identifiers, get_index):
401+
402+
def _find_mac_near_keyword(command, args, keywords, get_word_index):
403+
"""Searches a command's output for a MAC address near a keyword.
404+
405+
Each line of words in the output is case-insensitively searched for
406+
any of the given keywords. Upon a match, get_word_index is invoked
407+
to pick a word from the line, given the index of the match. For
408+
example, lambda i: 0 would get the first word on the line, while
409+
lambda i: i - 1 would get the word preceding the keyword.
410+
"""
411+
stdout = _get_command_stdout(command, args)
412+
if stdout is None:
413+
return None
414+
388415
first_local_mac = None
416+
for line in stdout:
417+
words = line.lower().rstrip().split()
418+
for i in range(len(words)):
419+
if words[i] in keywords:
420+
try:
421+
word = words[get_word_index(i)]
422+
mac = int(word.replace(_MAC_DELIM, b''), 16)
423+
except (ValueError, IndexError):
424+
# Virtual interfaces, such as those provided by
425+
# VPNs, do not have a colon-delimited MAC address
426+
# as expected, but a 16-byte HWAddr separated by
427+
# dashes. These should be ignored in favor of a
428+
# real MAC address
429+
pass
430+
else:
431+
if _is_universal(mac):
432+
return mac
433+
first_local_mac = first_local_mac or mac
434+
return first_local_mac or None
435+
436+
437+
def _find_mac_under_heading(command, args, heading):
438+
"""Looks for a MAC address under a heading in a command's output.
439+
440+
The first line of words in the output is searched for the given
441+
heading. Words at the same word index as the heading in subsequent
442+
lines are then examined to see if they look like MAC addresses.
443+
"""
444+
stdout = _get_command_stdout(command, args)
445+
if stdout is None:
446+
return None
447+
448+
keywords = stdout.readline().rstrip().split()
389449
try:
390-
proc = _popen(command, *args.split())
391-
if not proc:
392-
return None
393-
with proc:
394-
for line in proc.stdout:
395-
words = line.lower().rstrip().split()
396-
for i in range(len(words)):
397-
if words[i] in hw_identifiers:
398-
try:
399-
word = words[get_index(i)]
400-
mac = int(word.replace(b':', b''), 16)
401-
if _is_universal(mac):
402-
return mac
403-
first_local_mac = first_local_mac or mac
404-
except (ValueError, IndexError):
405-
# Virtual interfaces, such as those provided by
406-
# VPNs, do not have a colon-delimited MAC address
407-
# as expected, but a 16-byte HWAddr separated by
408-
# dashes. These should be ignored in favor of a
409-
# real MAC address
410-
pass
411-
except OSError:
412-
pass
450+
column_index = keywords.index(heading)
451+
except ValueError:
452+
return None
453+
454+
first_local_mac = None
455+
for line in stdout:
456+
try:
457+
words = line.rstrip().split()
458+
word = words[column_index]
459+
if len(word) == 17:
460+
mac = int(word.replace(_MAC_DELIM, b''), 16)
461+
elif _MAC_OMITS_LEADING_ZEROES:
462+
# (Only) on AIX the macaddr value given is not prefixed by 0, e.g.
463+
# en0 1500 link#2 fa.bc.de.f7.62.4 110854824 0 160133733 0 0
464+
# not
465+
# en0 1500 link#2 fa.bc.de.f7.62.04 110854824 0 160133733 0 0
466+
parts = word.split(_MAC_DELIM)
467+
if len(parts) == 6 and all(0 < len(p) <= 2 for p in parts):
468+
hexstr = b''.join(p.rjust(2, b'0') for p in parts)
469+
mac = int(hexstr, 16)
470+
else:
471+
continue
472+
else:
473+
continue
474+
except (ValueError, IndexError):
475+
# Virtual interfaces, such as those provided by
476+
# VPNs, do not have a colon-delimited MAC address
477+
# as expected, but a 16-byte HWAddr separated by
478+
# dashes. These should be ignored in favor of a
479+
# real MAC address
480+
pass
481+
else:
482+
if _is_universal(mac):
483+
return mac
484+
first_local_mac = first_local_mac or mac
413485
return first_local_mac or None
414486

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

425500
def _ip_getnode():
426501
"""Get the hardware address on Unix by running ip."""
427502
# This works on Linux with iproute2.
428-
mac = _find_mac('ip', 'link', [b'link/ether'], lambda i: i+1)
503+
mac = _find_mac_near_keyword('ip', 'link', [b'link/ether'], lambda i: i+1)
429504
if mac:
430505
return mac
431506
return None
@@ -439,17 +514,17 @@ def _arp_getnode():
439514
return None
440515

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

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

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

464539
def _netstat_getnode():
465540
"""Get the hardware address on Unix by running netstat."""
466-
# This might work on AIX, Tru64 UNIX.
467-
first_local_mac = None
468-
try:
469-
proc = _popen('netstat', '-ia')
470-
if not proc:
471-
return None
472-
with proc:
473-
words = proc.stdout.readline().rstrip().split()
474-
try:
475-
i = words.index(b'Address')
476-
except ValueError:
477-
return None
478-
for line in proc.stdout:
479-
try:
480-
words = line.rstrip().split()
481-
word = words[i]
482-
if len(word) == 17 and word.count(b':') == 5:
483-
mac = int(word.replace(b':', b''), 16)
484-
if _is_universal(mac):
485-
return mac
486-
first_local_mac = first_local_mac or mac
487-
except (ValueError, IndexError):
488-
pass
489-
except OSError:
490-
pass
491-
return first_local_mac or None
541+
# This works on AIX and might work on Tru64 UNIX.
542+
return _find_mac_under_heading('netstat', '-ian', b'Address')
492543

493544
def _ipconfig_getnode():
494545
"""Get the hardware address on Windows by running ipconfig.exe."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix uuid.getnode() on platforms with '.' as MAC Addr delimiter as well
2+
fix for MAC Addr format that omits a leading 0 in MAC Addr values.
3+
Currently, AIX is the only know platform with these settings.
4+
Patch by Michael Felt.

0 commit comments

Comments
 (0)