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

Commit 0bcbfa4

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

File tree

Expand file treeCollapse file tree

3 files changed

+177
-93
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+177
-93
lines changed

‎Lib/test/test_uuid.py

Copy file name to clipboardExpand all lines: Lib/test/test_uuid.py
+50-21Lines changed: 50 additions & 21 deletions
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

Copy file name to clipboardExpand all lines: Lib/uuid.py
+123-72Lines changed: 123 additions & 72 deletions
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."""
+4Lines changed: 4 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.