Merge ~dan-emmons/ubuntu/+source/sosreport:sos-jammy-4.10.2 into ubuntu/+source/sosreport:ubuntu/jammy-devel

Proposed by Dan Emmons
Status: Needs review
Proposed branch: ~dan-emmons/ubuntu/+source/sosreport:sos-jammy-4.10.2
Merge into: ubuntu/+source/sosreport:ubuntu/jammy-devel
Diff against target: 5640 lines (+2274/-802)
103 files modified
.cirrus.yml (+43/-28)
README.md (+0/-5)
debian/changelog (+20/-0)
debian/control (+2/-0)
debian/copyright (+18/-0)
debian/patches/series (+0/-1)
dev/null (+0/-50)
docs/conf.py (+2/-2)
man/en/sos.1 (+2/-3)
plugins_overview.py (+6/-2)
sos.spec (+14/-5)
sos/__init__.py (+1/-1)
sos/archive.py (+14/-9)
sos/cleaner/__init__.py (+126/-154)
sos/cleaner/archives/__init__.py (+178/-12)
sos/cleaner/archives/sos.py (+3/-1)
sos/cleaner/mappings/__init__.py (+74/-9)
sos/cleaner/mappings/ip_map.py (+13/-8)
sos/cleaner/mappings/ipv6_map.py (+42/-37)
sos/cleaner/mappings/mac_map.py (+14/-8)
sos/cleaner/parsers/__init__.py (+3/-0)
sos/cleaner/parsers/hostname_parser.py (+2/-2)
sos/cleaner/parsers/ip_parser.py (+2/-2)
sos/cleaner/parsers/ipv6_parser.py (+3/-3)
sos/cleaner/parsers/keyword_parser.py (+2/-2)
sos/cleaner/parsers/mac_parser.py (+2/-2)
sos/cleaner/parsers/username_parser.py (+2/-2)
sos/collector/__init__.py (+14/-7)
sos/collector/clusters/ocp.py (+1/-1)
sos/component.py (+2/-5)
sos/options.py (+10/-7)
sos/policies/distros/debian.py (+1/-1)
sos/report/__init__.py (+14/-7)
sos/report/plugins/__init__.py (+70/-28)
sos/report/plugins/aap_containerized.py (+87/-9)
sos/report/plugins/aap_controller.py (+19/-5)
sos/report/plugins/aap_eda.py (+50/-7)
sos/report/plugins/aap_gateway.py (+26/-3)
sos/report/plugins/aap_hub.py (+20/-0)
sos/report/plugins/apt.py (+2/-1)
sos/report/plugins/aws.py (+81/-0)
sos/report/plugins/azure.py (+1/-1)
sos/report/plugins/block.py (+4/-0)
sos/report/plugins/boom.py (+5/-1)
sos/report/plugins/boot.py (+2/-0)
sos/report/plugins/candlepin.py (+8/-5)
sos/report/plugins/ceph_mon.py (+5/-4)
sos/report/plugins/charmed_mysql.py (+228/-78)
sos/report/plugins/dnf.py (+3/-1)
sos/report/plugins/drbd.py (+53/-6)
sos/report/plugins/firewalld.py (+2/-0)
sos/report/plugins/foreman.py (+73/-5)
sos/report/plugins/foreman_installer.py (+2/-2)
sos/report/plugins/gcp.py (+10/-21)
sos/report/plugins/insights.py (+3/-0)
sos/report/plugins/juju.py (+5/-0)
sos/report/plugins/kdump.py (+45/-2)
sos/report/plugins/logs.py (+1/-1)
sos/report/plugins/loki.py (+189/-0)
sos/report/plugins/lustre.py (+3/-0)
sos/report/plugins/md.py (+1/-0)
sos/report/plugins/microk8s.py (+4/-3)
sos/report/plugins/networking.py (+2/-0)
sos/report/plugins/networkmanager.py (+7/-0)
sos/report/plugins/opensearch.py (+66/-0)
sos/report/plugins/openshift_ovn.py (+24/-14)
sos/report/plugins/openstack_aodh.py (+2/-3)
sos/report/plugins/openstack_ceilometer.py (+8/-5)
sos/report/plugins/openstack_cinder.py (+4/-10)
sos/report/plugins/openstack_designate.py (+5/-3)
sos/report/plugins/openstack_glance.py (+4/-4)
sos/report/plugins/openstack_heat.py (+4/-3)
sos/report/plugins/openstack_instack.py (+14/-18)
sos/report/plugins/openstack_ironic.py (+4/-3)
sos/report/plugins/openstack_keystone.py (+3/-3)
sos/report/plugins/openstack_neutron.py (+3/-8)
sos/report/plugins/openstack_nova.py (+15/-14)
sos/report/plugins/openstack_sahara.py (+3/-3)
sos/report/plugins/openstack_swift.py (+3/-4)
sos/report/plugins/openstack_trove.py (+4/-3)
sos/report/plugins/podman.py (+2/-1)
sos/report/plugins/powerpc.py (+3/-1)
sos/report/plugins/pulpcore.py (+31/-11)
sos/report/plugins/release.py (+2/-2)
sos/report/plugins/rhc.py (+4/-3)
sos/report/plugins/rhui_containerized.py (+85/-0)
sos/report/plugins/saltmaster.py (+14/-1)
sos/report/plugins/scsi.py (+1/-0)
sos/report/plugins/snapm.py (+38/-0)
sos/report/plugins/spyre.py (+97/-0)
sos/report/plugins/sssd.py (+2/-1)
sos/report/plugins/subscription_manager.py (+4/-0)
sos/report/plugins/tftpserver.py (+17/-7)
sos/report/reporting.py (+6/-6)
sos/upload/__init__.py (+50/-51)
sos/upload/targets/__init__.py (+7/-4)
sos/upload/targets/redhat.py (+4/-5)
sos/utilities.py (+27/-1)
tests/cleaner_tests/existing_archive.py (+7/-4)
tests/product_tests/foreman/foreman_tests.py (+12/-1)
tests/report_tests/options_tests/options_tests.py (+1/-1)
tests/test_data/foreman_setup.sh (+16/-23)
tests/unittests/cleaner_tests.py (+37/-22)
Reviewer Review Type Date Requested Status
Arif Ali (community) Approve
Skia Pending
git-ubuntu import Pending
Review via email: mp+497896@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Arif Ali (arif-ali) wrote :

Thanks for the work on this

Uploaded

D: Setting host argument.
Checking signature on .changes
gpg: ../sosreport_4.10.2-0ubuntu0~22.04.1_source.changes: Valid signature from 7E6C0B9EB5944CEB
Checking signature on .dsc
gpg: ../sosreport_4.10.2-0ubuntu0~22.04.1.dsc: Valid signature from 7E6C0B9EB5944CEB
Package includes an .orig.tar.gz file although the debian revision suggests
that it might not be required.
Uploading to ubuntu (via ftp to upload.ubuntu.com):
  Uploading sosreport_4.10.2-0ubuntu0~22.04.1.dsc: done.
  Uploading sosreport_4.10.2.orig.tar.gz: done.
  Uploading sosreport_4.10.2-0ubuntu0~22.04.1.debian.tar.xz: done.
  Uploading sosreport_4.10.2-0ubuntu0~22.04.1_source.buildinfo: done.
  Uploading sosreport_4.10.2-0ubuntu0~22.04.1_source.changes: done.
Successfully uploaded packages.

review: Approve

Unmerged commits

6115374... by Dan Emmons

Update changelog

1d02bc5... by Dan Emmons

Add gpg to Recommends

07dc55f... by Dan Emmons

Sync copyright with Debian

647516c... by Dan Emmons

Remove patches now not needed

2fb376f... by Dan Emmons

import of 4.10.2

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.cirrus.yml b/.cirrus.yml
2index 1555c18..7db1c60 100644
3--- a/.cirrus.yml
4+++ b/.cirrus.yml
5@@ -6,13 +6,14 @@ env:
6 FEDORA_NAME: "fedora-42"
7 FEDORA_PRIOR_NAME: "fedora-41"
8
9+ DEBIAN_13_NAME: "debian-13"
10 DEBIAN_12_NAME: "debian-12"
11 DEBIAN_11_NAME: "debian-11"
12
13 UBUNTU_DEVEL_NAME: "ubuntu-25.10"
14 UBUNTU_LATEST_NAME: "ubuntu-25.04"
15- UBUNTU_NAME: "ubuntu-24.04"
16- UBUNTU_PRIOR_NAME: "ubuntu-22.04"
17+ UBUNTU_2404_NAME: "ubuntu-24.04"
18+ UBUNTU_2204_NAME: "ubuntu-22.04"
19
20 CENTOS_9_NAME: "centos-stream-9"
21
22@@ -25,17 +26,18 @@ env:
23
24 # Images exist on GCP already
25 CENTOS_9_FAMILY_NAME: "centos-stream-9"
26+ DEBIAN_13_FAMILY_NAME: "debian-13"
27 DEBIAN_12_FAMILY_NAME: "debian-12"
28 DEBIAN_11_FAMILY_NAME: "debian-11"
29 FEDORA_FAMILY_NAME: "fedora-cloud-42-x86-64"
30 FEDORA_PRIOR_FAMILY_NAME: "fedora-cloud-41-x86-64"
31
32+ UBUNTU_DEVEL_FAMILY_NAME: "ubuntu-2510-amd64"
33 UBUNTU_DEB_FAMILY_NAME: "ubuntu-minimal-2504-amd64"
34 UBUNTU_LATEST_FAMILY_NAME: "ubuntu-2504-amd64"
35- UBUNTU_FAMILY_NAME: "ubuntu-2404-lts-amd64"
36- UBUNTU_PRIOR_FAMILY_NAME: "ubuntu-2204-lts"
37 UBUNTU_SNAP_FAMILY_NAME: "ubuntu-2404-lts-amd64"
38- UBUNTU_DEVEL_FAMILY_NAME: "ubuntu-2510-amd64"
39+ UBUNTU_2404_FAMILY_NAME: "ubuntu-2404-lts-amd64"
40+ UBUNTU_2204_FAMILY_NAME: "ubuntu-2204-lts"
41
42 # Curl-command prefix for downloading task artifacts, simply add the
43 # the url-encoded task name, artifact name, and path as a suffix.
44@@ -106,14 +108,14 @@ rpm_build_task:
45 PROJECT: ${CENTOS_PROJECT}
46 BUILD_NAME: ${CENTOS_9_NAME}
47 VM_FAMILY_NAME: ${CENTOS_9_FAMILY_NAME}
48- - env: &fedora
49- PROJECT: ${FEDORA_PROJECT}
50- BUILD_NAME: ${FEDORA_NAME}
51- VM_FAMILY_NAME: ${FEDORA_FAMILY_NAME}
52- - env: &fedoraprior
53- PROJECT: ${FEDORA_PROJECT}
54- BUILD_NAME: ${FEDORA_PRIOR_NAME}
55- VM_FAMILY_NAME: ${FEDORA_PRIOR_FAMILY_NAME}
56+# - env: &fedora
57+# PROJECT: ${FEDORA_PROJECT}
58+# BUILD_NAME: ${FEDORA_NAME}
59+# VM_FAMILY_NAME: ${FEDORA_FAMILY_NAME}
60+# - env: &fedoraprior
61+# PROJECT: ${FEDORA_PROJECT}
62+# BUILD_NAME: ${FEDORA_PRIOR_NAME}
63+# VM_FAMILY_NAME: ${FEDORA_PRIOR_FAMILY_NAME}
64 setup_script: |
65 dnf clean all
66 dnf -y install rpm-build rpmdevtools gettext python3-devel python3-pexpect python3-pyyaml
67@@ -151,11 +153,16 @@ deb_build_task:
68 PROJECT: ${DEBIAN_PROJECT}
69 BUILD_NAME: ${DEBIAN_12_NAME}
70 VM_FAMILY_NAME: ${DEBIAN_12_FAMILY_NAME}
71+ - env: &debian-13-deb-pkg
72+ PROJECT: ${DEBIAN_PROJECT}
73+ BUILD_NAME: ${DEBIAN_13_NAME}
74+ VM_FAMILY_NAME: ${DEBIAN_13_FAMILY_NAME}
75 - env: &ubuntu-latest-deb-pkg
76 PROJECT: ${UBUNTU_PROJECT}
77 BUILD_NAME: ${UBUNTU_LATEST_NAME}
78 VM_FAMILY_NAME: ${UBUNTU_DEB_FAMILY_NAME}
79 setup_script: |
80+ [[ ${BUILD_NAME} == "${DEBIAN_11_NAME}" ]] && sudo sed -i '/-backports/ s/^/#/' /etc/apt/sources.list
81 apt update --allow-releaseinfo-change
82 apt -y install devscripts equivs python3-pip
83 mk-build-deps
84@@ -206,18 +213,18 @@ report_stageone_task:
85 gce_instance: *standardvm
86 matrix:
87 - env: *centos9
88- - env: *fedora
89- - env: *fedoraprior
90- - env: &ubuntu
91+# - env: *fedora
92+# - env: *fedoraprior
93+ - env: &ubuntu-2404
94 PKG: "snap"
95 PROJECT: ${UBUNTU_PROJECT}
96- BUILD_NAME: "${UBUNTU_NAME} - ${PKG}"
97- VM_FAMILY_NAME: ${UBUNTU_FAMILY_NAME}
98- - env: &ubuntuprior
99+ BUILD_NAME: "${UBUNTU_2404_NAME} - ${PKG}"
100+ VM_FAMILY_NAME: ${UBUNTU_2404_FAMILY_NAME}
101+ - env: &ubuntu-2204
102 PKG: "snap"
103 PROJECT: ${UBUNTU_PROJECT}
104- BUILD_NAME: "${UBUNTU_PRIOR_NAME} - ${PKG}"
105- VM_FAMILY_NAME: ${UBUNTU_PRIOR_FAMILY_NAME}
106+ BUILD_NAME: "${UBUNTU_2204_NAME} - ${PKG}"
107+ VM_FAMILY_NAME: ${UBUNTU_2204_FAMILY_NAME}
108 - env: &ubuntu-latest-snap
109 PKG: "snap"
110 PROJECT: ${UBUNTU_PROJECT}
111@@ -229,6 +236,10 @@ report_stageone_task:
112 BUILD_NAME: "${UBUNTU_LATEST_NAME} - ${PKG}"
113 DEB_BUILD_NAME: ${UBUNTU_LATEST_NAME}
114 VM_FAMILY_NAME: ${UBUNTU_LATEST_FAMILY_NAME}
115+ - env: &debian-13
116+ <<: *debian-13-deb-pkg
117+ PKG: "deb"
118+ DEB_BUILD_NAME: ${BUILD_NAME}
119 - env: &debian-12
120 <<: *debian-12-deb-pkg
121 PKG: "deb"
122@@ -239,6 +250,7 @@ report_stageone_task:
123 DEB_BUILD_NAME: ${BUILD_NAME}
124 setup_script: &setup |
125 if [ $(command -v apt) ]; then
126+ [[ ${BUILD_NAME} == "${DEBIAN_11_NAME}" ]] && sudo sed -i '/-backports/ s/^/#/' /etc/apt/sources.list
127 [[ "$(dpkg -l sos)" ]] && apt -y purge sos ubuntu-server
128 [[ "$(dpkg -l sosreport)" ]] && apt -y purge sosreport ubuntu-server
129 apt update --allow-releaseinfo-change
130@@ -309,11 +321,11 @@ report_stagetwo_task:
131 gce_instance: *standardvm
132 matrix:
133 - env: *centos9
134- - env: *fedora
135- - env: *ubuntu
136+# - env: *fedora
137+ - env: *ubuntu-2404
138 - env: *ubuntu-latest-snap
139 - env: *ubuntu-latest-deb
140- - env: *debian-12
141+ - env: *debian-13
142 setup_script: *setup
143 install_pexpect_script: |
144 if [ $(command -v apt) ]; then
145@@ -349,13 +361,16 @@ report_foreman_task:
146 alias: "foreman_integration"
147 name: "Integration Test - Foreman ${FOREMAN_VER} - ${BUILD_NAME}"
148 depends_on: stageone_report
149+ environment:
150+ FOREMAN_VER: "3.15"
151+ KATELLO_VER: "4.17"
152 gce_instance: &bigvm
153 <<: *standardvm
154- type: e2-standard-2
155+ type: e2-highmem-4
156 matrix:
157- - env:
158- <<: *debian-11
159- FOREMAN_VER: "3.7"
160+ - env: *centos9
161+ - env: *debian-12
162+ - env: *ubuntu-2204
163 setup_script: *setup
164 foreman_setup_script: ./tests/test_data/foreman_setup.sh
165 main_script: PYTHONPATH=tests/ avocado run -p TESTLOCAL=true --max-parallel-tasks=1 -t foreman tests/product_tests/foreman/
166diff --git a/.github/workflows/snap.yaml b/.github/workflows/snap.yaml
167deleted file mode 100644
168index d7d8935..0000000
169--- a/.github/workflows/snap.yaml
170+++ /dev/null
171@@ -1,43 +0,0 @@
172-name: snap
173-on:
174- push:
175- branches:
176- - main
177- release:
178- types:
179- - published
180-
181-jobs:
182- build:
183- runs-on: ubuntu-22.04
184- concurrency:
185- group: snap-build
186- cancel-in-progress: true
187- steps:
188- - uses: actions/checkout@v3
189- with:
190- fetch-depth: 0
191- - uses: snapcore/action-build@v1
192- id: build-snap
193- # Make sure the snap is installable
194- - run: |
195- sudo apt -y remove sosreport
196- sudo snap install --classic --dangerous ${{ steps.build-snap.outputs.snap }}
197- sudo snap alias sosreport.sos sos
198- # Do some testing with the snap
199- - run: |
200- sudo sos help
201- - uses: snapcore/action-publish@v1
202- if: ${{ github.event_name == 'push' }}
203- env:
204- SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
205- with:
206- snap: ${{ steps.build-snap.outputs.snap }}
207- release: "latest/edge"
208- - uses: snapcore/action-publish@v1
209- if: ${{ github.event_name == 'release' }}
210- env:
211- SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
212- with:
213- snap: ${{ steps.build-snap.outputs.snap }}
214- release: "latest/candidate"
215diff --git a/README.md b/README.md
216index c940ec8..0842741 100644
217--- a/README.md
218+++ b/README.md
219@@ -114,11 +114,6 @@ You can simply run from the git checkout now:
220 ```
221 $ sudo ./bin/sos report
222 ```
223-The command `sosreport` is still available, as a legacy redirector,
224-and can be used like this:
225-```
226-$ sudo ./bin/sosreport
227-```
228
229 To see a list of all available plugins and plugin options, run
230 ```
231diff --git a/debian/changelog b/debian/changelog
232index 43ae474..2d3374e 100644
233--- a/debian/changelog
234+++ b/debian/changelog
235@@ -1,3 +1,23 @@
236+sosreport (4.10.2-0ubuntu0~22.04.1) jammy; urgency=medium
237+
238+ * New 4.10.2 upstream release. (LP: #2136302)
239+
240+ * For more details, full release note is available here:
241+ - https://github.com/sosreport/sos/releases/tag/4.10.2
242+
243+ * d/control: Add gpg to Recommends so that we are able to encrypt and
244+ decrypt sos reports
245+
246+ * d/copyright: Aligned copyright with upstream Debian
247+
248+ * Former patch, now fixed:
249+ - d/p/0002-component-Grab-tmpdir-from-policy.patch
250+
251+ * Remaining patches:
252+ - d/p/0001-debian-change-tmp-dir-location.patch
253+
254+ -- Dan Emmons <dan.emmons@canonical.com> Fri, 19 Dec 2025 17:58:00 +0000
255+
256 sosreport (4.9.2-0ubuntu0~22.04.1) jammy; urgency=medium
257
258 * New 4.9.2 upstream release. (LP: #2114840)
259diff --git a/debian/control b/debian/control
260index f26edad..460ce51 100644
261--- a/debian/control
262+++ b/debian/control
263@@ -30,6 +30,8 @@ Depends:
264 python3-yaml,
265 ${misc:Depends},
266 ${python3:Depends},
267+Recommends:
268+ gpg,
269 Description: Set of tools to gather troubleshooting data from a system
270 Sos is a set of tools that gathers information about system
271 hardware and configuration. The information can then be used for
272diff --git a/debian/copyright b/debian/copyright
273index f79e87b..f4906bf 100644
274--- a/debian/copyright
275+++ b/debian/copyright
276@@ -34,6 +34,24 @@ Files: sos/policies/distros/rocky.py
277 Copyright: (C) Louis Abel <label@rockylinux.org>
278 License: GPL-2
279
280+Files: sos/report/plugins/aap_containerized.py
281+Copyright: (C) 2025 Nagoor Shaik <nshaik@redhat.com>
282+License: GPL-2
283+
284+Files: sos/report/plugins/aap_eda.py
285+Copyright: (C) 2025 Rudnei Bertol Jr <rudnei@redhat.com>
286+ (C) 2025 Nagoor Shaik <nshaik@redhat.com>
287+License: GPL-2
288+
289+Files: sos/report/plugins/aap_gateway.py
290+Copyright: (C) 2024 Lucas Benedito <lbenedit@redhat.com>
291+ (C) 2025 Nagoor Shaik <nshaik@redhat.com>
292+License: GPL-2
293+
294+Files: sos/report/plugins/aws.py
295+Copyright: (C) 2025, Javier Blanco <javier@jblanco.es>
296+License: GPL-2
297+
298 Files: sos/report/plugins/elastic.py
299 Copyright: (C) 2018 Amit Ghadge <amitg.b14@gmail.com>
300 License: GPL-2
301diff --git a/debian/patches/0002-component-Grab-tmpdir-from-policy.patch b/debian/patches/0002-component-Grab-tmpdir-from-policy.patch
302deleted file mode 100644
303index d7a56e4..0000000
304--- a/debian/patches/0002-component-Grab-tmpdir-from-policy.patch
305+++ /dev/null
306@@ -1,31 +0,0 @@
307-From: Arif Ali <arif.ali@canonical.com>
308-Date: Fri, 20 Jun 2025 11:33:07 +0100
309-Subject: [PATCH] [component] Grab tmpdir from policy
310-
311-Rather than using /var/tmp by default, grab the tmp_dir from the
312-policy. The default will be from tempfile.gettempdir() from the
313-sos.policies, so should be getting it from the OS default.
314-
315-Closes: #4061
316-
317-Signed-off-by: Arif Ali <arif.ali@canonical.com>
318-Forwarded: https://github.com/sosreport/sos/pull/4062
319-Bug-Upstream: https://github.com/sosreport/sos/issues/4061
320-Origin: upstream, https://github.com/sosreport/sos/commit/57bbc89a0c6406a391a90c1330f5c532ee0b2c66
321----
322- sos/component.py | 2 +-
323- 1 file changed, 1 insertion(+), 1 deletion(-)
324-
325-diff --git a/sos/component.py b/sos/component.py
326-index d0660cf..a110c27 100644
327---- a/sos/component.py
328-+++ b/sos/component.py
329-@@ -168,7 +168,7 @@ class SoSComponent():
330- if self.opts.tmp_dir:
331- tmpdir = os.path.abspath(self.opts.tmp_dir)
332- else:
333-- tmpdir = os.getenv('TMPDIR', None) or '/var/tmp'
334-+ tmpdir = os.getenv('TMPDIR', None) or self.policy.get_tmp_dir(None)
335-
336- if os.getenv('HOST', None) and os.getenv('container', None):
337- tmpdir = os.path.join(os.getenv('HOST'), tmpdir.lstrip('/'))
338diff --git a/debian/patches/series b/debian/patches/series
339index 272c46e..3d9fdd4 100644
340--- a/debian/patches/series
341+++ b/debian/patches/series
342@@ -1,2 +1 @@
343 0001-debian-change-tmp-dir-location.patch
344-0002-component-Grab-tmpdir-from-policy.patch
345diff --git a/docs/conf.py b/docs/conf.py
346index b1b1968..fc9c526 100644
347--- a/docs/conf.py
348+++ b/docs/conf.py
349@@ -58,9 +58,9 @@ project_copyright = '2014, Bryn Reeves'
350 # built documents.
351 #
352 # The short X.Y version.
353-version = '4.9.2'
354+version = '4.10.2'
355 # The full version, including alpha/beta/rc tags.
356-release = '4.9.2'
357+release = '4.10.2'
358
359 # The language for content autogenerated by Sphinx. Refer to documentation
360 # for a list of supported languages.
361diff --git a/man/en/sos.1 b/man/en/sos.1
362index cb91053..1ba3165 100644
363--- a/man/en/sos.1
364+++ b/man/en/sos.1
365@@ -10,8 +10,7 @@ sos \- A unified tool for collecting system logs and other debug information
366 support representatives, and the like to assist in troubleshooting issues with
367 a system or group of systems.
368
369-The most well known function is \fB sos report\fR or \fBsos report\fR as it was
370-previously known.
371+The most well known function is \fBsos report\fR.
372
373 An sos archive is typically requested by support organizations to collect baseline
374 configuration and system data from which to begin the troubleshooting process.
375@@ -31,7 +30,7 @@ services, or system architecture is detected.
376
377 See \fBsos report --help\fR and \fBman sos-report\fR for more information.
378
379-May also be invoked via the alias \fBrep\fR or the deprecated command \fBsos report\fR.
380+May also be invoked via the alias \fBrep\fR.
381
382 .TP
383 .B collect
384diff --git a/plugins_overview.py b/plugins_overview.py
385index f7ef042..b315519 100644
386--- a/plugins_overview.py
387+++ b/plugins_overview.py
388@@ -55,7 +55,10 @@ def add_all_items(method, dest, plugfd, wrapopen=r'\(', wrapclose=r'\)'):
389 # dirty hack to remove spaces and "Plugin"
390 if "Plugin" not in it:
391 continue
392- it = it.strip(' ()')[0:-6]
393+ if "=" in it:
394+ it = re.sub(r"Plugin.*", "Plugin", it)
395+ plug_col_len = 9 if "PluginOpt" in it else 6
396+ it = it.strip(' ()')[:-plug_col_len]
397 if len(it):
398 dest.append(it)
399 # list of specs separated by comma ..
400@@ -94,7 +97,8 @@ for plugfile in sorted(os.listdir(PLUGDIR)):
401 pfd_content = pfd.read().replace('\n', '')
402 add_all_items(
403 "from sos.report.plugins import ", plugs_data[plugname]['distros'],
404- pfd_content, wrapopen='', wrapclose='(class|from|import)'
405+ pfd_content, wrapopen='',
406+ wrapclose=r'(class|from|import|#|\))'
407 )
408 add_all_items("profiles = ", plugs_data[plugname]['profiles'],
409 pfd_content, wrapopen='')
410diff --git a/sos.spec b/sos.spec
411index b7a010e..fa3f3b9 100644
412--- a/sos.spec
413+++ b/sos.spec
414@@ -1,6 +1,6 @@
415 Summary: A set of tools to gather troubleshooting information from a system
416 Name: sos
417-Version: 4.9.2
418+Version: 4.10.2
419 Release: 1%{?dist}
420 Source0: https://github.com/sosreport/sos/archive/%{name}-%{version}.tar.gz
421 License: GPL-2.0-only
422@@ -33,20 +33,20 @@ support technicians and developers.
423 %prep
424 %setup -qn %{name}-%{version}
425
426-%if 0%{?fedora} >= 39
427+%if 0%{?fedora} >= 39 || 0%{?rhel} >= 11
428 %generate_buildrequires
429 %pyproject_buildrequires
430 %endif
431
432 %build
433-%if 0%{?fedora} >= 39
434+%if 0%{?fedora} >= 39 || 0%{?rhel} >= 11
435 %pyproject_wheel
436 %else
437 %py3_build
438 %endif
439
440 %install
441-%if 0%{?fedora} >= 39
442+%if 0%{?fedora} >= 39 || 0%{?rhel} >= 11
443 %pyproject_install
444 %pyproject_save_files sos
445 %else
446@@ -69,7 +69,7 @@ rm -rf %{buildroot}/usr/config/
447 # internationalization is currently broken. Uncomment this line once fixed.
448 # %%files -f %%{name}.lang
449 %files
450-%if 0%{?fedora} >= 39
451+%if 0%{?fedora} >= 39 || 0%{?rhel} >= 11
452 %{_bindir}/sos
453 %else
454 %{_sbindir}/sos
455@@ -89,6 +89,15 @@ rm -rf %{buildroot}/usr/config/
456 %config(noreplace) %{_sysconfdir}/sos/sos.conf
457
458 %changelog
459+* Mon Dec 15 2025 Pavel Moravec <pmoravec@redhat.com> = 4.10.2
460+- New upstream release
461+
462+* Wed Oct 15 2025 Pavel Moravec <pmoravec@redhat.com> = 4.10.1
463+- New upstream release
464+
465+* Mon Aug 18 2025 Jake Hunsaker <jacob.r.hunsaker@gmail.com> = 4.10.0
466+- New upstream release
467+
468 * Tue Jun 17 2025 Pavel Moravec <pmoravec@redhat.com> = 4.9.2
469 - New upstream release
470
471diff --git a/sos/__init__.py b/sos/__init__.py
472index 1e292e3..259593f 100644
473--- a/sos/__init__.py
474+++ b/sos/__init__.py
475@@ -14,7 +14,7 @@
476 This module houses the i18n setup and message function. The default is to use
477 gettext to internationalize messages.
478 """
479-__version__ = "4.9.2"
480+__version__ = "4.10.2"
481
482 import os
483 import sys
484diff --git a/sos/archive.py b/sos/archive.py
485index 41a6974..0ac3e76 100644
486--- a/sos/archive.py
487+++ b/sos/archive.py
488@@ -726,18 +726,24 @@ class TarFileArchive(FileCacheArchive):
489 return f"{self._archive_root}.{self._suffix}"
490
491 def _build_archive(self, method):
492+ _mode = 'w'
493 if method == 'auto':
494 method = 'xz' if find_spec('lzma') is not None else 'gzip'
495- _comp_mode = method.strip('ip')
496- self._archive_name = f"{self._archive_name}.{_comp_mode}"
497+ if method is not None:
498+ _comp_mode = method.strip('ip')
499+ self._archive_name = f"{self._archive_name}.{_comp_mode}"
500+ self._suffix += f".{_comp_mode}"
501+ _mode = f"w:{_comp_mode}"
502 # tarfile does not currently have a consistent way to define comnpress
503 # level for both xz and gzip ('preset' for xz, 'compresslevel' for gz)
504- if method == 'gzip':
505- kwargs = {'compresslevel': 6}
506- else:
507- kwargs = {'preset': 3}
508- with tarfile.open(self._archive_name, mode=f"w:{_comp_mode}",
509- **kwargs) as tar:
510+ kwargs = {
511+ None: {},
512+ 'gzip': {'compresslevel': 6},
513+ 'xz': {'preset': 3}
514+ }
515+ with tarfile.open(self._archive_name,
516+ mode=_mode,
517+ **kwargs[method]) as tar:
518 # Add commonly reviewed files first, so that they can be more
519 # easily read from memory without needing to extract
520 # the whole archive
521@@ -751,7 +757,6 @@ class TarFileArchive(FileCacheArchive):
522 # want the names used in the archive to be relative.
523 tar.add(self._archive_root, arcname=self._name,
524 filter=self.copy_permissions_filter)
525- self._suffix += f".{_comp_mode}"
526 return self.name()
527
528
529diff --git a/sos/cleaner/__init__.py b/sos/cleaner/__init__.py
530index 175da18..2e502d2 100644
531--- a/sos/cleaner/__init__.py
532+++ b/sos/cleaner/__init__.py
533@@ -13,10 +13,9 @@ import json
534 import logging
535 import os
536 import shutil
537-import tempfile
538 import fnmatch
539
540-from concurrent.futures import ThreadPoolExecutor
541+from concurrent.futures import ProcessPoolExecutor
542 from datetime import datetime
543 from pwd import getpwuid
544
545@@ -35,8 +34,13 @@ from sos.cleaner.archives.sos import (SoSReportArchive, SoSReportDirectory,
546 SoSCollectorDirectory)
547 from sos.cleaner.archives.generic import DataDirArchive, TarballArchive
548 from sos.cleaner.archives.insights import InsightsArchive
549-from sos.utilities import (get_human_readable, import_module, ImporterHelper,
550- file_is_binary)
551+from sos.utilities import (get_human_readable, import_module,
552+ ImporterHelper, is_executable)
553+
554+
555+# an auxiliary method to kick off child processes over its instances
556+def obfuscate_arc_files(arc, flist):
557+ return arc.obfuscate_arc_files(flist)
558
559
560 class SoSCleaner(SoSComponent):
561@@ -62,7 +66,8 @@ class SoSCleaner(SoSComponent):
562 In the case of IP addresses, support is for IPv4 and IPv6 - effort is made
563 to keep network topology intact so that later analysis is as accurate and
564 easily understandable as possible. If an IP address is encountered that we
565- cannot determine the netmask for, a random IP address is used instead.
566+ cannot determine the netmask for, a private IP address from 172.17.0.0/22
567+ range is used instead.
568
569 For IPv6, note that IPv4-mapped addresses, e.g. ::ffff:10.11.12.13, are
570 NOT supported currently, and will remain unobfuscated.
571@@ -92,7 +97,8 @@ class SoSCleaner(SoSComponent):
572 'no_update': False,
573 'keep_binary_files': False,
574 'target': '',
575- 'usernames': []
576+ 'usernames': [],
577+ 'treat_certificates': 'obfuscate'
578 }
579
580 def __init__(self, parser=None, args=None, cmdline=None, in_place=False,
581@@ -111,8 +117,9 @@ class SoSCleaner(SoSComponent):
582 self.policy = hook_commons['policy']
583 self.manifest = hook_commons['manifest']
584 self.from_cmdline = False
585+ # precede 'report -t' option above 'cleaner --jobs'
586 if not hasattr(self.opts, 'jobs'):
587- self.opts.jobs = 4
588+ self.opts.jobs = self.opts.threads
589 self.opts.archive_type = 'auto'
590 self.soslog = logging.getLogger('sos')
591 self.ui_log = logging.getLogger('sos_ui')
592@@ -129,14 +136,20 @@ class SoSCleaner(SoSComponent):
593
594 self.cleaner_md = self.manifest.components.add_section('cleaner')
595
596- skip_cleaning_files = self.opts.skip_cleaning_files
597+ cleaner_dir = os.path.dirname(self.opts.map_file) \
598+ if self.opts.map_file else '/etc/sos/cleaner'
599+ parser_args = [
600+ self.cleaner_mapping,
601+ cleaner_dir,
602+ self.opts.skip_cleaning_files,
603+ ]
604 self.parsers = [
605- SoSHostnameParser(self.cleaner_mapping, skip_cleaning_files),
606- SoSIPParser(self.cleaner_mapping, skip_cleaning_files),
607- SoSIPv6Parser(self.cleaner_mapping, skip_cleaning_files),
608- SoSMacParser(self.cleaner_mapping, skip_cleaning_files),
609- SoSKeywordParser(self.cleaner_mapping, skip_cleaning_files),
610- SoSUsernameParser(self.cleaner_mapping, skip_cleaning_files)
611+ SoSHostnameParser(*parser_args),
612+ SoSIPParser(*parser_args),
613+ SoSIPv6Parser(*parser_args),
614+ SoSMacParser(*parser_args),
615+ SoSKeywordParser(*parser_args),
616+ SoSUsernameParser(*parser_args),
617 ]
618
619 for _parser in self.opts.disable_parsers:
620@@ -289,6 +302,15 @@ third party.
621 clean_grp.add_argument('--usernames', dest='usernames', default=[],
622 action='extend',
623 help='List of usernames to obfuscate')
624+ clean_grp.add_argument('--treat-certificates', default='obfuscate',
625+ choices=['obfuscate', 'keep', 'remove'],
626+ dest='treat_certificates',
627+ help=(
628+ 'How to treat certificate files '
629+ '[.csr .crt .pem]. Defaults to "obfuscate" '
630+ 'after convert the file to text. '
631+ '"Key" certificate files are always '
632+ 'removed.'))
633
634 def set_target_path(self, path):
635 """For use by report and collect to set the TARGET option appropriately
636@@ -308,14 +330,19 @@ third party.
637 check_type = self.opts.archive_type.replace('-', '_')
638 for archive in self.archive_types:
639 if archive.type_name == check_type:
640- _arc = archive(self.opts.target, self.tmpdir)
641+ _arc = archive(self.opts.target, self.tmpdir,
642+ self.opts.keep_binary_files,
643+ self.opts.treat_certificates)
644 else:
645 for arc in self.archive_types:
646 if arc.check_is_type(self.opts.target):
647- _arc = arc(self.opts.target, self.tmpdir)
648+ _arc = arc(self.opts.target, self.tmpdir,
649+ self.opts.keep_binary_files,
650+ self.opts.treat_certificates)
651 break
652 if not _arc:
653 return
654+ self.main_archive = _arc
655 self.report_paths.append(_arc)
656 if _arc.is_nested:
657 self.report_paths.extend(_arc.get_nested_archives())
658@@ -554,7 +581,8 @@ third party.
659 try:
660 msg = (
661 f"Found {len(self.report_paths)} total reports to obfuscate, "
662- f"processing up to {self.opts.jobs} concurrently\n"
663+ f"processing up to {self.opts.jobs} concurrently within one "
664+ "archive\n"
665 )
666 self.ui_log.info(msg)
667 if self.opts.keep_binary_files:
668@@ -562,9 +590,33 @@ third party.
669 "WARNING: binary files that potentially contain sensitive "
670 "information will NOT be removed from the final archive\n"
671 )
672- pool = ThreadPoolExecutor(self.opts.jobs)
673- pool.map(self.obfuscate_report, self.report_paths, chunksize=1)
674- pool.shutdown(wait=True)
675+ if (self.opts.treat_certificates == "obfuscate"
676+ and not is_executable("openssl")):
677+ self.opts.treat_certificates = "remove"
678+ self.ui_log.warning(
679+ "WARNING: No `openssl` command available. Replacing "
680+ "`--treat-certificates` from `obfuscate` to `remove`."
681+ )
682+ if self.opts.treat_certificates == "obfuscate":
683+ self.ui_log.warning(
684+ "WARNING: certificate files that potentially contain "
685+ "sensitive information will be CONVERTED to text and "
686+ "OBFUSCATED in the final archive.\n"
687+ )
688+ elif self.opts.treat_certificates == "keep":
689+ self.ui_log.warning(
690+ "WARNING: certificate files that potentially contain "
691+ "sensitive information will be KEPT in the final "
692+ "archive as is.\n"
693+ )
694+ elif self.opts.treat_certificates == "remove":
695+ self.ui_log.warning(
696+ "WARNING: certificate files that potentially contain "
697+ "sensitive information will be REMOVED in the final "
698+ "archive.\n")
699+ for report_path in self.report_paths:
700+ self.ui_log.info(f"Obfuscating {report_path.archive_path}")
701+ self.obfuscate_report(report_path)
702 # finally, obfuscate the nested archive if one exists
703 if self.nested_archive:
704 self._replace_obfuscated_archives()
705@@ -635,6 +687,8 @@ third party.
706
707 for ritem in prepper.regex_items[pname]:
708 _parser.mapping.add_regex_item(ritem)
709+ # we must initialize stuff inside (cloned processes') archive - REALLY?
710+ archive.set_parsers(self.parsers)
711
712 def get_preppers(self):
713 """
714@@ -661,6 +715,7 @@ third party.
715 for prepper in self.get_preppers():
716 for archive in self.report_paths:
717 self._prepare_archive_with_prepper(archive, prepper)
718+ self.main_archive.set_parsers(self.parsers)
719
720 def obfuscate_report(self, archive): # pylint: disable=too-many-branches
721 """Individually handle each archive or directory we've discovered by
722@@ -668,8 +723,9 @@ third party.
723
724 Positional arguments:
725
726- :param report str: Filepath to the directory or archive
727+ :param archive str: Filepath to the directory or archive
728 """
729+
730 try:
731 arc_md = self.cleaner_md.add_section(archive.archive_name)
732 start_time = datetime.now()
733@@ -679,30 +735,47 @@ third party.
734 archive.extract()
735 archive.report_msg("Beginning obfuscation...")
736
737- for fname in archive.get_file_list():
738- short_name = fname.split(archive.archive_name + '/')[1]
739- if archive.should_skip_file(short_name):
740- continue
741- if (not self.opts.keep_binary_files and
742- archive.should_remove_file(short_name)):
743- # We reach this case if the option --keep-binary-files
744- # was not used, and the file is in a list to be removed
745- archive.remove_file(short_name)
746- continue
747- if (self.opts.keep_binary_files and
748- (file_is_binary(fname) or
749- archive.should_remove_file(short_name))):
750- # We reach this case if the option --keep-binary-files
751- # is used. In this case we want to make sure
752- # the cleaner doesn't try to clean a binary file
753- continue
754- try:
755- count = self.obfuscate_file(fname, short_name,
756- archive.archive_name)
757- if count:
758- archive.update_sub_count(short_name, count)
759- except Exception as err:
760- self.log_debug(f"Unable to parse file {short_name}: {err}")
761+ file_list = list(archive.get_files())
762+ # we can't call simple
763+ # executor.map(archive.obfuscate_arc_files,archive.get_files())
764+ # because a child process does not carry forward internal changes
765+ # (e.g. mappings' datasets) from one call of obfuscate_arc_files
766+ # method to another. Each obfuscate_arc_files method starts with
767+ # vanilla parent archive, that is initialised *once* at its
768+ # beginning via initializer=archive.load_parser_entries
769+ # - but not afterwards..
770+ #
771+ # So we must pass list of all files for each worker at the
772+ # beginning. This means less granularity of the child processes
773+ # work (one worker can finish much sooner than the other), but
774+ # it is the best we can have (or have found)
775+ #
776+ # At least, the "file_list[i::self.opts.jobs]" means subsequent
777+ # files (speculativelly of similar size and content) are
778+ # distributed to different processes, which attempts to split the
779+ # load evenly. Yet better approach might be reorderig file_list
780+ # based on files' sizes.
781+
782+ files_obfuscated_count = total_sub_count = removed_file_count = 0
783+ archive_list = [archive for i in range(self.opts.jobs)]
784+ with ProcessPoolExecutor(
785+ max_workers=self.opts.jobs,
786+ initializer=archive.load_parser_entries) as executor:
787+ futures = executor.map(obfuscate_arc_files, archive_list,
788+ [file_list[i::self.opts.jobs] for i in
789+ range(self.opts.jobs)])
790+ for (foc, tsc, rfc) in futures:
791+ files_obfuscated_count += foc
792+ total_sub_count += tsc
793+ removed_file_count += rfc
794+
795+ # As there is no easy way to get dataset dicts from child
796+ # processes' mappings, we can reload our own parent-process
797+ # archive from the disk files. The trick is that sequence of
798+ # files/entries is the source of truth of *sequence* of calling
799+ # *all* mapping.all(item) methods - so replaying this will
800+ # generate the right datasets!
801+ archive.load_parser_entries()
802
803 try:
804 self.obfuscate_directory_names(archive)
805@@ -737,95 +810,20 @@ third party.
806 end_time = datetime.now()
807 arc_md.add_field('end_time', end_time)
808 arc_md.add_field('run_time', end_time - start_time)
809- arc_md.add_field('files_obfuscated', len(archive.file_sub_list))
810- arc_md.add_field('total_substitutions', archive.total_sub_count)
811+ arc_md.add_field('files_obfuscated', files_obfuscated_count)
812+ arc_md.add_field('total_substitutions', total_sub_count)
813 rmsg = ''
814- if archive.removed_file_count:
815+ if removed_file_count:
816 rmsg = " [removed %s unprocessable files]"
817- rmsg = rmsg % archive.removed_file_count
818+ rmsg = rmsg % removed_file_count
819 archive.report_msg(f"Obfuscation completed{rmsg}")
820
821 except Exception as err:
822 self.ui_log.info("Exception while processing "
823 f"{archive.archive_name}: {err}")
824
825- def obfuscate_file(self, filename, short_name=None, arc_name=None):
826- # pylint: disable=too-many-locals
827- """Obfuscate and individual file, line by line.
828-
829- Lines processed, even if no substitutions occur, are then written to a
830- temp file without our own tmpdir. Once the file has been completely
831- iterated through, if there have been substitutions then the temp file
832- overwrites the original file. If there are no substitutions, then the
833- original file is left in place.
834-
835- Positional arguments:
836-
837- :param filename str: Filename relative to the extracted
838- archive root
839- """
840- if not filename:
841- # the requested file doesn't exist in the archive
842- return None
843- subs = 0
844- if not short_name:
845- short_name = filename.split('/')[-1]
846- if not os.path.islink(filename):
847- # don't run the obfuscation on the link, but on the actual file
848- # at some other point.
849- _parsers = [
850- _p for _p in self.parsers if not
851- any(
852- _skip.match(short_name) for _skip in _p.skip_patterns
853- )
854- ]
855- if not _parsers:
856- self.log_debug(
857- f"Skipping obfuscation of {short_name or filename} due to "
858- f"matching file skip pattern"
859- )
860- return 0
861- self.log_debug(f"Obfuscating {short_name or filename}",
862- caller=arc_name)
863- with tempfile.NamedTemporaryFile(mode='w', dir=self.tmpdir) \
864- as tfile:
865- with open(filename, 'r', encoding='utf-8',
866- errors='replace') as fname:
867- for line in fname:
868- try:
869- line, count = self.obfuscate_line(line, _parsers)
870- subs += count
871- tfile.write(line)
872- except Exception as err:
873- self.log_debug(f"Unable to obfuscate {short_name}:"
874- f"{err}", caller=arc_name)
875- tfile.seek(0)
876- if subs:
877- shutil.copyfile(tfile.name, filename)
878-
879- _ob_short_name = self.obfuscate_string(short_name.split('/')[-1])
880- _ob_filename = short_name.replace(short_name.split('/')[-1],
881- _ob_short_name)
882-
883- if _ob_filename != short_name:
884- arc_path = filename.split(short_name)[0]
885- _ob_path = os.path.join(arc_path, _ob_filename)
886- # ensure that any plugin subdirs that contain obfuscated strings
887- # get created with obfuscated counterparts
888- if not os.path.islink(filename):
889- os.rename(filename, _ob_path)
890- else:
891- # generate the obfuscated name of the link target
892- _target_ob = self.obfuscate_string(os.readlink(filename))
893- # remove the unobfuscated original symlink first, in case the
894- # symlink name hasn't changed but the target has
895- os.remove(filename)
896- # create the newly obfuscated symlink, pointing to the
897- # obfuscated target name, which may not exist just yet, but
898- # when the actual file is obfuscated, will be created
899- os.symlink(_target_ob, _ob_path)
900-
901- return subs
902+ def obfuscate_file(self, filename):
903+ self.main_archive.obfuscate_arc_files([filename])
904
905 def obfuscate_symlinks(self, archive):
906 """Iterate over symlinks in the archive and obfuscate their names.
907@@ -894,6 +892,8 @@ third party.
908 )
909 os.rename(_dirname, _ob_arc_dir)
910
911+ # TODO: this is a duplicate method from SoSObfuscationArchive but we can't
912+ # easily remove either of them..?
913 def obfuscate_string(self, string_data):
914 for parser in self.parsers:
915 try:
916@@ -902,34 +902,6 @@ third party.
917 self.log_info(f"Error obfuscating string data: {err}")
918 return string_data
919
920- def obfuscate_line(self, line, parsers=None):
921- """Run a line through each of the obfuscation parsers, keeping a
922- cumulative total of substitutions done on that particular line.
923-
924- Positional arguments:
925-
926- :param line str: The raw line as read from the file being
927- processed
928- :param parsers: A list of parser objects to obfuscate
929- with. If None, use all.
930-
931- Returns the fully obfuscated line and the number of substitutions made
932- """
933- # don't iterate over blank lines, but still write them to the tempfile
934- # to maintain the same structure when we write a scrubbed file back
935- count = 0
936- if not line.strip():
937- return line, count
938- if parsers is None:
939- parsers = self.parsers
940- for parser in parsers:
941- try:
942- line, _count = parser.parse_line(line)
943- count += _count
944- except Exception as err:
945- self.log_debug(f"failed to parse line: {err}", parser.name)
946- return line, count
947-
948 def write_stats_to_manifest(self):
949 """Write some cleaner-level, non-report-specific stats to the manifest
950 """
951diff --git a/sos/cleaner/archives/__init__.py b/sos/cleaner/archives/__init__.py
952index a70ae5d..e918e2e 100644
953--- a/sos/cleaner/archives/__init__.py
954+++ b/sos/cleaner/archives/__init__.py
955@@ -13,10 +13,12 @@ import os
956 import shutil
957 import stat
958 import tarfile
959+import tempfile
960 import re
961
962 from concurrent.futures import ProcessPoolExecutor
963-from sos.utilities import file_is_binary
964+from sos.utilities import (file_is_binary, sos_get_command_output,
965+ file_is_certificate)
966
967
968 # python older than 3.8 will hit a pickling error when we go to spawn a new
969@@ -54,7 +56,7 @@ class SoSObfuscationArchive():
970 class. All report-level operations should be contained within this class.
971 """
972
973- file_sub_list = []
974+ files_obfuscated_count = 0
975 total_sub_count = 0
976 removed_file_count = 0
977 type_name = 'undetermined'
978@@ -62,7 +64,8 @@ class SoSObfuscationArchive():
979 is_nested = False
980 prep_files = {}
981
982- def __init__(self, archive_path, tmpdir):
983+ def __init__(self, archive_path, tmpdir, keep_binary_files,
984+ treat_certificates):
985 self.archive_path = archive_path
986 self.final_archive_path = self.archive_path
987 self.tmpdir = tmpdir
988@@ -74,10 +77,158 @@ class SoSObfuscationArchive():
989 self.is_extracted = False
990 self._load_self()
991 self.archive_root = ''
992+ self.keep_binary_files = keep_binary_files
993+ self.treat_certificates = treat_certificates
994+ self.parsers = ()
995 self.log_info(
996 f"Loaded {self.archive_path} as type {self.description}"
997 )
998
999+ def obfuscate_string(self, string_data):
1000+ for parser in self.parsers:
1001+ try:
1002+ string_data = parser.parse_string_for_keys(string_data)
1003+ except Exception as err:
1004+ self.log_info(f"Error obfuscating string data: {err}")
1005+ return string_data
1006+
1007+ # TODO: merge content to obfuscate_arc_files as that is the only place we
1008+ # call obfuscate_filename ?
1009+ def obfuscate_filename(self, short_name, filename):
1010+ _ob_short_name = self.obfuscate_string(short_name.split('/')[-1])
1011+ _ob_filename = short_name.replace(short_name.split('/')[-1],
1012+ _ob_short_name)
1013+
1014+ if _ob_filename != short_name:
1015+ arc_path = filename.split(short_name)[0]
1016+ _ob_path = os.path.join(arc_path, _ob_filename)
1017+ # ensure that any plugin subdirs that contain obfuscated strings
1018+ # get created with obfuscated counterparts
1019+ if not os.path.islink(filename):
1020+ os.rename(filename, _ob_path)
1021+ else:
1022+ # generate the obfuscated name of the link target
1023+ _target_ob = self.obfuscate_string(os.readlink(filename))
1024+ # remove the unobfuscated original symlink first, in case the
1025+ # symlink name hasn't changed but the target has
1026+ os.remove(filename)
1027+ # create the newly obfuscated symlink, pointing to the
1028+ # obfuscated target name, which may not exist just yet, but
1029+ # when the actual file is obfuscated, will be created
1030+ os.symlink(_target_ob, _ob_path)
1031+
1032+ def set_parsers(self, parsers):
1033+ self.parsers = parsers # TODO: include this in __init__?
1034+
1035+ def load_parser_entries(self):
1036+ for parser in self.parsers:
1037+ parser.load_map_entries()
1038+
1039+ def obfuscate_line(self, line, parsers=None):
1040+ """Run a line through each of the obfuscation parsers, keeping a
1041+ cumulative total of substitutions done on that particular line.
1042+
1043+ Positional arguments:
1044+
1045+ :param line str: The raw line as read from the file being
1046+ processed
1047+ :param parsers: A list of parser objects to obfuscate
1048+ with. If None, use all.
1049+
1050+ Returns the fully obfuscated line and the number of substitutions made
1051+ """
1052+ # don't iterate over blank lines, but still write them to the tempfile
1053+ # to maintain the same structure when we write a scrubbed file back
1054+ count = 0
1055+ if not line.strip():
1056+ return line, count
1057+ if parsers is None:
1058+ parsers = self.parsers
1059+ for parser in parsers:
1060+ try:
1061+ line, _count = parser.parse_line(line)
1062+ count += _count
1063+ except Exception as err:
1064+ self.log_debug(f"failed to parse line: {err}", parser.name)
1065+ return line, count
1066+
1067+ def obfuscate_arc_files(self, flist):
1068+ for filename in flist:
1069+ self.log_debug(f" pid={os.getpid()}: obfuscating {filename}")
1070+ try:
1071+ short_name = filename.split(self.archive_name + '/')[1]
1072+ if self.should_skip_file(short_name):
1073+ continue
1074+ if (not self.keep_binary_files and
1075+ self.should_remove_file(short_name)):
1076+ # We reach this case if the option --keep-binary-files
1077+ # was not used, and the file is in a list to be removed
1078+ self.remove_file(short_name)
1079+ continue
1080+ if (self.keep_binary_files and
1081+ (file_is_binary(filename) or
1082+ self.should_remove_file(short_name))):
1083+ # We reach this case if the option --keep-binary-files
1084+ # is used. In this case we want to make sure
1085+ # the cleaner doesn't try to clean a binary file
1086+ continue
1087+ if os.path.islink(filename):
1088+ # don't run the obfuscation on the link, but on the actual
1089+ # file at some other point.
1090+ continue
1091+ is_certificate = file_is_certificate(filename)
1092+ if is_certificate:
1093+ if is_certificate == "certificatekey":
1094+ # Always remove certificate Key files
1095+ self.remove_file(short_name)
1096+ continue
1097+ if self.treat_certificates == "keep":
1098+ continue
1099+ if self.treat_certificates == "remove":
1100+ self.remove_file(short_name)
1101+ continue
1102+ if self.treat_certificates == "obfuscate":
1103+ self.certificate_to_text(filename)
1104+ _parsers = [
1105+ _p for _p in self.parsers if not
1106+ any(
1107+ _skip.match(short_name) for _skip in _p.skip_patterns
1108+ )
1109+ ]
1110+ if not _parsers:
1111+ self.log_debug(
1112+ f"Skipping obfuscation of {short_name or filename} "
1113+ f"due to matching file skip pattern"
1114+ )
1115+ continue
1116+ self.log_debug(f"Obfuscating {short_name or filename}")
1117+ subs = 0
1118+ with tempfile.NamedTemporaryFile(mode='w', dir=self.tmpdir) \
1119+ as tfile:
1120+ with open(filename, 'r', encoding='utf-8',
1121+ errors='replace') as fname:
1122+ for line in fname:
1123+ try:
1124+ line, cnt = self.obfuscate_line(line, _parsers)
1125+ subs += cnt
1126+ tfile.write(line)
1127+ except Exception as err:
1128+ self.log_debug(f"Unable to obfuscate "
1129+ f"{short_name}: {err}")
1130+ tfile.seek(0)
1131+ if subs:
1132+ shutil.copyfile(tfile.name, filename)
1133+ self.update_sub_count(subs)
1134+
1135+ self.obfuscate_filename(short_name, filename)
1136+
1137+ except Exception as err:
1138+ self.log_debug(f" pid={os.getpid()}: caught exception on "
1139+ f"obfuscating file {filename}: {err}")
1140+
1141+ return (self.files_obfuscated_count, self.total_sub_count,
1142+ self.removed_file_count)
1143+
1144 @classmethod
1145 def check_is_type(cls, arc_path):
1146 """Check if the archive is a well-known type we directly support"""
1147@@ -120,14 +271,18 @@ class SoSObfuscationArchive():
1148 """Helper to easily format ui messages on a per-report basis"""
1149 self.ui_log.info(f"{self.ui_name + ' :':<50} {msg}")
1150
1151- def _fmt_log_msg(self, msg):
1152- return f"[cleaner:{self.archive_name}] {msg}"
1153+ def _fmt_log_msg(self, msg, caller=None):
1154+ return f"[cleaner{f':{caller}' if caller else ''}" \
1155+ f"[{self.archive_name}]] {msg}"
1156+
1157+ def log_debug(self, msg, caller=None):
1158+ self.soslog.debug(self._fmt_log_msg(msg, caller))
1159
1160- def log_debug(self, msg):
1161- self.soslog.debug(self._fmt_log_msg(msg))
1162+ def log_info(self, msg, caller=None):
1163+ self.soslog.info(self._fmt_log_msg(msg, caller))
1164
1165- def log_info(self, msg):
1166- self.soslog.info(self._fmt_log_msg(msg))
1167+ def log_error(self, msg, caller=None):
1168+ self.soslog.error(self._fmt_log_msg(msg, caller))
1169
1170 def _load_skip_list(self):
1171 """Provide a list of files and file regexes to skip obfuscation on
1172@@ -150,6 +305,16 @@ class SoSObfuscationArchive():
1173 except Exception:
1174 return False
1175
1176+ def certificate_to_text(self, fname):
1177+ """Convert a certificate to text. This is used when cleaner encounters
1178+ a certificate file and the option 'treat_certificates' is 'obfuscate'.
1179+ """
1180+ self.log_info(f"Converting certificate file '{fname}' to text")
1181+ sos_get_command_output(
1182+ f"openssl storeutl -noout -text -certs {str(fname)}",
1183+ to_file=f"{fname}.text")
1184+ os.remove(fname)
1185+
1186 def remove_file(self, fname):
1187 """Remove a file from the archive. This is used when cleaner encounters
1188 a binary file, which we cannot reliably obfuscate.
1189@@ -201,6 +366,7 @@ class SoSObfuscationArchive():
1190 self.report_msg("Extracting...")
1191 self.extracted_path = self.extract_self()
1192 self.is_extracted = True
1193+ self.tarobj = None # we can't pickle this & not further needed
1194 else:
1195 self.extracted_path = self.archive_path
1196 # if we're running as non-root (e.g. collector), then we can have a
1197@@ -326,7 +492,7 @@ class SoSObfuscationArchive():
1198 if os.path.islink(_fname):
1199 yield _fname
1200
1201- def get_file_list(self):
1202+ def get_files(self):
1203 """Iterator for a list of files in the archive, to allow clean to
1204 iterate over.
1205
1206@@ -345,11 +511,11 @@ class SoSObfuscationArchive():
1207 dir_list.append(dirname)
1208 return dir_list
1209
1210- def update_sub_count(self, fname, count):
1211+ def update_sub_count(self, count):
1212 """Called when a file has finished being parsed and used to track
1213 total substitutions made and number of files that had changes made
1214 """
1215- self.file_sub_list.append(fname)
1216+ self.files_obfuscated_count += 1
1217 self.total_sub_count += count
1218
1219 def get_file_path(self, fname):
1220diff --git a/sos/cleaner/archives/sos.py b/sos/cleaner/archives/sos.py
1221index ace71d5..bf010ec 100644
1222--- a/sos/cleaner/archives/sos.py
1223+++ b/sos/cleaner/archives/sos.py
1224@@ -69,7 +69,9 @@ class SoSCollectorArchive(SoSObfuscationArchive):
1225 for fname in os.listdir(_path):
1226 arc_name = os.path.join(_path, fname)
1227 if 'sosreport-' in fname and tarfile.is_tarfile(arc_name):
1228- archives.append(SoSReportArchive(arc_name, self.tmpdir))
1229+ archives.append(SoSReportArchive(arc_name, self.tmpdir,
1230+ self.keep_binary_files,
1231+ self.treat_certificates))
1232 return archives
1233
1234
1235diff --git a/sos/cleaner/mappings/__init__.py b/sos/cleaner/mappings/__init__.py
1236index b5009bb..2a04ff2 100644
1237--- a/sos/cleaner/mappings/__init__.py
1238+++ b/sos/cleaner/mappings/__init__.py
1239@@ -9,8 +9,9 @@
1240 # See the LICENSE file in the source distribution for further information.
1241
1242 import re
1243-
1244-from threading import Lock
1245+import os
1246+import tempfile
1247+from pathlib import Path
1248
1249
1250 class SoSMap():
1251@@ -28,11 +29,33 @@ class SoSMap():
1252 ignore_short_items = False
1253 match_full_words_only = False
1254
1255- def __init__(self):
1256+ def __init__(self, workdir):
1257 self.dataset = {}
1258 self._regexes_made = set()
1259 self.compiled_regexes = []
1260- self.lock = Lock()
1261+ self.cname = self.__class__.__name__.lower()
1262+ # workdir's default value '/tmp' is used just by avocado tests,
1263+ # otherwise we override it to /etc/sos/cleaner (or map_file dir)
1264+ self.workdir = workdir
1265+ self.cache_dir = os.path.join(self.workdir, 'cleaner_cache',
1266+ self.cname)
1267+ self.load_entries()
1268+
1269+ def load_entries(self):
1270+ """ Load cached entries from the disk. This method must be called when
1271+ we initialize a Map instance and whenever we want to retrieve
1272+ self.dataset (e.g. to store default_mapping file). The later is
1273+ essential since a concurrent Map can add more objects to the cache,
1274+ so we need to update self.dataset up to date.
1275+
1276+ Keep in mind that size of self.dataset is usually bigger than number
1277+ of files in the corresponding cleaner's directory: directory contains
1278+ just whole items (e.g. IP addresses) while dataset contains more
1279+ derived objects (e.g. subnets).
1280+ """
1281+
1282+ Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
1283+ self.load_new_entries_from_dir(1)
1284
1285 def ignore_item(self, item):
1286 """Some items need to be completely ignored, for example link-local or
1287@@ -46,6 +69,36 @@ class SoSMap():
1288 return True
1289 return False
1290
1291+ def add_sanitised_item_to_dataset(self, item):
1292+ try:
1293+ self.dataset[item] = self.sanitize_item(item)
1294+ except Exception:
1295+ self.dataset[item] = item
1296+ if self.compile_regexes:
1297+ self.add_regex_item(item)
1298+
1299+ def load_new_entries_from_dir(self, counter):
1300+ # this is a performance hack; there can be gaps in counter values as
1301+ # e.g. sanitised item #14 is an IP address (in file) while item #15
1302+ # is its network (in dataset but not in files). So the next file
1303+ # number is 16. The diffs should be at most 2, the above is so far
1304+ # the only type of "underneath dataset growth". But let be
1305+ # conservative and test next 5 numbers "only".
1306+ no_files_cnt = 5
1307+ while no_files_cnt > 0:
1308+ fname = os.path.join(self.cache_dir, f"{counter}")
1309+ while os.path.isfile(fname):
1310+ no_files_cnt = 5
1311+ with open(fname, 'r', encoding='utf-8') as f:
1312+ item = f.read()
1313+ if not self.dataset.get(item, False):
1314+ self.add_sanitised_item_to_dataset(item)
1315+ counter += 1
1316+ fname = os.path.join(self.cache_dir, f"{counter}")
1317+ # no next file, but try a new next ones until no_files_cnt==0
1318+ no_files_cnt -= 1
1319+ counter += 1
1320+
1321 def add(self, item):
1322 """Add a particular item to the map, generating an obfuscated pair
1323 for it.
1324@@ -56,11 +109,23 @@ class SoSMap():
1325 """
1326 if self.ignore_item(item):
1327 return item
1328- with self.lock:
1329- self.dataset[item] = self.sanitize_item(item)
1330- if self.compile_regexes:
1331- self.add_regex_item(item)
1332- return self.dataset[item]
1333+
1334+ tmpfile = None
1335+ while not self.dataset.get(item, False):
1336+ if not tmpfile:
1337+ # pylint: disable=consider-using-with
1338+ tmpfile = tempfile.NamedTemporaryFile(dir=self.cache_dir)
1339+ with open(tmpfile.name, 'w', encoding='utf-8') as f:
1340+ f.write(item)
1341+ try:
1342+ counter = len(self.dataset) + 1
1343+ os.link(tmpfile.name, os.path.join(self.cache_dir,
1344+ f"{counter}"))
1345+ self.add_sanitised_item_to_dataset(item)
1346+ except FileExistsError:
1347+ self.load_new_entries_from_dir(counter)
1348+
1349+ return self.dataset[item]
1350
1351 def add_regex_item(self, item):
1352 """Add an item to the regexes dict and then re-sort the list that the
1353diff --git a/sos/cleaner/mappings/ip_map.py b/sos/cleaner/mappings/ip_map.py
1354index be6230d..9c5376c 100644
1355--- a/sos/cleaner/mappings/ip_map.py
1356+++ b/sos/cleaner/mappings/ip_map.py
1357@@ -9,7 +9,6 @@
1358 # See the LICENSE file in the source distribution for further information.
1359
1360 import ipaddress
1361-import random
1362
1363 from sos.cleaner.mappings import SoSMap
1364
1365@@ -45,6 +44,11 @@ class SoSIPMap(SoSMap):
1366 network_first_octet = 100
1367 skip_network_octets = ['127', '169', '172', '192']
1368 compile_regexes = False
1369+ # counter for obfuscating a single IP address; the value stands for
1370+ # 172.17.0.0; we use a private block of IP addresses and ignore
1371+ # 172.16.0.0/16 block as those addresses are more often used in real
1372+ # (an attempt to prevent confusion)
1373+ _saddr_cnt = 2886795264
1374
1375 def ip_in_dataset(self, ipaddr):
1376 """There are multiple ways in which an ip address could be handed to us
1377@@ -162,13 +166,14 @@ class SoSIPMap(SoSMap):
1378 return self._new_obfuscated_single_address()
1379
1380 def _new_obfuscated_single_address(self):
1381- def _gen_address():
1382- _octets = []
1383- for _ in range(0, 4):
1384- _octets.append(random.randint(11, 99))
1385- return f"{_octets[0]}.{_octets[1]}.{_octets[2]}.{_octets[3]}"
1386-
1387- _addr = _gen_address()
1388+ # increment the counter and ignore *.0 and *.255 addresses
1389+ self._saddr_cnt += 1
1390+ while self._saddr_cnt % 256 in (0, 255):
1391+ self._saddr_cnt += 1
1392+ # split the counter value to four octets (i.e. % 256) to get an
1393+ # obfuscated IP address
1394+ _addr = f"{self._saddr_cnt >> 24}.{(self._saddr_cnt >> 16) % 256}." \
1395+ f"{(self._saddr_cnt >> 8) % 256}.{self._saddr_cnt % 256}"
1396 if _addr in self.dataset.values():
1397 return self._new_obfuscated_single_address()
1398 return _addr
1399diff --git a/sos/cleaner/mappings/ipv6_map.py b/sos/cleaner/mappings/ipv6_map.py
1400index 10b1cbc..053c6df 100644
1401--- a/sos/cleaner/mappings/ipv6_map.py
1402+++ b/sos/cleaner/mappings/ipv6_map.py
1403@@ -10,39 +10,9 @@
1404
1405 import ipaddress
1406
1407-from random import getrandbits
1408 from sos.cleaner.mappings import SoSMap
1409
1410
1411-def generate_hextets(hextets):
1412- """Generate a random set of hextets, based on the length of the source
1413- hextet. If any hextets are compressed, keep that compression.
1414-
1415- E.G. '::1234:bcd' will generate a leading empty '' hextet, followed by two
1416- 4-character hextets.
1417-
1418- :param hextets: The extracted hextets from a source address
1419- :type hextets: ``list``
1420-
1421- :returns: A set of randomized hextets for use in an obfuscated
1422- address
1423- :rtype: ``list``
1424- """
1425- return [random_hex(4) if h else '' for h in hextets]
1426-
1427-
1428-def random_hex(length):
1429- """Generate a string of size length of random hex characters.
1430-
1431- :param length: The number of characters to generate
1432- :type length: ``int``
1433-
1434- :returns: A string of ``length`` hex characters
1435- :rtype: ``str``
1436- """
1437- return f"{getrandbits(4*length):0{length}x}"
1438-
1439-
1440 class SoSIPv6Map(SoSMap):
1441 """Mapping for IPv6 addresses and networks.
1442
1443@@ -137,6 +107,9 @@ class ObfuscatedIPv6Network():
1444 an obfuscation string is not passed, one will be created during init.
1445 """
1446
1447+ # dict of counters for obfuscated hexes generation
1448+ ob_counters = {}
1449+
1450 def __init__(self, addr, obfuscation='', used_hexes=None):
1451 """Basic setup for the obfuscated network. Minor validation on the addr
1452 used to create the instance, as well as on an optional ``obfuscation``
1453@@ -179,6 +152,37 @@ class ObfuscatedIPv6Network():
1454 def original_address(self):
1455 return self.addr.compressed
1456
1457+ def generate_hextets(self, hextets):
1458+ """Generate a set of obfuscated hextets, based on the length of the
1459+ source hextet. If any hextets are compressed, keep that compression.
1460+
1461+ E.G. '::1234:bcd' will generate a leading empty '' hextet, followed by
1462+ two 4-character hextets, e.g. '::0005:0006'.
1463+
1464+ :param hextets: The extracted hextets from a source address
1465+ :type hextets: ``list``
1466+
1467+ :returns: A set of generated hextets for use in an obfuscated
1468+ address
1469+ :rtype: ``list``
1470+ """
1471+ return [self.obfuscate_hex(4) if h else '' for h in hextets]
1472+
1473+ def obfuscate_hex(self, length):
1474+ """Generate a string of size length of hex characters. Due to the need
1475+ of deterministic generation in concurrent cleaner, generation starts
1476+ from zero values and is incremented by one (for a given length).
1477+
1478+ :param length: The number of characters to generate
1479+ :type length: ``int``
1480+
1481+ :returns: A string of ``length`` hex characters
1482+ :rtype: ``str``
1483+ """
1484+ val = self.ob_counters.get(length, 0) + 1
1485+ self.ob_counters[length] = val
1486+ return f"{val:0{length}x}"
1487+
1488 def _obfuscate_network_address(self):
1489 """Generate the obfuscated pair for the network address. This is
1490 determined based on the netmask of the network this class was built
1491@@ -202,7 +206,7 @@ class ObfuscatedIPv6Network():
1492 sos-specific identifier that could never be seen in the wild,
1493 '534f:'
1494
1495- We then randomize the subnet hextet.
1496+ We then obfuscate the subnet hextet.
1497 """
1498 _hextets = self.network_addr.split(':')[1:]
1499 _ob_hex = ['534f']
1500@@ -213,24 +217,25 @@ class ObfuscatedIPv6Network():
1501 # Set the leading bits to 53, but increment upwards from there for
1502 # when we exceed 256 networks obfuscated in this manner.
1503 _start = 53 + (len(self.first_hexes) // 256)
1504- _ob_hex = f"{_start}{random_hex(2)}"
1505+ _ob_hex = f"{_start}{self.obfuscate_hex(2)}"
1506 while _ob_hex in self.first_hexes:
1507 # prevent duplicates
1508- _ob_hex = f"{_start}{random_hex(2)}"
1509+ _ob_hex = f"{_start}{self.obfuscate_hex(2)}"
1510 self.first_hexes.append(_ob_hex)
1511 _ob_hex = [_ob_hex]
1512- _ob_hex.extend(generate_hextets(_hextets))
1513+ ext = self.generate_hextets(_hextets)
1514+ _ob_hex.extend(ext)
1515 return ':'.join(_ob_hex)
1516
1517 def _obfuscate_private_address(self):
1518 """The first 8 bits will always be 'fd', the next 40 bits are meant
1519 to be a global ID, followed by 16 bits for the subnet. To keep things
1520 relatively simply we maintain the first hextet as 'fd53', and then
1521- randomize any remaining hextets
1522+ obfuscate any remaining hextets.
1523 """
1524 _hextets = self.network_addr.split(':')[1:]
1525 _ob_hex = ['fd53']
1526- _ob_hex.extend(generate_hextets(_hextets))
1527+ _ob_hex.extend(self.generate_hextets(_hextets))
1528 return ':'.join(_ob_hex)
1529
1530 def obfuscate_host_address(self, addr):
1531@@ -259,7 +264,7 @@ class ObfuscatedIPv6Network():
1532 def _generate_address(host):
1533 return ''.join([
1534 self._obfuscated_network,
1535- ':'.join(generate_hextets(host.split(':')))
1536+ ':'.join(self.generate_hextets(host.split(':')))
1537 ])
1538
1539 if addr.compressed not in self.hosts:
1540diff --git a/sos/cleaner/mappings/mac_map.py b/sos/cleaner/mappings/mac_map.py
1541index cf6744e..78403be 100644
1542--- a/sos/cleaner/mappings/mac_map.py
1543+++ b/sos/cleaner/mappings/mac_map.py
1544@@ -8,7 +8,6 @@
1545 #
1546 # See the LICENSE file in the source distribution for further information.
1547
1548-import random
1549 import re
1550
1551 from sos.cleaner.mappings import SoSMap
1552@@ -20,8 +19,9 @@ class SoSMacMap(SoSMap):
1553 MAC addresses added to this map will be broken into two halves, vendor and
1554 device like how MAC addresses are normally crafted. For the vendor hextets,
1555 obfuscation will take the form of 53:4f:53, or 'SOS' in hex. The following
1556- device hextets will be randomized, for example a MAC address of
1557- '60:55:cb:4b:c9:27' may be obfuscated into '53:4f:53:79:ac:29' or similar
1558+ device hextets will be obfuscated by a series of suffixes starting from
1559+ zeroes. For example a MAC address of '60:55:cb:4b:c9:27' may be obfuscated
1560+ into '53:4f:53:00:00:1a' or similar.
1561
1562 This map supports both 48-bit and 64-bit MAC addresses.
1563
1564@@ -49,6 +49,7 @@ class SoSMacMap(SoSMap):
1565 mac6_template = '53:4f:53:ff:fe:%s:%s:%s'
1566 mac6_quad_template = '534f:53ff:fe%s:%s%s'
1567 compile_regexes = False
1568+ ob_hextets_cnt = 0
1569
1570 def add(self, item):
1571 item = item.replace('-', ':').lower().strip('=.,').strip()
1572@@ -59,15 +60,20 @@ class SoSMacMap(SoSMap):
1573 return super().get(item)
1574
1575 def sanitize_item(self, item):
1576- """Randomize the device hextets, and append those to our 'vendor'
1577+ """Obfuscate the device hextets, and append those to our 'vendor'
1578 hextet
1579 """
1580 hexdigits = "0123456789abdcef"
1581- hextets = []
1582- for _ in range(0, 3):
1583- hextets.append(''.join(random.choice(hexdigits) for x in range(2)))
1584+ self.ob_hextets_cnt += 1
1585+ # we need to convert the counter to a triple of double hex-digits
1586+ hextets = [
1587+ self.ob_hextets_cnt >> 16,
1588+ (self.ob_hextets_cnt >> 8) % 256,
1589+ self.ob_hextets_cnt % 256
1590+ ]
1591+ hextets = tuple(f'{hexdigits[i//16]}{hexdigits[i % 16]}'
1592+ for i in hextets)
1593
1594- hextets = tuple(hextets)
1595 # match 64-bit IPv6 MAC addresses matching MM:MM:MM:FF:FE:SS:SS:SS
1596 if re.match('(([0-9a-fA-F]{2}:){7}[0-9a-fA-F]{2})', item):
1597 return self.mac6_template % hextets
1598diff --git a/sos/cleaner/parsers/__init__.py b/sos/cleaner/parsers/__init__.py
1599index e422f32..9ca22af 100644
1600--- a/sos/cleaner/parsers/__init__.py
1601+++ b/sos/cleaner/parsers/__init__.py
1602@@ -55,6 +55,9 @@ class SoSCleanerParser():
1603 self.skip_cleaning_files = skip_cleaning_files
1604 self._generate_skip_regexes()
1605
1606+ def load_map_entries(self):
1607+ self.mapping.load_entries()
1608+
1609 def _generate_skip_regexes(self):
1610 """Generate the regexes for the parser's configured parser_skip_files
1611 or global skip_cleaning_files, so that we don't regenerate them on
1612diff --git a/sos/cleaner/parsers/hostname_parser.py b/sos/cleaner/parsers/hostname_parser.py
1613index 2828732..357e345 100644
1614--- a/sos/cleaner/parsers/hostname_parser.py
1615+++ b/sos/cleaner/parsers/hostname_parser.py
1616@@ -21,8 +21,8 @@ class SoSHostnameParser(SoSCleanerParser):
1617 r'(((\b|_)[a-zA-Z0-9-\.]{1,200}\.[a-zA-Z]{1,63}(\b|_)))'
1618 ]
1619
1620- def __init__(self, config, skip_cleaning_files=[]):
1621- self.mapping = SoSHostnameMap()
1622+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1623+ self.mapping = SoSHostnameMap(workdir)
1624 super().__init__(config, skip_cleaning_files)
1625
1626 def parse_line(self, line):
1627diff --git a/sos/cleaner/parsers/ip_parser.py b/sos/cleaner/parsers/ip_parser.py
1628index 0545175..b0925c8 100644
1629--- a/sos/cleaner/parsers/ip_parser.py
1630+++ b/sos/cleaner/parsers/ip_parser.py
1631@@ -45,6 +45,6 @@ class SoSIPParser(SoSCleanerParser):
1632 map_file_key = 'ip_map'
1633 compile_regexes = False
1634
1635- def __init__(self, config, skip_cleaning_files=[]):
1636- self.mapping = SoSIPMap()
1637+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1638+ self.mapping = SoSIPMap(workdir)
1639 super().__init__(config, skip_cleaning_files)
1640diff --git a/sos/cleaner/parsers/ipv6_parser.py b/sos/cleaner/parsers/ipv6_parser.py
1641index c8d37db..315241f 100644
1642--- a/sos/cleaner/parsers/ipv6_parser.py
1643+++ b/sos/cleaner/parsers/ipv6_parser.py
1644@@ -27,7 +27,7 @@ class SoSIPv6Parser(SoSCleanerParser):
1645 # a trailing prefix for the network bits.
1646 r"(?<![:\\.\\-a-z0-9])((([0-9a-f]{1,4})(:[0-9a-f]{1,4}){7})|"
1647 r"(([0-9a-f]{1,4}(:[0-9a-f]{0,4}){0,5}))([^.])::(([0-9a-f]{1,4}"
1648- r"(:[0-9a-f]{1,4}){0,5})?))(/\d{1,3})?(?![:\\a-z0-9])"
1649+ r"(:[0-9a-f]{1,4}){0,5})?)(\/\d{1,3})?)(?!([a-z0-9]|:[a-z0-9]))"
1650 ]
1651 parser_skip_files = [
1652 'etc/dnsmasq.conf.*',
1653@@ -35,8 +35,8 @@ class SoSIPv6Parser(SoSCleanerParser):
1654 ]
1655 compile_regexes = False
1656
1657- def __init__(self, config, skip_cleaning_files=[]):
1658- self.mapping = SoSIPv6Map()
1659+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1660+ self.mapping = SoSIPv6Map(workdir)
1661 super().__init__(config, skip_cleaning_files)
1662
1663 def get_map_contents(self):
1664diff --git a/sos/cleaner/parsers/keyword_parser.py b/sos/cleaner/parsers/keyword_parser.py
1665index 685fb47..f9dc6e3 100644
1666--- a/sos/cleaner/parsers/keyword_parser.py
1667+++ b/sos/cleaner/parsers/keyword_parser.py
1668@@ -20,8 +20,8 @@ class SoSKeywordParser(SoSCleanerParser):
1669 name = 'Keyword Parser'
1670 map_file_key = 'keyword_map'
1671
1672- def __init__(self, config, skip_cleaning_files=[]):
1673- self.mapping = SoSKeywordMap()
1674+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1675+ self.mapping = SoSKeywordMap(workdir)
1676 super().__init__(config, skip_cleaning_files)
1677
1678 def _parse_line(self, line):
1679diff --git a/sos/cleaner/parsers/mac_parser.py b/sos/cleaner/parsers/mac_parser.py
1680index 35882eb..2068693 100644
1681--- a/sos/cleaner/parsers/mac_parser.py
1682+++ b/sos/cleaner/parsers/mac_parser.py
1683@@ -53,8 +53,8 @@ class SoSMacParser(SoSCleanerParser):
1684 map_file_key = 'mac_map'
1685 compile_regexes = False
1686
1687- def __init__(self, config, skip_cleaning_files=[]):
1688- self.mapping = SoSMacMap()
1689+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1690+ self.mapping = SoSMacMap(workdir)
1691 super().__init__(config, skip_cleaning_files)
1692
1693 def reduce_mac_match(self, match):
1694diff --git a/sos/cleaner/parsers/username_parser.py b/sos/cleaner/parsers/username_parser.py
1695index 7af19ce..664b562 100644
1696--- a/sos/cleaner/parsers/username_parser.py
1697+++ b/sos/cleaner/parsers/username_parser.py
1698@@ -26,8 +26,8 @@ class SoSUsernameParser(SoSCleanerParser):
1699 map_file_key = 'username_map'
1700 regex_patterns = []
1701
1702- def __init__(self, config, skip_cleaning_files=[]):
1703- self.mapping = SoSUsernameMap()
1704+ def __init__(self, config, workdir, skip_cleaning_files=[]):
1705+ self.mapping = SoSUsernameMap(workdir)
1706 super().__init__(config, skip_cleaning_files)
1707
1708 def _parse_line(self, line):
1709diff --git a/sos/collector/__init__.py b/sos/collector/__init__.py
1710index 87a36a5..87149ce 100644
1711--- a/sos/collector/__init__.py
1712+++ b/sos/collector/__init__.py
1713@@ -130,6 +130,7 @@ class SoSCollector(SoSComponent):
1714 'ssh_user': 'root',
1715 'timeout': 600,
1716 'transport': 'auto',
1717+ 'treat_certificates': 'obfuscate',
1718 'verify': False,
1719 'usernames': [],
1720 'upload': False,
1721@@ -516,6 +517,15 @@ class SoSCollector(SoSComponent):
1722 cleaner_grp.add_argument('--usernames', dest='usernames', default=[],
1723 action='extend',
1724 help='List of usernames to obfuscate')
1725+ cleaner_grp.add_argument('--treat-certificates', default='obfuscate',
1726+ choices=['obfuscate', 'keep', 'remove'],
1727+ dest='treat_certificates',
1728+ help=(
1729+ 'How to treat the certificate files '
1730+ '[.csr .crt .pem]. Defaults to "obfuscate"'
1731+ ' after convert the file to text. '
1732+ '"Key" certificate files are always '
1733+ 'removed.'))
1734
1735 @classmethod
1736 def display_help(cls, section):
1737@@ -1405,19 +1415,16 @@ this utility or remote systems that it connects to.
1738 if do_clean:
1739 _dir = os.path.join(self.tmpdir, self.archive._name)
1740 cleaner.obfuscate_file(
1741- os.path.join(_dir, 'sos_logs', 'sos.log'),
1742- short_name='sos.log'
1743+ os.path.join(_dir, 'sos_logs', 'sos.log')
1744 )
1745 cleaner.obfuscate_file(
1746- os.path.join(_dir, 'sos_logs', 'ui.log'),
1747- short_name='ui.log'
1748+ os.path.join(_dir, 'sos_logs', 'ui.log')
1749 )
1750 cleaner.obfuscate_file(
1751- os.path.join(_dir, 'sos_reports', 'manifest.json'),
1752- short_name='manifest.json'
1753+ os.path.join(_dir, 'sos_reports', 'manifest.json')
1754 )
1755
1756- arc_name = self.archive.finalize(self.opts.compression_type)
1757+ arc_name = self.archive.finalize(method=None)
1758 final_name = os.path.join(self.sys_tmp, os.path.basename(arc_name))
1759 if do_clean:
1760 final_name = cleaner.obfuscate_string(
1761diff --git a/sos/collector/clusters/ocp.py b/sos/collector/clusters/ocp.py
1762index 265c1e4..a293323 100644
1763--- a/sos/collector/clusters/ocp.py
1764+++ b/sos/collector/clusters/ocp.py
1765@@ -79,7 +79,7 @@ class ocp(Cluster):
1766 self._oc_cmd = 'oc'
1767 if self.primary.host.in_container():
1768 _oc_path = self.primary.run_command(
1769- 'which oc', chroot=self.primary.host.sysroot
1770+ 'which oc'
1771 )
1772 if _oc_path['status'] == 0:
1773 self._oc_cmd = os.path.join(
1774diff --git a/sos/component.py b/sos/component.py
1775index d0660cf..0056067 100644
1776--- a/sos/component.py
1777+++ b/sos/component.py
1778@@ -168,10 +168,7 @@ class SoSComponent():
1779 if self.opts.tmp_dir:
1780 tmpdir = os.path.abspath(self.opts.tmp_dir)
1781 else:
1782- tmpdir = os.getenv('TMPDIR', None) or '/var/tmp'
1783-
1784- if os.getenv('HOST', None) and os.getenv('container', None):
1785- tmpdir = os.path.join(os.getenv('HOST'), tmpdir.lstrip('/'))
1786+ tmpdir = os.getenv('TMPDIR', None) or self.policy.get_tmp_dir(None)
1787
1788 # no standard library method exists for this, so call out to stat to
1789 # avoid bringing in a dependency on psutil
1790@@ -303,7 +300,7 @@ class SoSComponent():
1791 if not self.preset:
1792 self.preset = self.policy.probe_preset()
1793 # now merge preset options to opts
1794- opts.merge(self.preset.opts)
1795+ opts.merge(self.preset.opts, prefer_new=True)
1796 # re-apply any cmdline overrides to the preset
1797 opts = self.apply_options_from_cmdline(opts)
1798
1799diff --git a/sos/options.py b/sos/options.py
1800index ec80d9a..26354b5 100644
1801--- a/sos/options.py
1802+++ b/sos/options.py
1803@@ -28,7 +28,7 @@ def str_to_bool(val):
1804
1805 class SoSOptions():
1806
1807- def _merge_opt(self, opt, src, is_default):
1808+ def _merge_opt(self, opt, src, is_default, prefer_new):
1809 def _unset(val):
1810 return (val == "" or val is None)
1811
1812@@ -39,9 +39,11 @@ class SoSOptions():
1813 # - we replace unset option by a real value
1814 # - new default is set, or
1815 # - non-sequential variable keeps its default value
1816+ # pylint: disable=too-many-boolean-expressions
1817 if (_unset(oldvalue) and not _unset(newvalue)) or \
1818 is_default or \
1819- ((opt not in self._nondefault) and (not _is_seq(newvalue))):
1820+ ((opt not in self._nondefault) and (not _is_seq(newvalue))) or \
1821+ prefer_new:
1822 # Overwrite atomic values
1823 setattr(self, opt, newvalue)
1824 if is_default:
1825@@ -52,11 +54,11 @@ class SoSOptions():
1826 # Concatenate sequence types
1827 setattr(self, opt, newvalue + oldvalue)
1828
1829- def _merge_opts(self, src, is_default):
1830+ def _merge_opts(self, src, is_default, prefer_new):
1831 if not isinstance(src, dict):
1832 src = vars(src)
1833 for arg in self.arg_names:
1834- self._merge_opt(arg, src, is_default)
1835+ self._merge_opt(arg, src, is_default, prefer_new)
1836
1837 def __str(self, quote=False, sep=" ", prefix="", suffix=""):
1838 """Format a SoSOptions object as a human or machine readable string.
1839@@ -124,7 +126,7 @@ class SoSOptions():
1840 :returntype: SoSOptions
1841 """
1842 opts = SoSOptions(**vars(args), arg_defaults=arg_defaults)
1843- opts._merge_opts(args, True)
1844+ opts._merge_opts(args, True, False)
1845 return opts
1846
1847 @classmethod
1848@@ -233,7 +235,7 @@ class SoSOptions():
1849 if not key.split('.')[0] in self.skip_plugins:
1850 self.plugopts.append(key + '=' + val)
1851
1852- def merge(self, src, skip_default=True):
1853+ def merge(self, src, skip_default=True, prefer_new=False):
1854 """Merge another set of ``SoSOptions`` into this object.
1855
1856 Merge two ``SoSOptions`` objects by setting unset or default
1857@@ -241,12 +243,13 @@ class SoSOptions():
1858
1859 :param src: the ``SoSOptions`` object to copy from
1860 :param is_default: ``True`` if new default values are to be set.
1861+ :param prefer_new: ``False`` if new default is not preferred.
1862 """
1863 for arg in self.arg_names:
1864 if not hasattr(src, arg):
1865 continue
1866 if getattr(src, arg) is not None or not skip_default:
1867- self._merge_opt(arg, src, False)
1868+ self._merge_opt(arg, src, False, prefer_new=prefer_new)
1869
1870 def dict(self, preset_filter=True):
1871 """Return this ``SoSOptions`` option values as a dictionary of
1872diff --git a/sos/policies/distros/debian.py b/sos/policies/distros/debian.py
1873index 0d90603..6f3cbc5 100644
1874--- a/sos/policies/distros/debian.py
1875+++ b/sos/policies/distros/debian.py
1876@@ -16,7 +16,7 @@ class DebianPolicy(LinuxPolicy):
1877 vendor_urls = [('Community Website', 'https://www.debian.org/')]
1878 os_release_name = 'Debian'
1879 os_release_file = '/etc/debian_version'
1880- _tmp_dir = "/tmp"
1881+ _tmp_dir = "/var/tmp"
1882 name_pattern = 'friendly'
1883 valid_subclasses = [DebianPlugin]
1884 PATH = "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" \
1885diff --git a/sos/report/__init__.py b/sos/report/__init__.py
1886index 074afcf..e50de8e 100644
1887--- a/sos/report/__init__.py
1888+++ b/sos/report/__init__.py
1889@@ -149,7 +149,8 @@ class SoSReport(SoSComponent):
1890 'upload_s3_object_prefix': None,
1891 'upload_target': None,
1892 'add_preset': '',
1893- 'del_preset': ''
1894+ 'del_preset': '',
1895+ 'treat_certificates': 'obfuscate'
1896 }
1897
1898 def __init__(self, parser, args, cmdline):
1899@@ -396,6 +397,15 @@ class SoSReport(SoSComponent):
1900 cleaner_grp.add_argument('--usernames', dest='usernames', default=[],
1901 action='extend',
1902 help='List of usernames to obfuscate')
1903+ cleaner_grp.add_argument('--treat-certificates', default='obfuscate',
1904+ choices=['obfuscate', 'keep', 'remove'],
1905+ dest='treat_certificates',
1906+ help=(
1907+ 'How to treat the certificate files '
1908+ '[.csr .crt .pem]. Defaults to "obfuscate"'
1909+ ' after convert the file to text. '
1910+ ' "Key" certificate files are always '
1911+ 'removed.'))
1912
1913 @classmethod
1914 def display_help(cls, section):
1915@@ -1571,13 +1581,10 @@ class SoSReport(SoSComponent):
1916 # Now, separately clean the log files that cleaner also wrote to
1917 if do_clean:
1918 _dir = os.path.join(self.tmpdir, self.archive._name)
1919- cleaner.obfuscate_file(os.path.join(_dir, 'sos_logs', 'sos.log'),
1920- short_name='sos.log')
1921- cleaner.obfuscate_file(os.path.join(_dir, 'sos_logs', 'ui.log'),
1922- short_name='ui.log')
1923+ cleaner.obfuscate_file(os.path.join(_dir, 'sos_logs', 'sos.log'))
1924+ cleaner.obfuscate_file(os.path.join(_dir, 'sos_logs', 'ui.log'))
1925 cleaner.obfuscate_file(
1926- os.path.join(_dir, 'sos_reports', 'manifest.json'),
1927- short_name='manifest.json'
1928+ os.path.join(_dir, 'sos_reports', 'manifest.json')
1929 )
1930
1931 # Now, just (optionally) pack the report and print work outcome; let
1932diff --git a/sos/report/plugins/__init__.py b/sos/report/plugins/__init__.py
1933index 459c016..e1d90d9 100644
1934--- a/sos/report/plugins/__init__.py
1935+++ b/sos/report/plugins/__init__.py
1936@@ -532,6 +532,7 @@ class Plugin():
1937 kernel_mods = ()
1938 services = ()
1939 containers = ()
1940+ runtime = None
1941 architectures = None
1942 archive = None
1943 profiles = ()
1944@@ -1669,7 +1670,8 @@ class Plugin():
1945 self.manifest.files.append(manifest_data)
1946
1947 def add_copy_spec(self, copyspecs, sizelimit=None, maxage=None,
1948- tailit=True, pred=None, tags=[], container=None):
1949+ tailit=True, pred=None, tags=[], container=None,
1950+ runas=None):
1951 """Add a file, directory, or globs matching filepaths to the archive
1952
1953 :param copyspecs: Files, directories, or globs matching filepaths
1954@@ -1697,6 +1699,10 @@ class Plugin():
1955 :param container: Container(s) from which this file should be copied
1956 :type container: ``str`` or a ``list`` of strings
1957
1958+ :param runas: When collecting data from a container, run it under this
1959+ user.
1960+ :type runas: ``str``
1961+
1962 `copyspecs` will be expanded and/or globbed as appropriate. Specifying
1963 a directory here will cause the plugin to attempt to collect the entire
1964 directory, recursively.
1965@@ -1806,14 +1812,14 @@ class Plugin():
1966 if isinstance(container, str):
1967 container = [container]
1968 for con in container:
1969- if not self.container_exists(con):
1970+ if not self.container_exists(con) and runas is None:
1971 continue
1972 _tail = False
1973 if sizelimit:
1974 # to get just the size, stat requires a literal '%s'
1975 # which conflicts with python string formatting
1976 cmd = f"stat -c %s {copyspec}"
1977- ret = self.exec_cmd(cmd, container=con)
1978+ ret = self.exec_cmd(cmd, container=con, runas=runas)
1979 if ret['status'] == 0:
1980 try:
1981 consize = int(ret['output'])
1982@@ -1833,7 +1839,7 @@ class Plugin():
1983 f"{ret['output']}")
1984 continue
1985 self.container_copy_paths.append(
1986- (con, copyspec, sizelimit, _tail, _spec_tags)
1987+ (con, copyspec, sizelimit, _tail, _spec_tags, runas)
1988 )
1989 self._log_info(
1990 f"added collection of '{copyspec}' from container "
1991@@ -2059,7 +2065,7 @@ class Plugin():
1992 def add_dir_listing(self, paths, tree=False, recursive=False, chroot=True,
1993 env=None, sizelimit=None, pred=None, subdir=None,
1994 tags=[], runas=None, container=None,
1995- suggest_filename=None):
1996+ suggest_filename=None, extra_opts=None):
1997 """
1998 Used as a way to standardize our collections of directory listings,
1999 either as an output of `ls` or `tree` depending on if the `tree`
2000@@ -2082,12 +2088,14 @@ class Plugin():
2001
2002 if container:
2003 paths = [p for p in paths if
2004- self.container_path_exists(p, container=container)]
2005+ self.container_path_exists(p, container=container,
2006+ runas=runas)]
2007 else:
2008 paths = [p for p in paths if self.path_exists(p)]
2009
2010 if not tree:
2011- options = f"alZ{'R' if recursive else ''}"
2012+ options = (f"alZ{'R' if recursive else ''}"
2013+ f"{extra_opts if extra_opts else ''}")
2014 else:
2015 options = 'lp'
2016
2017@@ -2105,7 +2113,8 @@ class Plugin():
2018 sizelimit=None, pred=None, subdir=None,
2019 changes=False, foreground=False, tags=[],
2020 priority=10, cmd_as_tag=False, container=None,
2021- to_file=False, runas=None, snap_cmd=False):
2022+ to_file=False, runas=None, snap_cmd=False,
2023+ runtime=None):
2024 """Run a program or a list of programs and collect the output
2025
2026 Output will be limited to `sizelimit`, collecting the last X amount
2027@@ -2184,6 +2193,9 @@ class Plugin():
2028
2029 :param snap_cmd: Are the commands being run from a snap?
2030 :type snap_cmd: ``bool``
2031+
2032+ :param runtime: Specific runtime to use to run container cmd
2033+ :type runtime: ``str``
2034 """
2035 if isinstance(cmds, str):
2036 cmds = [cmds]
2037@@ -2198,7 +2210,8 @@ class Plugin():
2038 if container:
2039 ocmd = cmd
2040 container_cmd = (ocmd, container)
2041- cmd = self.fmt_container_cmd(container, cmd)
2042+ cmd = self.fmt_container_cmd(container, cmd, runtime=runtime,
2043+ runas=runas)
2044 if not cmd:
2045 self._log_debug(f"Skipping command '{ocmd}' as the "
2046 f"requested container '{container}' does "
2047@@ -2547,7 +2560,8 @@ class Plugin():
2048 self.manifest.commands.append(manifest_cmd)
2049 if container_cmd:
2050 self._add_container_cmd_to_manifest(manifest_cmd.copy(),
2051- container_cmd)
2052+ container_cmd,
2053+ suggest_filename)
2054 return result
2055
2056 def collect_cmd_output(self, cmd, suggest_filename=None,
2057@@ -2629,7 +2643,7 @@ class Plugin():
2058 def exec_cmd(self, cmd, timeout=None, stderr=True, chroot=True,
2059 runat=None, env=None, binary=False, pred=None,
2060 foreground=False, container=False, quotecmd=False,
2061- runas=None):
2062+ runas=None, runtime=None):
2063 """Execute a command right now and return the output and status, but
2064 do not save the output within the archive.
2065
2066@@ -2674,6 +2688,10 @@ class Plugin():
2067 :param runas: Run the `cmd` as the `runas` user
2068 :type runas: ``str``
2069
2070+ :param runtime: Specific runtime to use to execute the
2071+ container command
2072+ :type runtime: ``str``
2073+
2074 :returns: Command exit status and output
2075 :rtype: ``dict``
2076 """
2077@@ -2696,8 +2714,9 @@ class Plugin():
2078 self._log_info(f"Cannot run cmd '{cmd}' in container "
2079 f"{container}: no runtime detected on host.")
2080 return _default
2081- if self.container_exists(container):
2082- cmd = self.fmt_container_cmd(container, cmd, quotecmd)
2083+ if self.container_exists(container, runtime) or runas is not None:
2084+ cmd = self.fmt_container_cmd(container, cmd, quotecmd,
2085+ runtime, runas)
2086 else:
2087 self._log_info(f"Cannot run cmd '{cmd}' in container "
2088 f"{container}: no such container is running.")
2089@@ -2731,7 +2750,7 @@ class Plugin():
2090 'tags': tags
2091 })
2092
2093- def _add_container_cmd_to_manifest(self, manifest, contup):
2094+ def _add_container_cmd_to_manifest(self, manifest, contup, suggest_fname):
2095 """Adds a command collection to the manifest for a particular container
2096 and creates a symlink to that collection from the relevant
2097 sos_containers/ location
2098@@ -2752,7 +2771,7 @@ class Plugin():
2099
2100 _cdir = f"sos_containers/{container}/sos_commands/{self.name()}"
2101 _outloc = f"../../../../{manifest['filepath']}"
2102- cmdfn = self._mangle_command(cmd)
2103+ cmdfn = suggest_fname if suggest_fname else self._mangle_command(cmd)
2104 conlnk = f"{_cdir}/{cmdfn}"
2105
2106 # If check_path return None, it means that the sym link already exits,
2107@@ -2777,17 +2796,20 @@ class Plugin():
2108 return self.policy.runtimes[pol_runtime]
2109 return None
2110
2111- def container_exists(self, name):
2112+ def container_exists(self, name, runtime=None):
2113 """If a container runtime is present, check to see if a container with
2114 a given name is currently running
2115
2116 :param name: The name or ID of the container to check presence of
2117 :type name: ``str``
2118
2119+ :param runtime: The runtime to use
2120+ :type runtime: ``str``
2121+
2122 :returns: ``True`` if `name` exists, else ``False``
2123 :rtype: ``bool``
2124 """
2125- _runtime = self._get_container_runtime()
2126+ _runtime = self._get_container_runtime(runtime or self.runtime)
2127 if _runtime is not None:
2128 return (_runtime.container_exists(name) or
2129 _runtime.get_container_by_name(name) is not None)
2130@@ -2811,16 +2833,19 @@ class Plugin():
2131 return [c for c in _containers if re.match(regex, c[1])]
2132 return []
2133
2134- def get_container_by_name(self, name):
2135+ def get_container_by_name(self, name, runtime=None):
2136 """Get the container ID for a specific container
2137
2138 :param name: The name of the container
2139 :type name: ``str``
2140
2141+ :param runtime: The runtime to use
2142+ :type runtime: ``str``
2143+
2144 :returns: The ID of the container if it exists
2145 :rtype: ``str`` or ``None``
2146 """
2147- _runtime = self._get_container_runtime()
2148+ _runtime = self._get_container_runtime(runtime)
2149 if _runtime is not None:
2150 return _runtime.get_container_by_name(name)
2151 return None
2152@@ -2914,7 +2939,8 @@ class Plugin():
2153 cmd = _runtime.get_logs_command(_con[1])
2154 self.add_cmd_output(cmd, **kwargs)
2155
2156- def fmt_container_cmd(self, container, cmd, quotecmd=False):
2157+ def fmt_container_cmd(self, container, cmd, quotecmd=False, runtime=None,
2158+ runas=None):
2159 """Format a command to be executed by the loaded ``ContainerRuntime``
2160 in a specified container
2161
2162@@ -2927,12 +2953,21 @@ class Plugin():
2163 :param quotecmd: Whether the cmd should be quoted.
2164 :type quotecmd: ``bool``
2165
2166+ :param runtime: The specific runtime to use to run the command
2167+ within the container
2168+ :type runtime: ``str``
2169+
2170+ :param runas: What user runs the container. If set, we trust
2171+ the container really runs (we dont keep them atm)
2172+ :type runas: ``str``
2173+
2174 :returns: The command to execute so that the specified `cmd` will run
2175 within the `container` and not on the host
2176 :rtype: ``str``
2177 """
2178- if self.container_exists(container):
2179- _runtime = self._get_container_runtime()
2180+ if self.container_exists(container, runtime) or \
2181+ ((_runtime := self._get_container_runtime(runtime)) and
2182+ runas is not None):
2183 return _runtime.fmt_container_cmd(container, cmd, quotecmd)
2184 return ''
2185
2186@@ -3171,7 +3206,7 @@ class Plugin():
2187 "files from containers. Skipping collections.")
2188 return
2189 for contup in self.container_copy_paths:
2190- con, path, sizelimit, tailit, tags = contup
2191+ con, path, sizelimit, tailit, tags, runas = contup
2192 self._log_info(f"collecting '{path}' from container '{con}'")
2193
2194 arcdest = f"sos_containers/{con}/{path.lstrip('/')}"
2195@@ -3180,15 +3215,21 @@ class Plugin():
2196
2197 cpcmd = rt.get_copy_command(
2198 con, path, dest, sizelimit=sizelimit if tailit else None
2199- )
2200- cpret = self.exec_cmd(cpcmd, timeout=10)
2201+ ) if runas is None else rt.fmt_container_cmd(con, f"cat {path}",
2202+ False)
2203+ cpret = self.exec_cmd(cpcmd, timeout=10, runas=runas)
2204
2205 if cpret['status'] == 0:
2206- if tailit:
2207+ if tailit or runas is not None:
2208 # current runtimes convert files sent to stdout to tar
2209 # archives, with no way to control that
2210 self.archive.add_string(cpret['output'], arcdest)
2211 self._add_container_file_to_manifest(con, path, arcdest, tags)
2212+ self.copied_files.append({
2213+ 'srcpath': path,
2214+ 'dstpath': arcdest,
2215+ 'symlink': "no"
2216+ })
2217 else:
2218 self._log_info(f"error copying '{path}' from container "
2219 f"'{con}': {cpret['output']}")
2220@@ -3427,7 +3468,7 @@ class Plugin():
2221 if verify_cmd:
2222 self.add_cmd_output(verify_cmd)
2223
2224- def container_path_exists(self, path, container):
2225+ def container_path_exists(self, path, container, runas=None):
2226 """Check if a path exists inside a container before
2227 collecting a dir listing
2228
2229@@ -3441,7 +3482,8 @@ class Plugin():
2230 :returns: True if the path exists in the container, else False
2231 :rtype: ``bool``
2232 """
2233- return self.exec_cmd(f"test -e {path}", container=container)
2234+ return self.exec_cmd(f"test -e {path}", container=container,
2235+ runas=runas)
2236
2237 def path_exists(self, path):
2238 """Helper to call the sos.utilities wrapper that allows the
2239diff --git a/sos/report/plugins/aap_containerized.py b/sos/report/plugins/aap_containerized.py
2240index c33b209..7baa5fb 100644
2241--- a/sos/report/plugins/aap_containerized.py
2242+++ b/sos/report/plugins/aap_containerized.py
2243@@ -42,9 +42,23 @@ class AAPContainerized(Plugin, RedHatPlugin):
2244 # Check if username is passed as argument
2245 username = self.get_option("username")
2246 if not username:
2247- self._log_error("Username is mandatory to collect "
2248- "AAP containerized setup logs")
2249- return
2250+ self._log_warn("AAP username is missing, use '-k "
2251+ "aap_containerized.username=<user>' to set it")
2252+ ps = self.exec_cmd("ps aux")
2253+ if ps["status"] == 0:
2254+ podman_users = set()
2255+ for line in ps["output"].splitlines():
2256+ if ("/usr/bin/podman" in line) and \
2257+ ("/.local/share/containers/storage/" in line):
2258+ user, _ = line.split(maxsplit=1)
2259+ podman_users.add(user)
2260+ if len(podman_users) == 1:
2261+ username = podman_users.pop()
2262+ self._log_warn(f"AAP username detected as '{username}'")
2263+ else:
2264+ self._log_error("Unable to determine AAP username, "
2265+ "terminating plugin.")
2266+ return
2267
2268 # Grab aap installation directory under user's home
2269 if not self.get_option("directory"):
2270@@ -97,16 +111,63 @@ class AAPContainerized(Plugin, RedHatPlugin):
2271 # Collect AAP container names
2272 aap_containers = self._get_aap_container_names(username)
2273
2274- # Copy podman container log files in plugin sub directory
2275- # under aap_containers_log
2276+ # Copy podman container log and inspect files
2277+ # into their respective sub directories
2278 for container in aap_containers:
2279- log_file = f"{container}.log"
2280 self.add_cmd_output(
2281 f"su - {username} -c 'podman logs {container}'",
2282- suggest_filename=f"{log_file}",
2283- subdir="aap_containers_log"
2284+ suggest_filename=f"{container}.log",
2285+ subdir="aap_container_logs"
2286+ )
2287+ self.add_cmd_output(
2288+ f"su - {username} -c 'podman inspect {container}'",
2289+ suggest_filename=container,
2290+ subdir="podman_inspect_logs"
2291 )
2292
2293+ # command outputs from various containers
2294+ # the su command is needed because mimicking it via runas leads to
2295+ # stuck command execution
2296+ pod_cmds = {
2297+ "automation-controller-task": [
2298+ "awx-manage check_license --data",
2299+ "awx-manage list_instances",
2300+ ],
2301+ "automation-gateway": [
2302+ "automation-gateway-service status",
2303+ "aap-gateway-manage print_settings",
2304+ "aap-gateway-manage authenticators",
2305+ "aap-gateway-manage showmigrations",
2306+ "aap-gateway-manage list_services",
2307+ "aap-gateway-manage feature_flags --list",
2308+ "aap-gateway-manage --version",
2309+ ],
2310+ "automation-controller-web": [
2311+ "awx-manage showmigrations",
2312+ "awx-manage list_instances",
2313+ "awx-manage run_dispatcher --status",
2314+ "awx-manage run_callback_receiver --status",
2315+ "awx-manage check_license --data",
2316+ "awx-manage run_wsrelay --status",
2317+ ],
2318+ "automation-eda-api": [
2319+ "aap-eda-manage --version",
2320+ "aap-eda-manage showmigrations",
2321+ ],
2322+ "receptor": [
2323+ "receptorctl status",
2324+ "receptorclt work list",
2325+ ],
2326+ }
2327+ for pod, cmds in pod_cmds.items():
2328+ if pod in aap_containers:
2329+ for cmd in cmds:
2330+ fname = self._mangle_command(cmd)
2331+ self.add_cmd_output(f"su - {username} -c 'podman exec -it"
2332+ f" {pod} bash -c \"{cmd}\"'",
2333+ suggest_filename=fname,
2334+ subdir=pod)
2335+
2336 # Function to fetch podman container names
2337 def _get_aap_container_names(self, username):
2338 try:
2339@@ -126,8 +187,8 @@ class AAPContainerized(Plugin, RedHatPlugin):
2340 'dumb-init -- /usr/bin/supervisord',
2341 'dumb-init -- /usr/bin/launch_awx_web.sh',
2342 'dumb-init -- /usr/bin/launch_awx_task.sh',
2343- 'pulpcore-content --name pulp-content --bind',
2344 'dumb-init -- aap-eda-manage',
2345+ 'pulpcore-content --name pulp-content --bind 127.0.0.1',
2346 ]
2347
2348 ps_output = self.exec_cmd("ps --noheaders -eo args")
2349@@ -137,3 +198,20 @@ class AAPContainerized(Plugin, RedHatPlugin):
2350 if process in ps_output['output']:
2351 return True
2352 return False
2353+
2354+ def postproc(self):
2355+ # Mask PASSWORD from print_settings command
2356+ jreg = r'((["\']?PASSWORD["\']?\s*[:=]\s*)[rb]?["\'])(.*?)(["\'])'
2357+ self.do_cmd_output_sub(
2358+ "aap-gateway-manage print_settings",
2359+ jreg,
2360+ r'\1**********\4')
2361+
2362+ # Mask SECRET_KEY from print_settings command
2363+ jreg = r'((SECRET_KEY\s*=\s*)([rb]?["\']))(.*?)(["\'])'
2364+ self.do_cmd_output_sub(
2365+ "aap-gateway-manage print_settings",
2366+ jreg,
2367+ r'\1**********\5')
2368+
2369+# vim: set et ts=4 sw=4 :
2370diff --git a/sos/report/plugins/aap_controller.py b/sos/report/plugins/aap_controller.py
2371index 62a52a3..afb2508 100644
2372--- a/sos/report/plugins/aap_controller.py
2373+++ b/sos/report/plugins/aap_controller.py
2374@@ -10,6 +10,7 @@
2375 # See the LICENSE file in the source distribution for further information.
2376
2377 from sos.report.plugins import Plugin, RedHatPlugin
2378+from sos.utilities import sos_parse_version
2379
2380
2381 class AAPControllerPlugin(Plugin, RedHatPlugin):
2382@@ -22,7 +23,6 @@ class AAPControllerPlugin(Plugin, RedHatPlugin):
2383 'automation-controller-ui',
2384 'automation-controller')
2385 commands = ('awx-manage',)
2386- services = ('automation-controller',)
2387
2388 def setup(self):
2389 self.add_copy_spec([
2390@@ -44,13 +44,12 @@ class AAPControllerPlugin(Plugin, RedHatPlugin):
2391 ])
2392
2393 self.add_cmd_output([
2394- "awx-manage --version",
2395+ "automation-controller-service status",
2396+ "awx-manage showmigrations",
2397 "awx-manage list_instances",
2398 "awx-manage run_dispatcher --status",
2399 "awx-manage run_callback_receiver --status",
2400 "awx-manage check_license --data",
2401- "awx-manage run_wsbroadcast --status",
2402- "awx-manage run_wsrelay --status",
2403 "supervisorctl status",
2404 "/var/lib/awx/venv/awx/bin/pip freeze",
2405 "/var/lib/awx/venv/awx/bin/pip freeze -l",
2406@@ -59,6 +58,17 @@ class AAPControllerPlugin(Plugin, RedHatPlugin):
2407 "umask -p",
2408 ])
2409
2410+ # run_wsbroadcast is replaced with run_wsrelay in AAP 2.4 and above
2411+ awx_version = self.collect_cmd_output('awx-manage --version')
2412+ if awx_version['status'] == 0:
2413+ if (
2414+ sos_parse_version(awx_version['output'].strip()) >
2415+ sos_parse_version('4.4.99')
2416+ ):
2417+ self.add_cmd_output("awx-manage run_wsrelay --status")
2418+ else:
2419+ self.add_cmd_output("awx-manage run_wsbroadcast --status")
2420+
2421 self.add_dir_listing([
2422 '/var/lib/awx',
2423 '/var/lib/awx/venv',
2424@@ -68,7 +78,7 @@ class AAPControllerPlugin(Plugin, RedHatPlugin):
2425
2426 def postproc(self):
2427 # remove database password
2428- jreg = r"(\s*\'PASSWORD\'\s*:(\s))(?:\"){1,}(.+)(?:\"){1,}"
2429+ jreg = r"(\s*'PASSWORD'\s*:\s*)('.*')"
2430 repl = r"\1********"
2431 self.do_path_regex_sub("/etc/tower/conf.d/postgres.py", jreg, repl)
2432
2433@@ -87,5 +97,9 @@ class AAPControllerPlugin(Plugin, RedHatPlugin):
2434 repl = r"\1********"
2435 self.do_path_regex_sub("/etc/tower/conf.d/channels.py", jreg, repl)
2436
2437+ # remove secret key
2438+ jreg = r"(\s*'SECRET_KEY'\s*:\s*)(\".*\")"
2439+ repl = r"\1********"
2440+ self.do_path_regex_sub("/etc/tower/conf.d/gateway.py", jreg, repl)
2441
2442 # vim: set et ts=4 sw=4 :
2443diff --git a/sos/report/plugins/aap_eda.py b/sos/report/plugins/aap_eda.py
2444index 7da276e..7bfd822 100644
2445--- a/sos/report/plugins/aap_eda.py
2446+++ b/sos/report/plugins/aap_eda.py
2447@@ -1,4 +1,5 @@
2448 # Copyright (c) 2025 Rudnei Bertol Jr <rudnei@redhat.com>
2449+# Copyright (c) 2025 Nagoor Shaik <nshaik@redhat.com>
2450
2451 # This file is part of the sos project: https://github.com/sosreport/sos
2452 #
2453@@ -9,6 +10,7 @@
2454 # See the LICENSE file in the source distribution for further information.
2455
2456 from sos.report.plugins import Plugin, RedHatPlugin
2457+from sos.utilities import sos_parse_version
2458
2459
2460 class AAPEDAControllerPlugin(Plugin, RedHatPlugin):
2461@@ -19,6 +21,12 @@ class AAPEDAControllerPlugin(Plugin, RedHatPlugin):
2462 'automation-eda-controller-server')
2463
2464 def setup(self):
2465+
2466+ pkg_name = 'automation-eda-controller'
2467+ pkg = self.policy.package_manager.pkg_by_name(f'{pkg_name}')
2468+ if pkg is not None:
2469+ self.eda_pkg_ver = '.'.join(pkg['version'])
2470+
2471 if self.get_option("all_logs"):
2472 self.add_copy_spec([
2473 "/etc/ansible-automation-platform/",
2474@@ -40,19 +48,54 @@ class AAPEDAControllerPlugin(Plugin, RedHatPlugin):
2475 "/etc/ansible-automation-platform/eda/server.key",
2476 ])
2477
2478- self.add_cmd_output("aap-eda-manage --version")
2479+ self.add_cmd_output([
2480+ "aap-eda-manage --version",
2481+ "aap-eda-manage showmigrations",
2482+ ])
2483+
2484 self.add_dir_listing([
2485 "/etc/ansible-automation-platform/",
2486 "/var/log/ansible-automation-platform/",
2487 ], recursive=True)
2488
2489- self.add_cmd_output("su - eda -c 'export'",
2490- suggest_filename="eda_export")
2491+ self.add_cmd_output("su - eda -c 'env'",
2492+ suggest_filename="eda_environment")
2493+
2494+ pkg_name = 'automation-eda-controller'
2495+ pkg = self.policy.package_manager.pkg_by_name(f'{pkg_name}')
2496+
2497+ # EDA version in 2.5 release starts with 1.1.0 version
2498+ eda_pkg_ver = getattr(self, 'eda_pkg_ver', '0.0.0')
2499+ if sos_parse_version(eda_pkg_ver) > sos_parse_version('1.0.99'):
2500+ self.add_cmd_output([
2501+ "automation-eda-controller-service status",
2502+ "automation-eda-controller-event-stream-service status",
2503+ ])
2504+ else:
2505+ # systemd service status which starts with "automation-eda"
2506+ result = self.exec_cmd(
2507+ 'systemctl list-units --type=service \
2508+ --no-legend automation-eda*'
2509+ )
2510+ if result['status'] == 0:
2511+ for svc in result['output'].splitlines():
2512+ eda_svc = svc.split()
2513+ if not eda_svc:
2514+ continue
2515+ self.add_service_status(eda_svc[0])
2516
2517 def postproc(self):
2518- self.do_path_regex_sub(
2519- "/etc/ansible-automation-platform/eda/environment",
2520- r"(EDA_SECRET_KEY|EDA_DB_PASSWORD)(\s*)(=|:)(\s*)(.*)",
2521- r'\1\2\3\4********')
2522+ # EDA Version in AAP 2.4 uses environment file to store configuration
2523+ eda_pkg_ver = getattr(self, 'eda_pkg_ver', '0.0.0')
2524+ if sos_parse_version(eda_pkg_ver) < sos_parse_version('1.0.99'):
2525+ file_path = "/etc/ansible-automation-platform/eda/environment"
2526+ regex = r"(EDA_SECRET_KEY|EDA_DB_PASSWORD)(\s*)(=|:)(\s*)(.*)"
2527+ replacement = r'\1\2\3\4********'
2528+ # EDA version in AAP 2.5 and above use yaml file for configuration
2529+ else:
2530+ file_path = "/etc/ansible-automation-platform/eda/settings.yaml"
2531+ regex = r"(\s*)(PASSWORD|MQ_USER_PASSWORD|SECRET_KEY)(:\s*)(.*$)"
2532+ replacement = r'\1\2\3********'
2533
2534+ self.do_path_regex_sub(file_path, regex, replacement)
2535 # vim: set et ts=4 sw=4 :
2536diff --git a/sos/report/plugins/aap_gateway.py b/sos/report/plugins/aap_gateway.py
2537index 206a341..ea95531 100644
2538--- a/sos/report/plugins/aap_gateway.py
2539+++ b/sos/report/plugins/aap_gateway.py
2540@@ -1,4 +1,5 @@
2541 # Copyright (c) 2024 Lucas Benedito <lbenedit@redhat.com>
2542+# Copyright (c) 2025 Nagoor Shaik <nshaik@redhat.com>
2543
2544 # This file is part of the sos project: https://github.com/sosreport/sos
2545 #
2546@@ -35,17 +36,39 @@ class AAPGatewayPlugin(Plugin, RedHatPlugin):
2547 "/etc/ansible-automation-platform/gateway/*.cert",
2548 ])
2549
2550- self.add_cmd_output("aap-gateway-manage list_services")
2551+ self.add_cmd_output([
2552+ "automation-gateway-service status",
2553+ "aap-gateway-manage print_settings",
2554+ "aap-gateway-manage authenticators",
2555+ "aap-gateway-manage showmigrations",
2556+ "aap-gateway-manage list_services",
2557+ "aap-gateway-manage feature_flags --list",
2558+ "aap-gateway-manage --version",
2559+ ])
2560 self.add_dir_listing("/etc/ansible-automation-platform/",
2561 recursive=True)
2562
2563 def postproc(self):
2564 # remove database password
2565- jreg = r"(DATABASE_PASSWORD)(\s*)(=|:)(\s*)(.*)"
2566- repl = r"\1\2\3\4********"
2567+ jreg = r"(\s*'PASSWORD'\s*:\s*)('.*')"
2568+ repl = r"\1********"
2569 self.do_path_regex_sub(
2570 "/etc/ansible-automation-platform/gateway/settings.py",
2571 jreg,
2572 repl)
2573
2574+ # Mask PASSWORD from print_settings command
2575+ jreg = r'((["\']?PASSWORD["\']?\s*[:=]\s*)[rb]?["\'])(.*?)(["\'])'
2576+ self.do_cmd_output_sub(
2577+ "aap-gateway-manage print_settings",
2578+ jreg,
2579+ r'\1**********\4')
2580+
2581+ # Mask SECRET_KEY from print_settings command
2582+ jreg = r'((SECRET_KEY\s*=\s*)([rb]?["\']))(.*?)(["\'])'
2583+ self.do_cmd_output_sub(
2584+ "aap-gateway-manage print_settings",
2585+ jreg,
2586+ r'\1**********\5')
2587+
2588 # vim: set et ts=4 sw=4 :
2589diff --git a/sos/report/plugins/aap_hub.py b/sos/report/plugins/aap_hub.py
2590index f10b984..3c3986c 100644
2591--- a/sos/report/plugins/aap_hub.py
2592+++ b/sos/report/plugins/aap_hub.py
2593@@ -26,7 +26,27 @@ class AAPAutomationHub(Plugin, RedHatPlugin):
2594 "/var/log/ansible-automation-platform/hub/pulpcore-content.log*",
2595 "/var/log/nginx/automationhub.access.log*",
2596 "/var/log/nginx/automationhub.error.log*",
2597+ ])
2598+
2599+ # systemd service status which starts with "pulpcore"
2600+ result = self.exec_cmd(
2601+ 'systemctl list-units --type=service --no-legend pulpcore*'
2602+ )
2603+ if result['status'] == 0:
2604+ for svc in result['output'].splitlines():
2605+ pulpcore_svc = svc.split()
2606+ if not pulpcore_svc:
2607+ continue
2608+ self.add_service_status(pulpcore_svc[0])
2609+
2610+ self.add_service_status([
2611+ "nginx",
2612+ "redis"
2613+ ])
2614
2615+ self.add_forbidden_path([
2616+ "/etc/ansible-automation-platform/redis/server.crt",
2617+ "/etc/ansible-automation-platform/redis/server.key",
2618 ])
2619
2620 self.add_dir_listing([
2621diff --git a/sos/report/plugins/apt.py b/sos/report/plugins/apt.py
2622index 464cfb9..c018074 100644
2623--- a/sos/report/plugins/apt.py
2624+++ b/sos/report/plugins/apt.py
2625@@ -32,7 +32,8 @@ class Apt(Plugin, DebianPlugin, UbuntuPlugin):
2626 "apt-get check",
2627 "apt-config dump",
2628 "apt-cache stats",
2629- "apt-cache policy"
2630+ "apt-cache policy",
2631+ "apt-mark showhold"
2632 ])
2633 dpkg_result = self.exec_cmd(
2634 "dpkg-query -W -f='${binary:Package}\t${status}\n'"
2635diff --git a/sos/report/plugins/aws.py b/sos/report/plugins/aws.py
2636new file mode 100644
2637index 0000000..ba819be
2638--- /dev/null
2639+++ b/sos/report/plugins/aws.py
2640@@ -0,0 +1,81 @@
2641+# Copyright (C) 2025, Javier Blanco <javier@jblanco.es>
2642+
2643+# This file is part of the sos project: https://github.com/sosreport/sos
2644+#
2645+# This copyrighted material is made available to anyone wishing to use,
2646+# modify, copy, or redistribute it subject to the terms and conditions of
2647+# version 2 of the GNU General Public License.
2648+#
2649+# See the LICENSE file in the source distribution for further information.
2650+
2651+from sos.report.plugins import Plugin, IndependentPlugin
2652+
2653+
2654+class Aws(Plugin, IndependentPlugin):
2655+
2656+ short_desc = 'AWS EC2 instance metadata'
2657+
2658+ plugin_name = 'aws'
2659+ profiles = ('virt',)
2660+
2661+ def _is_ec2(self):
2662+ try:
2663+ with open('/sys/devices/virtual/dmi/id/sys_vendor',
2664+ encoding='utf-8') as f:
2665+ return 'Amazon' in f.read()
2666+ except FileNotFoundError:
2667+ return False
2668+
2669+ # Called by sos to determine if the plugin will run
2670+ def check_enabled(self):
2671+ return self._is_ec2()
2672+
2673+ def setup(self):
2674+ if not self._is_ec2():
2675+ self.soslog.info(
2676+ "Not an EC2 instance; skipping AWS metadata collection")
2677+ return
2678+
2679+ # Using IMDSv2 if possible, if not, going IMDSv1
2680+ # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
2681+
2682+ # Try to get an IMDSv2 token
2683+ token_url = 'http://169.254.169.254/latest/api/token'
2684+ token_cmd = f'curl -sS -X PUT -H \
2685+ X-aws-ec2-metadata-token-ttl-seconds:21600 {token_url}'
2686+
2687+ try:
2688+ token = self.exec_cmd(token_cmd, timeout=1)['output']
2689+ except Exception:
2690+ token = ''
2691+
2692+ # Add header only if token retrieval succeeded
2693+ token_header = ''
2694+
2695+ if token:
2696+ token_header = f'-H X-aws-ec2-metadata-token:{token}'
2697+
2698+ # List of metadata paths we want to get
2699+ metadata_paths = [
2700+ 'hostname',
2701+ 'instance-id',
2702+ 'instance-life-cycle',
2703+ 'instance-type',
2704+ 'placement/availability-zone-id',
2705+ ]
2706+
2707+ base_url = 'http://169.254.169.254/latest/meta-data/'
2708+
2709+ # Loop on the metadata paths
2710+ for path in metadata_paths:
2711+ meta_url = base_url + path
2712+ safe_name = path.replace('/', '_')
2713+ self.add_cmd_output(
2714+ f'curl -sS {token_header} {meta_url}',
2715+ suggest_filename=f'aws_metadata_{safe_name}.txt'
2716+ )
2717+
2718+ # Those metadata entries do not include any sensitive information.
2719+ # No need to mask any data.
2720+
2721+# vim: set et ts=4 sw=4 :
2722diff --git a/sos/report/plugins/azure.py b/sos/report/plugins/azure.py
2723index 585d74a..3ead906 100644
2724--- a/sos/report/plugins/azure.py
2725+++ b/sos/report/plugins/azure.py
2726@@ -43,7 +43,7 @@ class Azure(Plugin, UbuntuPlugin):
2727 self.add_cmd_output((
2728 'curl -s -H Metadata:true --noproxy "*" '
2729 '"http://169.254.169.254/metadata/instance/compute?'
2730- 'api-version=2021-01-01&format=json"'
2731+ 'api-version=2023-07-01&format=json"'
2732 ), suggest_filename='instance_metadata.json')
2733
2734
2735diff --git a/sos/report/plugins/block.py b/sos/report/plugins/block.py
2736index 7c564bd..3872f44 100644
2737--- a/sos/report/plugins/block.py
2738+++ b/sos/report/plugins/block.py
2739@@ -26,6 +26,7 @@ class Block(Plugin, IndependentPlugin):
2740 })
2741
2742 self.add_dir_listing('/dev', tags=['ls_dev'], recursive=True)
2743+ self.add_dir_listing('/dev/', recursive=True, extra_opts='n')
2744 self.add_dir_listing('/sys/block', recursive=True)
2745
2746 self.add_cmd_output("blkid -c /dev/null", tags="blkid")
2747@@ -40,6 +41,7 @@ class Block(Plugin, IndependentPlugin):
2748
2749 # legacy location for non-/run distributions
2750 self.add_copy_spec([
2751+ "/dev/disk/by-dname/",
2752 "/etc/blkid.tab",
2753 "/run/blkid/blkid.tab",
2754 "/proc/partitions",
2755@@ -69,4 +71,6 @@ class Block(Plugin, IndependentPlugin):
2756 self.add_cmd_output(f'cryptsetup luksDump /dev/{dev}')
2757 self.add_cmd_output(f'clevis luks list -d /dev/{dev}')
2758
2759+ self.add_copy_spec("/etc/crypttab")
2760+
2761 # vim: set et ts=4 sw=4 :
2762diff --git a/sos/report/plugins/boom.py b/sos/report/plugins/boom.py
2763index a872d1b..e3a27dd 100644
2764--- a/sos/report/plugins/boom.py
2765+++ b/sos/report/plugins/boom.py
2766@@ -25,6 +25,9 @@ class Boom(Plugin, RedHatPlugin):
2767 )
2768
2769 def setup(self):
2770+ # Skip collecting cached boot images
2771+ self.add_forbidden_path("/boot/boom/cache/*.img")
2772+
2773 self.add_copy_spec([
2774 "/boot/boom",
2775 "/boot/loader/entries",
2776@@ -34,7 +37,8 @@ class Boom(Plugin, RedHatPlugin):
2777
2778 self.add_cmd_output([
2779 "boom list -VV",
2780- "boom profile list -VV"
2781+ "boom cache list -VV",
2782+ "boom profile list -VV",
2783 ])
2784
2785 # vim: set et ts=4 sw=4 :
2786diff --git a/sos/report/plugins/boot.py b/sos/report/plugins/boot.py
2787index f3c93d1..8f72a0a 100644
2788--- a/sos/report/plugins/boot.py
2789+++ b/sos/report/plugins/boot.py
2790@@ -56,5 +56,7 @@ class Boot(Plugin, IndependentPlugin):
2791 self.add_cmd_output(f"lsinitrd {image}", priority=100)
2792 self.add_cmd_output(f"lsinitramfs -l {image}", priority=100)
2793
2794+ # Capture Open Virtual Machine Firmware information
2795+ self.add_copy_spec("/sys/firmware/efi/ovmf_debug_log")
2796
2797 # vim: set et ts=4 sw=4 :
2798diff --git a/sos/report/plugins/candlepin.py b/sos/report/plugins/candlepin.py
2799index 174cf16..3c655f2 100644
2800--- a/sos/report/plugins/candlepin.py
2801+++ b/sos/report/plugins/candlepin.py
2802@@ -21,6 +21,7 @@ class Candlepin(Plugin, RedHatPlugin):
2803 packages = ('candlepin',)
2804
2805 dbhost = None
2806+ dbport = None
2807 dbpasswd = None
2808 env = None
2809
2810@@ -31,6 +32,7 @@ class Candlepin(Plugin, RedHatPlugin):
2811 # and for DB password, search for
2812 # org.quartz.dataSource.myDS.password=..
2813 self.dbhost = "localhost"
2814+ self.dbport = 5432
2815 self.dbpasswd = ""
2816 cfg_file = "/etc/candlepin/candlepin.conf"
2817 try:
2818@@ -41,10 +43,11 @@ class Candlepin(Plugin, RedHatPlugin):
2819 if not line or line[0] == '#':
2820 continue
2821 if match(r"^\s*org.quartz.dataSource.myDS.URL=\S+", line):
2822- self.dbhost = line.split('=')[1]
2823- # separate hostname from value like
2824+ # separate hostname and port from the jdbc url
2825 # jdbc:postgresql://localhost:5432/candlepin
2826- self.dbhost = self.dbhost.split('/')[2].split(':')[0]
2827+ jdbcurl = line.split('=')[1].split('/')[2].split(':')
2828+ self.dbhost = jdbcurl[0]
2829+ self.dbport = jdbcurl[1]
2830 if match(r"^\s*org.quartz.dataSource.myDS.password=\S+", line):
2831 self.dbpasswd = line.split('=')[1]
2832 except (IOError, IndexError):
2833@@ -122,9 +125,9 @@ class Candlepin(Plugin, RedHatPlugin):
2834 a large amount of quoting in sos logs referencing the command being run
2835 """
2836 csvformat = "-A -F , -X" if csv else ""
2837- _dbcmd = "psql --no-password -h %s -p 5432 -U candlepin \
2838+ _dbcmd = "psql --no-password -h %s -p %s -U candlepin \
2839 -d candlepin %s -c %s"
2840- return _dbcmd % (self.dbhost, csvformat, quote(query))
2841+ return _dbcmd % (self.dbhost, self.dbport, csvformat, quote(query))
2842
2843 def postproc(self):
2844 reg = r"(((.*)(pass|token|secret)(.*))=)(.*)"
2845diff --git a/sos/report/plugins/ceph_mon.py b/sos/report/plugins/ceph_mon.py
2846index dadb96e..89ab12c 100644
2847--- a/sos/report/plugins/ceph_mon.py
2848+++ b/sos/report/plugins/ceph_mon.py
2849@@ -175,10 +175,11 @@ class CephMON(Plugin, RedHatPlugin, UbuntuPlugin):
2850 self.add_cmd_output("ceph osd tree --format json-pretty",
2851 subdir="json_output",
2852 tags="ceph_osd_tree")
2853- self.add_cmd_output(
2854- [f"ceph tell mon.{mid} mon_status" for mid in self.get_ceph_ids()],
2855- subdir="json_output",
2856- )
2857+ for mid in self.get_ceph_ids():
2858+ self.add_cmd_output([
2859+ f"ceph tell mon.{mid} mon_status",
2860+ f"ceph tell mon.{mid} sessions",
2861+ ], subdir="json_output")
2862
2863 self.add_cmd_output([f"ceph {cmd}" for cmd in ceph_cmds])
2864
2865diff --git a/sos/report/plugins/charmed_mysql.py b/sos/report/plugins/charmed_mysql.py
2866index dd31121..acc414c 100644
2867--- a/sos/report/plugins/charmed_mysql.py
2868+++ b/sos/report/plugins/charmed_mysql.py
2869@@ -7,27 +7,67 @@
2870 # See the LICENSE file in the source distribution for further information.#
2871
2872 import os
2873+import tempfile
2874
2875 from sos.report.plugins import Plugin, PluginOpt, UbuntuPlugin
2876+from sos.utilities import is_executable
2877
2878
2879 class CharmedMySQL(Plugin, UbuntuPlugin):
2880 """
2881 The Charmed MySQL plugin is used to collect MySQL configuration and logs
2882- from the Charmed MySQL snap package. It also collects MySQL Router
2883- and MySQL Shell configuration and logs where available,
2884- journal logs for the snap, and snap info.
2885+ from the Charmed MySQL snap package or K8s deployment.
2886+ It also collects MySQL Router and MySQL Shell configuration and logs
2887+ where available, journal logs for the snap, and snap info.
2888
2889 If the `dumpdbs` option is set to `True`, the plugin will also try and
2890 collect the names of the databases that the user has access to. The
2891 `mysql` user is used by default, but that can be set using the `dbuser`
2892 option. When using the `dumpdbs` option, you must then provide the
2893- password for the user using the `dbpass` option.
2894+ password for the user using the `dbpass` option or the `MYSQL_PWD`
2895+ environment variable.
2896 """
2897
2898 short_desc = "Charmed MySQL"
2899 plugin_name = "charmed_mysql"
2900- packages = ("charmed-mysql",)
2901+
2902+ mysql_queries = [
2903+ # Get databases user has access to
2904+ "show databases;",
2905+
2906+ # Get unit operations from MySQL to see, for example,
2907+ # if a unit is stuck on joining the cluster
2908+ "select * from mysql.juju_units_operations;",
2909+
2910+ # Get the cluster group replication status
2911+ ("select * from performance_schema.replication_group_members "
2912+ "order by MEMBER_HOST;"),
2913+
2914+ # Get connection stats
2915+ "show global status like \"%conne%\";",
2916+
2917+ # Get errors per client and host
2918+ # Useful for problens like an app disconnectting randomly
2919+ "select * from performance_schema.host_cache;",
2920+
2921+ # Get InnoDB status for any deadlocks, etc.
2922+ "show ENGINE InnoDB STATUS;",
2923+ ]
2924+
2925+ snap_package = "charmed-mysql"
2926+ snap_path_common = "/var/snap/charmed-mysql/common"
2927+ snap_path_current = "/var/snap/charmed-mysql/current"
2928+
2929+ kube_cmd = "kubectl"
2930+ selector = "app.kubernetes.io/name=mysql-k8s"
2931+
2932+ conf_paths = {
2933+ "MYSQL_CONF": "/etc/mysql",
2934+ "MYSQL_LOGS": "/var/log/mysql",
2935+ "MYSQL_ROUTER_CONF": "/etc/mysqlrouter",
2936+ "MYSQL_ROUTER_LOGS": "/var/log/mysqlrouter",
2937+ "MYSQL_SHELL_LOGS": "/var/log/mysqlsh",
2938+ }
2939
2940 option_list = [
2941 PluginOpt(
2942@@ -42,103 +82,213 @@ class CharmedMySQL(Plugin, UbuntuPlugin):
2943 "dumpdbs", default=False, val_type=bool,
2944 desc="Get name of all databases"
2945 ),
2946+ PluginOpt(
2947+ "logs_since", default="48h", val_type=str,
2948+ desc="How far back to fetch logs with kubectl --since, K8s only"
2949+ ),
2950 ]
2951
2952- def setup(self):
2953- # Set default paths for snap
2954- snap_path_common = "/var/snap/charmed-mysql/common"
2955- snap_path_current = "/var/snap/charmed-mysql/current"
2956-
2957- # Check if deployment is in a Kubernetes cluster
2958- k8s_deploy = any("KUBERNETES" in key for key in os.environ)
2959-
2960- # If in a Kubernetes cluster, set the snap paths to "None" as
2961- # MySQL configuration does not exist in snap paths
2962- if k8s_deploy:
2963- snap_path_common = ""
2964- snap_path_current = ""
2965-
2966- # Set the configuration paths
2967- conf_paths = {
2968- "MYSQL_CONF": f"{snap_path_current}/etc/mysql",
2969- "MYSQL_LOGS": f"{snap_path_common}/var/log/mysql",
2970- "MYSQL_ROUTER_CONF": f"{snap_path_current}/etc/mysqlrouter",
2971- "MYSQL_ROUTER_LOGS": f"{snap_path_common}/var/log/mysqlrouter",
2972- "MYSQL_SHELL_LOGS": f"{snap_path_common}/var/log/mysqlsh",
2973- }
2974+ def _get_db_credentials(self):
2975+ db_user = self.get_option("dbuser")
2976+ db_pass = self.get_option("dbpass")
2977
2978+ if "MYSQL_PWD" in os.environ and not db_pass:
2979+ self.soslog.info(
2980+ "MYSQL_PWD present: Using MYSQL_PWD environment variable, "
2981+ "user did not provide password."
2982+ )
2983+ db_pass = os.environ["MYSQL_PWD"]
2984+ elif not db_pass:
2985+ self.soslog.warning(
2986+ "dumpdbs_error: option is set, but username and password "
2987+ "are not provided"
2988+ )
2989+ return None, None
2990+
2991+ return db_user, db_pass
2992+
2993+ def _determine_namespaces(self):
2994+ namespaces = self.exec_cmd(
2995+ f"{self.kube_cmd} get pods -A -l {self.selector} "
2996+ "-o jsonpath='{.items[*].metadata.namespace}'"
2997+ )
2998+ if namespaces['status'] == 0:
2999+ return list(set(namespaces['output'].strip().split()))
3000+ return []
3001+
3002+ def _get_pod_names(self, namespace):
3003+ pods = self.exec_cmd(
3004+ f"{self.kube_cmd} -n {namespace} get pods -l {self.selector} "
3005+ "-o jsonpath='{.items[*].metadata.name}'"
3006+ )
3007+ if pods['status'] == 0:
3008+ return pods['output'].strip().split()
3009+ return []
3010+
3011+ def _collect_per_namespace(self, namespace, logs_since):
3012+ kube_cmd = f"{self.kube_cmd} -n {namespace}"
3013+
3014+ # Describe the resources
3015+ self.add_cmd_output([
3016+ f"{kube_cmd} get all -l {self.selector} -o wide",
3017+ f"{kube_cmd} describe pods -l {self.selector}",
3018+ ])
3019+
3020+ # Get pod logs
3021+ self.add_cmd_output(
3022+ f"{kube_cmd} logs -l {self.selector} --since={logs_since} "
3023+ "--all-containers=true --prefix --all-pods"
3024+ )
3025+
3026+ mysql_cont = "mysql"
3027+ pods = self._get_pod_names(namespace)
3028+
3029+ # Get the config and logs from each pod
3030+ dump_files_path = self.get_cmd_output_path()
3031+ for name, path in self.conf_paths.items():
3032+ for pod in pods:
3033+ os.makedirs(f"{dump_files_path}/{pod}/{name}", exist_ok=True)
3034+ copy_cmd = (
3035+ f"{kube_cmd} cp -c {mysql_cont} {pod}:{path} "
3036+ f"{dump_files_path}/{pod}/{name}"
3037+ )
3038+ self.exec_cmd(copy_cmd)
3039+
3040+ self.add_forbidden_path([
3041+ f"{dump_files_path}/*/MYSQL_CONF/*.pem",
3042+ f"{dump_files_path}/*/MYSQL_CONF/*.key",
3043+ ])
3044+
3045+ # If dumpdbs is set, then get all databases
3046+ if self.get_option("dumpdbs"):
3047+ db_user, db_pass = self._get_db_credentials()
3048+ if not db_user or not db_pass:
3049+ return
3050+
3051+ opts = f"-h 127.0.0.1 -u{db_user}"
3052+ sql_cmd = f"mysql {opts} -e"
3053+ queries = " ".join(
3054+ query.replace('\"', '\\\"')
3055+ for query in self.mysql_queries
3056+ )
3057+
3058+ with tempfile.TemporaryDirectory() as tmpdir:
3059+ pwd_file = "mysql_pwd"
3060+ pwd_path = tmpdir + "/" + pwd_file
3061+
3062+ with open(pwd_path, "w", encoding="utf8") as f:
3063+ f.write(db_pass)
3064+
3065+ for pod in pods:
3066+ mkdir_cmd = (
3067+ f"{kube_cmd} exec -c {mysql_cont} {pod} -- "
3068+ f"mkdir -p {tmpdir}"
3069+ )
3070+ self.exec_cmd(mkdir_cmd)
3071+
3072+ copy_cmd = (
3073+ f"{kube_cmd} cp -c {mysql_cont} "
3074+ f"{pwd_path} {pod}:{pwd_path}"
3075+ )
3076+ self.exec_cmd(copy_cmd)
3077+
3078+ queries_cmd = (
3079+ f"{kube_cmd} exec -c {mysql_cont} {pod} -- "
3080+ f"sh -lc 'MYSQL_PWD=$(cat {pwd_path}) "
3081+ f"{sql_cmd} \"{queries}\"' "
3082+ f"&& rm -rf {tmpdir}"
3083+ )
3084+ self.add_cmd_output(
3085+ queries_cmd,
3086+ suggest_filename=f"{pod}_dbs.txt",
3087+ )
3088+
3089+ def _join_conf_path(self, base, *parts):
3090+ stripped_parts = [p.lstrip(os.path.sep) for p in parts]
3091+ return os.path.join(base, *stripped_parts)
3092+
3093+ def _process_k8s(self):
3094+ logs_since = self.get_option("logs_since")
3095+ namespaces = self._determine_namespaces()
3096+ for namespace in namespaces:
3097+ self._collect_per_namespace(namespace, logs_since)
3098+
3099+ def _process_snap(self):
3100 # Ignore private keys
3101 self.add_forbidden_path([
3102- f"{conf_paths['MYSQL_CONF']}/*.pem",
3103- f"{conf_paths['MYSQL_CONF']}/*.key",
3104+ self._join_conf_path(
3105+ self.snap_path_current,
3106+ self.conf_paths["MYSQL_CONF"],
3107+ "*.pem"
3108+ ),
3109+ self._join_conf_path(
3110+ self.snap_path_current,
3111+ self.conf_paths["MYSQL_CONF"],
3112+ "*.key"
3113+ ),
3114 ])
3115
3116 # Include the files we want to get
3117 self.add_copy_spec([
3118- conf_paths["MYSQL_CONF"],
3119- conf_paths["MYSQL_LOGS"],
3120- conf_paths["MYSQL_ROUTER_CONF"],
3121- conf_paths["MYSQL_ROUTER_LOGS"],
3122- conf_paths["MYSQL_SHELL_LOGS"],
3123+ self._join_conf_path(
3124+ self.snap_path_current,
3125+ self.conf_paths["MYSQL_CONF"]
3126+ ),
3127+ self._join_conf_path(
3128+ self.snap_path_common,
3129+ self.conf_paths["MYSQL_LOGS"]
3130+ ),
3131+ self._join_conf_path(
3132+ self.snap_path_current,
3133+ self.conf_paths["MYSQL_ROUTER_CONF"]
3134+ ),
3135+ self._join_conf_path(
3136+ self.snap_path_common,
3137+ self.conf_paths["MYSQL_ROUTER_LOGS"]
3138+ ),
3139+ self._join_conf_path(
3140+ self.snap_path_common,
3141+ self.conf_paths["MYSQL_SHELL_LOGS"]
3142+ ),
3143 ])
3144
3145- # Only applicable if not in a Kubernetes cluster
3146- # as `snap info` and `journalctl` are not available
3147- # in the Kubernetes container
3148- if not k8s_deploy:
3149- # Get snap logs
3150- self.add_journal("snap.charmed-mysql.*")
3151+ # Get snap logs
3152+ self.add_journal("snap.charmed-mysql.*")
3153
3154- # Get snap info
3155- self.add_cmd_output("snap info charmed-mysql")
3156+ # Get snap info
3157+ self.add_cmd_output("snap info charmed-mysql")
3158
3159 # If dumpdbs is set, then get all databases
3160 if self.get_option("dumpdbs"):
3161-
3162- db_user = self.get_option("dbuser")
3163- db_pass = self.get_option("dbpass")
3164-
3165- # Check password is not already an environment variable
3166- # and user did not supply a password
3167- if "MYSQL_PWD" in os.environ and not db_pass:
3168- self.soslog.info(
3169- "MYSQL_PWD present: Using MYSQL_PWD environment variable, "
3170- "user did not provide password."
3171- )
3172- db_pass = os.environ["MYSQL_PWD"]
3173- elif not db_pass: # Environment variable not set and no password
3174- self.soslog.warning(
3175- "dumpdbs_error: option is set, but username and password "
3176- "are not provided"
3177- )
3178+ db_user, db_pass = self._get_db_credentials()
3179+ if not db_user or not db_pass:
3180 return
3181
3182 mysql_env = {"MYSQL_PWD": db_pass}
3183 opts = f"-h 127.0.0.1 -u{db_user}"
3184 sql_cmd = f"mysql {opts} -e"
3185- queries = [
3186- # Get databases user has access to
3187- "'show databases;'",
3188
3189- # Get unit operations from MySQL to see, for example,
3190- # if a unit is stuck on joining the cluster
3191- "'select * from mysql.juju_units_operations;'",
3192+ self.add_cmd_output(
3193+ [f"{sql_cmd} '{query}'" for query in self.mysql_queries],
3194+ env=mysql_env,
3195+ )
3196
3197- # Get the cluster group replication status
3198- ("'select * from performance_schema.replication_group_members "
3199- "order by MEMBER_HOST;'"),
3200+ def check_enabled(self):
3201+ # Check for snap package
3202+ if self.is_installed(self.snap_package):
3203+ return True
3204
3205- # Get connection stats
3206- "'show global status like \"%conne%\";'",
3207+ # Check for kubectl and pods
3208+ return (
3209+ is_executable(self.kube_cmd, self.sysroot) and
3210+ bool(self._determine_namespaces())
3211+ )
3212
3213- # Get errors per client and host
3214- # Useful for problens like an app disconnectting randomly
3215- "'select * from performance_schema.host_cache;'"
3216- ]
3217+ def setup(self):
3218+ if self.is_installed(self.snap_package):
3219+ self._process_snap()
3220
3221- self.add_cmd_output(
3222- [f"{sql_cmd} {query}" for query in queries],
3223- env=mysql_env
3224- )
3225+ if is_executable(self.kube_cmd, self.sysroot):
3226+ self._process_k8s()
3227
3228 # vim: set et ts=4 sw=4 :
3229diff --git a/sos/report/plugins/dnf.py b/sos/report/plugins/dnf.py
3230index 6922495..d79d58c 100644
3231--- a/sos/report/plugins/dnf.py
3232+++ b/sos/report/plugins/dnf.py
3233@@ -78,8 +78,10 @@ class DNFPlugin(Plugin, RedHatPlugin):
3234 self.add_cmd_output([
3235 "dnf --version",
3236 "dnf list extras",
3237+ "dnf updateinfo info security",
3238+ "dnf updateinfo list --available",
3239 "package-cleanup --dupes",
3240- "package-cleanup --problems"
3241+ "package-cleanup --problems",
3242 ])
3243
3244 self.add_cmd_output("dnf list installed",
3245diff --git a/sos/report/plugins/drbd.py b/sos/report/plugins/drbd.py
3246index ecd6817..4e2758d 100644
3247--- a/sos/report/plugins/drbd.py
3248+++ b/sos/report/plugins/drbd.py
3249@@ -7,27 +7,74 @@
3250 # See the LICENSE file in the source distribution for further information.
3251
3252 from sos.report.plugins import Plugin, RedHatPlugin, UbuntuPlugin
3253+from sos.utilities import find
3254
3255
3256-class DRDB(Plugin, RedHatPlugin, UbuntuPlugin):
3257+class DRBD(Plugin, RedHatPlugin, UbuntuPlugin):
3258
3259 short_desc = 'Distributed Replicated Block Device (DRBD)'
3260
3261 plugin_name = 'drbd'
3262 profiles = ('storage',)
3263- packages = ('drbd*-utils',)
3264+ commands = ('drbdsetup',)
3265+
3266+ # In case of kernel bugs, drbdsetup may hang indefinitely.
3267+ # Set a shorter than default timeout.
3268+ cmd_timeout = 60
3269+
3270+ def add_drbd_thread_stacks(self):
3271+ stacks = ""
3272+ for pid_file in find("*_pid", "/sys/kernel/debug/drbd/resources/"):
3273+ with open(pid_file, 'r', encoding='utf-8') as f:
3274+ pid = f.read().strip()
3275+ stacks += f"--- {pid_file}: {pid} ---\n"
3276+ if pid.isdigit():
3277+ try:
3278+ sfn = f"/proc/{pid}/stack"
3279+ with open(sfn, 'r', encoding='utf-8') as sf:
3280+ stacks += sf.read()
3281+ except Exception as e:
3282+ stacks += f"Could not read /proc/{pid}/stack: {e}\n"
3283+ stacks += "\n"
3284+
3285+ self.add_string_as_file(stacks, "drbd_thread_stacks")
3286
3287 def setup(self):
3288 self.add_cmd_output([
3289- "drbd-overview",
3290- "drbdadm dump-xml",
3291- "drbdsetup status all",
3292+ "drbdadm dump",
3293+ "drbdadm -d -vvv adjust all",
3294+ "drbdsetup status -vs all",
3295 "drbdsetup show all"
3296 ])
3297+ self.add_drbd_thread_stacks()
3298+ for kmod in find("drbd*.ko*", "/lib/modules"):
3299+ self.add_cmd_output([
3300+ f"modinfo {kmod}",
3301+ ], suggest_filename=f"modinfo_{kmod.replace('/', '_')}")
3302 self.add_copy_spec([
3303 "/etc/drbd.conf",
3304 "/etc/drbd.d/*",
3305- "/proc/drbd"
3306+ "/proc/drbd",
3307+ "/sys/kernel/debug/drbd/*",
3308+ "/var/lib/drbd/*",
3309+ "/var/lib/drbd-support/*",
3310+ "/var/lib/linstor.d/*"
3311 ])
3312
3313+ def postproc(self):
3314+ # Scrub nodehash from /var/lib/drbd-support/registration.json
3315+ nodehash_re = r'("nodehash"\s*:\s*")[a-zA-Z0-9]+"'
3316+ repl = r'\1********"'
3317+ self.do_path_regex_sub(
3318+ '/var/lib/drbd-support/registration.json',
3319+ nodehash_re, repl
3320+ )
3321+
3322+ # Scrub shared secret from *.{conf,res} files and command outputs
3323+ secret_re = r'(shared-secret\s+\"?).[^\"]+(\"?\s*;)'
3324+ repl = r'\1********\2'
3325+ self.do_path_regex_sub(r'.*\.(conf|res)', secret_re, repl)
3326+ self.do_cmd_output_sub("drbdadm dump", secret_re, repl)
3327+ self.do_cmd_output_sub("drbdsetup show all", secret_re, repl)
3328+
3329 # vim: set et ts=4 sw=4 :
3330diff --git a/sos/report/plugins/firewalld.py b/sos/report/plugins/firewalld.py
3331index 0d218d7..2f2b1d9 100644
3332--- a/sos/report/plugins/firewalld.py
3333+++ b/sos/report/plugins/firewalld.py
3334@@ -47,6 +47,8 @@ class FirewallD(Plugin, RedHatPlugin):
3335 "firewall-cmd --get-log-denied",
3336 "firewall-cmd --list-all-zones",
3337 "firewall-cmd --permanent --list-all-zones",
3338+ "firewall-cmd --list-all-policies",
3339+ "firewall-cmd --permanent --list-all-policies",
3340 "firewall-cmd --permanent --direct --get-all-chains",
3341 "firewall-cmd --permanent --direct --get-all-rules",
3342 "firewall-cmd --permanent --direct --get-all-passthroughs",
3343diff --git a/sos/report/plugins/foreman.py b/sos/report/plugins/foreman.py
3344index 1d00d59..bdb2d4a 100644
3345--- a/sos/report/plugins/foreman.py
3346+++ b/sos/report/plugins/foreman.py
3347@@ -25,6 +25,7 @@ class Foreman(Plugin):
3348 packages = ('foreman',)
3349 apachepkg = None
3350 dbhost = "localhost"
3351+ dbport = 5432
3352 dbpasswd = ""
3353 env = {"PGPASSWORD": ""}
3354 option_list = [
3355@@ -33,7 +34,9 @@ class Foreman(Plugin):
3356 PluginOpt('proxyfeatures', default=False,
3357 desc='collect features of smart proxies'),
3358 PluginOpt('puma-gc', default=False,
3359- desc='collect Puma GC stats')
3360+ desc='collect Puma GC stats'),
3361+ PluginOpt('cvfilters', default=False,
3362+ desc='collect content view filters definition')
3363 ]
3364 pumactl = 'pumactl %s -S /usr/share/foreman/tmp/puma.state'
3365
3366@@ -56,6 +59,8 @@ class Foreman(Plugin):
3367 continue
3368 if production_scope and match(r"\s+host:\s+\S+", line):
3369 self.dbhost = line.split()[1]
3370+ if production_scope and match(r"\s+port:\s+\S+", line):
3371+ self.dbport = line.split()[1]
3372 if production_scope and match(r"\s+password:\s+\S+", line):
3373 self.dbpasswd = line.split()[1]
3374 # if line starts with a text, it is a different scope
3375@@ -184,6 +189,8 @@ class Foreman(Plugin):
3376 env=self.env)
3377 self.collect_foreman_db()
3378 self.collect_proxies()
3379+ if self.get_option('cvfilters'):
3380+ self.collect_cv_filters()
3381
3382 def collect_foreman_db(self):
3383 # pylint: disable=too-many-locals
3384@@ -240,6 +247,16 @@ class Foreman(Plugin):
3385 'LIMIT 100'
3386 )
3387
3388+ smartcmd = (
3389+ "SELECT sp.id, sp.name, sp.url, sp.download_policy, "
3390+ "STRING_AGG(f.name, ', ') AS features "
3391+ "FROM smart_proxies AS sp "
3392+ "INNER JOIN smart_proxy_features AS spf "
3393+ "ON sp.id = spf.smart_proxy_id "
3394+ "INNER JOIN features AS f ON spf.feature_id = f.id "
3395+ "GROUP BY sp.id"
3396+ )
3397+
3398 # Populate this dict with DB queries that should be saved directly as
3399 # postgres formats them. The key will be the filename in the foreman
3400 # plugin directory, with the value being the DB query to run
3401@@ -255,8 +272,7 @@ class Foreman(Plugin):
3402 'audits_table_count': 'select count(*) from audits',
3403 'logs_table_count': 'select count(*) from logs',
3404 'fact_names_prefixes': factnamescmd,
3405- 'smart_proxies': 'select name,url,download_policy ' +
3406- 'from smart_proxies'
3407+ 'smart_proxies': smartcmd
3408 }
3409
3410 # Same as above, but tasks should be in CSV output
3411@@ -305,6 +321,58 @@ class Foreman(Plugin):
3412 subdir='smart_proxies_features',
3413 timeout=10)
3414
3415+ def collect_cv_filters(self):
3416+ """ Collect content view filters definition if requested """
3417+ cv_filters_cmd = (
3418+ "select f.id, f.name, f.type, f.content_view_id, "
3419+ "cv.name as content_view_name, f.description "
3420+ "from katello_content_view_filters as f "
3421+ "inner join katello_content_views as cv "
3422+ "on f.content_view_id = cv.id"
3423+ )
3424+ cv_pkg_rules_cmd = (
3425+ "select id, content_view_filter_id, name, min_version, "
3426+ "max_version from katello_content_view_package_filter_rules"
3427+ )
3428+ cv_group_rules_cmd = (
3429+ "select id, content_view_filter_id, name, uuid "
3430+ "from katello_content_view_package_group_filter_rules"
3431+ )
3432+ cv_errata_rules_cmd = (
3433+ "select id, content_view_filter_id, errata_id, start_date, "
3434+ "end_date, types from "
3435+ "katello_content_view_erratum_filter_rules"
3436+ )
3437+ cv_module_rules_cmd = (
3438+ "select * from "
3439+ "katello_content_view_module_stream_filter_rules"
3440+ )
3441+ cv_docker_rules_cmd = (
3442+ "select id, content_view_filter_id, name "
3443+ "from katello_content_view_docker_filter_rules"
3444+ )
3445+ cv_deb_rules_cmd = (
3446+ "select id, content_view_filter_id, name, version, "
3447+ "min_version, max_version "
3448+ "from katello_content_view_deb_filter_rules"
3449+ )
3450+
3451+ filter_tables = {
3452+ 'katello_cv_filters': cv_filters_cmd,
3453+ 'katello_cv_package_rules': cv_pkg_rules_cmd,
3454+ 'katello_cv_group_rules': cv_group_rules_cmd,
3455+ 'katello_cv_errata_rules': cv_errata_rules_cmd,
3456+ 'katello_cv_module_rules': cv_module_rules_cmd,
3457+ 'katello_cv_docker_rules': cv_docker_rules_cmd,
3458+ 'katello_cv_deb_rules': cv_deb_rules_cmd
3459+ }
3460+
3461+ for table, query in filter_tables.items():
3462+ _cmd = self.build_query_cmd(query)
3463+ self.add_cmd_output(_cmd, suggest_filename=table,
3464+ subdir='content_view_filters',
3465+ timeout=600, sizelimit=100, env=self.env)
3466+
3467 def build_query_cmd(self, query, csv=False, binary="psql"):
3468 """
3469 Builds the command needed to invoke the pgsql query as the postgres
3470@@ -316,8 +384,8 @@ class Foreman(Plugin):
3471 if csv:
3472 query = f"COPY ({query}) TO STDOUT " \
3473 "WITH (FORMAT 'csv', DELIMITER ',', HEADER)"
3474- _dbcmd = "%s --no-password -h %s -p 5432 -U foreman -d foreman -c %s"
3475- return _dbcmd % (binary, self.dbhost, quote(query))
3476+ _dbcmd = "%s --no-password -h %s -p %s -U foreman -d foreman -c %s"
3477+ return _dbcmd % (binary, self.dbhost, self.dbport, quote(query))
3478
3479 def postproc(self):
3480 self.do_path_regex_sub(
3481diff --git a/sos/report/plugins/foreman_installer.py b/sos/report/plugins/foreman_installer.py
3482index 73d95b3..f55cde2 100644
3483--- a/sos/report/plugins/foreman_installer.py
3484+++ b/sos/report/plugins/foreman_installer.py
3485@@ -57,8 +57,8 @@ class ForemanInstaller(Plugin, DebianPlugin, UbuntuPlugin):
3486 # also hide passwords in yet different formats
3487 self.do_path_regex_sub(
3488 install_logs,
3489- r"((\.|_|-)password(=\'|=|\", \"))(\w*)",
3490- r"\1********")
3491+ r"password(\", \"|=|\" value: \"|\": \")(.*?)(\", \".*|\"]]|\"|$)",
3492+ r"password\1********\3")
3493 self.do_path_regex_sub(
3494 "/var/log/foreman-installer/foreman-proxy*",
3495 r"(\s*proxy_password\s=) (.*)",
3496diff --git a/sos/report/plugins/gcp.py b/sos/report/plugins/gcp.py
3497index fb040bc..24b5032 100644
3498--- a/sos/report/plugins/gcp.py
3499+++ b/sos/report/plugins/gcp.py
3500@@ -6,7 +6,6 @@
3501 #
3502 # See the LICENSE file in the source distribution for further information.
3503 import json
3504-from http.client import HTTPResponse
3505 from typing import Any
3506 from urllib import request
3507 from urllib.error import URLError
3508@@ -27,32 +26,24 @@ class GCP(Plugin, IndependentPlugin):
3509 "Metadata server.")
3510 ]
3511
3512- METADATA_ROOT = "http://metadata.google.internal/computeMetadata/v1/"
3513+ PRODUCT_PATH = "/sys/devices/virtual/dmi/id/product_name"
3514+
3515 METADATA_QUERY = "http://metadata.google.internal/computeMetadata/v1/" \
3516 "?recursive=true"
3517 REDACTED = "[--REDACTED--]"
3518 metadata = None
3519
3520- # A line we will be looking for in the dmesg output. If it's there,
3521- # that means we're running on a Google Cloud Compute instance.
3522- GOOGLE_DMI = "DMI: Google Google Compute Engine/Google " \
3523- "Compute Engine, BIOS Google"
3524-
3525 def check_enabled(self):
3526 """
3527- Checks if this plugin should be executed at all. In this case, it
3528- will check the `dmesg` command output to see if the system is
3529- running on a Google Cloud Compute instance.
3530+ Checks if this plugin should be executed based on the presence of
3531+ GCE entry in sysfs.
3532 """
3533- dmesg = self.exec_cmd("dmesg")
3534- if dmesg['status'] != 0:
3535- return False
3536- return self.GOOGLE_DMI in dmesg['output']
3537+ with open(self.PRODUCT_PATH, encoding='utf-8') as sys_file:
3538+ return "Google Compute Engine" in sys_file.read()
3539
3540 def setup(self):
3541 """
3542 Collect the following info:
3543- * Metadata from the Metadata server
3544 * `gcloud auth list` output
3545 * Any google services output from journal
3546 """
3547@@ -64,7 +55,7 @@ class GCP(Plugin, IndependentPlugin):
3548 self.add_journal(units="google*", tags=['gcp'])
3549
3550 def collect(self):
3551- # Get and store Metadata
3552+ # Collect Metadata from the server
3553 with self.collection_file('metadata.json', tags=['gcp']) as mfile:
3554 try:
3555 self.metadata = self.get_metadata()
3556@@ -78,12 +69,10 @@ class GCP(Plugin, IndependentPlugin):
3557 Retrieves metadata from the Metadata Server and transforms it into a
3558 dictionary object.
3559 """
3560- response = self._query_address(self.METADATA_QUERY)
3561- response_body = response.read().decode()
3562+ response_body = self._query_address(self.METADATA_QUERY)
3563 return json.loads(response_body)
3564
3565- @staticmethod
3566- def _query_address(url: str) -> HTTPResponse:
3567+ def _query_address(self, url: str) -> str:
3568 """
3569 Query the given url address with headers required by Google Metadata
3570 Server.
3571@@ -96,7 +85,7 @@ class GCP(Plugin, IndependentPlugin):
3572 f"Failed to communicate with Metadata Server "
3573 f"(code: {response.code}): " +
3574 response.read().decode())
3575- return response
3576+ return response.read().decode()
3577 except URLError as err:
3578 raise RuntimeError(
3579 "Failed to communicate with Metadata Server: " + str(err)) \
3580diff --git a/sos/report/plugins/insights.py b/sos/report/plugins/insights.py
3581index be6a8b2..c010ba5 100644
3582--- a/sos/report/plugins/insights.py
3583+++ b/sos/report/plugins/insights.py
3584@@ -51,6 +51,8 @@ class RedHatInsights(Plugin, RedHatPlugin):
3585 "insights-client --test-connection --net-debug",
3586 timeout=30
3587 )
3588+ self.add_cmd_output("insights-client --version")
3589+ self.add_cmd_output("insights-client --status", timeout=30)
3590
3591 self.add_dir_listing(["/etc/rhsm", "/sys/kernel", "/var/lib/sss"],
3592 recursive=True)
3593@@ -64,5 +66,6 @@ class RedHatInsights(Plugin, RedHatPlugin):
3594 self.do_file_sub(
3595 conf, r'(proxy[\t\ ]*=.*)(:)(.*)(@.*)', r'\1\2********\4'
3596 )
3597+ self.do_paths_http_sub(["/var/log/insights-client/*"])
3598
3599 # vim: set et ts=4 sw=4 :
3600diff --git a/sos/report/plugins/juju.py b/sos/report/plugins/juju.py
3601index e6a52bf..031573e 100644
3602--- a/sos/report/plugins/juju.py
3603+++ b/sos/report/plugins/juju.py
3604@@ -218,6 +218,11 @@ class Juju(Plugin, UbuntuPlugin):
3605 keys_regex = fr"(^\s*({'|'.join(protect_keys)})\s*:\s*)(.*)"
3606 sub_regex = r"\1*********"
3607 self.do_path_regex_sub(agents_path, keys_regex, sub_regex)
3608+
3609+ # Redact keys from Nova compute logs
3610+ self.do_path_regex_sub("/var/log/juju/unit-nova-compute-(.*).log(.*)",
3611+ r"auth\(key=(.*)\)", r"auth(key=******)")
3612+
3613 # Redact certificates
3614 self.do_file_private_sub(agents_path)
3615 self.do_cmd_private_sub('juju controllers')
3616diff --git a/sos/report/plugins/kdump.py b/sos/report/plugins/kdump.py
3617index 5192e4e..a4b74ed 100644
3618--- a/sos/report/plugins/kdump.py
3619+++ b/sos/report/plugins/kdump.py
3620@@ -7,8 +7,8 @@
3621 # See the LICENSE file in the source distribution for further information.
3622
3623 import platform
3624-from sos.report.plugins import Plugin, PluginOpt, RedHatPlugin, DebianPlugin, \
3625- UbuntuPlugin, CosPlugin, AzurePlugin
3626+from sos.report.plugins import (Plugin, PluginOpt, RedHatPlugin, DebianPlugin,
3627+ UbuntuPlugin, CosPlugin, AzurePlugin)
3628
3629
3630 class KDump(Plugin):
3631@@ -25,6 +25,7 @@ class KDump(Plugin):
3632 "/proc/sys/kernel/panic",
3633 "/proc/sys/kernel/panic_on_oops",
3634 "/sys/kernel/kexec_loaded",
3635+ "/sys/kernel/kexec",
3636 "/sys/kernel/fadump",
3637 "/sys/kernel/fadump_enabled",
3638 "/sys/kernel/fadump_registered",
3639@@ -102,12 +103,36 @@ class RedHatKDump(KDump, RedHatPlugin):
3640 if self.get_option("get-vm-core"):
3641 self.add_copy_spec(f"{path}/*/vmcore", sizelimit=2048, maxage=24)
3642
3643+ # collect status via kdumpctl
3644+ self.add_cmd_output([
3645+ "kdumpctl status",
3646+ "kdumpctl estimate",
3647+ ])
3648+
3649
3650 class DebianKDump(KDump, DebianPlugin, UbuntuPlugin):
3651
3652 files = ('/etc/default/kdump-tools',)
3653 packages = ('kdump-tools',)
3654
3655+ option_list = [
3656+ PluginOpt("get-vm-core", default=False, val_type=bool,
3657+ desc="collect memory dumps")
3658+ ]
3659+
3660+ def read_kdump_conffile(self):
3661+ """ Parse /etc/default/kdump-tools """
3662+ path = "/var/crash"
3663+
3664+ kdump = '/etc/default/kdump-tools'
3665+ with open(kdump, 'r', encoding='UTF-8') as file:
3666+ for line in file:
3667+ line = line.strip()
3668+ if line.startswith("KDUMP_COREDIR"):
3669+ path = line.split('=')[1].strip('"')
3670+
3671+ return path
3672+
3673 def setup(self):
3674 super().setup()
3675
3676@@ -121,6 +146,24 @@ class DebianKDump(KDump, DebianPlugin, UbuntuPlugin):
3677 "/etc/default/kdump-tools"
3678 ])
3679
3680+ try:
3681+ path = self.read_kdump_conffile()
3682+ except Exception: # pylint: disable=broad-except
3683+ # set default path of coredir
3684+ path = "/var/crash"
3685+
3686+ self.add_dir_listing(path, recursive=True)
3687+ self.add_copy_spec([
3688+ f"{path}/kexec_cmd",
3689+ f"{path}/kdump_lock",
3690+ f"{path}/*/dmesg*",
3691+ ])
3692+ self.add_copy_spec(f"{path}/linux-image-*", sizelimit=2048, maxage=24)
3693+
3694+ # collect the latest dump created in the last 24hrs <= 2GB
3695+ if self.get_option("get-vm-core"):
3696+ self.add_copy_spec(f"{path}/*/dump*", sizelimit=2048, maxage=24)
3697+
3698
3699 class CosKDump(KDump, CosPlugin):
3700
3701diff --git a/sos/report/plugins/logs.py b/sos/report/plugins/logs.py
3702index 7f5ab6e..ebe43c9 100644
3703--- a/sos/report/plugins/logs.py
3704+++ b/sos/report/plugins/logs.py
3705@@ -56,7 +56,7 @@ class LogsBase(Plugin):
3706 ])
3707
3708 self.add_cmd_output("journalctl --disk-usage")
3709- self.add_dir_listing('/var/log', recursive=True)
3710+ self.add_dir_listing('/var/log', recursive=True, extra_opts='s')
3711
3712 # collect journal logs if:
3713 # - there is some data present, either persistent or runtime only
3714diff --git a/sos/report/plugins/loki.py b/sos/report/plugins/loki.py
3715new file mode 100644
3716index 0000000..ab2a0ec
3717--- /dev/null
3718+++ b/sos/report/plugins/loki.py
3719@@ -0,0 +1,189 @@
3720+# Copyright (C) 2025 Canonical Ltd.,
3721+# Mateusz Kulewicz <mateusz.kulewicz@canonical.com>
3722+
3723+# This file is part of the sos project: https://github.com/sosreport/sos
3724+#
3725+# This copyrighted material is made available to anyone wishing to use,
3726+# modify, copy, or redistribute it subject to the terms and conditions of
3727+# version 2 of the GNU General Public License.
3728+#
3729+# See the LICENSE file in the source distribution for further information.
3730+
3731+from typing import Optional
3732+from datetime import datetime, timedelta, timezone
3733+from json import JSONDecodeError, dumps, loads
3734+from sos.report.plugins import Plugin, IndependentPlugin, PluginOpt
3735+
3736+
3737+class Loki(Plugin, IndependentPlugin):
3738+ """
3739+ Collects logs and configuration from Loki.
3740+
3741+ This plugin interacts with the Loki API to fetch logs based on specified
3742+ labels and provides options for pagination and label detection.
3743+ It also collects relevant configuration files and masks sensitive
3744+ information. It works with both charmed and non-charmed Loki.
3745+ To fetch internal Loki logs, run it from the Loki container.
3746+ You can also run it from another machine and fetch only logs from
3747+ Loki API, by providing the following parameters:
3748+ `-k loki.collect-logs=true -k loki.endpoint=LOKI_URL`
3749+
3750+ Usage:
3751+ sos report -o loki -k loki.collect-logs=true \
3752+ -k loki.labels=severity:charm -k loki.detect-labels=true \
3753+ -k loki.paginate=true -k loki.endpoint=LOKI_URL
3754+ """
3755+
3756+ short_desc = 'Loki service'
3757+ plugin_name = 'loki'
3758+ profiles = ('services', )
3759+ LOKI_QUERY_LIMIT = 5000
3760+ MAX_PAGINATION_ITERATIONS = 100
3761+
3762+ packages = ('loki', )
3763+
3764+ option_list = [
3765+ PluginOpt('collect-logs', default=False,
3766+ desc='collect logs from Loki API'),
3767+ PluginOpt('detect-labels', default=False,
3768+ desc=('fetch logs for all available labels. '
3769+ 'May result in multiple files with the same logs')),
3770+ PluginOpt('paginate', default=False,
3771+ desc='fetch all available logs from Loki API.'),
3772+ PluginOpt('labels', default='', val_type=str,
3773+ desc='colon-delimited list of labels to fetch logs from'),
3774+ PluginOpt('endpoint', default='http://localhost:3100', val_type=str,
3775+ desc=('loki endpoint to fetch logs from. '
3776+ 'Defaults to http://localhost:3100.')),
3777+ ]
3778+
3779+ def query_command(self, endpoint, label, start: Optional[datetime],
3780+ end: Optional[datetime]):
3781+ if not end:
3782+ end = datetime.now(timezone.utc)
3783+ if not start:
3784+ start = end - timedelta(days=1)
3785+
3786+ start_formatted = start.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
3787+ end_formatted = end.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
3788+
3789+ command = (
3790+ f"curl -G -s '{endpoint}/loki/api/v1/query_range' "
3791+ f"--data-urlencode 'query={{{label}=~\".+\"}}' "
3792+ f"--data-urlencode 'start={start_formatted}' "
3793+ f"--data-urlencode 'end={end_formatted}' "
3794+ f"--data-urlencode 'limit={Loki.LOKI_QUERY_LIMIT}' "
3795+ )
3796+ return command
3797+
3798+ def get_logs(self, endpoint, label, start: Optional[datetime],
3799+ end: Optional[datetime]):
3800+ output = self.exec_cmd(self.query_command(endpoint, label, start, end))
3801+ try:
3802+ return loads(output["output"])
3803+ except JSONDecodeError:
3804+ # if an error is returned from Loki API, the output will be str
3805+ self._log_warn((f"An error was returned from Loki API on label "
3806+ f"{label}. "
3807+ f"Error message stored, not querying further."))
3808+ return output["output"]
3809+
3810+ def get_earliest_log_timestamp(self, logs):
3811+ log_streams = logs["data"]["result"]
3812+ # use now as a comparison
3813+ earliest_log = int(datetime.now().timestamp()*1_000_000_000)
3814+ for stream in log_streams:
3815+ for log in stream["values"]:
3816+ timestamp = int(log[0])
3817+ earliest_log = min(earliest_log, timestamp)
3818+ return earliest_log
3819+
3820+ def get_logs_for_label(self, endpoint, label, paginate):
3821+ logs = self.get_logs(endpoint, label, None, None)
3822+ with self.collection_file(f'{label}.log') as logfile:
3823+ logfile.write(dumps(logs, indent=2))
3824+ if isinstance(logs, str):
3825+ # don't paginate if error was returned
3826+ return
3827+
3828+ if paginate:
3829+ earliest_log = self.get_earliest_log_timestamp(logs)
3830+ previous_earliest_log = int(
3831+ datetime.now().timestamp()*1_000_000_000
3832+ )
3833+ iterations_count = 0
3834+ while iterations_count < Loki.MAX_PAGINATION_ITERATIONS and \
3835+ earliest_log < previous_earliest_log:
3836+ log_timestamp = datetime.fromtimestamp(
3837+ earliest_log / 1_000_000_000)
3838+ new_logs = self.get_logs(endpoint, label, None, log_timestamp)
3839+ with self.collection_file(f'{label}.log.{iterations_count}') \
3840+ as logfile:
3841+ logfile.write(dumps(new_logs, indent=2))
3842+ if isinstance(new_logs, str):
3843+ # don't paginate further if error was returned
3844+ return
3845+ previous_earliest_log = earliest_log
3846+ earliest_log = \
3847+ self.get_earliest_log_timestamp(new_logs)
3848+ # exit at most after 100 pages to avoid infinite loops
3849+ iterations_count += 1
3850+
3851+ def setup(self):
3852+ els_config_file = self.path_join("/etc/loki/*.yaml")
3853+ self.add_copy_spec(els_config_file)
3854+
3855+ # charms using cos-coordinated-workers have their config elsewhere
3856+ coordinated_workers_config_file = self.path_join("/etc/worker/*.yaml")
3857+ self.add_copy_spec(coordinated_workers_config_file)
3858+
3859+ self.add_copy_spec("/var/log/loki/*")
3860+ self.add_cmd_output("pebble logs loki -n 10000")
3861+
3862+ if self.get_option("collect-logs"):
3863+ endpoint = self.get_option("endpoint") or "http://localhost:3100"
3864+ self.labels = []
3865+ if labels_option := self.get_option("labels"):
3866+ if isinstance(labels_option, str) and labels_option:
3867+ self.labels.extend(labels_option.split(":"))
3868+
3869+ if self.get_option("detect-labels"):
3870+ labels_cmd = self.collect_cmd_output(
3871+ f"curl -G -s '{endpoint}/loki/api/v1/labels'"
3872+ )
3873+ labels_json = loads(labels_cmd["output"])
3874+ self.labels.extend(labels_json["data"])
3875+
3876+ def collect(self):
3877+ endpoint = self.get_option("endpoint") or "http://localhost:3100"
3878+ for label in self.labels:
3879+ paginate = self.get_option("paginate")
3880+ self.get_logs_for_label(endpoint, label, paginate)
3881+
3882+ def postproc(self):
3883+ protect_keys = [
3884+ "access_key_id",
3885+ "secret_access_key",
3886+ ]
3887+ loki_files = [
3888+ "/etc/loki/loki-local-config.yaml",
3889+ "/etc/loki/config.yaml",
3890+ "/etc/loki/local-config.yaml",
3891+ "/etc/loki/loki.yaml",
3892+ "/etc/worker/config.yaml",
3893+ ]
3894+
3895+ match_exp_multil = fr"({'|'.join(protect_keys)})\s*(:|=)(\S*\n.*?\\n)"
3896+ match_exp = fr"({'|'.join(protect_keys)})\s*(:|=)\s*[a-zA-Z0-9]*"
3897+
3898+ for file in loki_files:
3899+ self.do_file_sub(
3900+ file, match_exp_multil,
3901+ r"\1\2*********"
3902+ )
3903+ self.do_file_sub(
3904+ file, match_exp,
3905+ r"\1\2*********"
3906+ )
3907+
3908+# vim: set et ts=4 sw=4 :
3909diff --git a/sos/report/plugins/lustre.py b/sos/report/plugins/lustre.py
3910index 1f19584..afaa264 100644
3911--- a/sos/report/plugins/lustre.py
3912+++ b/sos/report/plugins/lustre.py
3913@@ -49,6 +49,9 @@ class Lustre(Plugin, RedHatPlugin):
3914 ["version", "health_check", "debug"]
3915 )
3916
3917+ # copy lnet settings if present
3918+ self.add_copy_spec("/etc/lnet.conf")
3919+
3920 # Client Specific
3921 self.add_cmd_output([
3922 "lfs df",
3923diff --git a/sos/report/plugins/md.py b/sos/report/plugins/md.py
3924index d49d8d9..b3ea95c 100644
3925--- a/sos/report/plugins/md.py
3926+++ b/sos/report/plugins/md.py
3927@@ -15,6 +15,7 @@ class Md(Plugin, IndependentPlugin):
3928
3929 plugin_name = 'md'
3930 profiles = ('storage',)
3931+ packages = ('mdadm',)
3932
3933 def setup(self):
3934 self.add_cmd_output("mdadm -D /dev/md*")
3935diff --git a/sos/report/plugins/microk8s.py b/sos/report/plugins/microk8s.py
3936index 72ef87a..183c3ea 100644
3937--- a/sos/report/plugins/microk8s.py
3938+++ b/sos/report/plugins/microk8s.py
3939@@ -42,9 +42,10 @@ class Microk8s(Plugin, UbuntuPlugin):
3940 'status',
3941 'version'
3942 ]
3943- self.add_copy_spec(
3944- "/var/snap/microk8s/current/credentials/client.config"
3945- )
3946+ self.add_copy_spec([
3947+ "/var/snap/microk8s/current/args/*",
3948+ "/var/snap/microk8s/current/credentials/client.config",
3949+ ])
3950
3951 self.add_cmd_output([
3952 f"{self.microk8s_cmd} {subcmd}" for subcmd in microk8s_subcmds
3953diff --git a/sos/report/plugins/networking.py b/sos/report/plugins/networking.py
3954index c831efc..51ab815 100644
3955--- a/sos/report/plugins/networking.py
3956+++ b/sos/report/plugins/networking.py
3957@@ -62,6 +62,7 @@ class Networking(Plugin):
3958 "/etc/network*",
3959 "/etc/nsswitch.conf",
3960 "/etc/resolv.conf",
3961+ "/etc/gai.conf",
3962 "/etc/xinetd.conf",
3963 "/etc/xinetd.d",
3964 "/etc/yp.conf",
3965@@ -71,6 +72,7 @@ class Networking(Plugin):
3966 "/sys/class/net/*/statistics/",
3967 "/etc/nmstate/",
3968 "/var/lib/lldpad/",
3969+ "/etc/services",
3970 ])
3971
3972 self.add_forbidden_path([
3973diff --git a/sos/report/plugins/networkmanager.py b/sos/report/plugins/networkmanager.py
3974index f9853fc..06b2b10 100644
3975--- a/sos/report/plugins/networkmanager.py
3976+++ b/sos/report/plugins/networkmanager.py
3977@@ -22,6 +22,8 @@ class NetworkManager(Plugin, RedHatPlugin, UbuntuPlugin):
3978 "/etc/NetworkManager/system-connections/",
3979 "/usr/lib/NetworkManager/system-connections/",
3980 "/run/NetworkManager/system-connections/",
3981+ "/var/run/NetworkManager/system-connections/",
3982+ "/var/run/NetworkManager/backups/",
3983 ]
3984
3985 self.add_copy_spec(self.system_connection_files)
3986@@ -33,8 +35,13 @@ class NetworkManager(Plugin, RedHatPlugin, UbuntuPlugin):
3987 "/usr/lib/NetworkManager/conf.d",
3988 "/run/NetworkManager/conf.d",
3989 "/var/lib/NetworkManager/NetworkManager-intern.conf",
3990+ "/var/run/NetworkManager",
3991 ])
3992
3993+ self.add_forbidden_path(
3994+ "/var/run/NetworkManager/secret_key"
3995+ )
3996+
3997 self.add_journal(units="NetworkManager")
3998
3999 self.add_cmd_output("NetworkManager --print-config")
4000diff --git a/sos/report/plugins/opensearch.py b/sos/report/plugins/opensearch.py
4001new file mode 100644
4002index 0000000..c079449
4003--- /dev/null
4004+++ b/sos/report/plugins/opensearch.py
4005@@ -0,0 +1,66 @@
4006+# Copyright (C) 2025 Henry AlOudaimy <henry.oudaimy@gmail.com>
4007+#
4008+# This file is part of the sos project: https://github.com/sosreport/sos
4009+#
4010+# This copyrighted material is made available to anyone wishing to use,
4011+# modify, copy, or redistribute it subject to the terms and conditions of
4012+# version 2 of the GNU General Public License.
4013+#
4014+# See the LICENSE file in the source distribution for further information.
4015+
4016+import re
4017+from sos.report.plugins import Plugin, IndependentPlugin
4018+
4019+
4020+class OpenSearch(Plugin, IndependentPlugin):
4021+
4022+ short_desc = 'OpenSearch service'
4023+ plugin_name = 'opensearch'
4024+ profiles = ('services', )
4025+
4026+ packages = ('opensearch',)
4027+ services = ('opensearch',)
4028+
4029+ def get_hostname_port(self, opensearch_config_file):
4030+ """ Get hostname and port number """
4031+ hostname = "localhost"
4032+ port = "9200"
4033+ try:
4034+ with open(opensearch_config_file, encoding='UTF-8') as fread:
4035+ for line in fread:
4036+ network_host = re.search(r'(^network.host):(.*)', line)
4037+ network_port = re.search(r'(^http.port):(.*)', line)
4038+ if network_host and len(network_host.groups()) == 2:
4039+ hostname = network_host.groups()[-1].strip()
4040+ hostname = re.sub(r'"|\'', '', hostname)
4041+ continue
4042+ if network_port and len(network_port.groups()) == 2:
4043+ port = network_port.groups()[-1].strip()
4044+ except Exception as err: # pylint: disable=broad-except
4045+ self._log_info(f"Failed to parse {opensearch_config_file}: {err}")
4046+ return hostname, port
4047+
4048+ def setup(self):
4049+ opensearch_config_file = self.path_join(
4050+ "/etc/opensearch/opensearch.yml"
4051+ )
4052+ self.add_copy_spec(opensearch_config_file)
4053+
4054+ if self.get_option("all_logs"):
4055+ self.add_copy_spec("/var/log/opensearch/*")
4056+ else:
4057+ self.add_copy_spec("/var/log/opensearch/*.log")
4058+
4059+ host, port = self.get_hostname_port(opensearch_config_file)
4060+ endpoint = host + ":" + port
4061+ self.add_cmd_output([
4062+ f"curl -X GET '{endpoint}/_cluster/settings?pretty'",
4063+ f"curl -X GET '{endpoint}/_cluster/health?pretty'",
4064+ f"curl -X GET '{endpoint}/_cluster/stats?pretty'",
4065+ f"curl -X GET '{endpoint}/_cat/nodes?v'",
4066+ f"curl -X GET '{endpoint}/_cat/indices'",
4067+ f"curl -X GET '{endpoint}/_cat/shards'",
4068+ f"curl -X GET '{endpoint}/_cat/aliases'",
4069+ ])
4070+
4071+# vim: set et ts=4 sw=4 :
4072diff --git a/sos/report/plugins/openshift_ovn.py b/sos/report/plugins/openshift_ovn.py
4073index cb48057..f8f8e66 100644
4074--- a/sos/report/plugins/openshift_ovn.py
4075+++ b/sos/report/plugins/openshift_ovn.py
4076@@ -1,5 +1,4 @@
4077 # Copyright (C) 2021 Nadia Pinaeva <npinaeva@redhat.com>
4078-
4079 # This file is part of the sos project: https://github.com/sosreport/sos
4080 #
4081 # This copyrighted material is made available to anyone wishing to use,
4082@@ -8,6 +7,7 @@
4083 #
4084 # See the LICENSE file in the source distribution for further information.
4085
4086+import glob
4087 from sos.report.plugins import Plugin, RedHatPlugin
4088
4089
4090@@ -18,9 +18,11 @@ class OpenshiftOVN(Plugin, RedHatPlugin):
4091 plugin_name = "openshift_ovn"
4092 containers = ('ovnkube-master', 'ovnkube-node', 'ovn-ipsec',
4093 'ovnkube-controller')
4094+ runtime = 'crio'
4095 profiles = ('openshift',)
4096
4097 def setup(self):
4098+
4099 all_logs = self.get_option("all_logs")
4100
4101 self.add_copy_spec([
4102@@ -50,21 +52,29 @@ class OpenshiftOVN(Plugin, RedHatPlugin):
4103 'cluster/status OVN_Northbound',
4104 'ovn-appctl -t /var/run/ovn/ovnsb_db.ctl ' +
4105 'cluster/status OVN_Southbound'],
4106- container='ovnkube-master')
4107- self.add_cmd_output([
4108- 'ovs-appctl -t /var/run/ovn/ovn-controller.*.ctl ' +
4109- 'ct-zone-list'],
4110- container='ovnkube-node')
4111- self.add_cmd_output([
4112- 'ovs-appctl -t /var/run/ovn/ovn-controller.*.ctl ' +
4113- 'ct-zone-list'],
4114- container='ovnkube-controller')
4115+ container='ovnkube-master',
4116+ runtime='crio')
4117+ # We need to determine the actual file name to send
4118+ # to the command
4119+ files = glob.glob("/var/run/ovn/ovn-controller.*.ctl")
4120+ for file in files:
4121+ self.add_cmd_output([
4122+ f"ovs-appctl -t {file} ct-zone-list"],
4123+ container='ovnkube-node',
4124+ runtime='crio')
4125+ self.add_cmd_output([
4126+ f"ovs-appctl -t {file} ct-zone-list"],
4127+ container='ovnkube-controller',
4128+ runtime='crio')
4129 # Collect ovs ct-zone-list directly on host for interconnect setup.
4130- self.add_cmd_output([
4131- 'ovs-appctl -t /var/run/ovn-ic/ovn-controller.*.ctl ' +
4132- 'ct-zone-list'])
4133+ files = glob.glob("/var/run/ovn-ic/ovn-controller.*.ctl")
4134+ for file in files:
4135+ self.add_cmd_output([
4136+ f"ovs-appctl -t {file} ct-zone-list"],
4137+ runtime='crio')
4138 self.add_cmd_output([
4139 'ovs-appctl -t ovs-monitor-ipsec tunnels/show',
4140 'ipsec status',
4141 'certutil -L -d sql:/etc/ipsec.d'],
4142- container='ovn-ipsec')
4143+ container='ovn-ipsec',
4144+ runtime='crio')
4145diff --git a/sos/report/plugins/openstack_aodh.py b/sos/report/plugins/openstack_aodh.py
4146index f558cd1..a7ba5cb 100644
4147--- a/sos/report/plugins/openstack_aodh.py
4148+++ b/sos/report/plugins/openstack_aodh.py
4149@@ -75,9 +75,8 @@ class OpenStackAodh(Plugin):
4150
4151 def postproc(self):
4152 protect_keys = [
4153- "admin_password", "connection_password", "host_password",
4154- "os_password", "password", "qpid_password", "rabbit_password",
4155- "memcache_secret_key"
4156+ ".*_key",
4157+ "(.*_)?password",
4158 ]
4159 connection_keys = ["connection", "backend_url", "transport_url"]
4160
4161diff --git a/sos/report/plugins/openstack_ceilometer.py b/sos/report/plugins/openstack_ceilometer.py
4162index c763ce1..833f74c 100644
4163--- a/sos/report/plugins/openstack_ceilometer.py
4164+++ b/sos/report/plugins/openstack_ceilometer.py
4165@@ -52,12 +52,15 @@ class OpenStackCeilometer(Plugin):
4166
4167 def postproc(self):
4168 protect_keys = [
4169- "admin_password", "connection_password", "host_password",
4170- "memcache_secret_key", "os_password", "password", "qpid_password",
4171- "rabbit_password", "readonly_user_password", "secret_key",
4172- "ssl_key_password", "telemetry_secret", "metering_secret"
4173+ ".*_key",
4174+ ".*_pass(wd|word)?",
4175+ ".*_secret",
4176+ "password",
4177+ ]
4178+ connection_keys = [
4179+ ".*_urls?",
4180+ "connection",
4181 ]
4182- connection_keys = ["connection", "backend_url", "transport_url"]
4183
4184 join_con_keys = "|".join(connection_keys)
4185
4186diff --git a/sos/report/plugins/openstack_cinder.py b/sos/report/plugins/openstack_cinder.py
4187index 26ac024..4c5a092 100644
4188--- a/sos/report/plugins/openstack_cinder.py
4189+++ b/sos/report/plugins/openstack_cinder.py
4190@@ -143,16 +143,10 @@ class OpenStackCinder(Plugin):
4191
4192 def postproc(self):
4193 protect_keys = [
4194- "admin_password", "backup_tsm_password", "chap_password",
4195- "nas_password", "cisco_fc_fabric_password", "coraid_password",
4196- "eqlx_chap_password", "fc_fabric_password",
4197- "hitachi_auth_password", "hitachi_horcm_password",
4198- "hp3par_password", "hplefthand_password", "memcache_secret_key",
4199- "netapp_password", "netapp_sa_password", "nexenta_password",
4200- "password", "qpid_password", "rabbit_password", "san_password",
4201- "ssl_key_password", "vmware_host_password", "zadara_password",
4202- "zfssa_initiator_password", "hmac_keys", "zfssa_target_password",
4203- "os_privileged_user_password", "transport_url"
4204+ ".*_pass(wd|word)?",
4205+ ".*_keys?",
4206+ "password",
4207+ "transport_url",
4208 ]
4209 connection_keys = ["connection"]
4210
4211diff --git a/sos/report/plugins/openstack_designate.py b/sos/report/plugins/openstack_designate.py
4212index a152c5a..5814100 100644
4213--- a/sos/report/plugins/openstack_designate.py
4214+++ b/sos/report/plugins/openstack_designate.py
4215@@ -85,9 +85,11 @@ class OpenStackDesignate(Plugin):
4216
4217 def postproc(self):
4218 protect_keys = [
4219- "password", "connection", "transport_url", "admin_password",
4220- "ssl_key_password", "ssl_client_key_password",
4221- "memcache_secret_key"
4222+ ".*_key",
4223+ ".*_pass(wd|word)?",
4224+ "password",
4225+ "connection",
4226+ "transport_url",
4227 ]
4228 regexp = fr"(^\s*({'|'.join(protect_keys)})\s*=\s*)(.*)"
4229
4230diff --git a/sos/report/plugins/openstack_glance.py b/sos/report/plugins/openstack_glance.py
4231index 9b80857..70530eb 100644
4232--- a/sos/report/plugins/openstack_glance.py
4233+++ b/sos/report/plugins/openstack_glance.py
4234@@ -98,10 +98,10 @@ class OpenStackGlance(Plugin):
4235
4236 def postproc(self):
4237 protect_keys = [
4238- "admin_password", "password", "qpid_password", "rabbit_password",
4239- "s3_store_secret_key", "ssl_key_password",
4240- "vmware_server_password", "transport_url",
4241- "memcache_secret_key"
4242+ ".*_key",
4243+ ".*_pass(wd|word)?",
4244+ "password",
4245+ "transport_url",
4246 ]
4247 connection_keys = ["connection"]
4248
4249diff --git a/sos/report/plugins/openstack_heat.py b/sos/report/plugins/openstack_heat.py
4250index 32069a4..e1fffc7 100644
4251--- a/sos/report/plugins/openstack_heat.py
4252+++ b/sos/report/plugins/openstack_heat.py
4253@@ -117,9 +117,10 @@ class OpenStackHeat(Plugin):
4254
4255 def postproc(self):
4256 protect_keys = [
4257- "admin_password", "memcache_secret_key", "password",
4258- "qpid_password", "rabbit_password", "stack_domain_admin_password",
4259- "transport_url", "auth_encryption_key",
4260+ ".*_key",
4261+ ".*_pass(wd|word)?",
4262+ "password",
4263+ "transport_url",
4264 ]
4265 connection_keys = ["connection"]
4266
4267diff --git a/sos/report/plugins/openstack_instack.py b/sos/report/plugins/openstack_instack.py
4268index 01b839e..a62036c 100644
4269--- a/sos/report/plugins/openstack_instack.py
4270+++ b/sos/report/plugins/openstack_instack.py
4271@@ -21,13 +21,18 @@ NON_CONTAINERIZED_DEPLOY = [
4272 '/home/stack/undercloud.conf'
4273 ]
4274 CONTAINERIZED_DEPLOY = [
4275- '/var/log/heat-launcher/',
4276+ '/etc/puppet/hieradata/',
4277+ '/etc/rhosp-release',
4278+ '/home/stack/*-deploy',
4279+ '/home/stack/.tripleo/history',
4280 '/home/stack/ansible.log',
4281 '/home/stack/config-download/',
4282 '/home/stack/install-undercloud.log',
4283+ '/home/stack/overcloud_install.log',
4284 '/home/stack/undercloud-install-*.tar.bzip2',
4285- '/home/stack/.tripleo/history',
4286+ '/home/stack/undercloud.conf',
4287 '/var/lib/tripleo-config/',
4288+ '/var/log/heat-launcher/',
4289 '/var/log/tripleo-container-image-prepare.log',
4290 ]
4291 UNDERCLOUD_CONF_PATH = '/home/stack/undercloud.conf'
4292@@ -119,22 +124,9 @@ class OpenStackInstack(Plugin):
4293 # do_file_sub is case insensitive, so protected_keys can be lowercase
4294 # only
4295 protected_keys = [
4296- "os_password",
4297- "undercloud_admin_password",
4298- "undercloud_ceilometer_metering_secret",
4299- "undercloud_ceilometer_password",
4300- "undercloud_ceilometer_snmpd_password",
4301- "undercloud_db_password",
4302- "undercloud_glance_password",
4303- "undercloud_heat_password",
4304- "undercloud_heat_stack_domain_admin_password",
4305- "undercloud_horizon_secret_key",
4306- "undercloud_ironic_password",
4307- "undercloud_neutron_password",
4308- "undercloud_nova_password",
4309- "undercloud_rabbit_password",
4310- "undercloud_swift_password",
4311- "undercloud_tuskar_password",
4312+ ".*_password",
4313+ ".*_secret",
4314+ ".*_key",
4315 ]
4316 regexp = fr"(({'|'.join(protected_keys)})=)(.*)"
4317 self.do_file_sub("/home/stack/.instack/install-undercloud.log",
4318@@ -149,6 +141,10 @@ class OpenStackInstack(Plugin):
4319 r'(password=)\w+',
4320 r'\1*********')
4321
4322+ self.do_file_sub('/home/stack/overcloud_install.log',
4323+ r'(Found key: \\".*password.*\\" value: )(\\".+?\\")',
4324+ r'\1\\"*********\\"')
4325+
4326
4327 class RedHatRDOManager(OpenStackInstack, RedHatPlugin):
4328
4329diff --git a/sos/report/plugins/openstack_ironic.py b/sos/report/plugins/openstack_ironic.py
4330index b63e240..7f9a366 100644
4331--- a/sos/report/plugins/openstack_ironic.py
4332+++ b/sos/report/plugins/openstack_ironic.py
4333@@ -142,9 +142,10 @@ class OpenStackIronic(Plugin):
4334
4335 def postproc(self):
4336 protect_keys = [
4337- "dns_passkey", "memcache_secret_key", "rabbit_password",
4338- "password", "qpid_password", "admin_password", "ssl_key_password",
4339- "os_password", "transport_url"
4340+ ".*_(pass)?key",
4341+ ".*_pass(wd|word)?",
4342+ "password",
4343+ "transport_url",
4344 ]
4345 connection_keys = ["connection", "sql_connection"]
4346
4347diff --git a/sos/report/plugins/openstack_keystone.py b/sos/report/plugins/openstack_keystone.py
4348index 220a037..52da44e 100644
4349--- a/sos/report/plugins/openstack_keystone.py
4350+++ b/sos/report/plugins/openstack_keystone.py
4351@@ -98,9 +98,9 @@ class OpenStackKeystone(Plugin):
4352
4353 def postproc(self):
4354 protect_keys = [
4355- "password", "qpid_password", "rabbit_password", "ssl_key_password",
4356- "ldap_dns_password", "neutron_admin_password", "host_password",
4357- "admin_password", "admin_token", "ca_password", "transport_url",
4358+ "(.*_)?password",
4359+ "admin_token",
4360+ "transport_url",
4361 "OIDCClientSecret",
4362 ]
4363 connection_keys = ["connection"]
4364diff --git a/sos/report/plugins/openstack_neutron.py b/sos/report/plugins/openstack_neutron.py
4365index 678b985..788afdc 100644
4366--- a/sos/report/plugins/openstack_neutron.py
4367+++ b/sos/report/plugins/openstack_neutron.py
4368@@ -123,14 +123,9 @@ class OpenStackNeutron(Plugin):
4369
4370 def postproc(self):
4371 protect_keys = [
4372- "rabbit_password", "qpid_password", "nova_admin_password",
4373- "xenapi_connection_password", "password", "server_auth",
4374- "admin_password", "metadata_proxy_shared_secret", "eapi_password",
4375- "crd_password", "primary_l3_host_password", "serverauth",
4376- "ucsm_password", "ha_vrrp_auth_password", "ssl_key_password",
4377- "nsx_password", "vcenter_password", "edge_appliance_password",
4378- "tenant_admin_password", "apic_password", "transport_url",
4379- "memcache_secret_key"
4380+ "(.*_)?(key|password|secret)",
4381+ "server_?auth",
4382+ "transport_url",
4383 ]
4384 connection_keys = ["connection"]
4385
4386diff --git a/sos/report/plugins/openstack_nova.py b/sos/report/plugins/openstack_nova.py
4387index 728aed1..1124862 100644
4388--- a/sos/report/plugins/openstack_nova.py
4389+++ b/sos/report/plugins/openstack_nova.py
4390@@ -29,6 +29,7 @@ class OpenStackNova(Plugin):
4391 var_puppet_gen = "/var/lib/config-data/puppet-generated/nova"
4392 service_name = "openstack-nova-api.service"
4393 apachepkg = None
4394+ postproc_dirs = ["/etc/nova/",]
4395
4396 def setup(self):
4397
4398@@ -141,24 +142,23 @@ class OpenStackNova(Plugin):
4399 self.add_copy_spec(specs)
4400
4401 def apply_regex_sub(self, regexp, subst):
4402- """ Apply regex substitution """
4403- self.do_path_regex_sub("/etc/nova/*", regexp, subst)
4404- for npath in ['', '_libvirt', '_metadata', '_placement']:
4405- self.do_path_regex_sub(
4406- f"{self.var_puppet_gen}{npath}/etc/nova/*",
4407- regexp, subst)
4408+ """ Apply regex substitution to all sensitive dirs """
4409+ for _dir in self.postproc_dirs:
4410+ self.do_path_regex_sub(f"{_dir}/*", regexp, subst)
4411+ for npath in ['', '_libvirt', '_metadata', '_placement']:
4412+ self.do_path_regex_sub(
4413+ f"{self.var_puppet_gen}{npath}{_dir}/*",
4414+ regexp, subst)
4415
4416 def postproc(self):
4417 protect_keys = [
4418- "ldap_dns_password", "neutron_admin_password", "rabbit_password",
4419- "qpid_password", "powervm_mgr_passwd", "virtual_power_host_pass",
4420- "xenapi_connection_password", "password", "host_password",
4421- "vnc_password", "admin_password", "connection_password",
4422- "memcache_secret_key", "s3_secret_key",
4423- "metadata_proxy_shared_secret", "fixed_key", "transport_url",
4424- "rbd_secret_uuid"
4425+ ".*_key",
4426+ ".*_pass(wd|word)?",
4427+ "password",
4428+ "metadata_proxy_shared_secret",
4429+ "rbd_secret_uuid",
4430 ]
4431- connection_keys = ["connection", "sql_connection"]
4432+ connection_keys = ["connection", "sql_connection", "transport_url"]
4433
4434 join_con_keys = "|".join(connection_keys)
4435
4436@@ -214,6 +214,7 @@ class RedHatNova(OpenStackNova, RedHatPlugin):
4437 apachepkg = "httpd"
4438 nova = False
4439 packages = ('openstack-selinux',)
4440+ postproc_dirs = ["/etc/nova/", "/var/lib/openstack/config/nova"]
4441
4442 def setup(self):
4443 super().setup()
4444diff --git a/sos/report/plugins/openstack_sahara.py b/sos/report/plugins/openstack_sahara.py
4445index eeb5c97..f14eb0b 100644
4446--- a/sos/report/plugins/openstack_sahara.py
4447+++ b/sos/report/plugins/openstack_sahara.py
4448@@ -46,9 +46,9 @@ class OpenStackSahara(Plugin):
4449
4450 def postproc(self):
4451 protect_keys = [
4452- "admin_password", "memcache_secret_key", "password",
4453- "qpid_password", "rabbit_password", "ssl_key_password",
4454- "xenapi_connection_password", "transport_url"
4455+ "(.*_)?password",
4456+ "memcache_secret_key",
4457+ "transport_url",
4458 ]
4459 connection_keys = ["connection"]
4460
4461diff --git a/sos/report/plugins/openstack_swift.py b/sos/report/plugins/openstack_swift.py
4462index 9f3e2ef..22ee068 100644
4463--- a/sos/report/plugins/openstack_swift.py
4464+++ b/sos/report/plugins/openstack_swift.py
4465@@ -55,10 +55,9 @@ class OpenStackSwift(Plugin):
4466
4467 def postproc(self):
4468 protect_keys = [
4469- "ldap_dns_password", "neutron_admin_password", "rabbit_password",
4470- "qpid_password", "powervm_mgr_passwd", "virtual_power_host_pass",
4471- "xenapi_connection_password", "password", "host_password",
4472- "vnc_password", "admin_password", "transport_url"
4473+ ".*_pass(wd|word)?",
4474+ "password",
4475+ "transport_url",
4476 ]
4477 connection_keys = ["connection", "sql_connection"]
4478
4479diff --git a/sos/report/plugins/openstack_trove.py b/sos/report/plugins/openstack_trove.py
4480index 4eb7a95..d97209e 100644
4481--- a/sos/report/plugins/openstack_trove.py
4482+++ b/sos/report/plugins/openstack_trove.py
4483@@ -45,9 +45,10 @@ class OpenStackTrove(Plugin):
4484
4485 def postproc(self):
4486 protect_keys = [
4487- "default_password_length", "notifier_queue_password",
4488- "rabbit_password", "replication_password", "admin_password",
4489- "dns_passkey", "transport_url", "memcache_secret_key"
4490+ ".*_(password|key)",
4491+ "default_password_length",
4492+ "dns_passkey",
4493+ "transport_url",
4494 ]
4495 connection_keys = ["connection"]
4496
4497diff --git a/sos/report/plugins/podman.py b/sos/report/plugins/podman.py
4498index 5269933..973ba0a 100644
4499--- a/sos/report/plugins/podman.py
4500+++ b/sos/report/plugins/podman.py
4501@@ -64,7 +64,8 @@ class Podman(Plugin, RedHatPlugin, UbuntuPlugin):
4502 'ps -a',
4503 'stats --no-stream --all',
4504 'version',
4505- 'volume ls'
4506+ 'volume ls',
4507+ 'system df -v',
4508 ]
4509
4510 self.add_cmd_output([f"podman {s}" for s in subcmds])
4511diff --git a/sos/report/plugins/powerpc.py b/sos/report/plugins/powerpc.py
4512index cb216ee..5c888d0 100644
4513--- a/sos/report/plugins/powerpc.py
4514+++ b/sos/report/plugins/powerpc.py
4515@@ -93,7 +93,9 @@ class PowerPC(Plugin, IndependentPlugin):
4516 f"ctsnap -xrunrpttr -d {ctsnap_path}",
4517 "lsdevinfo",
4518 "lsslot",
4519- "amsstat"
4520+ "amsstat",
4521+ "lsslot -c phb",
4522+ "lsslot -c pci",
4523 ])
4524
4525 # Due to the lack of options in invscout for generating log files
4526diff --git a/sos/report/plugins/pulpcore.py b/sos/report/plugins/pulpcore.py
4527index d4afc91..4b126e7 100644
4528--- a/sos/report/plugins/pulpcore.py
4529+++ b/sos/report/plugins/pulpcore.py
4530@@ -32,6 +32,7 @@ class PulpCore(Plugin, IndependentPlugin):
4531 staticroot = "/var/lib/pulp/assets"
4532 uploaddir = "/var/lib/pulp/media/upload"
4533 env = {"PGPASSWORD": dbpasswd}
4534+ settings_file = "/etc/pulp/settings.py"
4535
4536 def parse_settings_config(self):
4537 """ Parse pulp settings """
4538@@ -47,7 +48,7 @@ class PulpCore(Plugin, IndependentPlugin):
4539 return val
4540
4541 try:
4542- with open("/etc/pulp/settings.py", 'r', encoding='UTF-8') as file:
4543+ with open(self.settings_file, 'r', encoding='UTF-8') as file:
4544 # split the lines to "one option per line" format
4545 for line in file.read() \
4546 .replace(',', ',\n').replace('{', '{\n') \
4547@@ -87,23 +88,36 @@ class PulpCore(Plugin, IndependentPlugin):
4548 self.env = {"PGPASSWORD": self.dbpasswd}
4549
4550 def setup(self):
4551+ self.runas = self.in_container = None
4552+ rhui_podman_ps = self.exec_cmd("podman ps --filter name=rhui5-rhua",
4553+ runas="rhui")
4554+ if rhui_podman_ps['status'] == 0:
4555+ lines = rhui_podman_ps['output'].splitlines()
4556+ if len(lines) > 1: # we know there is a container of given name
4557+ self.runas = 'rhui'
4558+ self.in_container = 'rhui5-rhua'
4559+ self.settings_file = '/var/lib/rhui/config/pulp/settings.py'
4560 self.parse_settings_config()
4561
4562 self.add_copy_spec([
4563 "/etc/pulp/settings.py",
4564 "/etc/pki/pulp/*"
4565- ])
4566+ ], runas=self.runas, container=self.in_container)
4567+
4568 # skip collecting certificate keys
4569 self.add_forbidden_path("/etc/pki/pulp/**/*.key")
4570
4571 self.add_cmd_output("curl -ks https://localhost/pulp/api/v3/status/",
4572- suggest_filename="pulp_status")
4573+ suggest_filename="pulp_status", runas=self.runas,
4574+ container=self.in_container)
4575 dynaconf_env = {"LC_ALL": "en_US.UTF-8",
4576 "PULP_SETTINGS": "/etc/pulp/settings.py",
4577 "DJANGO_SETTINGS_MODULE": "pulpcore.app.settings"}
4578- self.add_cmd_output("dynaconf list", env=dynaconf_env)
4579+ self.add_cmd_output("dynaconf list", env=dynaconf_env,
4580+ runas=self.runas, container=self.in_container)
4581 for _dir in [self.staticroot, self.uploaddir]:
4582- self.add_dir_listing(_dir)
4583+ self.add_dir_listing(_dir, runas=self.runas,
4584+ container=self.in_container)
4585
4586 task_days = self.get_option('task-days')
4587 for table in ['core_task', 'core_taskgroup',
4588@@ -113,13 +127,17 @@ class PulpCore(Plugin, IndependentPlugin):
4589 "AND table_schema = 'public' AND column_name NOT IN"
4590 " ('args', 'kwargs', 'enc_args', 'enc_kwargs'))"
4591 " TO STDOUT;")
4592- col_out = self.exec_cmd(self.build_query_cmd(_query), env=self.env)
4593+ col_out = self.exec_cmd(self.build_query_cmd(_query, csv=False),
4594+ env=self.env,
4595+ runas=self.runas,
4596+ container=self.in_container)
4597 columns = col_out['output'] if col_out['status'] == 0 else '*'
4598 _query = (f"select {columns} from {table} where pulp_last_updated"
4599 f"> NOW() - interval '{task_days} days' order by"
4600 " pulp_last_updated")
4601- _cmd = self.build_query_cmd(_query)
4602- self.add_cmd_output(_cmd, env=self.env, suggest_filename=table)
4603+ _cmd = self.build_query_cmd(_query, csv=True)
4604+ self.add_cmd_output(_cmd, env=self.env, suggest_filename=table,
4605+ runas=self.runas, container=self.in_container)
4606
4607 # collect tables sizes, ordered
4608 _cmd = self.build_query_cmd(
4609@@ -135,10 +153,12 @@ class PulpCore(Plugin, IndependentPlugin):
4610 "pg_total_relation_size(reltoastrelid) AS toast_bytes "
4611 "FROM pg_class c LEFT JOIN pg_namespace n ON "
4612 "n.oid = c.relnamespace WHERE relkind = 'r') a) a order by "
4613- "total_bytes DESC"
4614+ "total_bytes DESC",
4615+ csv=False
4616 )
4617 self.add_cmd_output(_cmd, suggest_filename='pulpcore_db_tables_sizes',
4618- env=self.env)
4619+ env=self.env, runas=self.runas,
4620+ container=self.in_container)
4621
4622 def build_query_cmd(self, query, csv=False):
4623 """
4624@@ -150,7 +170,7 @@ class PulpCore(Plugin, IndependentPlugin):
4625 """
4626 if csv:
4627 query = f"COPY ({query}) TO STDOUT " \
4628- "WITH (FORMAT 'csv', DELIMITER ',', HEADER)"
4629+ "WITH (FORMAT 'csv', DELIMITER ';', HEADER)"
4630 _dbcmd = "psql --no-password -h %s -p %s -U %s -d %s -c %s"
4631 return _dbcmd % (self.dbhost, self.dbport,
4632 self.dbuser, self.dbname, quote(query))
4633diff --git a/sos/report/plugins/pxe.py b/sos/report/plugins/pxe.py
4634deleted file mode 100644
4635index 3468d1d..0000000
4636--- a/sos/report/plugins/pxe.py
4637+++ /dev/null
4638@@ -1,50 +0,0 @@
4639-# This file is part of the sos project: https://github.com/sosreport/sos
4640-#
4641-# This copyrighted material is made available to anyone wishing to use,
4642-# modify, copy, or redistribute it subject to the terms and conditions of
4643-# version 2 of the GNU General Public License.
4644-#
4645-# See the LICENSE file in the source distribution for further information.
4646-
4647-from sos.report.plugins import (Plugin, RedHatPlugin, DebianPlugin,
4648- UbuntuPlugin, PluginOpt)
4649-
4650-
4651-class Pxe(Plugin):
4652-
4653- short_desc = 'PXE service'
4654- plugin_name = "pxe"
4655- profiles = ('sysmgmt', 'network')
4656- option_list = [
4657- PluginOpt('tftpboot', default=False,
4658- desc='collect content from tftpboot path')
4659- ]
4660-
4661-
4662-class RedHatPxe(Pxe, RedHatPlugin):
4663-
4664- files = ('/usr/sbin/pxeos',)
4665- packages = ('system-config-netboot-cmd',)
4666-
4667- def setup(self):
4668- super().setup()
4669- self.add_cmd_output("/usr/sbin/pxeos -l")
4670- self.add_copy_spec("/etc/dhcpd.conf")
4671- if self.get_option("tftpboot"):
4672- self.add_copy_spec("/tftpboot")
4673-
4674-
4675-class DebianPxe(Pxe, DebianPlugin, UbuntuPlugin):
4676-
4677- packages = ('tftpd-hpa',)
4678-
4679- def setup(self):
4680- super().setup()
4681- self.add_copy_spec([
4682- "/etc/dhcp/dhcpd.conf",
4683- "/etc/default/tftpd-hpa"
4684- ])
4685- if self.get_option("tftpboot"):
4686- self.add_copy_spec("/var/lib/tftpboot")
4687-
4688-# vim: set et ts=4 sw=4 :
4689diff --git a/sos/report/plugins/release.py b/sos/report/plugins/release.py
4690index 8c8a91a..fd32096 100644
4691--- a/sos/report/plugins/release.py
4692+++ b/sos/report/plugins/release.py
4693@@ -6,8 +6,8 @@
4694 #
4695 # See the LICENSE file in the source distribution for further information.
4696
4697-from sos.report.plugins import Plugin, RedHatPlugin, \
4698- DebianPlugin, UbuntuPlugin, CosPlugin
4699+from sos.report.plugins import (Plugin, RedHatPlugin, DebianPlugin,
4700+ UbuntuPlugin, CosPlugin)
4701
4702
4703 class Release(Plugin, UbuntuPlugin, CosPlugin):
4704diff --git a/sos/report/plugins/rhc.py b/sos/report/plugins/rhc.py
4705index 67f5167..2ecf9e4 100644
4706--- a/sos/report/plugins/rhc.py
4707+++ b/sos/report/plugins/rhc.py
4708@@ -8,7 +8,7 @@
4709 #
4710 # See the LICENSE file in the source distribution for further information.
4711
4712-from sos.report.plugins import Plugin, RedHatPlugin
4713+from sos.report.plugins import Plugin, RedHatPlugin, SoSPredicate
4714
4715
4716 class Rhc(Plugin, RedHatPlugin):
4717@@ -30,9 +30,10 @@ class Rhc(Plugin, RedHatPlugin):
4718 "/var/log/rhc-worker-playbook",
4719 ])
4720
4721- self.add_cmd_output([
4722+ self.add_cmd_output(
4723 "rhc status",
4724- ])
4725+ pred=SoSPredicate(self, services=["rhsm"])
4726+ )
4727
4728 def postproc(self):
4729 # hide workers/foreman_rh_cloud.toml FORWARDER_PASSWORD
4730diff --git a/sos/report/plugins/rhui_containerized.py b/sos/report/plugins/rhui_containerized.py
4731new file mode 100644
4732index 0000000..6059106
4733--- /dev/null
4734+++ b/sos/report/plugins/rhui_containerized.py
4735@@ -0,0 +1,85 @@
4736+# Copyright (C) 2025 Red Hat, Inc., Pavel Moravec <pmoravec@redhat.com>
4737+
4738+# This file is part of the sos project: https://github.com/sosreport/sos
4739+#
4740+# This copyrighted material is made available to anyone wishing to use,
4741+# modify, copy, or redistribute it subject to the terms and conditions of
4742+# version 2 of the GNU General Public License.
4743+#
4744+# See the LICENSE file in the source distribution for further information.
4745+
4746+import fnmatch
4747+from sos.report.plugins import Plugin, RedHatPlugin
4748+
4749+
4750+class RhuiContainer(Plugin, RedHatPlugin):
4751+
4752+ short_desc = 'Red Hat Update Infrastructure in Containers'
4753+
4754+ plugin_name = "rhui_containerized"
4755+ services = ("rhui_rhua", )
4756+ files = ("/var/lib/rhui/config/rhua/rhui-tools.conf", )
4757+
4758+ def setup(self):
4759+ self.add_copy_spec([
4760+ "/var/lib/rhui/config/rhua/rhui-tools.conf",
4761+ "/var/lib/rhui/config/rhua/registered_subscriptions.conf",
4762+ "/var/lib/rhui/pki/*",
4763+ "/var/lib/rhui/cache/*",
4764+ "/var/lib/rhui/root/.rhui/*",
4765+ "/var/lib/rhui/log/*",
4766+ ])
4767+ # skip collecting certificate keys
4768+ self.add_forbidden_path("/var/lib/rhui/pki/**/*.key")
4769+
4770+ # call rhui-manager commands with 1m timeout and
4771+ # with an env. variable ensuring that "RHUI Username:"
4772+ # even unanswered prompt gets collected
4773+ # TODO: is the timeout and env applicable?
4774+ rhui_cont_exe = "podman exec rhui5-rhua rhui-manager --noninteractive"
4775+ for subcmd in ["status", "cert info"]:
4776+ suggest_fname = f"rhui-manager_{subcmd.replace(' ', '_')}"
4777+ self.add_cmd_output(f"{rhui_cont_exe} {subcmd}",
4778+ runas="rhui",
4779+ runat="/var/lib/rhui",
4780+ suggest_filename=suggest_fname)
4781+
4782+ self.add_dir_listing('/var/lib/rhui/remote_share', recursive=True)
4783+
4784+ # collect postgres diagnostics data
4785+ # ideally, postgresql plugin would do so but that would need bigger
4786+ # changes to the plugin redundantly affecting its execution on
4787+ # non-RHUI systems
4788+ pghome = '/var/lib/pgsql'
4789+ self.add_cmd_output(f"du -sh {pghome}", container='rhui5-rhua',
4790+ runas='rhui')
4791+ # Copy PostgreSQL log and config files.
4792+ # we must first find all their names, since `stat` inside add_copy_spec
4793+ # does not treat globs at all
4794+ podman_find = self.exec_cmd("podman exec rhui5-rhua find "
4795+ f"{pghome}/data", runas="rhui")
4796+ if podman_find['status'] == 0:
4797+ allfiles = podman_find['output'].splitlines()
4798+ logfiles = fnmatch.filter(allfiles, '*.log')
4799+ configs = fnmatch.filter(allfiles, '*.conf')
4800+ self.add_copy_spec(logfiles, container='rhui5-rhua', runas='rhui')
4801+ self.add_copy_spec(configs, container='rhui5-rhua', runas='rhui')
4802+ # copy PG_VERSION and postmaster.opts
4803+ for file in ["PG_VERSION", "postmaster.opts"]:
4804+ self.add_copy_spec(f"{pghome}/data/{file}",
4805+ container='rhui5-rhua', runas='rhui')
4806+
4807+ def postproc(self):
4808+ # hide registry_password value in rhui-tools.conf
4809+ self.do_path_regex_sub("/var/lib/rhui/config/rhua/rhui-tools.conf",
4810+ r"(.+_pass(word|):)\s*(.+)",
4811+ r"\1 ********")
4812+ # obfuscate two cookies for login session
4813+ for cookie in ["csrftoken", "sessionid"]:
4814+ self.do_path_regex_sub(
4815+ r"/var/lib/rhui/root/\.rhui/.*/cookies.txt",
4816+ fr"({cookie}\s+)(\S+)",
4817+ r"\1********")
4818+
4819+
4820+# vim: set et ts=4 sw=4 :
4821diff --git a/sos/report/plugins/saltmaster.py b/sos/report/plugins/saltmaster.py
4822index 6edd3fb..110a609 100644
4823--- a/sos/report/plugins/saltmaster.py
4824+++ b/sos/report/plugins/saltmaster.py
4825@@ -21,6 +21,8 @@ class SaltMaster(Plugin, IndependentPlugin):
4826 packages = ('salt-master', 'salt-api',)
4827
4828 def setup(self):
4829+ self.collected_pillar_roots = []
4830+
4831 if self.get_option("all_logs"):
4832 self.add_copy_spec("/var/log/salt")
4833 else:
4834@@ -61,11 +63,22 @@ class SaltMaster(Plugin, IndependentPlugin):
4835 cfg_pillar_roots = []
4836 all_pillar_roots.extend(cfg_pillar_roots)
4837
4838+ self.collected_pillar_roots = all_pillar_roots
4839 self.add_copy_spec(all_pillar_roots)
4840
4841 def postproc(self):
4842- regexp = r'(^\s+.*(pass|secret|(?<![A-z])key(?![A-z])).*:\ ).+$'
4843+ regexp = (
4844+ r'(^\s*.*(pass|secret|(?<![A-z])key(?![A-z])|'
4845+ r'api_?key|encryption_?key).*:\ ).+'
4846+ )
4847 subst = r'\1******'
4848+
4849 self.do_path_regex_sub("/etc/salt/*", regexp, subst)
4850
4851+ for pillar_root in self.collected_pillar_roots:
4852+ normalized_root = pillar_root if pillar_root.endswith('/') else (
4853+ f"{pillar_root}/"
4854+ )
4855+ self.do_path_regex_sub(f"{normalized_root}*", regexp, subst)
4856+
4857 # vim: set et ts=4 sw=4 :
4858diff --git a/sos/report/plugins/scsi.py b/sos/report/plugins/scsi.py
4859index 1680d20..4e27d8c 100644
4860--- a/sos/report/plugins/scsi.py
4861+++ b/sos/report/plugins/scsi.py
4862@@ -60,6 +60,7 @@ class Scsi(Plugin, IndependentPlugin):
4863 "lsscsi -s",
4864 "lsscsi -L",
4865 "lsscsi -iw",
4866+ "lsscsi -t",
4867 ])
4868
4869 scsi_hosts = glob("/sys/class/scsi_host/*")
4870diff --git a/sos/report/plugins/snapm.py b/sos/report/plugins/snapm.py
4871new file mode 100644
4872index 0000000..2c327b0
4873--- /dev/null
4874+++ b/sos/report/plugins/snapm.py
4875@@ -0,0 +1,38 @@
4876+# Copyright (C) 2025 Red Hat, Inc., Bryn M. Reeves <bmr@redhat.com>
4877+# This file is part of the sos project: https://github.com/sosreport/sos
4878+#
4879+# This copyrighted material is made available to anyone wishing to use,
4880+# modify, copy, or redistribute it subject to the terms and conditions of
4881+# version 2 of the GNU General Public License.
4882+#
4883+# See the LICENSE file in the source distribution for further information.
4884+
4885+from sos.report.plugins import Plugin, RedHatPlugin
4886+
4887+
4888+class Snapm(Plugin, RedHatPlugin):
4889+
4890+ short_desc = 'Snapsot manager'
4891+
4892+ plugin_name = 'snapm'
4893+ profiles = ('boot', 'system', 'storage')
4894+
4895+ packages = (
4896+ 'python3-snapm',
4897+ 'snapm',
4898+ )
4899+
4900+ def setup(self):
4901+ self.add_copy_spec([
4902+ "/etc/snapm",
4903+ ])
4904+
4905+ self.add_cmd_output([
4906+ "snapm snapset list",
4907+ "snapm snapshot list",
4908+ "snapm plugin list",
4909+ "snapm schedule list",
4910+ "snapm -vv --debug=all snapset show",
4911+ ])
4912+
4913+# vim: set et ts=4 sw=4 :
4914diff --git a/sos/report/plugins/spyre.py b/sos/report/plugins/spyre.py
4915new file mode 100644
4916index 0000000..a681eeb
4917--- /dev/null
4918+++ b/sos/report/plugins/spyre.py
4919@@ -0,0 +1,97 @@
4920+# This file is part of the sos project: https://github.com/sosreport/sos
4921+#
4922+# This copyrighted material is made available to anyone wishing to use,
4923+# modify, copy, or redistribute it subject to the terms and conditions of
4924+# version 2 of the GNU General Public License.
4925+#
4926+# See the LICENSE file in the source distribution for further information.
4927+#
4928+# This plugin enables collection of logs for system with IBM Spyre card
4929+
4930+from sos.report.plugins import Plugin, IndependentPlugin
4931+
4932+
4933+class Spyre(Plugin, IndependentPlugin):
4934+ """Spyre chip is IBM’s AI accelerator, designed to handle AI inferencing
4935+ and workloads.
4936+
4937+ The Spyre plugin collects data about the Spyre card’s VFIO device node
4938+ tree, configuration files, and more.
4939+ """
4940+
4941+ short_desc = 'IBM Spyre Accelerator Information'
4942+ plugin_name = 'spyre'
4943+ architectures = ('ppc.*',)
4944+
4945+ @staticmethod
4946+ def get_ibm_spyre_devices(lspci_output):
4947+ """Extract PCI domain, bus, device, function for devices that match:
4948+ - Vendor ID = 0x1014 (IBM)
4949+ - Device ID = 0x06a7 or 0x06a8
4950+
4951+ Parameters
4952+ ----------
4953+ lspci_out : str
4954+ The output string from `lspci -n`.
4955+
4956+ Returns
4957+ -------
4958+ list of tuples
4959+ A list of (domain, bus, device, function) tuples for each matching
4960+ card.
4961+ """
4962+
4963+ spyre_cards = []
4964+
4965+ if not lspci_output:
4966+ return None
4967+
4968+ for line in lspci_output.splitlines():
4969+ if not line.strip():
4970+ continue
4971+
4972+ try:
4973+ pci_addr, _class, ids, _rest = line.split(maxsplit=3)
4974+ vendor, device = ids.lower().split(":")
4975+ except ValueError:
4976+ continue
4977+
4978+ if vendor == "1014" and device in ("06a7", "06a8"):
4979+ if pci_addr.count(":") == 1:
4980+ pci_addr = "0000:" + pci_addr
4981+ try:
4982+ domain, bus, dev_func = pci_addr.split(":")
4983+ pci_device, function = dev_func.split(".")
4984+ except ValueError:
4985+ continue
4986+
4987+ spyre_cards.append((domain, bus, pci_device, function))
4988+
4989+ return spyre_cards
4990+
4991+ def setup(self):
4992+
4993+ lspci = self.exec_cmd("lspci -n")
4994+ if lspci['status'] != 0:
4995+ return
4996+
4997+ spyre_cards = self.get_ibm_spyre_devices(lspci['output'])
4998+
4999+ # Nothing to collect if spyre card is not found
5000+ if not spyre_cards:
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches