1#!/usr/bin/env python3

2# encoding: utf-8

3# Copyright 2015 Canonical Ltd.

4# Written by:

5# Shawn Wang <shawn.wang@canonical.com>

6# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

7# Jonathan Cave <jonathan.cave@canonical.com>

8#

9# This program is free software: you can redistribute it and/or modify

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

11# as published by the Free Software Foundation.

12#

13# This program is distributed in the hope that it will be useful,

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

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

16# GNU General Public License for more details.

17#

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

19# along with this program. If not, see <http://www.gnu.org/licenses/>.

20

21"""

22 dkms_info provides device package information.

23

24 supported package types:

25 - dkms (Dynamic Kernel Module Support): provides kernel modules

26 - non-dkms: packages that with modaliases header and don't exist in dkms list

27 - hardware modalias: might be unused dkms or config package

28 - oemalias: It is like non-dkms(w/o modalias)

29 supported output format:

30 - onelines: one line per packages with matched modaliases information

31 - dumps: json output (fully information)

32"""

33

34 import argparse

35 import fnmatch

36 import functools

37 import email.parser

38 import io

39 import json

40 import logging

41 import os

42 import subprocess

43 import sys

44

45

46_logger = logging.getLogger(None)

47

48

49@functools.lru_cache()

50 def get_system_module_list():

51 """

52 Use lsmod to list current kernel modules.

53

54 :returns:

55 A list of module names that are loaded into the current kernel.

56 """

57 _logger.info("Looking at inserted kernel modules")

58 modules = []

59 with io.open("/proc/modules", "rt", encoding="UTF-8") as stream:

60 for line in stream.readlines():

61 modules.append(line.split()[0].strip())

62 return modules

63

64

65@functools.lru_cache()

66 def get_system_modaliases():

67 r"""

68 List machine modaliases.

69

70 :returns:

71 dict of hardware modaliases.

72 key: modalias_type

73 value: list of modalias_string

74 """

75 _logger.info("Looking for modalias files in /sys/devices")

76

77 result = {}

78 name = "modalias"

79 for root, dirs, files in os.walk("/sys/devices/"):

80 if name in files: 80 ↛ 79 line 80 didn't jump to line 79 because the condition on line 80 was always true

81 with io.open(

82 os.path.join(root, name), "rt", encoding="UTF-8"

83 ) as stream:

84 data = stream.read().strip()

85 pattern_array = data.split(":", 1)

86 if len(pattern_array) < 2: 86 ↛ 87 line 86 didn't jump to line 87 because the condition on line 86 was never true

87 _logger.warning(

88 "skip pattern {}, not a valid modalias".format(data)

89 )

90 continue

91 (modalias_type, modalias_string) = pattern_array

92

93 if modalias_type not in result: 93 ↛ 95 line 93 didn't jump to line 95 because the condition on line 93 was always true

94 result[modalias_type] = []

95 if modalias_string not in result[modalias_type]: 95 ↛ 79 line 95 didn't jump to line 79

96 result[modalias_type].append(modalias_string)

97 return result

98

99

100 def get_installed_dkms_modules():

101 """

102 Query dkms_status from /var/lib/dkms/.

103

104 An installed dkms module has the below directory.

105 /var/lib/dkms/<dkms_name>/<dkms_ver>/<kernel_ver>

106

107 :returns:

108 list of (<dkms_name>, <dkms_ver>)

109 """

110 _dkmses = []

111 _logger.info("Querying dkms database")

112

113 path = "/var/lib/dkms/"

114 for root, dirs, files in os.walk(path):

115 if os.uname().release in dirs: 115 ↛ 114 line 115 didn't jump to line 114 because the condition on line 115 was always true

116 dkms = root[len(path) :].split("/")

117 if len(dkms) != 2: 117 ↛ 118 line 117 didn't jump to line 118 because the condition on line 117 was never true

118 continue

119 _dkmses.append(dkms)

120 return _dkmses

121

122

123@functools.lru_cache()

124 def match_patterns(patterns):

125 """

126 Check modalias patterns matched with modalias, or type is oemalias.

127

128 oemalias is a special pattern_type for oem.

129 :param patterns:

130 list of modalias pattern from debian package.

131 :returns:

132 list of modalias pattern matched with hardware modalias

133 """

134 _logger.info("Looking for modalias objects matching")

135 matched = []

136 if not patterns: 136 ↛ 137 line 136 didn't jump to line 137 because the condition on line 136 was never true

137 return matched

138 hw_modaliases = get_system_modaliases()

139 for pattern in patterns:

140 pattern_array = pattern.split(":", 1)

141 if len(pattern_array) < 2: 141 ↛ 142 line 141 didn't jump to line 142 because the condition on line 141 was never true

142 _logger.info("skip pattern {}, can't find type".format(pattern))

143 continue

144

145 (pattern_type, pattern_string) = pattern_array

146

147 if pattern_type == "oemalias":

148 matched.append(pattern)

149 if pattern_type not in hw_modaliases:

150 continue

151 for item in hw_modaliases[pattern_type]:

152 if fnmatch.fnmatch(item, pattern_string):

153 matched.append(pattern)

154 return matched

155

156

157 class DkmsPackage(object):

158 """

159 Handle DKMS type device package, DKMS is a kernel module framework.

160

161 It generate modules for installed kernel or different kernel versions.

162 The dkms modules will be copied to /lib/modulesa/`uname -r`/updates/dkms/.

163 Those modules might be load by modaliases information.

164 """

165

166 def __init__(self, name, version):

167 """

168 init of DkmsPackage, define all the attribute.

169

170 :param name:

171 DKMS module name

172 :param version:

173 DKMS module version

174 """

175 self.dkms_name = name

176 self.dkms_ver = version

177 self.pkg_name = self._query_package()

178 self.kernel_ver = os.uname().release

179 self.arch = os.uname().machine

180 self.mods = self._list_modules()

181 self.install_mods = self._list_install_modules()

182 self.pkg = None

183

184 def _query_package(self):

185 """

186 Query debian package of dkms.

187

188 Use dpkg -S to check dkms src path of debian package.

189

190 :return:

191 string of package name or None

192 """

193 path = "/usr/src/{}-{}/dkms.conf".format(self.dkms_name, self.dkms_ver)

194 _logger.info("Looking for packages that provide: %s", path)

195 dpkg_info_root = "/var/lib/dpkg/info"

196 for fn in os.listdir(dpkg_info_root):

197 if not fn.endswith(".list"):

198 continue

199 with io.open(

200 os.path.join(dpkg_info_root, fn), "rt", encoding="UTF-8"

201 ) as stream:

202 if path in stream.read():

203 return fn[: -len(".list")]

204 return None

205

206 def _list_modules(self):

207 """

208 List all the kernel modules that provide by the dkms package.

209

210 Module name (.ko) with "-" will be replace to "_" when module loaded.

211

212 :param path:

213 The directory to look at.

214 :return:

215 List of kernel modules

216 """

217 path = "/var/lib/dkms/{}/{}/{}/{}/module".format(

218 self.dkms_name, self.dkms_ver, self.kernel_ver, self.arch

219 )

220 _logger.info("Looking for kernel modules in %s", path)

221 result = []

222 for module_file in os.listdir(path):

223 (module, extension) = os.path.splitext(module_file)

224 if extension == ".ko":

225 result.append(module.replace("-", "_"))

226 return result

227

228 def _list_install_modules(self):

229 """

230 Return a dict of install_modules.

231

232 key is installed module name

233 value is tuple of matched patterns

234

235 :return:

236 Dict of installed dkms modules

237 """

238 install_mods = {}

239 for m in self.mods:

240 if m not in get_system_module_list():

241 continue

242 _logger.info("Inspecting module %s", m)

243

244 output = subprocess.check_output(

245 ["modinfo", m], universal_newlines=True

246 )

247 aliases = []

248 for line in output.splitlines():

249 if not line.startswith("alias:"):

250 continue

251

252 key, value = line.split(":", 1)

253 aliases.append(value.strip())

254

255 install_mods[m] = match_patterns(tuple(aliases))

256 return install_mods

257

258

259 def _headers_to_dist(pkg_str):

260 """

261 Convert rft822 headers string to dict.

262

263 :param headers:

264 deb822 headers object

265 :return:

266 dict, the key is lowercase of deb822 headers key

267 """

268

269 header = email.parser.Parser().parsestr(pkg_str)

270 target = {}

271 for key in header.keys():

272 target[key.lower()] = header[key]

273 return target

274

275

276 class DebianPackageHandler(object):

277 """Use rtf822(email) to handle the package information from file_object."""

278

279 def __init__(self, extra_pkgs=[], file_object=None):

280 """

281 DebianPackageHandler.

282

283 :param file_object:

284 default file open from /var/lib/dpkg/status,

285 where stored system package information

286 """

287 if file_object is None: 287 ↛ 288 line 287 didn't jump to line 288 because the condition on line 287 was never true

288 file_object = io.open(

289 "/var/lib/dpkg/status", "rt", encoding="UTF-8"

290 )

291 self._file_object = file_object

292 self.extra_pkgs = extra_pkgs

293 self.pkgs = self._get_device_pkgs()

294

295 def _gen_all_pkg_strs(self):

296 """

297 Get package information in /var/lib/dpkg/status.

298

299 :returns:

300 A generator of debian package.

301 """

302 _logger.info("Loading information about all packages")

303 for pkg_str in self._file_object.read().split("\n\n"):

304 yield pkg_str

305

306 def _get_device_pkgs(self):

307 """

308 Only device packages have debian package header 'Modaliases'.

309

310 This method get packages with the key ``modaliases``.

311 Use the method instead of get_all_pkgs for performance issues.

312

313 :returns:

314 A list of dict , the dict is converted from debian package header.

315 """

316

317 _logger.info("Looking for packages providing modaliases")

318 result = {}

319

320 for pkg_str in self._gen_all_pkg_strs():

321

322 for pkg in self.extra_pkgs: 322 ↛ 323 line 322 didn't jump to line 323 because the loop on line 322 never started

323 if pkg.pkg_name is None:

324 continue

325 pstr = "Package: {}".format(pkg.pkg_name)

326 if pstr in pkg_str:

327 _logger.info(

328 "Gathering information of package, {}".format(

329 pkg.pkg_name

330 )

331 )

332 pkg.pkg = _headers_to_dist(pkg_str)

333 break

334 else:

335 if "Modaliases:" in pkg_str:

336 pkg = _headers_to_dist(pkg_str)

337

338 (modalias_header, pattern_str) = (

339 pkg["modaliases"].strip(")").split("(")

340 )

341 patterns = pattern_str.split(", ")

342 patterns.sort()

343 pkg["match_patterns"] = match_patterns(tuple(patterns))

344

345 dpkgf = "/var/lib/dpkg/info/{}.list".format(pkg["package"])

346 with io.open(dpkgf, "rt", encoding="UTF-8") as stream:

347 if "/dkms.conf" in stream.read(): 347 ↛ 348 line 347 didn't jump to line 348 because the condition on line 347 was never true

348 pkg["unused_dkms"] = True

349 result[pkg["package"]] = pkg

350 return result

351

352 def to_json(self):

353 return json.dumps(

354 {"dkms": self.extra_pkgs, "non-dkms": self.pkgs},

355 default=lambda o: o.__dict__,

356 sort_keys=True,

357 indent=4,

358 )

359

360 def to_outline(self):

361 result = ""

362 for pkg in self.extra_pkgs:

363 if pkg.pkg is None:

364 continue

365 result = "{}\n{}_{}: {}".format(

366 result, pkg.pkg_name, pkg.pkg["version"], pkg.install_mods

367 )

368 for pkg_name, pkg in self.pkgs.items():

369 extra_str = ""

370 if "unused_dkms" in pkg:

371 extra_str = "- "

372 result = "{}\n{}{}_{}: {}".format(

373 result,

374 extra_str,

375 pkg_name,

376 pkg["version"],

377 pkg["match_patterns"],

378 )

379 return result

380

381

382 class DeviceInfo:

383 """

384 Implementation of the dkms-info command.

385

386 dkms_info provides device package information.

387

388 @EPILOG@

389

390 supported package types:

391 - dkms (Dynamic Kernel Module Support): provides kernel modules

392 - non-dkms: packages that with modaliases header and don't exist in dkms

393 list

394 - hardware modalias: might be unused dkms or config package

395 - oemalias: It is like non-dkms(w/o modalias)

396

397 supported output formats:

398 - onelines: one line per packages with matched modaliases information

399 - dumps: json output (fully information)

400 """

401

402 def main(self):

403 """Invoke dkms-info."""

404 parser = argparse.ArgumentParser()

405 parser.add_argument(

406 "--format",

407 default="onelines",

408 choices=["summary", "json"],

409 help=(

410 "Choose output format type: "

411 "summary (one line per packages) "

412 "or json (json format, fully information)"

413 ),

414 )

415 parser.add_argument(

416 "--output",

417 default=None,

418 help=("Output filename to store the output date"),

419 )

420 args = parser.parse_args()

421

422 logging.basicConfig(

423 level=logging.INFO, format="[%(relativeCreated)06dms] %(message)s"

424 )

425 _logger.info("Started")

426

427 dkms_pkgs = []

428 for dkms_name, dkms_ver in get_installed_dkms_modules():

429 dkms_pkg = DkmsPackage(dkms_name, dkms_ver)

430 dkms_pkgs.append(dkms_pkg)

431

432 output = sys.stdout

433 if args.output is not None:

434 output = open(args.output, "wt", encoding="UTF-8")

435

436 pkg_handler = DebianPackageHandler(extra_pkgs=dkms_pkgs)

437 if args.format == "summary":

438 output.write(pkg_handler.to_outline())

439 else:

440 output.write(pkg_handler.to_json())

441 _logger.info("Data collected")

442

443

444 if __name__ == "__main__":

445 DeviceInfo().main()