59
59
_LINUX = platform .system () == 'Linux'
60
60
_WINDOWS = platform .system () == 'Windows'
61
61
62
+ _MAC_DELIM = b':'
63
+ _MAC_OMITS_LEADING_ZEROES = False
64
+ if _AIX :
65
+ _MAC_DELIM = b'.'
66
+ _MAC_OMITS_LEADING_ZEROES = True
67
+
62
68
RESERVED_NCS , RFC_4122 , RESERVED_MICROSOFT , RESERVED_FUTURE = [
63
69
'reserved for NCS compatibility' , 'specified in RFC 4122' ,
64
70
'reserved for Microsoft compatibility' , 'reserved for future definition' ]
@@ -347,24 +353,32 @@ def version(self):
347
353
if self .variant == RFC_4122 :
348
354
return int ((self .int >> 76 ) & 0xf )
349
355
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 ))
356
364
if executable is None :
357
365
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
+
368
382
369
383
# For MAC (a.k.a. IEEE 802, or EUI-48) addresses, the second least significant
370
384
# bit of the first octet signifies whether the MAC address is universally (0)
@@ -384,48 +398,109 @@ def _popen(command, *args):
384
398
def _is_universal (mac ):
385
399
return not (mac & (1 << 41 ))
386
400
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
+
388
415
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 ()
389
449
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
413
485
return first_local_mac or None
414
486
487
+
488
+ # The following functions call external programs to 'get' a macaddr value to
489
+ # be used as basis for an uuid
415
490
def _ifconfig_getnode ():
416
491
"""Get the hardware address on Unix by running ifconfig."""
417
492
# This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes.
418
493
keywords = (b'hwaddr' , b'ether' , b'address:' , b'lladdr' )
419
494
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 )
421
496
if mac :
422
497
return mac
423
498
return None
424
499
425
500
def _ip_getnode ():
426
501
"""Get the hardware address on Unix by running ip."""
427
502
# 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 )
429
504
if mac :
430
505
return mac
431
506
return None
@@ -439,17 +514,17 @@ def _arp_getnode():
439
514
return None
440
515
441
516
# 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 )
443
518
if mac :
444
519
return mac
445
520
446
521
# 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 )
448
523
if mac :
449
524
return mac
450
525
451
526
# 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 )],
453
528
lambda i : i + 2 )
454
529
# Return None instead of 0.
455
530
if mac :
@@ -459,36 +534,12 @@ def _arp_getnode():
459
534
def _lanscan_getnode ():
460
535
"""Get the hardware address on Unix by running lanscan."""
461
536
# 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 )
463
538
464
539
def _netstat_getnode ():
465
540
"""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' )
492
543
493
544
def _ipconfig_getnode ():
494
545
"""Get the hardware address on Windows by running ipconfig.exe."""
0 commit comments