Skip to content

Commit 7c663bc

Browse files
authored
Add dda.utils.platform.get_machine_id function (#71)
1 parent 97a61ba commit 7c663bc

File tree

6 files changed

+196
-38
lines changed

6 files changed

+196
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1212

1313
- The paths used to search for local commands are no longer added to the Python search path and instead a sibling directory `pythonpath` is used
1414

15+
***Added:***
16+
17+
- Add `dda.utils.platform.get_machine_id` function
18+
1519
***Fixed:***
1620

1721
- Properly persist Python search path modifications for local commands when using subprocesses

docs/reference/api/platform.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Platform reference
2+
3+
-----
4+
5+
::: dda.utils.platform
6+
options:
7+
show_if_no_docstring: false
8+
show_root_heading: false
9+
show_root_toc_entry: false

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ nav:
6767
- Command: reference/api/command.md
6868
- Application: reference/api/app.md
6969
- Process: reference/api/process.md
70-
- Tools: reference/api/tools.md
7170
- Filesystem: reference/api/fs.md
71+
- Platform: reference/api/platform.md
7272
- Retries: reference/api/retry.md
73+
- Tools: reference/api/tools.md
7374
- CI: reference/api/ci.md
7475
- Config: reference/api/config.md
7576
- Constants: reference/api/constants.md

src/dda/utils/platform.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/dda/utils/platform/__init__.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# SPDX-FileCopyrightText: 2024-present Datadog, Inc. <dev@datadoghq.com>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
from __future__ import annotations
5+
6+
import contextlib
7+
import os
8+
import sys
9+
from functools import cache
10+
from typing import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from uuid import UUID
14+
15+
16+
if sys.platform == "win32":
17+
__PLATFORM_ID = "windows"
18+
__PLATFORM_NAME = "Windows"
19+
__DEFAULT_SHELL = os.environ.get("SHELL", os.environ.get("COMSPEC", "cmd"))
20+
21+
def __join_command_args(args: list[str]) -> str:
22+
import subprocess
23+
24+
return subprocess.list2cmdline(args)
25+
26+
def __get_machine_id() -> UUID | None:
27+
import winreg
28+
from uuid import UUID
29+
30+
with winreg.OpenKey(
31+
winreg.HKEY_LOCAL_MACHINE,
32+
r"SOFTWARE\Microsoft\Cryptography",
33+
0,
34+
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
35+
) as key:
36+
value, _ = winreg.QueryValueEx(key, "MachineGuid")
37+
return UUID(value)
38+
39+
40+
elif sys.platform == "darwin":
41+
__PLATFORM_ID = "macos"
42+
__PLATFORM_NAME = "macOS"
43+
__DEFAULT_SHELL = os.environ.get("SHELL", "zsh")
44+
45+
def __join_command_args(args: list[str]) -> str:
46+
import shlex
47+
48+
return shlex.join(args)
49+
50+
def __get_machine_id() -> UUID | None:
51+
import re
52+
import subprocess
53+
from uuid import UUID
54+
55+
process = subprocess.run(
56+
["ioreg", "-c", "IOPlatformExpertDevice", "-d2"], # noqa: S607
57+
stdout=subprocess.PIPE,
58+
stderr=subprocess.STDOUT,
59+
check=True,
60+
)
61+
match = re.search(rb'"IOPlatformUUID"\s*=\s*"([^"]+)"', process.stdout)
62+
return UUID(match.group(1).decode("utf-8")) if match else None
63+
64+
else:
65+
__PLATFORM_ID = "linux"
66+
__PLATFORM_NAME = "Linux"
67+
__DEFAULT_SHELL = os.environ.get("SHELL", "bash")
68+
69+
def __join_command_args(args: list[str]) -> str:
70+
import shlex
71+
72+
return shlex.join(args)
73+
74+
def __get_machine_id() -> UUID | None:
75+
from uuid import UUID
76+
77+
for path in (
78+
# https://utcc.utoronto.ca/~cks/space/blog/linux/DMIDataInSysfs
79+
# Section 7.2.1 of https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.0.0.pdf
80+
# https://github.com/torvalds/linux/blob/v6.14/drivers/firmware/dmi-id.c#L50
81+
# https://cloud.google.com/compute/docs/instances/get-uuid#linux
82+
"/sys/class/dmi/id/product_uuid",
83+
# https://www.freedesktop.org/software/systemd/man/latest/machine-id.html
84+
"/etc/machine-id",
85+
# https://wiki.debian.org/MachineId
86+
"/var/lib/dbus/machine-id",
87+
):
88+
with contextlib.suppress(Exception), open(path, encoding="utf-8") as f:
89+
return UUID(f.read().strip())
90+
91+
return None
92+
93+
94+
PLATFORM_ID = __PLATFORM_ID
95+
"""
96+
A short identifier for the current platform. Known values:
97+
98+
- `linux`
99+
- `windows`
100+
- `macos`
101+
"""
102+
PLATFORM_NAME = __PLATFORM_NAME
103+
"""
104+
The human readable name of the current platform. Known values:
105+
106+
- Linux
107+
- Windows
108+
- macOS
109+
"""
110+
DEFAULT_SHELL = __DEFAULT_SHELL
111+
"""
112+
The default shell for the current platform. Values are taken from environment variables, with
113+
platform-specific fallbacks.
114+
115+
Platform | Environment variables | Fallback
116+
--- | --- | ---
117+
`linux` | `SHELL` | `bash`
118+
`windows` | `SHELL`, `COMSPEC` | `cmd`
119+
`macos` | `SHELL` | `zsh`
120+
"""
121+
122+
123+
def join_command_args(args: list[str]) -> str:
124+
"""
125+
Create a valid shell command from a list of arguments.
126+
127+
Parameters:
128+
args: A list of command line arguments.
129+
130+
Returns:
131+
A single string of command line arguments.
132+
"""
133+
return __join_command_args(args)
134+
135+
136+
@cache
137+
def get_machine_id() -> UUID:
138+
"""
139+
Get a unique identifier for the current machine that is consistent across reboots and different
140+
processes. The following platform-specific methods are given priority:
141+
142+
Platform | Method
143+
--- | ---
144+
`linux` | The [`/sys/class/dmi/id/product_uuid`](https://utcc.utoronto.ca/~cks/space/blog/linux/DMIDataInSysfs), [`/etc/machine-id`](https://www.freedesktop.org/software/systemd/man/latest/machine-id.html) or [`/var/lib/dbus/machine-id`](https://wiki.debian.org/MachineId) files
145+
`windows` | The `HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid` registry key
146+
`macos` | The [`IOPlatformUUID`](https://developer.apple.com/documentation/iokit/kioplatformuuidkey/) property of the [`IOPlatformExpertDevice`](https://developer.apple.com/library/archive/technotes/tn1103/_index.html) node in the [I/O Registry](https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/TheRegistry/TheRegistry.html#//apple_ref/doc/uid/TP0000014-TP9)
147+
148+
As a fallback, the ID will be generated using the MAC address.
149+
"""
150+
machine_id: UUID | None = None
151+
with contextlib.suppress(Exception):
152+
machine_id = __get_machine_id()
153+
154+
if machine_id is not None:
155+
return machine_id
156+
157+
import uuid
158+
159+
return uuid.uuid5(uuid.NAMESPACE_DNS, str(uuid.getnode()))

tests/utils/test_platform.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,25 @@ def test_name(self):
4747
def test_default_shell(self):
4848
expected_shell = os.environ.get("SHELL", "bash")
4949
assert platform.DEFAULT_SHELL == expected_shell # noqa: SIM300
50+
51+
52+
class TestMachineId:
53+
def test_platform_specific(self):
54+
machine_id = str(platform.get_machine_id())
55+
assert str(platform.get_machine_id()) == machine_id
56+
57+
parts = machine_id.split("-")
58+
lengths = list(map(len, parts))
59+
assert lengths == [8, 4, 4, 4, 12]
60+
61+
def test_fallback(self, mocker):
62+
mocker.patch("dda.utils.platform.__get_machine_id", return_value=None)
63+
64+
platform.get_machine_id.cache_clear()
65+
machine_id = str(platform.get_machine_id())
66+
platform.get_machine_id.cache_clear()
67+
assert str(platform.get_machine_id()) == machine_id
68+
69+
parts = machine_id.split("-")
70+
lengths = list(map(len, parts))
71+
assert lengths == [8, 4, 4, 4, 12]

0 commit comments

Comments
 (0)