diff --git a/src/soliddriver_checks/cli.py b/src/soliddriver_checks/cli.py index 67b5c94..d364d5f 100644 --- a/src/soliddriver_checks/cli.py +++ b/src/soliddriver_checks/cli.py @@ -114,7 +114,7 @@ def check(dst): "out_format", type=click.Choice(FORMAT_TYPES), default="json", - help="Specify output format", + help="Specify output format (PDF is in Beta)", ) @click.option( "--query", diff --git a/src/soliddriver_checks/config/soliddriver-checks.conf b/src/soliddriver_checks/config/soliddriver-checks.conf index 7df9af0..d2e8cdd 100644 --- a/src/soliddriver_checks/config/soliddriver-checks.conf +++ b/src/soliddriver_checks/config/soliddriver-checks.conf @@ -12,6 +12,9 @@ { "name":"GPLv2" }, + { + "name":"GPL v2 only" + }, { "name":"GPL-2.0" }, diff --git a/src/soliddriver_checks/config/templates/kmp-checks.html.jinja b/src/soliddriver_checks/config/templates/kmp-checks.html.jinja index 7d14dfb..42de2b2 100644 --- a/src/soliddriver_checks/config/templates/kmp-checks.html.jinja +++ b/src/soliddriver_checks/config/templates/kmp-checks.html.jinja @@ -54,10 +54,15 @@ th.detail_6{ width:240px; background-color:#90EBCD; - }th.detail_7{ + } + th.detail_7{ width:240px; background-color:#90EBCD; } + th.detail_8{ + width:240px; + background-color:#90EBCD; + } td{ text-align:left; vertical-align: top; diff --git a/src/soliddriver_checks/utils/data_exporter.py b/src/soliddriver_checks/utils/data_exporter.py index b6b43dc..dce2a26 100644 --- a/src/soliddriver_checks/utils/data_exporter.py +++ b/src/soliddriver_checks/utils/data_exporter.py @@ -155,6 +155,7 @@ def _copy_work_sheep(self, title, ws): ws["A5"].value = _get_version() ws["A6"].value = _generate_timestamp() + ws.column_dimensions['A'].width = 200 class RPMsExporter: def __init__(self): @@ -168,20 +169,9 @@ def _summary_symbol_result(self, val): result = "" if unfound_no > 0: - # result = "Can not find symbols like {} ... in RPM! ".format( - # val["unfound"][0] - # ) - result = f"Number of symbols can not be found in KMP: {unfound_no}" if cs_mm_no > 0: - # result = ( - # result - # + "Symbols check sum like {} ... does not match in RPM!".format( - # val["checksum-mismatch"][0] - # ) - # ) - result = result + f" Number of symbols checksum does not match: {cs_mm_no}" return result @@ -231,15 +221,25 @@ def _get_summary_table(self, rpm_table): columns=[ "Vendor", "Total KMPs", - "Driver Checks", "License", "Signature", "Weak Module Invoked", + "Supported Flag/Signature Checks Failed", "Symbols Check Failed", + "Modalias Check Failed", ] ) - def drivers_check(rpms_sf, rpms_is): + def alias_check(alias): + num = 0 + + for i in range(len(alias)): + if alias.iat[i]["match_all"] or len(alias.iat[i]["unmatched_km_alias"]) > 0 or len(alias.iat[i]["unmatched_kmp_alias"]) > 0: + num += 1 + + return num + + def supported_sig_check(rpms_sf, rpms_is): """ rpms_sf: RPMs' driver supported flag rpm_is : RPMs' is_signed column @@ -258,7 +258,7 @@ def drivers_check(rpms_sf, rpms_is): if not found_issues: num += 1 - return num + return rpms - num def license_check(vld_lics, rpm_licenses, driver_license): count = 0 @@ -279,8 +279,8 @@ def license_check(vld_lics, rpm_licenses, driver_license): for v in vendors: df_vendor = df.loc[df["vendor"] == v] total = len(df_vendor.index) - dc = drivers_check(df_vendor["df-supported"], df_vendor["is-signed"]) - failed = len( + spc = supported_sig_check(df_vendor["df-supported"], df_vendor["is-signed"]) + symc = len( df_vendor.loc[df_vendor["sym-check"] == "failed", "sym-check"].index ) lic_check = license_check( @@ -294,16 +294,18 @@ def license_check(vld_lics, rpm_licenses, driver_license): ].index ) wm_invoked = len(df_vendor.loc[df_vendor["wm-invoked"], "wm-invoked"].index) + aliasc = alias_check(df_vendor["modalias"]) df_summary = df_summary.append( { "Vendor": v, "Total KMPs": total, - "Driver Checks": f"{dc} ({dc/total * 100:.2f}%)", "License": f"{lic_check} ({lic_check/total * 100:.2f}%)", "Signature": f"{no_sig} ({no_sig/total * 100:.2f}%)", "Weak Module Invoked": f"{wm_invoked} ({wm_invoked/total * 100:.2f}%)", - "Symbols Check Failed": f"{failed} ({failed/total * 100:.2f}%)", + "Supported Flag/Signature Checks Failed": f"{spc} ({spc/total * 100:.2f}%)", + "Symbols Check Failed": f"{symc} ({symc/total * 100:.2f}%)", + "Modalias Check Failed": f"{aliasc} ({aliasc/total * 100:.2f}%)", }, ignore_index=True, ) @@ -327,20 +329,22 @@ def _get_summary_table_html(self, rpm_table): for i, row in df_summary.iterrows(): vendor = row["Vendor"] total_rpms = row["Total KMPs"] - dc = row["Driver Checks"] + ssc = row["Supported Flag/Signature Checks Failed"] lic_check = row["License"] signature = row["Signature"] wm_invoked = row["Weak Module Invoked"] sym_failed = row["Symbols Check Failed"] + alias_failed = row["Modalias Check Failed"] row_passed = False if ( vendor != "" - and int(dc.split(" ")[0]) == total_rpms + and int(ssc.split(" ")[0]) == 0 and int(lic_check.split(" ")[0]) == total_rpms and int(signature.split(" ")[0]) == total_rpms and int(wm_invoked.split(" ")[0]) == total_rpms and int(sym_failed.split(" ")[0]) == 0 + and int(alias_failed.split(" ")[0]) == 0 ): row_passed = True with tr() as r: @@ -353,13 +357,6 @@ def _get_summary_table_html(self, rpm_table): tv.set_attribute("class", "important_failed") with td(total_rpms) as t: t.set_attribute("class", "summary_total") - with td(dc) as t: - if int(dc.split(" ")[0]) != total_rpms: - t.set_attribute( - "class", "critical_failed summary_number" - ) - else: - t.set_attribute("class", "summary_number") with td(lic_check) as t: if int(lic_check.split(" ")[0]) != total_rpms: t.set_attribute( @@ -381,6 +378,13 @@ def _get_summary_table_html(self, rpm_table): ) else: t.set_attribute("class", "summary_number") + with td(ssc) as t: + if int(ssc.split(" ")[0]) != 0: + t.set_attribute( + "class", "critical_failed summary_number" + ) + else: + t.set_attribute("class", "summary_number") with td(sym_failed) as t: if int(sym_failed.split(" ")[0]) != 0: t.set_attribute( @@ -388,6 +392,13 @@ def _get_summary_table_html(self, rpm_table): ) else: t.set_attribute("class", "summary_number") + with td(alias_failed) as t: + if int(alias_failed.split(" ")[0]) > 0: + t.set_attribute( + "class", "important_falied summary_number" + ) + else: + t.set_attribute("class", "summary_number") return tb @@ -399,11 +410,11 @@ def _rename_rpm_detail_columns(self, rpm_table): "path": "Path", "vendor": "Vendor", "signature": "Signature", - # "distribution": "Distribution", "license": "License", "wm-invoked": "Weak Module Invoked", - "df-supported": "Driver Checks", + "df-supported": "Supported Flag Check", "sym-check": "Symbols Check", + "modalias": "Modalias Check", "dv-licenses": "Driver Licenses", "is-signed": "is-signed", } @@ -435,6 +446,30 @@ def _fmt_driver_license_check(self, rpm_license, driver_licenses, vld_lics): ) return chk_result + + def _fmt_modalias_check(self, alias): + match_all = alias["match_all"] + km_unmatched = len(alias["unmatched_km_alias"]) + kmp_unmatched = len(alias["unmatched_kmp_alias"]) + + if match_all: + return "KMP can match all the devices! Highly not recommended!" + + message = "" + + if km_unmatched > 0: + message += "Alias found in kernel module but no match in it's package: " + for kmu in alias["unmatched_km_alias"]: + message += kmu + ", " + message += "\n" + + if kmp_unmatched > 0: + message += "Alias found in the package but no match in it's kernel module: " + for kmpu in alias["unmatched_kmp_alias"]: + message += kmpu + ", " + + return message + def _get_table_detail_html(self, rpm_table): df = self._rename_rpm_detail_columns(rpm_table) @@ -444,7 +479,7 @@ def _get_table_detail_html(self, rpm_table): tb.set_attribute("class", "table_center") with tr(): th("KMP Checks", colspan=6).set_attribute("class", f"detail_rpm") - th("Kernel Module Checks", colspan=2).set_attribute("class", f"detail_kernel_module") + th("Kernel Module Checks", colspan=3).set_attribute("class", f"detail_kernel_module") with tr(): th("Name").set_attribute("class", f"detail_0") th("Path").set_attribute("class", f"detail_1") @@ -453,7 +488,8 @@ def _get_table_detail_html(self, rpm_table): th(raw("LicenseKMP and it's kernel modules should use open source licenses.")).set_attribute("class", f"detail_4 tooltip") th(raw("Weak Module InvokedWeak Module is necessary to make 3rd party kernel modules installed for one kernel available to KABI-compatible kernels. ")).set_attribute("class", f"detail_5 tooltip") th(raw("Supported Flag/Signature\"supported\" flag:
\"yes\": Only supported by SUSE
\"external\": supported by both SUSE and vendor
")).set_attribute("class", f"detail_6 tooltip") - th(raw("Symbolssymbols check is to check whether the symbols in kernel modules matches the symbols in its package.")).set_attribute("class", f"detail_7 tooltip") + th(raw("SymbolsSymbols check is to check whether the symbols in kernel modules matches the symbols in its package.")).set_attribute("class", f"detail_7 tooltip") + th(raw("ModaliasModalias check is to check whether the modalias in kernel modules matches the modalias in its package.")).set_attribute("class", f"detail_8 tooltip") for i, row in df.iterrows(): with tr() as r: @@ -467,9 +503,11 @@ def _get_table_detail_html(self, rpm_table): sym_check = self._get_sym_check_failed( row["Symbols Check"] ).replace("\n", "
") - supported = row["Driver Checks"] + supported = row["Supported Flag Check"] is_signed = row["is-signed"] - dc_err = self._combine_driver_check_errs(supported, is_signed) + alias = row["Modalias Check"] + alias_chk = self._fmt_modalias_check(alias) + dc_err = self._supported_sig_errs(supported, is_signed) dv_license = row["Driver Licenses"] lcs_chk = self._fmt_driver_license_check( license, dv_license, vld_lic @@ -490,7 +528,6 @@ def _get_table_detail_html(self, rpm_table): else: ts = td(signature) ts.set_attribute("class", "important_failed") - # td(distribution) if lcs_chk == "": # license check if license == "": tl = td("No License") @@ -525,10 +562,17 @@ def _get_table_detail_html(self, rpm_table): t_w = td(raw(sym_check)) t_w.set_attribute("class", "critical_failed") r.set_attribute("class", "critical_failed_row") + if alias_chk == "": + t = td("All passed!") + t.set_attribute("class", "detail_pass") + else: + t_w = td(raw(alias_chk.replace("\n", "
"))) + t_w.set_attribute("class", "important_failed") + r.set_attribute("class", "important_failed_row") return tb - def _combine_driver_check_errs(self, sffds, nsds): + def _supported_sig_errs(self, sffds, nsds): sfds_l = self._get_supported_driver_failed(sffds) nsds_l = self._get_no_signed_driver(nsds) @@ -658,7 +702,16 @@ def _xlsx_create_vendor_summary(self, wb, rpm_table): sym_failed = Rule(type="expression", dxf=ctc_style) sym_failed.formula = ['VALUE(LEFT($G2, FIND(" ", $G2) - 1)) <> 0'] ws_vs.conditional_formatting.add(f"G2:G{last_record_row_no}", sym_failed) - + + ws_vs.column_dimensions['A'].width = 30 + ws_vs.column_dimensions['B'].width = 15 + ws_vs.column_dimensions['C'].width = 15 + ws_vs.column_dimensions['D'].width = 15 + ws_vs.column_dimensions['E'].width = 15 + ws_vs.column_dimensions['F'].width = 15 + ws_vs.column_dimensions['G'].width = 15 + ws_vs.column_dimensions['H'].width = 15 + def _xlsx_create_rpm_details(self, wb, rpm_table): df = self._rename_rpm_detail_columns(rpm_table) ws_rd = wb.create_sheet("KMPs details") @@ -695,7 +748,7 @@ def _xlsx_create_rpm_details(self, wb, rpm_table): ws_rd['A1'].border = header_border ws_rd['A1'].alignment = center_align - ws_rd.merge_cells('H1:I1') + ws_rd.merge_cells('H1:L1') ws_rd['H1'] = "Kernel Module Checks" ws_rd['H1'].font = header_font ws_rd['H1'].fill = header_fill @@ -741,8 +794,8 @@ def _xlsx_create_rpm_details(self, wb, rpm_table): ws_rd[cell_no].font = ctc_font ws_rd[cell_no].fill = ctc_fill ws_rd[cell_no].border = ctc_border - elif cols[col_idx] == "Driver Checks": - val = self._combine_driver_check_errs(val, row["is-signed"]) + elif cols[col_idx] == "Supported Flag Check": + val = self._supported_sig_errs(val, row["is-signed"]) if len(val) > 0: ws_rd[cell_no] = '\n'.join(val) ws_rd[cell_no].font = ctc_font @@ -769,6 +822,18 @@ def _xlsx_create_rpm_details(self, wb, rpm_table): ws_rd[cell_no].font = imt_font ws_rd[cell_no].fill = imt_fill ws_rd[cell_no].border = imt_border + elif cols[col_idx] == "Modalias Check": + alias_check = self._fmt_modalias_check(row["Modalias Check"]) + if alias_check != "": + ws_rd[cell_no] = alias_check + ws_rd[cell_no].font = imt_font + ws_rd[cell_no].fill = imt_fill + ws_rd[cell_no].border = imt_border + else: + val = "All passed!" + ws_rd[cell_no] = val + ws_rd[cell_no].alignment = center_align + else: # no format needed. ws_rd[cell_no] = str(val) @@ -783,6 +848,16 @@ def _xlsx_create_rpm_details(self, wb, rpm_table): sig_rule = Rule(type="expression", dxf=ctc_style) sig_rule.formula = ['=OR($D2 = "", $D2 = "(none)")'] ws_rd.conditional_formatting.add(f"D2:D{records}", sig_rule) + + ws_rd.column_dimensions['A'].width = 30 + ws_rd.column_dimensions['B'].width = 40 + ws_rd.column_dimensions['C'].width = 30 + ws_rd.column_dimensions['D'].width = 30 + ws_rd.column_dimensions['F'].width = 20 + ws_rd.column_dimensions['G'].width = 15 + ws_rd.column_dimensions['H'].width = 40 + ws_rd.column_dimensions['I'].width = 40 + ws_rd.column_dimensions['L'].width = 40 def _xlsx_create_report_workbook(self): wb = Workbook() diff --git a/src/soliddriver_checks/utils/data_reader.py b/src/soliddriver_checks/utils/data_reader.py index e701ea4..656eb55 100644 --- a/src/soliddriver_checks/utils/data_reader.py +++ b/src/soliddriver_checks/utils/data_reader.py @@ -7,6 +7,7 @@ import re from collections import namedtuple import tempfile +import fnmatch import json from scp import SCPClient from paramiko.ssh_exception import NoValidConnectionsError, SSHException @@ -90,8 +91,35 @@ def __init__(self, progress): "sym-check", "dv-licenses", "is-signed", + "modalias", ] + def _get_driver_alias(self, driver): + alias = run_cmd("/usr/sbin/modinfo --field=alias %s | grep pci:" % driver) + return alias.splitlines() + + def _get_rpm_modalias(self, rpm): + modalias = namedtuple("modalias", "kernel_flavor pci_re") + ml_pci_re = re.compile(r"modalias\((.*):(.*\:.*)\)") # example: modalias(kernel-default:pci:v000019A2d00000712sv*sd*bc*sc*i*) + ml_all_re = re.compile(r"modalias\((.*):(.*)\)") # example: packageand(kernel-default:primergy-be2iscsi) + raw_modalias = run_cmd("rpm -q --supplements %s" %rpm) + + alias_re = [] + for line in raw_modalias.splitlines(): + line = str(line, "utf-8").strip() + pci_rst = ml_pci_re.match(line) + all_rst = ml_all_re.match(line) + if pci_rst: + ker_flavor, pci = pci_rst.groups() + if "pci:" in pci: # only check PCI devices + alias_re.append(pci) + elif all_rst: # match all (*) should not be allowed + ker_flavor, rst = all_rst.groups() + if rst == "*": + alias_re.append(rst) + + return alias_re + def _driver_symbols_check(self, rpm_symbols, driver): symvers = run_cmd("/usr/sbin/modprobe --dump-modversions %s" % driver) @@ -147,7 +175,7 @@ def _get_driver_supported(self, driver): if values[0].strip() == "supported": if supported != "": # only allow appears once. - return "Multiple" + supported = supported + ", " + ":".join(values[1:]).strip() else: supported = ":".join(values[1:]).strip() @@ -203,6 +231,50 @@ def _fmt_driver_symbol(self, drivers): symbols[d] = d_info return symbols + + def _fmt_driver_modalias(self, kmp_alias, drivers): + key_match_all = 'match_all' + key_unmatched_km_alias = 'unmatched_km_alias' + key_unmatched_kmp_alias = 'unmatched_kmp_alias' + for a in kmp_alias: + if a == "*": # "use default-kernel:* to match all the devices is always a bad idea." + return {key_match_all:True, + key_unmatched_km_alias:[], + key_unmatched_kmp_alias:[]} + + unmatched_ker_alias = [] + unmatched_kmp_alias = kmp_alias.copy() + + for d in drivers: + for ker_a in drivers[d]["alias"]: + ker_a = str(ker_a, "utf-8").strip() + found = False + for kmp_a in kmp_alias: + if fnmatch.fnmatch(ker_a.strip(), kmp_a.strip()): + found = True + for uk in unmatched_kmp_alias: + if uk.strip() == kmp_a.strip(): + unmatched_kmp_alias.remove(uk) + break + break + if not found: + unmatched_ker_alias.append(ker_a) + + # There has some packages have "%5" in the package but use "_" in the kernel module + # So we have to match it again, but equal is enough. + r_ukmpalias = [v.replace("_", "%5F").replace("-", "%2D").replace(".", "%2E") for v in unmatched_ker_alias] + for i in range(len(unmatched_kmp_alias) - 1, -1, -1): + found_match = False + for j in range(len(unmatched_ker_alias) - 1, -1, -1): + if r_ukmpalias[j] == unmatched_kmp_alias[i]: + unmatched_ker_alias.pop(j) + found_match = True + if found_match: + unmatched_kmp_alias.pop(i) + + return {key_match_all:False, + key_unmatched_km_alias:unmatched_ker_alias, + key_unmatched_kmp_alias:unmatched_kmp_alias} def _is_driver_signed(self, driver): raw_info = run_cmd("/usr/sbin/modinfo %s" % driver) @@ -242,6 +314,7 @@ def _driver_checks(self, rpm: str): item["supported"] = self._get_driver_supported(driver) item["license"] = self._get_driver_license(driver) item["is_signed"] = self._is_driver_signed(driver) + item["alias"] = self._get_driver_alias(driver) dpath = str(driver) dpath = dpath[dpath.startswith(tmp.name) + len(tmp.name) - 1 :] @@ -293,11 +366,13 @@ def _format_rpm_info(self, rpm_files, raw_output, row_handlers, query="all"): symbols = dict() d_licenses = dict() is_signed = dict() + km_modalias = dict() if driver_checks is not None: supported = self._fmt_driver_supported(driver_checks) symbols = self._fmt_driver_symbol(driver_checks) d_licenses = self._fmt_driver_license(driver_checks) is_signed = self._fmt_driver_is_signed(driver_checks) + km_modalias = self._fmt_driver_modalias(self._get_rpm_modalias(rpm), driver_checks) if not self._query_filter(supported, query): continue @@ -315,7 +390,8 @@ def _format_rpm_info(self, rpm_files, raw_output, row_handlers, query="all"): supported, symbols, d_licenses, - is_signed + is_signed, + km_modalias ] ) diff --git a/src/soliddriver_checks/version.py b/src/soliddriver_checks/version.py index af218a2..1847ab2 100644 --- a/src/soliddriver_checks/version.py +++ b/src/soliddriver_checks/version.py @@ -2,4 +2,4 @@ Global version information used in soliddriver-checks and the package """ -__VERSION__ = "2.0.7" +__VERSION__ = "2.0.8-beta" diff --git a/tests/Dockerfile b/tests/Dockerfile index 48faff1..49212b8 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -2,8 +2,9 @@ FROM opensuse/leap:latest WORKDIR /root -RUN zypper --non-interactive in python3-pip kmod +RUN zypper --non-interactive in python39 python39-pip kmod +RUN zypper --non-interactive dup -RUN pip3 install rich pdfkit pandas \ -openpyxl click paramiko dominate +RUN pip3.9 install rich pdfkit pandas \ +openpyxl click paramiko dominate build diff --git a/tests/create_container_mac.sh b/tests/create_container_mac.sh index 5e49fb2..afbc0af 100755 --- a/tests/create_container_mac.sh +++ b/tests/create_container_mac.sh @@ -1,6 +1,6 @@ docker run \ -it --rm \ --mount type=bind,source=/Users/$USER/projects/github.com/SUSE/soliddriver-checks,target=/root/source_codes \ ---mount type=bind,source=/Users/$USER/projects/Lenovo/SSDP/01-Apr-2022,target=/root/rpms \ +--mount type=bind,source=/Users/$USER/projects/fujitsu/kmps-15-sp2,target=/root/rpms \ --mount type=bind,source=/Users/$USER/codes/tmp,target=/root/output_dir \ opensuse-leap-soliddriver-checks:latest /bin/bash