diff --git a/.gitignore b/.gitignore index 3b9ce42..a0a1235 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -SOURCES/sos-4.11.0.tar.gz +SOURCES/sos-4.11.2.tar.gz SOURCES/sos-audit-0.3-1.tgz diff --git a/.sos.metadata b/.sos.metadata index 0b7a84e..35e5753 100644 --- a/.sos.metadata +++ b/.sos.metadata @@ -1,2 +1,2 @@ -b95a041f0826234fcdc7ee452d31609972d5d556 SOURCES/sos-4.11.0.tar.gz +d8d5e3e1eb124345417341ef9e9aa84f5cae609f SOURCES/sos-4.11.2.tar.gz 00752b68ec5e1141192a9dab7d44377b8d637bf7 SOURCES/sos-audit-0.3-1.tgz diff --git a/SOURCES/0001-python3-walrus-operator-and-rhel8-changes-only.patch b/SOURCES/0001-python3-walrus-operator-and-rhel8-changes-only.patch index ef7044d..db5ae5e 100644 --- a/SOURCES/0001-python3-walrus-operator-and-rhel8-changes-only.patch +++ b/SOURCES/0001-python3-walrus-operator-and-rhel8-changes-only.patch @@ -23,22 +23,25 @@ res.append(ls[0]) except Exception as err: self.log_debug(f"Error parsing sos help: {err}") ---- a/sos/report/plugins/mongodb.py -+++ b/sos/report/plugins/mongodb.py -@@ -87,9 +87,11 @@ +--- a/sos/report/plugins/mongodb.py 2026-07-02 09:06:03.860609746 +0200 ++++ b/sos/report/plugins/mongodb.py 2026-07-02 09:08:00.003595562 +0200 +@@ -87,10 +87,13 @@ ) def setup(self): - if get_juju_info := self.path_exists('/var/lib/juju/db'): -+ if self.path_exists('/var/lib/juju/db'): -+ get_juju_info = self.path_exists('/var/lib/juju/db') ++ get_juju_info = self.path_exists('/var/lib/juju/db') ++ if get_juju_info: self.db_folder = "/var/lib/juju/db" - elif get_juju_info := self.path_exists('/var/snap/juju-db/curent/db'): -+ elif self.path_exists('/var/snap/juju-db/curent/db'): -+ get_juju_info = self.path_exists('/var/snap/juju-db/curent/db') - self.db_folder = "/var/snap/juju-db/current/db" +- self.db_folder = "/var/snap/juju-db/current/db" ++ else: ++ get_juju_info = self.path_exists('/var/snap/juju-db/current/db') ++ if get_juju_info: ++ self.db_folder = "/var/snap/juju-db/current/db" super().setup() + --- a/sos/report/plugins/loki.py 2025-11-24 11:20:56.237814760 +0100 +++ b/sos/report/plugins/loki.py 2025-11-24 11:28:37.466603011 +0100 @@ -143,7 +143,8 @@ @@ -51,43 +54,29 @@ if isinstance(labels_option, str) and labels_option: self.labels.extend(labels_option.split(":")) ---- a/sos/cleaner/archives/__init__.py 2025-09-22 19:44:51.272619200 +0200 -+++ b/sos/cleaner/archives/__init__.py 2025-09-22 23:28:15.116001268 +0200 -@@ -118,6 +118,8 @@ class SoSObfuscationArchive(): - self.parsers = parsers # TODO: include this in __init__? - - def load_parser_entries(self): -+ self.soslog = logging.getLogger('sos') -+ self.ui_log = logging.getLogger('sos_ui') - for parser in self.parsers: - parser.load_map_entries() - -@@ -150,6 +152,7 @@ class SoSObfuscationArchive(): - return line, count - - def obfuscate_arc_files(self, flist): -+ self.load_parser_entries() - for filename in flist: - self.log_debug(f" pid={os.getpid()}: obfuscating {filename}") - try: ---- a/sos/cleaner/__init__.py 2025-09-22 19:44:51.272619200 +0200 -+++ b/sos/cleaner/__init__.py 2025-09-22 23:32:17.606745778 +0200 -@@ -720,10 +720,11 @@ third party. - # based on files' sizes. - +--- a/sos/cleaner/__init__.py ++++ b/sos/cleaner/__init__.py +@@ -40,6 +40,8 @@ from sos.utilities import (get_human_rea + + # an auxiliary method to kick off child processes over its instances + def _obfuscate_arc_files(arc, input_queue, output_queue): ++ arc.soslog = logging.getLogger('sos') ++ arc.ui_log = logging.getLogger('sos_ui') + while True: + try: + file = input_queue.get() +--- a/sos/cleaner/__init__.py 2026-07-01 13:45:34.298955340 +0200 ++++ b/sos/cleaner/__init__.py 2026-07-01 13:47:15.978445308 +0200 +@@ -795,6 +795,9 @@ + # sentinel mark. That triggers the child processes to report back + # to output_queue some stats, and finish. files_obfuscated_count = total_sub_count = removed_file_count = 0 + # two nullification required before processes cloning + archive.soslog = None + archive.ui_log = None - archive_list = [archive for i in range(self.opts.jobs)] -- with ProcessPoolExecutor( -- max_workers=self.opts.jobs, -- initializer=archive.load_parser_entries) as executor: -+ with ProcessPoolExecutor(max_workers=self.opts.jobs) as executor: - futures = executor.map(obfuscate_arc_files, archive_list, - [file_list[i::self.opts.jobs] for i in - range(self.opts.jobs)]) - + input_queue = multiprocessing.Queue() + output_queue = multiprocessing.Queue() + --- a/sos/collector/sosnode.py 2026-04-02 10:15:34.743009569 +0200 +++ b/sos/collector/sosnode.py 2026-04-02 10:17:42.557250748 +0200 @@ -163,7 +163,8 @@ @@ -114,3 +103,65 @@ else: confs += glob.glob(_ent) +--- a/sos/report/plugins/charmed_cruise_control.py 2026-07-16 13:00:22.269714807 +0200 ++++ b/sos/report/plugins/charmed_cruise_control.py 2026-07-16 13:01:56.710522628 +0200 +@@ -35,7 +35,8 @@ + ) as f: + content = f.read().strip() + +- if match := re.match(r"balancer: (?P\w+),ADMIN", content): ++ match = re.match(r"balancer: (?P\w+),ADMIN", content) ++ if match: + pwd = match.group("pwd") + return f"-u balancer:{pwd}" + +--- a/sos/report/plugins/__init__.py ++++ b/sos/report/plugins/__init__.py +@@ -3262,7 +3262,10 @@ + # skip forbidden paths; since we might recursivelly copied + # whole directory, we must find the forbidden files in dest + # path and delete the unwanted +- base_dir = dest.removesuffix(f"{path.lstrip('/')}") ++ _suffix = path.lstrip('/') ++ base_dir = (dest[:-len(_suffix)] ++ if dest.endswith(_suffix) and _suffix ++ else dest) + for relname in [file.relative_to(base_dir).as_posix() + for file in Path(dest).rglob('*')]: + absname = f"/{relname}" + +--- a/sos/cleaner/__init__.py ++++ b/sos/cleaner/__init__.py +@@ -846,6 +846,8 @@ + # *all* mapping.all(item) methods - so replaying this will + # generate the right datasets! + archive.load_parser_entries() ++ archive.soslog = logging.getLogger('sos') ++ archive.ui_log = logging.getLogger('sos_ui') + + try: + self.obfuscate_directory_names(archive) + +--- a/sos/report/plugins/charmed_mongodb.py 2026-07-16 14:48:09.639484635 +0200 ++++ b/sos/report/plugins/charmed_mongodb.py 2026-07-16 14:49:02.337015148 +0200 +@@ -122,8 +122,8 @@ + encoding="utf-8", + ) as f: + data = yaml.safe_load(f) +- +- if sharding_conf := data.get("sharding", {}): ++ sharding_conf = data.get("sharding", {}) ++ if sharding_conf: + role = sharding_conf.get("clusterRole", "") + + return self._match_role(role) +@@ -144,7 +144,8 @@ + return None + + data = yaml.safe_load(result.get("output", "")) +- if sharding_conf := data.get("sharding", {}): ++ sharding_conf = data.get("sharding", {}) ++ if sharding_conf: + role = sharding_conf.get("clusterRole", "") + return self._match_role(role) + diff --git a/SOURCES/0002-remove-unsupported-python36-plugins.patch b/SOURCES/0002-remove-unsupported-python36-plugins.patch new file mode 100644 index 0000000..0b33a53 --- /dev/null +++ b/SOURCES/0002-remove-unsupported-python36-plugins.patch @@ -0,0 +1,1436 @@ +--- a/sos/report/plugins/charmed_cruise_control.py 2026-07-17 13:13:53.703490321 +0200 ++++ /dev/null 2026-07-04 09:21:02.628033198 +0200 +@@ -1,142 +0,0 @@ +-# This file is part of the sos project: https://github.com/sosreport/sos +-# +-# This copyrighted material is made available to anyone wishing to use, +-# modify, copy, or redistribute it subject to the terms and conditions of +-# version 2 of the GNU General Public License. +-# +-# See the LICENSE file in the source distribution for further information.# +- +-import glob +-import re +-from datetime import datetime +-from functools import cached_property +- +-from sos.report.plugins import Plugin, UbuntuPlugin +- +-PATHS = { +- "CONF": "/var/snap/charmed-kafka/current/etc/cruise-control", +- "LOGS": "/var/snap/charmed-kafka/common/var/log/cruise-control", +-} +- +-DATE_FORMAT = "%Y-%m-%d-%H" +- +- +-class CharmedCruiseControl(Plugin, UbuntuPlugin): +- short_desc = "Cruise Control (from Charmed Kafka)" +- plugin_name = "charmed_cruise_control" +- packages = ("charmed-kafka",) +- +- @cached_property +- def credentials_args(self) -> str: +- try: +- with open( +- f"{PATHS['CONF']}/cruisecontrol.credentials", +- encoding="utf-8", +- ) as f: +- content = f.read().strip() +- +- match = re.match(r"balancer: (?P\w+),ADMIN", content) +- if match: +- pwd = match.group("pwd") +- return f"-u balancer:{pwd}" +- +- return "" +- except FileNotFoundError: +- return "" +- +- def setup(self): +- if not self.credentials_args: +- # service not properly set-up, skip +- return +- +- # --- FILE EXCLUSIONS --- +- +- all_logs = self.get_option("all_logs") +- since = self.get_option("since") +- +- for file in glob.glob(f"{PATHS['LOGS']}/*"): +- date = re.search( +- pattern=r"([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2})", string=file +- ) +- +- # include files without date, aka current files +- if not date: +- continue +- +- file_dt = datetime.strptime(date.group(1), DATE_FORMAT) +- +- if ( +- since +- and not all_logs +- and file_dt < datetime.strptime(str(since), DATE_FORMAT) +- ): +- # skip files outside given range +- self.add_forbidden_path(file) +- +- # hide keys/stores +- self.add_forbidden_path([ +- f"{PATHS['CONF']}/*.pem", +- f"{PATHS['CONF']}/*.key", +- f"{PATHS['CONF']}/*.p12", +- f"{PATHS['CONF']}/*.jks", +- ]) +- +- # --- FILE INCLUSIONS --- +- +- self.add_copy_spec( +- [ +- f"{PATHS['CONF']}", +- f"{PATHS['LOGS']}", +- ] +- ) +- +- # --- SNAP LOGS --- +- +- if all_logs: +- self.add_cmd_output( +- "snap logs -n all charmed-kafka.cruise-control", +- suggest_filename="snap_logs_charmed-kafka_cruise-control", +- ) +- else: +- self.add_cmd_output( +- "snap logs -n 500 charmed-kafka.cruise-control", +- suggest_filename="snap_logs_charmed-kafka_cruise-control", +- ) +- +- # --- STATE, TASKS, PARTITION LOAD --- +- +- endpoints = { +- 'cruise-control-state': 'state?super_verbose=true', +- 'cluster-state': 'kafka_cluster_state?verbose=true', +- 'partition_load': 'partition_load', +- 'user-tasks': 'user_tasks', +- } +- +- url = 'localhost:9090/kafkacruisecontrol' +- +- for fname, api in endpoints.items(): +- self.add_cmd_output( +- f"curl {self.credentials_args} {url}/{api}", +- suggest_filename=fname, +- ) +- +- # --- JMX METRICS --- +- +- self.add_cmd_output( +- "curl localhost:9102/metrics", +- suggest_filename="jmx-metrics" +- ) +- +- def postproc(self): +- if not self.credentials_args: +- # service not properly set-up, skip +- return +- +- # --- SCRUB PASSWORDS --- +- +- for scrub_pattern in [r'(password=")[^"]*', r"(balancer: )[^,]*"]: +- self.do_path_regex_sub( +- f"{PATHS['CONF']}/*", +- scrub_pattern, +- r"\1*********", +- ) +--- a/sos/report/plugins/charmed_kafka.py 2026-06-16 10:22:51.000000000 +0200 ++++ /dev/null 2026-07-04 09:21:02.628033198 +0200 +@@ -1,222 +0,0 @@ +-# This file is part of the sos project: https://github.com/sosreport/sos +-# +-# This copyrighted material is made available to anyone wishing to use, +-# modify, copy, or redistribute it subject to the terms and conditions of +-# version 2 of the GNU General Public License. +-# +-# See the LICENSE file in the source distribution for further information.# +- +-import glob +-import json +-import re +-from datetime import datetime +-from functools import cached_property +-from typing import Optional +- +-from sos.report.plugins import Plugin, UbuntuPlugin +- +-PATHS = { +- "CONF": "/var/snap/charmed-kafka/current/etc/kafka", +- "LOGS": "/var/snap/charmed-kafka/common/var/log/kafka", +-} +- +-DATE_FORMAT = "%Y-%m-%d-%H" +- +- +-class CharmedKafka(Plugin, UbuntuPlugin): +- short_desc = "Charmed Kafka" +- plugin_name = "charmed_kafka" +- packages = ("charmed-kafka",) +- +- @cached_property +- def bootstrap_server(self) -> Optional[str]: +- try: +- lines = [] +- with open( +- f"{PATHS['CONF']}/client.properties", +- encoding="utf-8", +- ) as f: +- lines = f.readlines() +- +- for line in lines: +- if "bootstrap" in line: +- # ensure using internal address if +- # not set in client.properties +- return re.sub( +- r":(?!1)(\d+)", r":1\1", +- line.split("=")[1] +- ) +- +- return None +- except FileNotFoundError: +- return None +- +- @cached_property +- def default_bin_args(self) -> str: +- if not self.bootstrap_server: +- return "" +- +- return ( +- f"--bootstrap-server {self.bootstrap_server}" +- f" --command-config {PATHS['CONF']}/client.properties" +- ) +- +- def setup(self): +- if not self.bootstrap_server: +- # service not properly set-up by the charm, skip +- return +- +- # --- FILE EXCLUSIONS --- +- +- all_logs = self.get_option("all_logs") +- since = self.get_option("since") +- +- for file in glob.glob(f"{PATHS['LOGS']}/*"): +- date = re.search( +- pattern=r"([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2})", string=file +- ) +- +- # include files without date, aka current files +- if not date: +- continue +- +- file_dt = datetime.strptime(date.group(1), DATE_FORMAT) +- +- if ( +- since +- and not all_logs +- and file_dt < datetime.strptime(str(since), DATE_FORMAT) +- ): +- # skip files outside given range +- self.add_forbidden_path(file) +- +- # hide keys/stores +- self.add_forbidden_path([ +- f"{PATHS['CONF']}/*.pem", +- f"{PATHS['CONF']}/*.key", +- f"{PATHS['CONF']}/*.p12", +- f"{PATHS['CONF']}/*.jks", +- ]) +- +- # --- FILE INCLUSIONS --- +- +- self.add_copy_spec( +- [ +- f"{PATHS['CONF']}", +- f"{PATHS['LOGS']}", +- ] +- ) +- +- # --- SNAP LOGS --- +- +- if all_logs: +- self.add_cmd_output( +- "snap logs -n all charmed-kafka.daemon", +- suggest_filename="snap_logs_charmed-kafka_daemon", +- ) +- else: +- self.add_cmd_output( +- "snap logs -n 500 all charmed-kafka.daemon", +- suggest_filename="snap_logs_charmed-kafka_daemon", +- ) +- +- # --- TOPICS --- +- +- self.add_cmd_output( +- f"charmed-kafka.topics --describe {self.default_bin_args}", +- env={"KAFKA_OPTS": ""}, +- suggest_filename="kafka-topics", +- ) +- +- # --- CONFIGS --- +- +- for entity in ["topics", "clients", "users", "brokers", "ips"]: +- self.add_cmd_output( +- ( +- "charmed-kafka.configs --describe --all " +- f"--entity-type {entity} {self.default_bin_args}" +- ), +- env={"KAFKA_OPTS": ""}, +- suggest_filename=f"kafka-configs-{entity}", +- ) +- +- # --- ACLs --- +- +- self.add_cmd_output( +- f"charmed-kafka.acls --list {self.default_bin_args}", +- env={"KAFKA_OPTS": ""}, +- suggest_filename="kafka-acls", +- ) +- +- # --- JMX METRICS --- +- +- self.add_cmd_output( +- "curl localhost:9101/metrics", +- suggest_filename="jmx-metrics" +- ) +- +- def collect(self): +- +- # --- LOG DIRS --- +- +- log_dirs_output = self.exec_cmd( +- f"charmed-kafka.log-dirs --describe {self.default_bin_args}", +- env={"KAFKA_OPTS": ""}, +- ) +- log_dirs = {} +- +- # output has leading non-json lines that need cleaning, e.g: +- +- # Querying brokers for log directories information +- # Received log directory information from brokers 100,101,102 +- # {"brokers":[{"broker":100,"logDirs":... +- +- if log_dirs_output and log_dirs_output["status"] == 0: +- for line in log_dirs_output["output"].splitlines(): +- try: +- log_dirs = json.loads(line) +- break +- except json.JSONDecodeError: +- continue +- +- with self.collection_file("kafka-log-dirs") as f: +- f.write(json.dumps(log_dirs, indent=4)) +- +- # --- TRANSACTIONS --- +- +- transactions_list = self.exec_cmd( +- f"charmed-kafka.transactions {self.default_bin_args} list", +- env={"KAFKA_OPTS": ""}, +- ) +- transactional_ids = [] +- +- if transactions_list and transactions_list["status"] == 0: +- transactional_ids = transactions_list["output"].splitlines()[1:] +- +- transactions_outputs = [] +- for transactional_id in transactional_ids: +- transactions_describe = self.exec_cmd( +- ( +- f"charmed-kafka.transactions {self.default_bin_args}", +- f"describe --transactional-id {transactional_id}", +- ), +- ) +- +- if transactions_describe and transactions_describe["status"] == 0: +- transactions_outputs.append(transactions_describe["output"]) +- +- with self.collection_file("kafka-transactions") as f: +- f.write("\n".join(transactions_outputs)) +- +- def postproc(self): +- if not self.bootstrap_server: +- # service not properly set-up by the charm, skip +- return +- +- # --- SCRUB PASSWORDS --- +- +- self.do_path_regex_sub( +- f"{PATHS['CONF']}/*", +- r'(password=")[^"]*', +- r"\1*********", +- ) +--- a/sos/report/plugins/charmed_mongodb.py 2026-07-17 13:13:53.703669427 +0200 ++++ /dev/null 2026-07-04 09:21:02.628033198 +0200 +@@ -1,456 +0,0 @@ +-# This file is part of the sos project: https://github.com/sosreport/sos +-# +-# This copyrighted material is made available to anyone wishing to use, +-# modify, copy, or redistribute it subject to the terms and conditions of +-# version 2 of the GNU General Public License. +-# +-# See the LICENSE file in the source distribution for further information.# +- +-from enum import Enum +-from urllib.parse import quote_plus, urlencode +-import os +-import shutil +-from pathlib import Path +-import typing +-from typing import Dict, Optional, Tuple +- +-import yaml +- +-from sos.report.plugins import Plugin, PluginOpt, UbuntuPlugin +-from sos.utilities import is_executable +- +-DATE_FORMAT = "%Y-%m-%d-%H" +- +- +-class Substrate(Enum): +- VM = "vm" +- K8S = "k8s" +- +- +-Role = typing.Literal["replication", "shard", "config-server"] +- +- +-class CharmedMongoDB(Plugin, UbuntuPlugin): +- """The Charmed MongoDB plugin is used to collect MongoDB configuration +- and logs from the Charmed MongoDB snap package or K8s deployment. +- +- If all_logs is set to True, it collects all logs by default. +- The parameters `dbuser` and `dbpass` are used to dump database information, +- replicaset status, shard status, etc. You can provide those parameters with +- the environment variables `MONGODB_USER` and `MONGODB_PASSWORD` +- """ +- +- short_desc = "Charmed MongoDB" +- plugin_name = "charmed_mongodb" +- +- # Triggers +- +- packages = ("charmed-mongodb",) +- containers = ("mongod",) +- +- snap_package = "charmed-mongodb" +- snap_path_common = "/var/snap/charmed-mongodb/common" +- snap_path_current = "/var/snap/charmed-mongodb/current" +- +- kube_cmd = "kubectl" +- selector = "app.kubernetes.io/name=mongodb-k8s" +- +- conf_paths = { +- "MONGODB_CONF": "/etc/mongod", +- "MONGODB_LOGS": "/var/log/mongodb", +- } +- +- option_list = [ +- PluginOpt( +- name="dumpdbs", +- default=False, +- val_type=bool, +- desc="Set to true to dump server information.", +- ), +- PluginOpt( +- "dbuser", +- default="", +- val_type=str, +- desc="Username for database dump collection", +- ), +- PluginOpt( +- "dbpass", +- default="", +- val_type=str, +- desc="Password for database dump collection", +- ), +- ] +- +- regular_commands: Tuple[Tuple[str, str], ...] = ( +- ("EJSON.stringify(db.serverStatus())", "server_status.txt"), +- ("EJSON.stringify(rs.status())", "replicaset_status.txt"), +- ("EJSON.stringify(db.getUsers())", "db_users.txt"), +- ("EJSON.stringify(db.getRoles())", "db_roles.txt"), +- ( +- "EJSON.stringify(db.adminCommand({listDatabases: 1}))", +- "db_databases.txt", +- ), +- ) +- +- config_server_commands: Tuple[Tuple[str, str], ...] = ( +- ("EJSON.stringify(sh.status())", "shard_cluster_status.txt"), +- ("EJSON.stringify(sh.listShards())", "shard_shards.txt"), +- ) +- +- def _join_conf_path(self, base: str, *parts: str): +- stripped_parts = [p.lstrip(os.path.sep) for p in parts] +- return self.path_join(base, *stripped_parts) +- +- @staticmethod +- def _match_role(role: str) -> Optional[Role]: +- if role == "configsvr": +- return "config-server" +- if role == "shardsvr": +- return "shard" +- if role == "replication": +- return "replication" +- return None +- +- def vm_role(self) -> Optional[Role]: +- role: str = "replication" +- conf_path = self._join_conf_path( +- self.snap_path_current, self.conf_paths["MONGODB_CONF"] +- ) +- try: +- with open( +- f"{conf_path}/mongod.conf", +- encoding="utf-8", +- ) as f: +- data = yaml.safe_load(f) +- sharding_conf = data.get("sharding", {}) +- if sharding_conf: +- role = sharding_conf.get("clusterRole", "") +- +- return self._match_role(role) +- except FileNotFoundError: +- return None +- +- def k8s_role(self, kube_cmd: str, cont: str, pod: str) -> Optional[Role]: +- role: str = "replication" +- +- # Cat the configuration file. +- cat_conf_cmd = ( +- f"{kube_cmd} exec -c {cont} {pod} -- " +- f"cat {self.conf_paths['MONGODB_CONF']}/mongod.conf" +- ) +- result = self.exec_cmd(cat_conf_cmd) +- +- if result.get("status") != 0: +- return None +- +- data = yaml.safe_load(result.get("output", "")) +- sharding_conf = data.get("sharding", {}) +- if sharding_conf: +- role = sharding_conf.get("clusterRole", "") +- return self._match_role(role) +- +- def _get_db_credentials(self) -> Tuple[Optional[str], Optional[str]]: +- db_user = self.get_option("dbuser") +- db_pass = self.get_option("dbpass") +- +- if not db_user: +- if "MONGODB_USER" in os.environ: +- self.soslog.info( +- "MONGODB_USER present: Using MONGODB_USER environment" +- "variable, user did not provide username." +- ) +- db_user = os.environ["MONGODB_USER"] +- else: +- self.soslog.warning("error: Missing credentials (username)") +- return None, None +- +- if not db_pass: +- if "MONGODB_PWD" in os.environ: +- self.soslog.info( +- "MONGODB_PWD present: Using MONGODB_PWD environment " +- "variable, user did not provide password." +- ) +- db_pass = os.environ["MONGODB_PWD"] +- else: +- self.soslog.warning("error: Missing credentials (password)") +- return None, None +- +- return db_user, db_pass +- +- def _process_snap(self): +- role = self.vm_role() +- +- if not role: +- # The service is not properly set up by the charm, exiting. +- return +- +- base_conf = self._join_conf_path( +- self.snap_path_current, self.conf_paths["MONGODB_CONF"] +- ) +- base_logs = self._join_conf_path( +- self.snap_path_common, self.conf_paths["MONGODB_LOGS"] +- ) +- +- all_logs = self.get_option("all_logs") +- +- # Hide certificates and cluster keyfile. +- self.add_forbidden_path( +- [ +- f"{base_conf}/*.pem", +- f"{base_conf}/*.crt", +- f"{base_conf}/keyFile", +- ] +- ) +- self.add_copy_spec([base_conf]) +- +- if all_logs: +- self.add_copy_spec([f"{base_logs}/*"]) +- else: +- self.add_copy_spec([f"{base_logs}/*.log"]) +- +- lines = None if all_logs else 500 +- self.add_journal("snap.charmed-mongodb.*", lines=lines) +- +- self.add_cmd_output("snap info charmed-mongodb") +- +- # METRICS +- self.add_cmd_output( +- "curl http://127.0.0.1:9216/metrics", "mongodb_exporter-metrics" +- ) +- +- if not self.get_option("dumpdbs"): +- return +- +- db_user, db_pass = self._get_db_credentials() +- if not db_user or not db_pass: +- return +- +- mongodb_uri = self._build_uri(db_user, Substrate.VM) +- mongos_uri = self._build_uri(db_user, Substrate.VM, 27018) +- mongodb_cmd = "charmed-mongodb.mongosh" +- env = {"MONGODB_PWD": db_pass} +- +- # --- REGULAR INFORMATION --- +- for command, suggest_filename in self.regular_commands: +- self.add_cmd_output( +- ( +- f"sh -c '{mongodb_cmd} {mongodb_uri} --quiet " +- f"--eval \"{command}\"'" +- ), +- suggest_filename=suggest_filename, +- env=env, +- ) +- +- # --- SHARDING INFORMATION --- +- if role == "config-server": +- for command, suggest_filename in self.config_server_commands: +- command = ( +- f"sh -c '{mongodb_cmd} {mongos_uri} --quiet " +- f"--eval \"{command}\"'" +- ) +- self.add_cmd_output( +- command, +- suggest_filename=suggest_filename, +- env=env, +- ) +- +- def _build_uri( +- self, +- db_user: str, +- substrate: Substrate, +- port: int = 27017, +- ) -> str: +- base_conf = self.conf_paths["MONGODB_CONF"] +- args: Dict[str, str] = {"authSource": "admin"} +- if substrate == Substrate.VM: +- base_conf = self._join_conf_path(self.snap_path_current, base_conf) +- +- external_ca = Path(f"{base_conf}/external-ca.crt") +- external_cert = Path(f"{base_conf}/external-cert.pem") +- if external_ca.exists() and external_cert.exists(): +- args |= { +- "tls": "true", +- "tlsCertificateKeyFile": f"{external_cert}", +- "tlsCaFile": f"{external_ca}", +- } +- _args = urlencode(args) +- user = quote_plus(db_user) +- host = "127.0.0.1" +- return f"mongodb://{user}:${{MONGODB_PWD}}@{host}:{port}/admin?{_args}" +- +- def _determine_namespaces(self) -> list[str]: +- namespaces = self.exec_cmd( +- f"{self.kube_cmd} get pods -A -l {self.selector} " +- "-o jsonpath='{.items[*].metadata.namespace}'" +- ) +- if namespaces["status"] == 0: +- return list(set(namespaces["output"].strip().split())) +- return [] +- +- def _get_pod_names(self, namespace) -> list[str]: +- pods = self.exec_cmd( +- f"{self.kube_cmd} -n {namespace} get pods -l {self.selector} " +- "-o jsonpath='{.items[*].metadata.name}'" +- ) +- if pods["status"] == 0: +- return pods["output"].strip().split() +- return [] +- +- def _remote_exec( +- self, +- kube_cmd: str, +- cont: str, +- pod: str, +- mongod_cmd: str, +- uri: str, +- password: str, +- cmd: str, +- cmd_name: str, +- ): +- output_file = f"/tmp/eval_{cmd_name}" # nosec: B108 +- # We first execute the command and write into a file +- query_cmd = ( +- f"{kube_cmd} exec -c {cont} {pod} -- " +- f'sh -lc \'export MONGODB_PWD="{password}"; {mongod_cmd} {uri} ' +- f'--quiet --eval "{cmd}" > {output_file}\'' +- ) +- +- self.exec_cmd(query_cmd) +- +- # We then cat that file to have the command output. +- cat_cmd = ( +- f"{kube_cmd} exec -c {cont} {pod} -- " +- f"sh -lc 'cat {output_file} && rm {output_file}'" +- ) +- +- self.add_cmd_output( +- cmds=cat_cmd, +- suggest_filename=f"{pod}_{cmd_name}", +- ) +- +- def _collect_per_namespace(self, ns: str, all_logs: bool): +- kube_cmd = f"{self.kube_cmd} -n {ns}" +- +- mongodb_cont = "mongod" +- pods = self._get_pod_names(ns) +- logs_path = self.conf_paths["MONGODB_LOGS"] +- conf_path = self.conf_paths["MONGODB_CONF"] +- +- # Get the config and logs from each pod +- dump_files_path = self.get_cmd_output_path() +- for path in self.conf_paths.values(): +- for pod in pods: +- name_prefix = f"{dump_files_path}/pods/{pod}/{path}" +- os.makedirs(name_prefix, exist_ok=True) +- copy_cmd = ( +- f"{kube_cmd} cp -c {mongodb_cont} " +- f"{pod}:{path} {name_prefix}" +- ) +- self.exec_cmd(copy_cmd) +- +- for pod in pods: +- if all_logs: # This is all_logs +- self.add_copy_spec([f"{dump_files_path}/{pod}/{logs_path}/*"]) +- else: +- self.add_copy_spec( +- [f"{dump_files_path}/{pod}/{logs_path}/*.log"] +- ) +- +- self.add_forbidden_path( +- [ +- f"{dump_files_path}/pods/{pod}/{conf_path}/*.pem", +- f"{dump_files_path}/pods/{pod}/{conf_path}/*.crt", +- f"{dump_files_path}/pods/{pod}/{conf_path}/keyFile", +- ] +- ) +- self.add_copy_spec([f"{dump_files_path}/pods/{pod}/{conf_path}"]) +- +- # METRICS +- for pod in pods: +- query_cmd = ( +- f"{kube_cmd} exec -c {mongodb_cont} {pod} -- " +- "sh -lc curl localhost:9216/metrics" +- ) +- self.add_cmd_output( +- cmds=query_cmd, +- suggest_filename=f"mongodb_exporter-metrics-{pod}", +- ) +- +- if not self.get_option("dumpdbs"): +- return +- +- db_user, db_pass = self._get_db_credentials() +- if not db_user or not db_pass: +- return +- +- mongodb_uri = self._build_uri(db_user, Substrate.K8S) +- mongos_uri = self._build_uri(db_user, Substrate.K8S, 27018) +- mongodb_cmd = "mongosh" +- +- for pod in pods: +- role = self.k8s_role(kube_cmd, mongodb_cont, pod) +- for command, suggest_filename in self.regular_commands: +- self._remote_exec( +- kube_cmd=kube_cmd, +- cont=mongodb_cont, +- pod=pod, +- mongod_cmd=mongodb_cmd, +- uri=mongodb_uri, +- password=db_pass, +- cmd=command, +- cmd_name=suggest_filename, +- ) +- +- # --- SHARDING INFORMATION --- +- if role == "config-server": +- for command, suggest_filename in self.config_server_commands: +- self._remote_exec( +- kube_cmd=kube_cmd, +- cont=mongodb_cont, +- pod=pod, +- mongod_cmd=mongodb_cmd, +- uri=mongos_uri, +- password=db_pass, +- cmd=command, +- cmd_name=suggest_filename, +- ) +- +- def _process_k8s(self): +- all_logs = self.get_option("all_logs") or False +- namespaces = self._determine_namespaces() +- for namespace in namespaces: +- self._collect_per_namespace(namespace, all_logs) +- +- def setup(self) -> None: +- if self.is_installed(self.snap_package): +- self._process_snap() +- +- if is_executable(self.kube_cmd, self.sysroot): +- self._process_k8s() +- +- def postproc(self): +- if self.is_installed(self.snap_package): +- substrate = Substrate.VM +- if not self.vm_role(): +- # Service was not properly set up. +- return +- else: +- substrate = Substrate.K8S +- +- base_conf = self.conf_paths["MONGODB_CONF"] +- +- if substrate == Substrate.VM: +- base_conf = self._join_conf_path(self.snap_path_current, base_conf) +- else: +- base_conf = f"{self.get_cmd_output_path()}/pods/*/{base_conf}" +- shutil.rmtree( +- f"{self.get_cmd_output_path()}/pods", +- ignore_errors=True, +- ) +- +- # --- SCRUB PASSWORDS --- +- self.do_path_regex_sub( +- f"{base_conf}/*", +- regexp=r'("queryPassword": ")[^"]*"', +- subst=r"\1*********", +- ) +--- a/sos/report/plugins/charmed_mongos.py 2026-06-16 10:22:51.000000000 +0200 ++++ /dev/null 2026-07-04 09:21:02.628033198 +0200 +@@ -1,396 +0,0 @@ +-# This file is part of the sos project: https://github.com/sosreport/sos +-# +-# This copyrighted material is made available to anyone wishing to use, +-# modify, copy, or redistribute it subject to the terms and conditions of +-# version 2 of the GNU General Public License. +-# +-# See the LICENSE file in the source distribution for further information. +- +-from enum import Enum +-import shlex +-import shutil +-import secrets +-import string +-import subprocess +-from urllib.parse import quote, quote_plus, urlencode +-import os +-from pathlib import Path +-from typing import Dict, Optional, Tuple +- +-import yaml +- +-from sos.report.plugins import Plugin, PluginOpt, UbuntuPlugin +-from sos.utilities import is_executable +- +- +-class Substrate(Enum): +- VM = "vm" +- K8S = "k8s" +- +- +-class CharmedMongos(Plugin, UbuntuPlugin): +- """The Charmed Mongos plugin is used to collect Mongos configuration +- and logs from the Charmed Mongos snap package or K8s deployment. +- +- If all_logs is set to True, it collects all logs by default. +- The parameters `dbuser` and `dbpass` are used to dump database information, +- replicaset status, shard status, etc. You can provide those parameters with +- the environment variables `MONGOS_USER` and `MONGOS_PWD` +- """ +- +- short_desc = "Charmed Mongos" +- plugin_name = "charmed_mongos" +- +- # Triggers +- packages = ("charmed-mongodb",) +- containers = ("mongos",) +- +- snap_package = "charmed-mongodb" +- snap_path_common = "/var/snap/charmed-mongodb/common" +- snap_path_current = "/var/snap/charmed-mongodb/current" +- +- kube_cmd = "kubectl" +- selector = "app.kubernetes.io/name=mongos-k8s" +- +- conf_paths = { +- "MONGODB_CONF": "/etc/mongod", +- "MONGODB_LOGS": "/var/log/mongodb", +- } +- +- option_list = [ +- PluginOpt( +- name="dumpdbs", +- default=False, +- val_type=bool, +- desc="Set to true to dump server information.", +- ), +- PluginOpt( +- "dbuser", +- default="", +- val_type=str, +- desc="Username for database dump collection", +- ), +- PluginOpt( +- "dbpass", +- default="", +- val_type=str, +- desc="Password for database dump collection", +- ), +- ] +- +- mongos_commands: Tuple[Tuple[str, str], ...] = ( +- ("EJSON.stringify(db.serverStatus())", "server_status.txt"), +- ("EJSON.stringify(db.getUsers())", "db_users.txt"), +- ("EJSON.stringify(db.getRoles())", "db_roles.txt"), +- ( +- "EJSON.stringify(db.adminCommand({listDatabases: 1}))", +- "db_databases.txt", +- ), +- ("EJSON.stringify(sh.status())", "shard_cluster_status.txt"), +- ("EJSON.stringify(sh.listShards())", "shard_shards.txt"), +- ) +- +- def _join_conf_path(self, base: str, *parts: str): +- stripped_parts = [p.lstrip(os.path.sep) for p in parts] +- return self.path_join(base, *stripped_parts) +- +- def _get_db_credentials(self) -> Tuple[Optional[str], Optional[str]]: +- db_user = self.get_option("dbuser") +- db_pass = self.get_option("dbpass") +- +- if not db_user: +- if "MONGOS_USER" in os.environ: +- self.soslog.info( +- "MONGOS_USER present: Using MONGOS_USER environment " +- "variable, user did not provide username." +- ) +- db_user = os.environ["MONGOS_USER"] +- else: +- self.soslog.warning("error: Missing credentials (username)") +- return None, None +- +- if not db_pass: +- if "MONGOS_PWD" in os.environ: +- self.soslog.info( +- "MONGOS_PWD present: Using MONGOS_PWD environment " +- "variable, user did not provide password." +- ) +- db_pass = os.environ["MONGOS_PWD"] +- else: +- self.soslog.warning("error: Missing credentials (password)") +- return None, None +- +- return db_user, db_pass +- +- def vm_config_db(self) -> Optional[str]: +- _conf_file = f"{self.conf_paths['MONGODB_CONF']}/mongos.conf" +- conf_path = self._join_conf_path(self.snap_path_current, _conf_file) +- try: +- with open(conf_path, encoding="utf-8") as f: +- data = yaml.safe_load(f) +- +- return data.get("sharding", {}).get("configDB", None) +- except FileNotFoundError: +- return None +- +- def k8s_config_db( +- self, kube_cmd: str, cont: str, pod: str +- ) -> Optional[str]: +- # Cat the configuration file. +- cat_conf_cmd = ( +- f"{kube_cmd} exec -c {cont} {pod} -- " +- f"cat {self.conf_paths['MONGODB_CONF']}/mongos.conf" +- ) +- result = self.exec_cmd(cat_conf_cmd) +- +- if result.get("status") != 0: +- return None +- +- data = yaml.safe_load(result.get("output", "")) +- return data.get("sharding", {}).get("configDB", None) +- +- def _process_snap(self): +- config_db = self.vm_config_db() +- +- if not config_db: +- # The service is not properly set up by the charm, exiting. +- return +- +- base_conf = self._join_conf_path( +- self.snap_path_current, self.conf_paths["MONGODB_CONF"] +- ) +- base_logs = self._join_conf_path( +- self.snap_path_common, self.conf_paths["MONGODB_LOGS"] +- ) +- +- all_logs = self.get_option("all_logs") +- +- # Hide certificates and cluster keyfile. +- self.add_forbidden_path( +- [ +- f"{base_conf}/*.pem", +- f"{base_conf}/*.crt", +- f"{base_conf}/keyFile", +- f"{base_conf}/mongod.conf", +- ] +- ) +- self.add_copy_spec([base_conf]) +- +- if all_logs: +- self.add_copy_spec([f"{base_logs}/*"]) +- else: +- self.add_copy_spec([f"{base_logs}/*.log"]) +- +- lines = None if all_logs else 500 +- self.add_journal("snap.charmed-mongodb.mongos", lines=lines) +- +- self.add_cmd_output("snap info charmed-mongodb") +- +- if not self.get_option("dumpdbs"): +- return +- +- db_user, db_pass = self._get_db_credentials() +- if not db_user or not db_pass: +- return +- +- mongos_uri = self._build_uri(db_user, Substrate.VM) +- mongodb_cmd = "charmed-mongodb.mongosh" +- env = {"MONGOS_PWD": db_pass} +- +- # --- REGULAR INFORMATION --- +- for command, suggest_filename in self.mongos_commands: +- self.add_cmd_output( +- ( +- f"sh -c '{mongodb_cmd} {mongos_uri} " +- f"--quiet --eval \"{command}\"'" +- ), +- suggest_filename=suggest_filename, +- env=env, +- ) +- +- def _build_uri( +- self, +- db_user: str, +- substrate: Substrate, +- ) -> str: +- base_conf = self.conf_paths["MONGODB_CONF"] +- args: Dict[str, str] = {"authSource": "admin"} +- +- if substrate == Substrate.VM: +- base_conf = self._join_conf_path(self.snap_path_current, base_conf) +- +- external_ca = Path(f"{base_conf}/external-ca.crt") +- external_cert = Path(f"{base_conf}/external-cert.pem") +- if external_ca.exists() and external_cert.exists(): +- args |= { +- "tls": "true", +- "tlsCertificateKeyFile": f"{external_cert}", +- "tlsCaFile": f"{external_ca}", +- } +- _args = urlencode(args) +- user = quote_plus(db_user) +- host = "127.0.0.1:27018" +- socket_path = Path(f"{self.snap_path_current}/var/mongodb-27018.sock") +- if substrate == Substrate.VM and socket_path.exists(): +- host = quote(f"{socket_path}", safe="") +- +- if substrate == Substrate.K8S: +- return f"mongodb://{user}@{host}/admin?{_args}" +- return f"mongodb://{user}:${{MONGOS_PWD}}@{host}/admin?{_args}" +- +- def _determine_namespaces(self) -> list[str]: +- namespaces = self.exec_cmd( +- f"{self.kube_cmd} get pods -A -l {self.selector} " +- "-o jsonpath='{.items[*].metadata.namespace}'" +- ) +- if namespaces["status"] == 0: +- return list(set(namespaces["output"].strip().split())) +- return [] +- +- def _get_pod_names(self, namespace) -> list[str]: +- pods = self.exec_cmd( +- f"{self.kube_cmd} -n {namespace} get pods -l {self.selector} " +- "-o jsonpath='{.items[*].metadata.name}'" +- ) +- if pods["status"] == 0: +- return pods["output"].strip().split() +- return [] +- +- def _remote_exec( +- self, +- kube_cmd: str, +- cont: str, +- pod: str, +- mongod_cmd: str, +- uri: str, +- password: str, +- cmd: str, +- cmd_name: str, +- ): +- choices = string.ascii_letters + string.digits +- randstring = "".join([secrets.choice(choices) for _ in range(16)]) +- output_file = f"/tmp/eval_{cmd_name}_{randstring}.txt" # nosec: B108 +- # We first execute the command and write into a file +- query_cmd = shlex.split( +- f"{kube_cmd} exec -i -c {cont} {pod} -- " +- f'sh -lc \'{mongod_cmd} {uri} ' +- f'--quiet --eval "{cmd}" --password > {output_file}\'' +- ) +- +- # Pass the password as stdin that is forwarded to the +- # container through kubectl exec +- subprocess.run(query_cmd, input=password.encode(), check=False) +- +- # We then cat that file to have the command output. +- cat_cmd = ( +- f"{kube_cmd} exec -c {cont} {pod} -- " +- f"sh -lc 'cat {output_file} && rm {output_file}'" +- ) +- +- self.add_cmd_output( +- cmds=cat_cmd, +- suggest_filename=f"{pod}_{cmd_name}", +- ) +- +- def _collect_per_namespace(self, ns: str, all_logs: bool): +- kube_cmd = f"{self.kube_cmd} -n {ns}" +- +- mongodb_cont = "mongos" +- pods = self._get_pod_names(ns) +- logs_path = self.conf_paths["MONGODB_LOGS"] +- conf_path = self.conf_paths["MONGODB_CONF"] +- +- # Get the config and logs from each pod +- dump_files_path = self.get_cmd_output_path() +- for path in self.conf_paths.values(): +- for pod in pods: +- name_prefix = f"{dump_files_path}/pods/{pod}/{path}" +- os.makedirs(name_prefix, exist_ok=True) +- copy_cmd = ( +- f"{kube_cmd} cp -c {mongodb_cont} " +- f"{pod}:{path} {name_prefix}" +- ) +- self.exec_cmd(copy_cmd) +- +- for pod in pods: +- if all_logs: # This is all_logs +- self.add_copy_spec([f"{dump_files_path}/{pod}/{logs_path}/*"]) +- else: +- self.add_copy_spec( +- [f"{dump_files_path}/{pod}/{logs_path}/*.log"] +- ) +- +- self.add_forbidden_path( +- [ +- f"{dump_files_path}/pods/{pod}/{conf_path}/*.pem", +- f"{dump_files_path}/pods/{pod}/{conf_path}/*.crt", +- f"{dump_files_path}/pods/{pod}/{conf_path}/keyFile", +- f"{dump_files_path}/pods/{pod}/{conf_path}/mongod.conf", +- ] +- ) +- self.add_copy_spec([f"{dump_files_path}/pods/{pod}/{conf_path}"]) +- +- if not self.get_option("dumpdbs"): +- return +- +- db_user, db_pass = self._get_db_credentials() +- if not db_user or not db_pass: +- return +- +- mongos_uri = self._build_uri(db_user, Substrate.K8S) +- mongodb_cmd = "mongosh" +- +- for pod in pods: +- config_db = self.k8s_config_db(kube_cmd, mongodb_cont, pod) +- if not config_db: +- continue +- for command, suggest_filename in self.mongos_commands: +- self._remote_exec( +- kube_cmd=kube_cmd, +- cont=mongodb_cont, +- pod=pod, +- mongod_cmd=mongodb_cmd, +- uri=mongos_uri, +- password=db_pass, +- cmd=command, +- cmd_name=suggest_filename, +- ) +- +- def _process_k8s(self): +- all_logs = self.get_option("all_logs") or False +- namespaces = self._determine_namespaces() +- for namespace in namespaces: +- self._collect_per_namespace(namespace, all_logs) +- +- def setup(self) -> None: +- if self.is_installed(self.snap_package): +- self._process_snap() +- +- if is_executable(self.kube_cmd, self.sysroot): +- self._process_k8s() +- +- def postproc(self): +- if self.is_installed(self.snap_package): +- substrate = Substrate.VM +- if not self.vm_config_db(): +- # Service was not properly set up. +- return +- else: +- substrate = Substrate.K8S +- +- base_conf = self.conf_paths["MONGODB_CONF"] +- if substrate == Substrate.VM: +- base_conf = self._join_conf_path(self.snap_path_current, base_conf) +- else: +- base_conf = f"{self.get_cmd_output_path()}/pods/*/{base_conf}" +- shutil.rmtree( +- f"{self.get_cmd_output_path()}/pods", +- ignore_errors=True +- ) +- +- # --- SCRUB PASSWORDS --- +- self.do_path_regex_sub( +- f"{base_conf}/*", +- regexp=r'("queryPassword": ")[^"]*"', +- subst=r"\1*********", +- ) +--- a/sos/report/plugins/charmed_zookeeper.py 2026-06-16 10:22:51.000000000 +0200 ++++ /dev/null 2026-07-04 09:21:02.628033198 +0200 +@@ -1,205 +0,0 @@ +-# This file is part of the sos project: https://github.com/sosreport/sos +-# +-# This copyrighted material is made available to anyone wishing to use, +-# modify, copy, or redistribute it subject to the terms and conditions of +-# version 2 of the GNU General Public License. +-# +-# See the LICENSE file in the source distribution for further information.# +- +-import glob +-import json +-import os +-import re +-from datetime import datetime +-from functools import cached_property +- +-from sos.report.plugins import Plugin, UbuntuPlugin +- +-PATHS = { +- "CONF": "/var/snap/charmed-zookeeper/current/etc/zookeeper", +- "LOGS": "/var/snap/charmed-zookeeper/common/var/log/zookeeper", +- "DATA-LOG": "/var/snap/charmed-zookeeper/common/var/lib/zookeeper/data-log", # noqa: E501 # pylint:disable=line-too-long +- "DATA": "/var/snap/charmed-zookeeper/common/var/lib/zookeeper/data", +- "BIN": "/snap/charmed-zookeeper/current/opt/zookeeper/bin", +- "JRE": "/snap/charmed-zookeeper/current/usr/lib/jvm/java-11-openjdk-amd64/jre", # noqa: E501 # pylint:disable=line-too-long +-} +- +-DATE_FORMAT = "%Y-%m-%d-%H" +-CLIENT_JAAS = "client-jaas.cfg" +- +- +-class CharmedZooKeeper(Plugin, UbuntuPlugin): +- short_desc = "Charmed ZooKeeper" +- plugin_name = "charmed_zookeeper" +- packages = ("charmed-zookeeper",) +- +- default_env = { +- "JAVA_HOME": PATHS["JRE"], +- "CLIENT_JVMFLAGS": f"-Djava.security.auth.login.config={PATHS['CONF']}/{CLIENT_JAAS}", # noqa: E501 # pylint:disable=line-too-long +- "SERVER_JVMFLAGS": "", +- } +- +- @cached_property +- def super_password(self) -> str: +- try: +- with open( +- f"{PATHS['CONF']}/{CLIENT_JAAS}", +- "r", +- encoding="utf-8" +- ) as f: +- for line in f.readlines(): +- if "super" in line: +- pw = line.split("=")[1].replace(";", "") +- return pw.replace('"', "").strip() +- +- return "" +- except FileNotFoundError: +- return "" +- +- def setup(self): +- if not self.super_password: +- # service not properly set-up by the charm, skip +- return +- +- # --- FILE EXCLUSIONS --- +- +- all_logs = self.get_option("all_logs") +- since = self.get_option("since") +- +- for file in glob.glob(f"{PATHS['LOGS']}/*"): +- date = re.search( +- pattern=r"([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2})", string=file +- ) +- +- # include files without date, aka current files +- if not date: +- continue +- +- file_dt = datetime.strptime(date.group(1), DATE_FORMAT) +- +- if ( +- since +- and not all_logs +- and file_dt < datetime.strptime(str(since), DATE_FORMAT) +- ): +- # skip files outside given range +- self.add_forbidden_path(file) +- +- # hide keys/stores +- self.add_forbidden_path([ +- f"{PATHS['CONF']}/*.pem", +- f"{PATHS['CONF']}/*.key", +- f"{PATHS['CONF']}/*.p12", +- f"{PATHS['CONF']}/*.jks", +- f"{PATHS['CONF']}/{CLIENT_JAAS}", +- ]) +- +- # --- FILE INCLUSIONS --- +- +- self.add_copy_spec( +- [ +- f"{PATHS['CONF']}", +- f"{PATHS['LOGS']}", +- ] +- ) +- +- # --- SNAP LOGS --- +- +- if all_logs: +- self.add_cmd_output( +- "snap logs -n all charmed-zookeeper.daemon", +- suggest_filename="snap_logs_charmed-zookeeper_daemon", +- ) +- else: +- self.add_cmd_output( +- "snap logs -n 500 charmed-zookeeper.daemon", +- suggest_filename="snap_logs_charmed-zookeeper_daemon", +- ) +- +- # --- JMX METRICS --- +- +- self.add_cmd_output( +- "curl localhost:9998/metrics", +- suggest_filename="jmx-metrics" +- ) +- +- # --- PROVIDER METRICS --- +- +- self.add_cmd_output( +- "curl localhost:7000/metrics", +- suggest_filename="provider-metrics" +- ) +- +- def collect(self): +- # --- TRANSACTIONS --- +- all_logs = self.get_option("all_logs") +- since = self.get_option("since") +- +- for file in glob.glob(f"{PATHS['DATA-LOG']}/version-2/*"): +- transactions = self.exec_cmd( +- f"{PATHS['BIN']}/zkTxnLogToolkit.sh -d {file}", +- env=self.default_env, +- ) +- +- if not (transactions and transactions["status"] == 0): +- continue +- +- valid_dt_transactions = [] +- for transaction in transactions["output"].splitlines(): +- try: +- log_dt = datetime.strptime( +- " ".join(transaction.split()[:4]), +- "%m/%d/%y %I:%M:%S %p %Z", +- ) +- except ValueError: # must've been a bad line +- continue +- +- if ( +- since +- and not all_logs +- and log_dt < datetime.strptime(str(since), DATE_FORMAT) +- ): +- continue +- +- valid_dt_transactions.append(transaction) +- +- with self.collection_file( +- f"zookeeper-transaction-{os.path.basename(file)}" +- ) as f: +- f.write("\n".join(valid_dt_transactions)) +- +- # --- SNAPSHOT --- +- files = glob.glob(f"{PATHS['DATA']}/version-2/*") +- if not files: +- return +- +- most_recent_file = sorted(files)[-1] +- snapshot_json = self.exec_cmd( +- f"{PATHS['BIN']}/zkSnapShotToolkit.sh -json {most_recent_file}", +- env=self.default_env, +- ) +- snapshot = {} +- +- if snapshot_json and snapshot_json["status"] == 0: +- for line in snapshot_json["output"].splitlines(): +- try: +- snapshot = json.loads(line) +- break +- except json.JSONDecodeError: +- continue +- +- with self.collection_file("zookeeper-snapshot") as f: +- f.write(json.dumps(snapshot, indent=4)) +- +- def postproc(self): +- if not self.super_password: +- # service not properly set-up by the charm, skip +- return +- +- # --- SCRUB PASSWORDS --- +- +- self.do_path_regex_sub( +- f"{PATHS['CONF']}/*", +- r'(password=")[^"]*', +- r"\1*********", +- ) diff --git a/SOURCES/0003-revert-PR4092.patch b/SOURCES/0003-revert-PR4092.patch deleted file mode 100644 index 04539d0..0000000 --- a/SOURCES/0003-revert-PR4092.patch +++ /dev/null @@ -1,49 +0,0 @@ ---- a/sos/upload/targets/__init__.py 2025-09-16 19:57:27.294642506 +0200 -+++ b/sos/upload/targets/__init__.py 2025-09-16 19:59:44.498573843 +0200 -@@ -465,7 +465,7 @@ - self.upload_password or - self._upload_password) - -- def upload_sftp(self, user=None, password=None, user_dir=None): -+ def upload_sftp(self, user=None, password=None): - """Attempts to upload the archive to an SFTP location. - - Due to the lack of well maintained, secure, and generally widespread -@@ -540,13 +540,10 @@ - raise Exception("Unable to connect via SFTP to " - f"{self.get_upload_url_string()}") - -- # certain implementations require file to be put in the user dir -- put_cmd = ( -- f"put {self.upload_archive_name} " -- f"{f'{user_dir}/' if user_dir else ''}" -- f"{self._get_sftp_upload_name()}" -- ) -+ put_cmd = (f'put {self.upload_archive_name} ' -+ f'{self._get_sftp_upload_name()}') - ret.sendline(put_cmd) -+ - put_expects = [ - '100%', - pexpect.TIMEOUT, ---- a/sos/upload/targets/redhat.py 2025-09-16 19:57:36.804628207 +0200 -+++ b/sos/upload/targets/redhat.py 2025-09-16 20:00:52.578728154 +0200 -@@ -145,7 +145,7 @@ - return fname - - # pylint: disable=too-many-branches -- def upload_sftp(self, user=None, password=None, user_dir=None): -+ def upload_sftp(self, user=None, password=None): - """Override the base upload_sftp to allow for setting an on-demand - generated anonymous login for the RH SFTP server if a username and - password are not given -@@ -217,8 +217,7 @@ - f"{anon.status_code}): {anon.json()}" - ) - if _user and _token: -- return super().upload_sftp(user=_user, password=_token, -- user_dir=_user) -+ return super().upload_sftp(user=_user, password=_token) - raise Exception("Could not retrieve valid or anonymous credentials") - - def check_file_too_big(self, archive): diff --git a/SOURCES/0002-sosreport-binary.patch b/SOURCES/0003-sosreport-binary.patch similarity index 100% rename from SOURCES/0002-sosreport-binary.patch rename to SOURCES/0003-sosreport-binary.patch diff --git a/SOURCES/0004-revert-PR4092-and-PR4275.patch b/SOURCES/0004-revert-PR4092-and-PR4275.patch new file mode 100644 index 0000000..5b3b417 --- /dev/null +++ b/SOURCES/0004-revert-PR4092-and-PR4275.patch @@ -0,0 +1,121 @@ +diff --git a/sos/upload/targets/__init__.py b/sos/upload/targets/__init__.py +index 9e905e1063..6f72d963ea 100644 +--- a/sos/upload/targets/__init__.py ++++ b/sos/upload/targets/__init__.py +@@ -540,36 +540,7 @@ def upload_sftp(self, user=None, password=None, user_dir=None): + raise Exception("Unable to connect via SFTP to " + f"{self.get_upload_url_string()}") + +- # user_dir indicates that we need to switch into a specific user +- # directory. If ChRootDirectory is set, this happens automatically +- # so check if the PWD contains the user. If it does, we are +- # already in the user directory so set user_dir to None +- if user_dir: +- user_match = False +- ret.sendline('pwd') +- pwd_expects = [ +- 'sftp>', +- 'Invalid command.', +- pexpect.TIMEOUT, +- pexpect.EOF +- ] +- pwd_cmd = ret.expect(pwd_expects, timeout=10) == 0 +- if pwd_cmd: +- # Extract the path from child.before +- raw_output = ret.before +- user_match = re.search(user, raw_output) +- else: +- self.ui_log.warning("This server does not support the PWD " +- "command - unable to verify user " +- "directory. Attempting upload without " +- "changing to user directory.") +- user_dir = None +- +- if user_match: +- user_dir = None +- + # certain implementations require file to be put in the user dir +- # so we prepend the user directory to the file path + put_cmd = ( + f"put {self.upload_archive_name} " + f"{f'{user_dir}/' if user_dir else ''}" +diff --git a/sos/upload/targets/redhat.py b/sos/upload/targets/redhat.py +index 636a645fe6..0cfb80eecd 100644 +--- a/sos/upload/targets/redhat.py ++++ b/sos/upload/targets/redhat.py +@@ -158,7 +158,6 @@ def upload_sftp(self, user=None, password=None, user_dir=None): + " for obtaining SFTP auth token.") + _token = None + _user = None +- _user_dir = None + + # We may have a device token already if we attempted + # to upload via http but the upload failed. So +@@ -192,7 +191,6 @@ def upload_sftp(self, user=None, password=None, user_dir=None): + # credentials are valid + _user = json.loads(ret.text)['username'] + _token = json.loads(ret.text)['token'] +- _user_dir = f"/users/{_user}" + else: + self.ui_log.debug( + f"DEBUG: auth attempt failed (status: {ret.status_code}): " +@@ -220,7 +218,7 @@ def upload_sftp(self, user=None, password=None, user_dir=None): + ) + if _user and _token: + return super().upload_sftp(user=_user, password=_token, +- user_dir=_user_dir) ++ user_dir=_user) + raise Exception("Could not retrieve valid or anonymous credentials") + + def check_file_too_big(self, archive): +-- +2.47.0 +--- a/sos/upload/targets/__init__.py 2025-09-16 19:57:27.294642506 +0200 ++++ b/sos/upload/targets/__init__.py 2025-09-16 19:59:44.498573843 +0200 +@@ -465,7 +465,7 @@ + self.upload_password or + self._upload_password) + +- def upload_sftp(self, user=None, password=None, user_dir=None): ++ def upload_sftp(self, user=None, password=None): + """Attempts to upload the archive to an SFTP location. + + Due to the lack of well maintained, secure, and generally widespread +@@ -540,13 +540,10 @@ + raise Exception("Unable to connect via SFTP to " + f"{self.get_upload_url_string()}") + +- # certain implementations require file to be put in the user dir +- put_cmd = ( +- f"put {self.upload_archive_name} " +- f"{f'{user_dir}/' if user_dir else ''}" +- f"{self._get_sftp_upload_name()}" +- ) ++ put_cmd = (f'put {self.upload_archive_name} ' ++ f'{self._get_sftp_upload_name()}') + ret.sendline(put_cmd) ++ + put_expects = [ + '100%', + pexpect.TIMEOUT, +--- a/sos/upload/targets/redhat.py 2025-09-16 19:57:36.804628207 +0200 ++++ b/sos/upload/targets/redhat.py 2025-09-16 20:00:52.578728154 +0200 +@@ -145,7 +145,7 @@ + return fname + + # pylint: disable=too-many-branches +- def upload_sftp(self, user=None, password=None, user_dir=None): ++ def upload_sftp(self, user=None, password=None): + """Override the base upload_sftp to allow for setting an on-demand + generated anonymous login for the RH SFTP server if a username and + password are not given +@@ -217,8 +217,7 @@ + f"{anon.status_code}): {anon.json()}" + ) + if _user and _token: +- return super().upload_sftp(user=_user, password=_token, +- user_dir=_user) ++ return super().upload_sftp(user=_user, password=_token) + raise Exception("Could not retrieve valid or anonymous credentials") + + def check_file_too_big(self, archive): diff --git a/SOURCES/0005-foreman-installer-Scrub-secrets-in-CLI-arg-dumps.patch b/SOURCES/0005-foreman-installer-Scrub-secrets-in-CLI-arg-dumps.patch new file mode 100644 index 0000000..898d720 --- /dev/null +++ b/SOURCES/0005-foreman-installer-Scrub-secrets-in-CLI-arg-dumps.patch @@ -0,0 +1,37 @@ +From 2fab657e3909f76e31bc6c56a5ad26f86a9925e5 Mon Sep 17 00:00:00 2001 +From: Pavel Moravec +Date: Tue, 23 Jun 2026 17:17:01 +0200 +Subject: [PATCH] [foreman-installer] Scrub secrets in CLI arg dumps + +Installer logs dump CLI args we need to scrub. + +Closes: #4367 + +Signed-off-by: Pavel Moravec +--- + sos/report/plugins/foreman_installer.py | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/sos/report/plugins/foreman_installer.py b/sos/report/plugins/foreman_installer.py +index f55cde23..89eef2e4 100644 +--- a/sos/report/plugins/foreman_installer.py ++++ b/sos/report/plugins/foreman_installer.py +@@ -54,11 +54,12 @@ class ForemanInstaller(Plugin, DebianPlugin, UbuntuPlugin): + r"::(.*(token|secret|key|passw).*)\") value:) " + r"(.*)") + self.do_path_regex_sub(install_logs, logs_debug_reg, r"\1 \2 ********") +- # also hide passwords in yet different formats ++ # also hide passwords in yet different formats, including CLI arg dumps + self.do_path_regex_sub( + install_logs, +- r"password(\", \"|=|\" value: \"|\": \")(.*?)(\", \".*|\"]]|\"|$)", +- r"password\1********\3") ++ r"((?:password|consumer-key|consumer-secret|key-secret|secret-key|" ++ r"oauth-key|oauth-secret)(?:\", \"|\": \"|=))([^\"\n]*)(\"|$)", ++ r"\1********\3") + self.do_path_regex_sub( + "/var/log/foreman-installer/foreman-proxy*", + r"(\s*proxy_password\s=) (.*)", +-- +2.54.0 + diff --git a/SOURCES/0006-foremanctl-valkey-PR4376.patch b/SOURCES/0006-foremanctl-valkey-PR4376.patch new file mode 100644 index 0000000..8dadec4 --- /dev/null +++ b/SOURCES/0006-foremanctl-valkey-PR4376.patch @@ -0,0 +1,97 @@ +From 5e7a01d9df2bc4e1f2659ace959ea52228ee1eee Mon Sep 17 00:00:00 2001 +From: akumari +Date: Wed, 1 Jul 2026 09:26:52 +0530 +Subject: [PATCH 1/2] [foremanctl] Enable plugin for containerized deployments + +The plugin required foremanctl package which doesn't exist in +containerized mode. Add container detection to enable the plugin +and collect foremanctl health diagnostics for support cases. + +Signed-off-by: akumari +--- + sos/report/plugins/foremanctl.py | 29 +++++++++++++++++++++++++---- + 1 file changed, 25 insertions(+), 4 deletions(-) + +diff --git a/sos/report/plugins/foremanctl.py b/sos/report/plugins/foremanctl.py +index 1b6ba4a4..0425cc05 100644 +--- a/sos/report/plugins/foremanctl.py ++++ b/sos/report/plugins/foremanctl.py +@@ -19,6 +19,7 @@ class Foremanctl(Plugin, RedHatPlugin, DebianPlugin, UbuntuPlugin): + plugin_name = 'foremanctl' + profiles = ('sysmgmt',) + packages = ('foremanctl', ) ++ containers = ('foreman', 'foreman-proxy',) + + def setup(self): + self.add_copy_spec([ +@@ -29,14 +30,34 @@ class Foremanctl(Plugin, RedHatPlugin, DebianPlugin, UbuntuPlugin): + + self.add_cmd_output([ + "foremanctl features", ++ "foremanctl health", + ]) + + self.add_dir_listing(["/var/lib/foremanctl/"], recursive=True) + + def postproc(self): +- self.do_path_regex_sub("/var/lib/foremanctl/parameters.yaml", +- r"(foreman_initial_admin_password:\s*)(.*)", +- r"\1********") +- ++ # Scrub passwords, credentials, tokens, secrets, and keys ++ self.do_path_regex_sub( ++ "/var/lib/foremanctl/parameters.yaml", ++ r"((.*)?(passw|cred|token|secret|key).*(\:\s|=))(.*)", ++ r"\1********") ++ ++ # Scrub passwords from foremanctl logs - Pattern 1: key=value format ++ self.do_path_regex_sub( ++ "/var/log/foremanctl/foremanctl.*log*", ++ r"((passw|cred|token|secret|key)\w*\s*=\s*)(.*?)(\s|,|\"|'|$)", ++ r"\1********\4") ++ ++ # Scrub passwords from foremanctl logs - Pattern 2: "password something" format ++ self.do_path_regex_sub( ++ "/var/log/foremanctl/foremanctl.*log*", ++ r"(password\s+)(.*?)(\s|,|\"|$)", ++ r"\1********\3") ++ ++ # Scrub admin credentials in username:password format ++ self.do_path_regex_sub( ++ "/var/log/foremanctl/foremanctl.*log*", ++ r"(Admin credentials:\s+\w+:)(.*?)(\"|,|$)", ++ r"\1********\3") + + # vim: set et ts=4 sw=4 : +-- +2.54.0 + +From 3b6bbcb819e05aecf0fec4767588ea0376c9f218 Mon Sep 17 00:00:00 2001 +From: akumari +Date: Tue, 7 Jul 2026 14:06:40 +0530 +Subject: [PATCH 2/2] [valkey] Add container support for containerized + deployments + +Adds container support to the valkey plugin to enable it +for containerized deployments using foremanctl/quadlet. + +Signed-off-by: akumari +--- + sos/report/plugins/valkey.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/sos/report/plugins/valkey.py b/sos/report/plugins/valkey.py +index 26a74a56..f7b49253 100644 +--- a/sos/report/plugins/valkey.py ++++ b/sos/report/plugins/valkey.py +@@ -19,6 +19,7 @@ class Valkey(Plugin, IndependentPlugin): + profiles = ('services',) + + packages = ('valkey',) ++ containers = ('valkey',) + + var_puppet_gen = "/var/lib/config-data/puppet-generated/valkey" + +-- +2.54.0 + diff --git a/SOURCES/0007-policies-Prefer-most-specific-policy-when-multiple-m.patch b/SOURCES/0007-policies-Prefer-most-specific-policy-when-multiple-m.patch new file mode 100644 index 0000000..5a1f5d1 --- /dev/null +++ b/SOURCES/0007-policies-Prefer-most-specific-policy-when-multiple-m.patch @@ -0,0 +1,74 @@ +From 6085b2580173a7a43de8b541584c82481c617aa2 Mon Sep 17 00:00:00 2001 +From: asadawar +Date: Mon, 27 Jul 2026 17:50:42 +0530 +Subject: [PATCH] [policies] Prefer most specific policy when multiple match + +When multiple policies within the same module return True from +check(), the policy loader now selects the most specific one +(deepest in the class hierarchy) instead of whichever happens +to sort first alphabetically. + +Previously, import_policy() returned classes sorted by name via +inspect.getmembers(), and load() picked the first match. This +caused RHELPolicy to always win over RedHatCoreOSPolicy inside +a toolbox container on RHCOS, because uppercase 'H' sorts before +lowercase 'e' in ASCII. Both policies return True in that context +(RHEL for the container's /etc/redhat-release, RHCOS for the +host's /host/etc/os-release), but the more specific RHCOS policy +was never reached. + +This has existed since RedHatCoreOSPolicy was introduced in 2019 +(commit fa06bc09c95c) but was never visible because RHCOS had no +behavioral differences from RHEL until the archive naming change +in commit 0e919b6. + +The fix collects all matching policies from a module and sorts by +MRO depth (descending), so a subclass is always preferred over +its parent. This is safe because a subclass that returns True from +check() is by definition a more precise match than its parent. + +Assisted-by: Claude Code +Signed-off-by: asadawar +--- + sos/policies/__init__.py | 18 +++++++++++------- + 1 file changed, 11 insertions(+), 7 deletions(-) + +diff --git a/sos/policies/__init__.py b/sos/policies/__init__.py +index 35b3a532..b4920e82 100644 +--- a/sos/policies/__init__.py ++++ b/sos/policies/__init__.py +@@ -29,20 +29,24 @@ def import_policy(name): + return None + + +-def load(cache={}, sysroot=None, init=None, probe_runtime=True, ++def load(cache=None, sysroot=None, init=None, probe_runtime=True, + remote_exec=None, remote_check=''): ++ if cache is None: ++ cache = {} + if 'policy' in cache: + return cache.get('policy') + + import sos.policies.distros + helper = ImporterHelper(sos.policies.distros) ++ matches = [] + for module in helper.get_modules(): +- for policy in import_policy(module): +- if policy.check(remote=remote_check): +- cache['policy'] = policy(sysroot=sysroot, init=init, +- probe_runtime=probe_runtime, +- remote_exec=remote_exec) +- break ++ matches.extend([policy for policy in (import_policy(module) or []) ++ if policy.check(remote=remote_check)]) ++ if matches: ++ matches.sort(key=lambda p: len(p.__mro__), reverse=True) ++ cache['policy'] = matches[0](sysroot=sysroot, init=init, ++ probe_runtime=probe_runtime, ++ remote_exec=remote_exec) + + if sys.platform != 'linux': + raise Exception("SoS is not supported on this platform") +-- +2.55.0 + diff --git a/SOURCES/0008-processor-Limit-sys-devices-system-cpu-cpu-subdirs-c.patch b/SOURCES/0008-processor-Limit-sys-devices-system-cpu-cpu-subdirs-c.patch new file mode 100644 index 0000000..7300877 --- /dev/null +++ b/SOURCES/0008-processor-Limit-sys-devices-system-cpu-cpu-subdirs-c.patch @@ -0,0 +1,90 @@ +From d94095e55d69d5e4135df0193c9726c2789526ab Mon Sep 17 00:00:00 2001 +From: Pavel Moravec +Date: Wed, 29 Jul 2026 10:46:25 +0200 +Subject: [PATCH] [processor] Limit /sys/devices/system/cpu/cpu* subdirs + collected + +For systems with >500 CPUs, collecting all such directories means +millions of files to be collected, what excessivelly slows down the +plugin until its timeout. + +Limit the default number of such directories to 64, configurable via a +plugin option. + +Resolves: #4399 + +Signed-off-by: Pavel Moravec +--- + sos/report/plugins/processor.py | 41 ++++++++++++++++++++++++++++++++- + 1 file changed, 40 insertions(+), 1 deletion(-) + +diff --git a/sos/report/plugins/processor.py b/sos/report/plugins/processor.py +index 9375b5a1..593bd36b 100644 +--- a/sos/report/plugins/processor.py ++++ b/sos/report/plugins/processor.py +@@ -6,7 +6,9 @@ + # + # See the LICENSE file in the source distribution for further information. + +-from sos.report.plugins import Plugin, IndependentPlugin, SoSPredicate ++import re ++from sos.report.plugins import (Plugin, IndependentPlugin, SoSPredicate, ++ PluginOpt) + from sos.policies.distros.ubuntu import UbuntuPolicy + + +@@ -18,6 +20,11 @@ class Processor(Plugin, IndependentPlugin): + profiles = ('system', 'hardware', 'memory') + files = ('/proc/cpuinfo',) + packages = ('cpufreq-utils', 'cpuid') ++ option_list = [ ++ PluginOpt('max_cpu_dirs', default=64, val_type=int, ++ desc='Maximum number of cpu[0-9]+ directories ' ++ 'to collect from /sys/devices/system/cpu'), ++ ] + + cpu_kmods = [] + +@@ -43,7 +50,39 @@ class Processor(Plugin, IndependentPlugin): + # copy /sys/devices/system/cpu/cpuX with separately applied sizelimit + # this is required for systems with tens/hundreds of CPUs where the + # cumulative directory size exceeds 25MB or even 100MB. ++ # Limit cpu[0-9]* directories to avoid excessive collection. ++ # All non-cpu* directories are always collected. ++ max_cpu_dirs = self.get_option('max_cpu_dirs') ++ if max_cpu_dirs < 0: ++ self._log_info(f"Invalid max_cpu_dirs={max_cpu_dirs} value " ++ f"provided, replacing by 0." ++ ) ++ max_cpu_dirs = 0 + cdirs = self.listdir('/sys/devices/system/cpu') ++ ++ if len(cdirs) > max_cpu_dirs: ++ # separate cpu from non-cpu, then limit cpu dirs ++ cpu_pattern = re.compile(r'cpu(\d+)') ++ cpu_dirs = [] ++ other_dirs = [] ++ ++ for cdir in cdirs: ++ if cpu_pattern.fullmatch(cdir): ++ cpu_dirs.append(cdir) ++ else: ++ other_dirs.append(cdir) ++ ++ # Only limit if cpu_dirs specifically exceeds max ++ if len(cpu_dirs) > max_cpu_dirs: ++ self._log_info( ++ f"Limiting cpu directories from {len(cpu_dirs)} to " ++ f"{max_cpu_dirs} (use '-k processor.max_cpu_dirs=N' " ++ f"to change)." ++ ) ++ cpu_dirs = sorted(cpu_dirs)[:max_cpu_dirs] ++ ++ cdirs = other_dirs + cpu_dirs ++ + self.add_copy_spec([ + self.path_join('/sys/devices/system/cpu', cdir) for cdir in cdirs + ]) +-- +2.55.0 + diff --git a/SPECS/sos.spec b/SPECS/sos.spec index 31740b2..5333459 100644 --- a/SPECS/sos.spec +++ b/SPECS/sos.spec @@ -4,8 +4,8 @@ Summary: A set of tools to gather troubleshooting information from a system Name: sos -Version: 4.11.0 -Release: 1%{?dist} +Version: 4.11.2 +Release: 4%{?dist} Group: Applications/System Source0: https://github.com/sosreport/sos/archive/%{version}/sos-%{version}.tar.gz Source1: sos-audit-%{auditversion}.tgz @@ -23,8 +23,13 @@ Recommends: python3-pyyaml Conflicts: vdsm < 4.40 Obsoletes: sos-collector Patch1: 0001-python3-walrus-operator-and-rhel8-changes-only.patch -Patch2: 0002-sosreport-binary.patch -Patch3: 0003-revert-PR4092.patch +Patch2: 0002-remove-unsupported-python36-plugins.patch +Patch3: 0003-sosreport-binary.patch +Patch4: 0004-revert-PR4092-and-PR4275.patch +Patch5: 0005-foreman-installer-Scrub-secrets-in-CLI-arg-dumps.patch +Patch6: 0006-foremanctl-valkey-PR4376.patch +Patch7: 0007-policies-Prefer-most-specific-policy-when-multiple-m.patch +Patch8: 0008-processor-Limit-sys-devices-system-cpu-cpu-subdirs-c.patch %description Sos is a set of tools that gathers information about system @@ -38,6 +43,11 @@ support technicians and developers. %patch -P 1 -p1 %patch -P 2 -p1 %patch -P 3 -p1 +%patch -P 4 -p1 +%patch -P 5 -p1 +%patch -P 6 -p1 +%patch -P 7 -p1 +%patch -P 8 -p1 %build %py3_build @@ -110,6 +120,15 @@ of the system. Currently storage and filesystem commands are audited. %license LICENSE %changelog +* Wed Aug 05 2026 Jan Jansky = 4.11.2-4 +- Update to 4.11.2-4 + +* Fri Jul 17 2026 Jan Jansky = 4.11.2-2 +- Update to 4.11.2-2 + +* Wed Jul 15 2026 Jan Jansky = 4.11.2-1 +- Update to 4.11.2-1 + * Thu Apr 02 2026 Jan Jansky = 4.11.0-1 - Update to 4.11.0-1 Resolves: RHEL-157813