1"""
2Interface to SMBIOS/DMI
3
4(Parsing through dmidecode)
5
6External References
7-------------------
8| `Desktop Management Interface (DMI) <http://www.dmtf.org/standards/dmi>`_
9| `System Management BIOS <http://www.dmtf.org/standards/smbios>`_
10| `DMIdecode <http://www.nongnu.org/dmidecode/>`_
11
12"""
13
14import logging
15import re
16import uuid
17
18# Solve the Chicken and egg problem where grains need to run before any
19# of the modules are loaded and are generally available for any usage.
20import salt.modules.cmdmod
21import salt.utils.path
22
23log = logging.getLogger(__name__)
24
25
26def __virtual__():
27    """
28    Only work when dmidecode is installed.
29    """
30    return (
31        bool(salt.utils.path.which_bin(["dmidecode", "smbios"])),
32        "The smbios execution module failed to load: neither dmidecode nor smbios in"
33        " the path.",
34    )
35
36
37def get(string, clean=True):
38    """
39    Get an individual DMI string from SMBIOS info
40
41    string
42        The string to fetch. DMIdecode supports:
43          - ``bios-vendor``
44          - ``bios-version``
45          - ``bios-release-date``
46          - ``system-manufacturer``
47          - ``system-product-name``
48          - ``system-version``
49          - ``system-serial-number``
50          - ``system-uuid``
51          - ``baseboard-manufacturer``
52          - ``baseboard-product-name``
53          - ``baseboard-version``
54          - ``baseboard-serial-number``
55          - ``baseboard-asset-tag``
56          - ``chassis-manufacturer``
57          - ``chassis-type``
58          - ``chassis-version``
59          - ``chassis-serial-number``
60          - ``chassis-asset-tag``
61          - ``processor-family``
62          - ``processor-manufacturer``
63          - ``processor-version``
64          - ``processor-frequency``
65
66    clean
67      | Don't return well-known false information
68      | (invalid UUID's, serial 000000000's, etcetera)
69      | Defaults to ``True``
70
71    CLI Example:
72
73    .. code-block:: bash
74
75        salt '*' smbios.get system-uuid clean=False
76    """
77
78    val = _dmidecoder("-s {}".format(string)).strip()
79
80    # Cleanup possible comments in strings.
81    val = "\n".join([v for v in val.split("\n") if not v.startswith("#")])
82    if val.startswith("/dev/mem") or clean and not _dmi_isclean(string, val):
83        val = None
84
85    return val
86
87
88def records(rec_type=None, fields=None, clean=True):
89    """
90    Return DMI records from SMBIOS
91
92    type
93        Return only records of type(s)
94        The SMBIOS specification defines the following DMI types:
95
96        ====  ======================================
97        Type  Information
98        ====  ======================================
99         0    BIOS
100         1    System
101         2    Baseboard
102         3    Chassis
103         4    Processor
104         5    Memory Controller
105         6    Memory Module
106         7    Cache
107         8    Port Connector
108         9    System Slots
109        10    On Board Devices
110        11    OEM Strings
111        12    System Configuration Options
112        13    BIOS Language
113        14    Group Associations
114        15    System Event Log
115        16    Physical Memory Array
116        17    Memory Device
117        18    32-bit Memory Error
118        19    Memory Array Mapped Address
119        20    Memory Device Mapped Address
120        21    Built-in Pointing Device
121        22    Portable Battery
122        23    System Reset
123        24    Hardware Security
124        25    System Power Controls
125        26    Voltage Probe
126        27    Cooling Device
127        28    Temperature Probe
128        29    Electrical Current Probe
129        30    Out-of-band Remote Access
130        31    Boot Integrity Services
131        32    System Boot
132        33    64-bit Memory Error
133        34    Management Device
134        35    Management Device Component
135        36    Management Device Threshold Data
136        37    Memory Channel
137        38    IPMI Device
138        39    Power Supply
139        40    Additional Information
140        41    Onboard Devices Extended Information
141        42    Management Controller Host Interface
142        ====  ======================================
143
144    clean
145      | Don't return well-known false information
146      | (invalid UUID's, serial 000000000's, etcetera)
147      | Defaults to ``True``
148
149    CLI Example:
150
151    .. code-block:: bash
152
153        salt '*' smbios.records clean=False
154        salt '*' smbios.records 14
155        salt '*' smbios.records 4 core_count,thread_count,current_speed
156
157    """
158    if rec_type is None:
159        smbios = _dmi_parse(_dmidecoder(), clean, fields)
160    else:
161        smbios = _dmi_parse(_dmidecoder("-t {}".format(rec_type)), clean, fields)
162
163    return smbios
164
165
166def _dmi_parse(data, clean=True, fields=None):
167    """
168    Structurize DMI records into a nice list
169    Optionally trash bogus entries and filter output
170    """
171    dmi = []
172
173    # Detect & split Handle records
174    dmi_split = re.compile(
175        "(handle [0-9]x[0-9a-f]+[^\n]+)\n", re.MULTILINE + re.IGNORECASE
176    )
177    dmi_raw = iter(re.split(dmi_split, data)[1:])
178    for handle, dmi_raw in zip(dmi_raw, dmi_raw):
179        handle, htype = [hline.split()[-1] for hline in handle.split(",")][0:2]
180        dmi_raw = dmi_raw.split("\n")
181        # log.debug('%s record contains %s', handle, dmi_raw)
182        log.debug("Parsing handle %s", handle)
183
184        # The first line of a handle is a description of the type
185        record = {
186            "handle": handle,
187            "description": dmi_raw.pop(0).strip(),
188            "type": int(htype),
189        }
190
191        if not dmi_raw:
192            # empty record
193            if not clean:
194                dmi.append(record)
195            continue
196
197        # log.debug('%s record contains %s', record, dmi_raw)
198        dmi_data = _dmi_data(dmi_raw, clean, fields)
199        if dmi_data:
200            record["data"] = dmi_data
201            dmi.append(record)
202        elif not clean:
203            dmi.append(record)
204
205    return dmi
206
207
208def _dmi_data(dmi_raw, clean, fields):
209    """
210    Parse the raw DMIdecode output of a single handle
211    into a nice dict
212    """
213    dmi_data = {}
214
215    key = None
216    key_data = [None, []]
217    for line in dmi_raw:
218        if re.match(r"\t[^\s]+", line):
219            # Finish previous key
220            if key is not None:
221                # log.debug('Evaluating DMI key {0}: {1}'.format(key, key_data))
222                value, vlist = key_data
223                if vlist:
224                    if value is not None:
225                        # On the rare occasion
226                        # (I counted 1 on all systems we have)
227                        # that there's both a value <and> a list
228                        # just insert the value on top of the list
229                        vlist.insert(0, value)
230                    dmi_data[key] = vlist
231                elif value is not None:
232                    dmi_data[key] = value
233
234            # Family: Core i5
235            # Keyboard Password Status: Not Implemented
236            key, val = line.split(":", 1)
237            key = key.strip().lower().replace(" ", "_")
238            if (clean and key == "header_and_data") or (fields and key not in fields):
239                key = None
240                continue
241            else:
242                key_data = [_dmi_cast(key, val.strip(), clean), []]
243        elif key is None:
244            continue
245        elif re.match(r"\t\t[^\s]+", line):
246            # Installable Languages: 1
247            #        en-US
248            # Characteristics:
249            #        PCI is supported
250            #        PNP is supported
251            val = _dmi_cast(key, line.strip(), clean)
252            if val is not None:
253                # log.debug('DMI key %s gained list item %s', key, val)
254                key_data[1].append(val)
255
256    return dmi_data
257
258
259def _dmi_cast(key, val, clean=True):
260    """
261    Simple caster thingy for trying to fish out at least ints & lists from strings
262    """
263    if clean and not _dmi_isclean(key, val):
264        return
265    elif not re.match(r"serial|part|asset|product", key, flags=re.IGNORECASE):
266        if "," in val:
267            val = [el.strip() for el in val.split(",")]
268        else:
269            try:
270                val = int(val)
271            except Exception:  # pylint: disable=broad-except
272                pass
273
274    return val
275
276
277def _dmi_isclean(key, val):
278    """
279    Clean out well-known bogus values
280    """
281    if val is None or not val or re.match("none", val, flags=re.IGNORECASE):
282        # log.debug('DMI {0} value {1} seems invalid or empty'.format(key, val))
283        return False
284    elif "uuid" in key:
285        # Try each version (1-5) of RFC4122 to check if it's actually a UUID
286        for uuidver in range(1, 5):
287            try:
288                uuid.UUID(val, version=uuidver)
289                return True
290            except ValueError:
291                continue
292        log.trace("DMI %s value %s is an invalid UUID", key, val.replace("\n", " "))
293        return False
294    elif re.search("serial|part|version", key):
295        # 'To be filled by O.E.M.
296        # 'Not applicable' etc.
297        # 'Not specified' etc.
298        # 0000000, 1234667 etc.
299        # begone!
300        return (
301            not re.match(r"^[0]+$", val)
302            and not re.match(r"[0]?1234567[8]?[9]?[0]?", val)
303            and not re.search(
304                r"sernum|part[_-]?number|specified|filled|applicable",
305                val,
306                flags=re.IGNORECASE,
307            )
308        )
309    elif re.search("asset|manufacturer", key):
310        # AssetTag0. Manufacturer04. Begone.
311        return not re.search(
312            r"manufacturer|to be filled|available|asset|^no(ne|t)",
313            val,
314            flags=re.IGNORECASE,
315        )
316    else:
317        # map unspecified, undefined, unknown & whatever to None
318        return not re.search(
319            r"to be filled", val, flags=re.IGNORECASE
320        ) and not re.search(
321            r"un(known|specified)|no(t|ne)?"
322            r" (asset|provided|defined|available|present|specified)",
323            val,
324            flags=re.IGNORECASE,
325        )
326
327
328def _dmidecoder(args=None):
329    """
330    Call DMIdecode
331    """
332    dmidecoder = salt.utils.path.which_bin(["dmidecode", "smbios"])
333
334    if not args:
335        out = salt.modules.cmdmod._run_quiet(dmidecoder)
336    else:
337        out = salt.modules.cmdmod._run_quiet("{} {}".format(dmidecoder, args))
338
339    return out
340