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