sos/0002-remove-unsupported-python36-plugins.patch
Jan Jansky 137208b801 Update to 4.11.2-1
Resolves: RHEL-189440

Signed-off-by: Jan Jansky <jjansky@redhat.com>
2026-07-17 13:39:00 +02:00

1437 lines
47 KiB
Diff

--- 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<pwd>\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*********",
- )