1#!/usr/bin/env python3

2#

3# This file is part of Checkbox.

4#

5# Copyright 2014 Canonical Ltd.

6#

7# Checkbox is free software: you can redistribute it and/or modify

8# it under the terms of the GNU General Public License version 3,

9# as published by the Free Software Foundation.

10#

11# Checkbox is distributed in the hope that it will be useful,

12# but WITHOUT ANY WARRANTY; without even the implied warranty of

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

14# GNU General Public License for more details.

15#

16# You should have received a copy of the GNU General Public License

17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.

18#

19# Test for sane dmidecode output, particularly with respect to

20# various manufacturer information fields. Also, verify that the

21# system reports a chassis type that suits its class (server or

22# desktop/laptop)

23#

24# By: Rod Smith

25

26"""Script to test dmidecode output for sanity.

27

28:param --dmifile:

29 Input filename; optional. If specified, file is used instead of

30 dmidecode output.

31:param --show_dmi:

32 Print the DMI data used. For debugging purposes if errors are encountered.

33:param --test_versions:

34 Include chassis, system, and base boad version numbers among tests.

35:param --test_serials:

36 Include system and base board serial numbers among tests.

37:param cpu_check:

38 Don't perform usual tests, except for CPU test.

39:param desktop:

40 SUT is a desktop or laptop

41:param server:

42 SUT is a server

43"""

44

45 import re

46 import subprocess

47 import sys

48

49 from argparse import ArgumentParser

50

51

52 def find_in_section(stream, dmi_data, section, label, strings, find_empty):

53 """Search for a set of strings on a line in the output.

54

55 :param stream:

56 input text stream (dmidecode output)

57 :param section:

58 section label in which to search (e.g., "Chassis Information")

59 :param label:

60 label of line on which to search (e.g., "Type:")

61 :param strings:

62 set of strings for which to search (e.g., ["server", "blade"])

63 :param find_empty:

64 if True, matches empty label field (as if '""' were passed as

65 a strings value)

66 :returns found:

67 True if one or more of strings was found on "label" line in "section"

68 section, or if "label" line is empty AND find_empty is True;

69 False otherwise

70 """

71 start_looking = False

72 found = False

73 empty = True

74 for line in stream:

75 if line == section:

76 start_looking = True

77 if start_looking and re.search(label, line):

78 line_items = line.strip().split(":")

79 dmi_data[section][line_items[0]] = line_items[1]

80 empty = len(line.strip()) == len(label)

81 if empty and find_empty:

82 found = True

83 for s in strings:

84 if re.search(s, line, flags=re.IGNORECASE):

85 found = True

86 break

87 start_looking = False

88

89 return found

90

91

92 def standard_tests(args, stream, dmi_data):

93 """

94 Perform the standard set of tests.

95

96 :param args:

97 Arguments passed to script

98 :param stream:

99 Input stream containing dmidecode output

100 :returns retval:

101 Number of problems found

102 """

103 retval = 0

104 """

105 NOTE: System type is encoded in both the "Chassis Information" and "Base

106 Board Type" sections. The former is more reliable, so we do a whitelist

107 test on it -- the type MUST be specified correctly. The "Base Board Type"

108 section is less reliable, so rather than flag large numbers of systems

109 for having "Unknown", "Other", or something similar here, we just flag

110 it when it's at odds with the type passed on the command line. Also,

111 the "Base Board Type" may specify a desktop or tower system on servers

112 shipped in those form factors, so we don't flag that combination as an

113 error.

114 """

115 if args.test_type == "server":

116 if not find_in_section(

117 stream,

118 dmi_data,

119 "Chassis Information",

120 "Type:",

121 [

122 "server",

123 "rack mount",

124 "blade",

125 "other",

126 "expansion chassis",

127 "multi-system",

128 "tower",

129 ],

130 False,

131 ):

132 dmi_data["Chassis Information"][

133 "Type"

134 ] += " *** Incorrect or unknown server chassis type!"

135 retval += 1

136 if find_in_section(

137 stream,

138 dmi_data,

139 "Base Board Information",

140 "Type:",

141 ["portable", "notebook", "space-saving", "all in one"],

142 False,

143 ):

144 dmi_data["Base Board Information"][

145 "Type"

146 ] += " *** Incorrect server base board type!"

147 retval += 1

148 else:

149 if not find_in_section(

150 stream,

151 dmi_data,

152 "Chassis Information",

153 "Type:",

154 [

155 "notebook",

156 "portable",

157 "laptop",

158 "desktop",

159 "lunch box",

160 "space-saving",

161 "tower",

162 "all in one",

163 "hand held",

164 "convertible",

165 ],

166 False,

167 ):

168 dmi_data["Chassis Information"][

169 "Type"

170 ] += " *** Incorrect or unknown desktop chassis type!"

171 retval += 1

172 if find_in_section(

173 stream,

174 dmi_data,

175 "Base Board Information",

176 "Type:",

177 ["rack mount", "server", "multi-system", "interconnect board"],

178 False,

179 ):

180 dmi_data["Base Board Information"][

181 "Type"

182 ] += " *** Incorrect desktop base board type!"

183 retval += 1

184 if find_in_section(

185 stream,

186 dmi_data,

187 "Chassis Information",

188 "Manufacturer:",

189 [

190 "empty",

191 "chassis manufacture",

192 "null",

193 "insyde",

194 r"to be filled by o\.e\.m\.",

195 "no enclosure",

196 r"\.\.\.\.\.",

197 ],

198 True,

199 ):

200 dmi_data["Chassis Information"][

201 "Manufacturer"

202 ] += " *** Invalid chassis manufacturer!"

203 retval += 1

204 if find_in_section(

205 stream,

206 dmi_data,

207 "System Information",

208 "Manufacturer:",

209 [

210 "system manufacture",

211 "insyde",

212 "standard",

213 r"to be filled by o\.e\.m\.",

214 "no enclosure",

215 ],

216 True,

217 ):

218 dmi_data["System Information"][

219 "Manufacturer"

220 ] += " *** Invalid system manufacturer!"

221 retval += 1

222 if find_in_section(

223 stream,

224 dmi_data,

225 "Base Board Information",

226 "Manufacturer:",

227 [r"to be filled by o\.e\.m\."],

228 True,

229 ):

230 dmi_data["Base Board Information"][

231 "Manufacturer"

232 ] += " *** Invalid base board manufacturer!"

233 retval += 1

234 if find_in_section(

235 stream,

236 dmi_data,

237 "System Information",

238 "Product Name:",

239 ["system product name", r"to be filled by o\.e\.m\."],

240 False,

241 ):

242 dmi_data["System Information"][

243 "Product Name"

244 ] += " *** Invalid system product name!"

245 retval += 1

246 if find_in_section(

247 stream,

248 dmi_data,

249 "Base Board Information",

250 "Product Name:",

251 ["base board product name", r"to be filled by o\.e\.m\."],

252 False,

253 ):

254 dmi_data["Base Board Information"][

255 "Product Name"

256 ] += " *** Invalid base board product name!"

257 retval += 1

258 return retval

259

260

261 def version_tests(args, stream, dmi_data):

262 """

263 Perform the version tests.

264

265 :param args:

266 Arguments passed to script

267 :param stream:

268 Input stream containing dmidecode output

269 :returns retval:

270 Number of problems found

271 """

272 retval = 0

273 if find_in_section(

274 stream,

275 dmi_data,

276 "Chassis Information",

277 "Version:",

278 [r"to be filled by o\.e\.m\.", "empty", r"x\.x"],

279 False,

280 ):

281 dmi_data["Chassis Information"][

282 "Version"

283 ] += " *** Invalid chassis version!"

284 retval += 1

285 if find_in_section(

286 stream,

287 dmi_data,

288 "System Information",

289 "Version:",

290 [

291 r"to be filled by o\.e\.m\.",

292 r"\(none\)",

293 "null",

294 "system version",

295 "not applicable",

296 r"\.\.\.\.\.",

297 ],

298 False,

299 ):

300 dmi_data["System Information"][

301 "Version"

302 ] += " *** Invalid system information version!"

303 retval += 1

304 if find_in_section(

305 stream,

306 dmi_data,

307 "Base Board Information",

308 "Version:",

309 ["base board version", r"x\.x", "empty", r"to be filled by o\.e\.m\."],

310 False,

311 ):

312 dmi_data["Base Board Information"][

313 "Version"

314 ] += " *** Invalid base board version!"

315 retval += 1

316 return retval

317

318

319 def serial_tests(args, stream, dmi_data):

320 """

321 Perform the serial number tests.

322

323 :param args:

324 Arguments passed to script

325 :param stream:

326 Input stream containing dmidecode output

327 :returns retval:

328 Number of problems found

329 """

330 retval = 0

331 if find_in_section(

332 stream,

333 dmi_data,

334 "System Information",

335 "Serial Number:",

336 [r"to be filled by o\.e\.m\.", "system serial number", r"\.\.\.\.\."],

337 False,

338 ):

339 dmi_data["System Information"][

340 "Serial Number"

341 ] += " *** Invalid system information serial number!"

342 retval += 1

343 if find_in_section(

344 stream,

345 dmi_data,

346 "Base Board Information",

347 "Serial Number:",

348 [

349 "n/a",

350 "base board serial number",

351 r"to be filled by o\.e\.m\.",

352 "empty",

353 r"\.\.\.",

354 ],

355 False,

356 ):

357 dmi_data["Base Board Information"][

358 "Serial Number"

359 ] += " *** Invalid base board serial number!"

360 retval += 1

361 return retval

362

363

364 def main():

365 """Main function."""

366 parser = ArgumentParser()

367 parser.add_argument(

368 "test_type",

369 help="Test type ('server', 'desktop' or 'cpu-check').",

370 choices=["server", "desktop", "cpu-check"],

371 )

372 parser.add_argument("--dmifile", help="File to use in lieu of dmidecode.")

373 parser.add_argument(

374 "--show_dmi",

375 action="store_true",

376 help="Print DMI Data used for debugging purposes.",

377 )

378 parser.add_argument(

379 "--test_versions",

380 action="store_true",

381 help="Set to check version information",

382 )

383 parser.add_argument(

384 "--test_serials",

385 action="store_true",

386 help="Set to check serial number information",

387 )

388 args = parser.parse_args()

389

390 bad_data = False

391 dmi_data = {

392 "System Information": {},

393 "Base Board Information": {},

394 "Chassis Information": {},

395 "Processor Information": {},

396 }

397

398 # Command to retrieve DMI information

399 COMMAND = "dmidecode"

400 if args.dmifile:

401 COMMAND = ["cat", args.dmifile]

402 print("Reading " + args.dmifile + " as DMI data")

403 try:

404 dmi_out = subprocess.check_output(COMMAND).splitlines()

405 except subprocess.CalledProcessError as err:

406 print("Error running {}: {}".format(COMMAND, err))

407 return 1

408

409 # Convert the bytes output separately, line by line, because it's possible

410 # that someone put non-encodable characters in DMI, which cases a

411 # UnicodeDecodeError that is non-helpful. LP: 1655155

412 stream = []

413 for line in dmi_out:

414 try:

415 stream.append(line.decode("utf-8"))

416 except UnicodeDecodeError as ude:

417 print("DATA ERROR: {}".format(ude))

418 print("\tLINE NUMBER {}: {}".format(dmi_out.index(line) + 1, line))

419 stream.append("ERROR: BAD DATA FOUND HERE")

420 bad_data = True

421

422 if args.show_dmi:

423 print("===== DMI Data Used: =====")

424 for line in stream:

425 print(line)

426 print("===== DMI Output Complete =====")

427

428 retval = 0

429 if args.test_type == "server" or args.test_type == "desktop":

430 retval += standard_tests(args, stream, dmi_data)

431 if args.test_versions:

432 retval += version_tests(args, stream, dmi_data)

433 if args.test_serials:

434 retval += serial_tests(args, stream, dmi_data)

435 if find_in_section(

436 stream,

437 dmi_data,

438 "Processor Information",

439 "Version:",

440 ["sample", r"Genuine Intel\(R\) CPU 0000"],

441 False,

442 ):

443 dmi_data["Processor Information"][

444 "Version"

445 ] += " *** Invalid processor information!"

446 retval += 1

447

448 # In review of dmidecode data on 10/23/2014, no conspicuous problems

449 # found in BIOS Information section's Vendor, Version, or Release Date

450 # fields. Therefore, no tests based on these fields have been written.

451

452 for section in sorted(dmi_data.keys()):

453 print("{}:".format(section))

454 for item in sorted(dmi_data[section].keys()):

455 print("\t{}: {}".format(item, dmi_data[section][item]))

456 print("\n")

457

458 if retval > 0:

459 if retval == 1:

460 print("\nFailed 1 test (see above)")

461 else:

462 print("\nFailed {0} tests (see above)".format(retval))

463 else:

464 print("\nPassed all tests")

465

466 if bad_data:

467 print(

468 "\nBad Characters discovered in DMI output. Rerun with "

469 "the --show_dmi option to see more"

470 )

471

472 return retval

473

474

475 if __name__ == "__main__":

476 sys.exit(main())