Using the module API¶
get_mac_address() covers most uses of getmac. This page covers the internal methods used by getmac to resolve MAC addresses on various platforms.
Internally, getmac picks a method for each kind of lookup. The method is a class that knows a particular way of finding a MAC address, such as reading /sys/class/net/<iface>/address or running ip neighbor show. This page explains how those methods work, and how to implement a new method.
Warning
Most of what this page describes lives in getmac.getmac and is an internal API that may change between releases. Use it with care.
Where things live¶
getmac:get_mac_address()andget_default_interface()(the names in__all__), plussettingsand__version__, which can also be imported from it. Nothing else is imported into the top-level package.getmac.getmac: theMethodbase class, the built-in methods, theMETHODSlist, theMETHOD_CACHEandFALLBACK_CACHEdicts, and lower-level functions such asget_by_method()andinitialize_method_cache().getmac.variables: thesettings,constsandgvarsobjects, documented as theSettings,ConstantsandVariablesclasses.getmac.utils: helper functions used by methods, such aspopen()andsearch().
Method isn’t exported from the top-level package, so import it with from getmac.getmac import Method. Every built-in method and its docstring is listed in the API reference.
Methods 101¶
Methods¶
A method is a subclass of Method and should set 3 attributes:
platforms: a set with the platforms it works on, such as{"linux", "darwin"}.method_type: the kind of lookup it does (see below).network_request: whether using it sends traffic on the network.
A method overrides two functions:
test(): a cheap check that the method can work on this system, such as “is theipcommand installed?”get(): performs the lookup. It takes one argument, and returns the result orNone. The argument’s meaning depends on the method, e.g. for an interface method, it would be the interface name (e.g."eth0").
Method types¶
|
Used for |
|
|
|---|---|---|---|
|
|
IPv4 address |
MAC address |
|
|
IPv6 address |
MAC address |
|
Both |
IPv4 or IPv6 address |
MAC address |
|
|
Interface name |
MAC address |
|
|
Empty string |
Interface name |
Note
get_mac_address() with no arguments does a default_iface lookup, then an iface lookup of the interface it finds. If the default interface can’t be found or it doesn’t have a MAC, the first interface from socket.if_nameindex() that has a MAC and isn’t a loopback interface is used instead, except on Windows.
On Windows with network_request=True (the default), it first gets the IP address of the interface with the default route from fetch_ip_using_dns(), and does an ip4 lookup of that address. The default_iface lookup is only done if that doesn’t find a MAC.
Note
There is one cache per lookup type: ip4, ip6, iface and default_iface.
Platforms¶
The platform is a lowercase identifier, consts.PLATFORM, unless you override it with settings.OVERRIDE_PLATFORM. To see what your system is detected as:
from getmac.variables import consts
print(consts.PLATFORM) # For example, "linux"
The identifiers used by the built-in methods are listed in Method.VALID_PLATFORM_NAMES.
The platform "other" is used for methods that are generic enough to try on unknown systems, and are only used as a last resort if the current platform matches no method.
Choosing a method¶
The first time a lookup is performed for a particular type (e.g. ip4), the cache for that type is initialized by initialize_method_cache():
Selects methods in
METHODSwith a matchingmethod_type.Selects methods whose
platformsset includes the current platform, or"other"if nothing matches (see the section above).If
network_request=False, it excludes methods that havenetwork_request=True.Creates instances of the selected methods and calls their
test(). The first method that passes (returnsTrue) becomes the primary method, stored inMETHOD_CACHE[type]. The others that pass become fallbacks, stored in order inFALLBACK_CACHE[type]. Methods whosetest()returnsFalseor raises an exception are left out.
initialize_method_cache() will raise RuntimeError if:
No method has the requested type.
No method suits the platform, even after trying
"other".Every candidate method fails its
test().
Note
The cache is built lazily on first lookup and reused for the rest of the process’s life. Methods aren’t tested again, so later changes to METHODS, to OVERRIDE_PLATFORM, or to the system itself (such as installing a command) don’t affect a type whose cache is already built.
Falling back during a lookup¶
Each lookup calls get() on the primary method from the cache and:
If
get()returns a value, winner winner chicken dinner, that’s the result.If
get()returnsNone, the result isNone. The fallbacks are not tried.Nonemeans “not found”, for example “there’s no such interface”, not “this method doesn’t work”.If
get()raises an exception, or setsself.unusable = Trueand returnsNone, the method is removed from the cache. The first fallback becomes the primary method, and the lookup is retried with it using the same argument. The exception is logged as a warning, not raised.A
CalledProcessErrorwith exit code 1 is treated like returningNone, because many commands exit with 1 when the interface or host doesn’t exist. Any other exit code counts as a failure and leads to a fallback.With
network_request=False, methods that send network requests (theirnetwork_requestattribute isTrue) are skipped, both for the lookup and as fallbacks. They stay in the cache, since the cache may have been built by a lookup that allowed them.
IPv4 lookups and network requests¶
When get_mac_address() looks up an IPv4 address (ip= or hostname=) with network_request=True (the default), it first tries to make sure the host is in the system’s ARP table:
If
ArpFileis in theip4cache, it’s tried first, since reading/proc/net/arpis fast. If it fails, it’s removed from the cache, but no other method is tried yet.Otherwise, if
CtypesHostorArpingHostis in theip4cache, it’s made the primary method, since it sends an ARP request itself.If neither applies, an empty UDP packet is sent to the host on the port specified by
settings.PORTto force the system to populate the ARP table.
Then the host is looked up with the ip4 cache. If the UDP packet was sent and the host isn’t found, it’s looked up again until it’s found or settings.ARP_TIMEOUT seconds have passed (by default, it isn’t looked up again). IPv6 lookups send the UDP packet and use ARP_TIMEOUT the same way, but there’s no ArpFile or ARP request step.
Seeing which methods are used¶
Inspecting the caches¶
After a lookup, the caches show which methods were chosen. Converting a method instance to a string with str gives its class name.
from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE
# Force caches to be populated
get_mac_address()
# Print the name of each method in the cache
for method_type, method in METHOD_CACHE.items():
fallbacks = [str(m) for m in FALLBACK_CACHE[method_type]]
print(f"{method_type}: {method!s}, fallbacks: {fallbacks}")
Example output on a Linux system:
ip4: None, fallbacks: []
ip6: None, fallbacks: []
iface: SysIfaceFile, fallbacks: ['FcntlIface', 'IpLinkIface']
default_iface: DefaultIfaceLinuxRouteFile, fallbacks: ['DefaultIfaceIpRoute']
Two helper functions look methods up by name:
get_instance_from_cache()returns the cached instance of a method, primary or fallback, for one lookup type. The name must match the class name exactly, for exampleget_instance_from_cache("iface", "SysIfaceFile").get_method_by_name()returns the class with a given name fromMETHODS, ignoring case. This is howFORCE_METHODfinds a method.
Logging and debug output¶
getmac logs to the getmac logger with Python’s logging module. It attaches a NullHandler to that logger, so nothing is shown until logging is configured. settings.DEBUG controls how much detail is logged:
0(the default): warnings, errors, and a few debug messages, such as when a cache is built and the raw MAC that was found.1: failed tests, the contents of the caches, eachget()attempt, and how long the lookup took.2: the methods left after filtering by type, by platform, and bytest().3: every command that’s run.4: the output of every command, and the fullMETHODSlist.
import logging
from getmac import get_mac_address, settings
logging.basicConfig(format="%(levelname)-8s %(message)s", level=logging.DEBUG)
settings.DEBUG = 2
get_mac_address(interface="lo")
Example output on Linux, shortened:
DEBUG Initializing 'iface' method cache (platform: 'linux')
DEBUG 12 type-filtered methods for 'iface': SysIfaceFile, FcntlIface, LanscanIface, ...
DEBUG 6 platform-filtered methods for 'linux' (method_type='iface'): SysIfaceFile, FcntlIface, IfconfigWithIfaceArg, IfconfigOther, IpLinkIface, NetstatIface
DEBUG Test failed for method 'IfconfigWithIfaceArg'
DEBUG Test failed for method 'IfconfigOther'
DEBUG Test failed for method 'NetstatIface'
DEBUG 3 tested methods for 'iface': SysIfaceFile, FcntlIface, IpLinkIface
...
DEBUG Attempting get() (method='SysIfaceFile', method_type='iface', arg='lo')
DEBUG Raw MAC found: 00:00:00:00:00:00
The command-line equivalent is getmac -v -dd -i lo, see Command-Line Interface.
Calling lookups directly¶
get_by_method()¶
get_by_method() does one lookup of a given type (e.g., "ip4") using the caches and fallbacks described above.
It returns whatever the method’s get() returned, without the clean-up that get_mac_address() does. Many built-in methods return the MAC the way the command printed it, so pass the result through clean_mac() to get a lowercase, colon-separated MAC (aka what is normally returned from get_mac_address()).
from getmac import utils
from getmac.getmac import get_by_method
raw = get_by_method("iface", "lo")
print(repr(raw))
print(utils.clean_mac(raw))
Example output on Linux:
'00:00:00:00:00:00\n'
00:00:00:00:00:00
A few more things to know:
An empty argument returns
Noneand logs an error, except fordefault_ifacetype methods, which take no argument.It raises
RuntimeErrorwhen no method can be used, as described in Choosing a method.Its
network_requestargument has the same effect as ininitialize_method_cache(), and only applies if that type’s cache hasn’t been built yet.
get_default_interface()¶
get_default_interface() is get_by_method("default_iface"), so it can also raise RuntimeError. It doesn’t change the default interface that get_mac_address() remembers between calls (gvars.DEFAULT_IFACE).
from getmac import get_default_interface
try:
print(get_default_interface())
except RuntimeError as err:
print(err)
Settings¶
The settings are attributes of getmac.settings, an instance of Settings. Configuration has a short overview, and this section adds how each one interacts with method selection.
FORCE_METHODThe name of a method to use for every lookup, e.g.
"IpLinkIface". The name is looked up inMETHODS, ignoring case. If no method matches, the lookup logs an error and returnsNone. On the command line, use--force-method.A new instance is created for each lookup, and
test()is never called.platforms,method_typeandnetwork_requestare ignored, and the caches aren’t used or changed.There’s no fallback, and exceptions raised by the method are not caught.
It applies to every lookup type, including
get_mac_address()called with no arguments.For an IPv4 lookup with
network_request=True,get_mac_address()still builds theip4cache and sends the UDP packet described in IPv4 lookups and network requests before calling the forced method. The UDP packet isn’t sent if the forced method is"CtypesHost"or"ArpingHost"and it’s in theip4cache, since it sends an ARP request itself.One case differs: forcing
"ArpFile"for an IPv4 lookup withnetwork_request=True. TheArpFileinstance in theip4cache is then tried first, as described in IPv4 lookups and network requests, and the points above don’t apply to that attempt: it uses the cache, exceptions are caught, and a failure removes it from the cache. If that attempt finds a MAC, it’s returned, no UDP packet is sent, and the forced method isn’t called. Otherwise the UDP packet is sent and a new ArpFile instance runs as described above.
OVERRIDE_PLATFORMOverride the detected platform identifier to whatever you set, e.g.
"linux". Use--override-platformon the command line. It’s only read when a cache is built, so set it before the first lookup or reset the caches afterwards. It only changes which methods are chosen. Other platform-specific behavior inget_mac_address(), such as how the default interface is found on Windows, still follows the detected platform.PORTThe UDP port used to populate the ARP table, as described in IPv4 lookups and network requests.
ARP_TIMEOUTHow long to keep looking up a host after sending the UDP packet, as described in IPv4 lookups and network requests. Use
--arp-timeouton the command line.COMMAND_TIMEOUTHow long a command can run before it’s stopped. A method whose command is stopped is treated like one that raised an exception: it’s marked unusable and removed from the caches, and the next method is tried.
DEBUGHow much detail is logged, see Logging and debug output.
Adding a custom method¶
You can add your own methods at runtime, for example to support a command getmac doesn’t know about. If the method would be useful to others, consider contributing it to getmac, see Adding a new method.
Writing the class¶
The examples below use this method, which always returns the same MAC. Save it as my_methods.py.
from typing import Optional
from getmac.getmac import Method
class FixedMacIface(Method):
"""Always returns the same MAC address. Only useful as an example."""
platforms = {"linux", "darwin", "windows"}
method_type = "iface"
def test(self) -> bool:
return True
def get(self, arg: str) -> Optional[str]:
return "02:00:00:00:00:01"
Requirements to follow for implementing a method:
platformsmust include each platform the method should run on, see Platforms.method_typemust be one of the values in Method types.Set
network_request = Trueifget()can result in network traffic (e.g. an ARP request).test()should be cheap, such as checking that a command or file exists.check_command()andcheck_path()do this. If it raises, getmac treats it as a failed test.get()should return the MAC as astr, or returnNoneif it wasn’t found.When
get()can’t work on this system, it should raise an exception or setself.unusable = True, so that getmac moves on to the next method. See Falling back during a lookup.A
default_ifacemethod returns an interface name, and itsget()is called with an empty string, so define it asdef get(self, arg: str = "").
getmac.utils has helpers for running commands (popen()), reading files (read_file()) and matching output (search()), and Constants has regular expressions for MAC addresses, such as MAC_RE_COLON.
Here’s a more realistic method. It parses the output of ip -brief link show dev <iface>, which looks like lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>.
from typing import Optional
from getmac import utils
from getmac.getmac import Method
from getmac.variables import consts
class IpBriefLinkIface(Method):
"""Get the MAC of an interface from ``ip -brief link show dev <iface>``."""
platforms = {"linux"}
method_type = "iface"
def test(self) -> bool:
return utils.check_command("ip")
def get(self, arg: str) -> Optional[str]:
# If the interface doesn't exist, "ip" exits with code 1, which getmac
# treats as "not found". Other exit codes make getmac try the next method.
output = utils.popen("ip", f"-brief link show dev {arg}")
return utils.search(r"^\S+\s+\S+\s+" + consts.MAC_RE_COLON, output)
Registering it¶
Add the class to METHODS before the first lookup of its type (e.g. "iface"). The order of METHODS is the order methods are tested in, so insert it at the front to make it the primary method:
from getmac import get_mac_address
from getmac.getmac import METHODS
from my_methods import FixedMacIface
METHODS.insert(0, FixedMacIface) # Before the first "iface" lookup
print(get_mac_address(interface="eth0"))
Output:
02:00:00:00:00:01
If you append it instead, any built-in method of the same type that passes its test comes first, and yours becomes a fallback.
Change the list in place, as above, or replace the module attribute (getmac.getmac.METHODS = [...]).
If lookups already happened¶
Adding a method doesn’t affect a type whose cache is already built. To make getmac choose again, set that type’s entry in METHOD_CACHE to None and empty its FALLBACK_CACHE list. The next lookup of that type tests METHODS again.
from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS
from my_methods import FixedMacIface
print(get_mac_address(interface="lo")) # Builds the "iface" cache
METHODS.insert(0, FixedMacIface)
print(get_mac_address(interface="lo")) # Still uses the method chosen earlier
METHOD_CACHE["iface"] = None # Forget the "iface" selection
FALLBACK_CACHE["iface"] = []
print(get_mac_address(interface="lo")) # METHODS is tested again
Example output on Linux:
00:00:00:00:00:00
00:00:00:00:00:00
02:00:00:00:00:01
To start over completely, reset every type. Also clear gvars.DEFAULT_IFACE, where get_mac_address() remembers the default interface after the first call without arguments. Without that, a new default_iface method is never used by get_mac_address().
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE
from getmac.variables import gvars
def reset_method_caches() -> None:
"""Forget every method getmac has chosen, and the cached default interface."""
for method_type in METHOD_CACHE:
METHOD_CACHE[method_type] = None
FALLBACK_CACHE[method_type] = []
gvars.DEFAULT_IFACE = ""
check_command() also caches whether each command exists, in gvars.CHECK_COMMAND_CACHE. Clear that dict as well if commands were installed or removed since the first lookup.
To undo these changes in tests, see Undoing changes in tests.
Using a method without registering it¶
You can also put an instance straight into METHOD_CACHE. This skips METHODS, test() and the platform check. Fallbacks that are already cached are kept, but if that type’s cache hadn’t been built yet, there are none.
from getmac import get_mac_address
from getmac.getmac import METHOD_CACHE
from my_methods import FixedMacIface
METHOD_CACHE["iface"] = FixedMacIface()
print(get_mac_address(interface="eth0")) # 02:00:00:00:00:01
The instance stays the primary method until it fails and getmac falls back.
Forcing a custom method¶
FORCE_METHOD only finds methods that are in METHODS, so register the method first. Where it is in the list doesn’t matter.
from getmac import get_mac_address, settings
from getmac.getmac import METHODS
from my_methods import FixedMacIface
METHODS.append(FixedMacIface)
settings.FORCE_METHOD = "FixedMacIface"
print(get_mac_address(interface="eth0")) # 02:00:00:00:00:01
Removing a built-in method¶
To stop getmac from using a built-in method, for example one that misbehaves on your system, remove it from METHODS. As with adding a method, reset the type’s cache if a lookup of that type already happened.
from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS, SysIfaceFile
METHODS.remove(SysIfaceFile)
METHOD_CACHE["iface"] = None # Only needed if an "iface" lookup already happened
FALLBACK_CACHE["iface"] = []
get_mac_address(interface="lo")
print(METHOD_CACHE["iface"], [str(m) for m in FALLBACK_CACHE["iface"]])
Example output on Linux:
FcntlIface ['IpLinkIface']
Undoing changes in tests¶
Changes to METHODS, METHOD_CACHE, FALLBACK_CACHE and settings affect the whole process, so tests that make them should undo them afterwards. With pytest, the built-in monkeypatch fixture can give each test its own copy of the list and caches, and puts the originals back when the test ends:
import getmac.getmac as gm
import pytest
from getmac import get_mac_address
from getmac.variables import gvars
from my_methods import FixedMacIface
@pytest.fixture
def isolated_getmac(monkeypatch):
"""Give the test its own METHODS list and empty caches."""
monkeypatch.setattr(gm, "METHODS", [FixedMacIface, *gm.METHODS])
monkeypatch.setattr(gm, "METHOD_CACHE", dict.fromkeys(gm.METHOD_CACHE))
monkeypatch.setattr(gm, "FALLBACK_CACHE", {key: [] for key in gm.FALLBACK_CACHE})
monkeypatch.setattr(gvars, "DEFAULT_IFACE", "")
def test_fixed_mac_is_used(isolated_getmac):
assert get_mac_address(interface="eth0") == "02:00:00:00:00:01"
Use monkeypatch.setattr(settings, "FORCE_METHOD", ...) in the same way to change a setting for one test.