pax_global_header00006660000000000000000000000064146414742750014530gustar00rootroot0000000000000052 comment=714d992d1a9c91b0e797970fc162b31eab0c3f37 whatmaps-0.0.14/000077500000000000000000000000001464147427500134365ustar00rootroot00000000000000whatmaps-0.0.14/.gitignore000066400000000000000000000000711464147427500154240ustar00rootroot00000000000000.coverage .tox *.pyc *~ build/ cover/ whatmaps.egg-info/ whatmaps-0.0.14/Makefile000066400000000000000000000000331464147427500150720ustar00rootroot00000000000000check: flake8 nosetests3 whatmaps-0.0.14/NEWS000066400000000000000000000003411464147427500141330ustar00rootroot00000000000000whatmaps 0.0.14 --------------- Released July 2024 * Use unittest.mock * Avoid test functions dropped in python3.12 whatmaps 0.0.13 --------------- Released December 2022 * Run without lsb python modules * Make flake8 happy whatmaps-0.0.14/README000066400000000000000000000021211464147427500143120ustar00rootroot00000000000000After a security update of a library one needs to restart all programs that make use of this library in order for the security update to become effective. whatmaps takes a package name and looks (on Linux systems) in /proc/pid/maps for processes that map shared objects contained in a certain package. It then looks for the corresponding processes to find the services that need to be restarted. It currently supports Debian and Fedora but might also work well on other Debian or RPM based distributions. Usage ===== Pass the names of the packages that had shared objects updated: whatmaps The example output looks like: # whatmaps libssl0.9.8 WARNING: No service script found in 'wpasupplicant' for '['/sbin/wpa_supplicant']' - restart manually Services that possibly need to be restarted: postfix libvirt-bin ntpd Apt Integration =============== To enable automatic restarts during security updates on Debian based systems set: Whatmaps::Enable-Restart "1"; in /etc/apt/apt.conf.d/20services. See https://honk.sigxcpu.org/piki/projects/whatmaps/ whatmaps-0.0.14/apt/000077500000000000000000000000001464147427500142225ustar00rootroot00000000000000whatmaps-0.0.14/apt/20services000066400000000000000000000004021464147427500161260ustar00rootroot00000000000000// Set to 1 or true to enable service restarts on security updates Whatmaps::Enable-Restart "0"; // What updates are considered security updates Whatmaps::Security-Update-Origins { "${distro_id} stable"; "${distro_id} ${distro_codename}-security"; }; whatmaps-0.0.14/apt/50whatmaps_apt000066400000000000000000000004631464147427500170050ustar00rootroot00000000000000DPkg::Pre-Install-Pkgs { "/usr/bin/whatmaps --apt --restart --print-cmds=/var/lib/whatmaps/restart.sh || true" }; DPkg::Post-Invoke { "if [ -x /var/lib/whatmaps/restart.sh ]; then /var/lib/whatmaps/restart.sh; rm -f /var/lib/whatmaps/restart.sh; fi" }; DPkg::Tools::Options::/usr/bin/whatmaps::Version "2"; whatmaps-0.0.14/man/000077500000000000000000000000001464147427500142115ustar00rootroot00000000000000whatmaps-0.0.14/man/whatmaps.pod000066400000000000000000000024731464147427500165470ustar00rootroot00000000000000=head1 NAME whatmaps - Find and restart services after library upgrades =head1 SYNOPSIS =encoding utf8 B [--restart] [--print-cmds=I] pkg1 [pkg2 pkg3 ...] =head1 DESCRIPTION B tries to find a list of services that need to be restarted after a library upgrade because they map that particular library into their address space. It does this by extracting a list of shared objects from the packages given on the command line. It then looks at the list of running processes to find those that map the shared objects and uses the distributions package manager to find out the packages that ship them. It then prints a list of services in these packages that likely need to be restarted. By default it assumes all services in the found packages need a restart but it also keeps a internal distribution specific list of exceptions. If the I<--restart> option is given, the services are restarted without any prompting and with the I<--prind-cmds> option the restart commands are written to a file for later execution. On Debian systems B can also be run automatically by apt-get. See L for details. =head1 SEE ALSO apt(8) The latest version is available at L. =head1 AUTHOR Guido Günther =cut whatmaps-0.0.14/requirements.txt000066400000000000000000000000751464147427500167240ustar00rootroot00000000000000coverage>=2.85 flake8>=3 mock nose>=0.11.1 nosexcover>=1.0.7 whatmaps-0.0.14/run.py000077500000000000000000000001641464147427500146200ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from whatmaps.command import run if __name__ == '__main__': sys.exit(run()) whatmaps-0.0.14/setup.cfg000066400000000000000000000002121464147427500152520ustar00rootroot00000000000000[nosetests] cover-package=whatmaps [flake8] # E501: ignore line length # E265: block comment should start with '# ' ignore=E501,E265,W504whatmaps-0.0.14/setup.py000066400000000000000000000025411464147427500151520ustar00rootroot00000000000000#!/usr/bin/python3 # vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os from setuptools import setup data_files = [] if os.path.exists('/etc/debian_version'): data_files = [('../etc/apt/apt.conf.d/', ['apt/50whatmaps_apt']), ('../etc/apt/apt.conf.d/', ['apt/20services']), ] setup(name="whatmaps", author='Guido Günther', author_email='agx@sigxcpu.org', data_files=data_files, packages=['whatmaps'], entry_points={ 'console_scripts': ['whatmaps = whatmaps.command:run'], }, ) # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: whatmaps-0.0.14/tests/000077500000000000000000000000001464147427500146005ustar00rootroot00000000000000whatmaps-0.0.14/tests/__init__.py000066400000000000000000000000001464147427500166770ustar00rootroot00000000000000whatmaps-0.0.14/tests/context.py000066400000000000000000000036321464147427500166420ustar00rootroot00000000000000# this context.py should be included by all tests # idea from http://kennethreitz.com/repository-structure-and-python.html # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil import sys import tempfile import whatmaps sys.path.insert(0, os.path.abspath('..')) # the top or root dir of the git-buildpackage source tree to be used by tests projectdir = os.path.dirname(os.path.dirname(os.path.abspath(whatmaps.__file__))) _chdir_backup = None _tmpdirs = [] def chdir(dir): global _chdir_backup if not _chdir_backup: _chdir_backup = os.path.abspath(os.curdir) os.chdir(str(dir)) def new_tmpdir(name): global _tmpdirs prefix = 'whatmaps_%s_' % name tmpdir = TmpDir(prefix) _tmpdirs.append(tmpdir) return tmpdir def teardown(): if _chdir_backup: os.chdir(_chdir_backup) for tmpdir in _tmpdirs: tmpdir.rmdir() del _tmpdirs[:] class TmpDir(object): def __init__(self, suffix='', prefix='tmp'): self.path = tempfile.mkdtemp(suffix=suffix, prefix=prefix) def rmdir(self): if self.path and not os.getenv("WHATMAPS_TESTS_NOCLEAN"): shutil.rmtree(self.path) self.path = None def __repr__(self): return self.path def join(self, *args): return os.path.join(self.path, *args) whatmaps-0.0.14/tests/test_debiandistro.py000066400000000000000000000101661464147427500206640ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch try: import apt_pkg # noqa: F401 have_apt_pkg = True except ImportError: have_apt_pkg = False try: import lsb_release # noqa: F401 have_lsb_release = True except ImportError: have_lsb_release = False from whatmaps.debiandistro import DebianDistro from whatmaps.debianpkg import DebianPkg class TestDebianDistro(unittest.TestCase): def test_vars(self): """Check Debian distro vars""" self.assertEqual(DebianDistro.id, 'Debian') self.assertIsNotNone(DebianDistro._pkg_services) self.assertIsNotNone(DebianDistro._pkg_service_blacklist) self.assertIsNotNone(DebianDistro.service_blacklist) self.assertEqual(DebianDistro.restart_service_cmd('aservice'), ['invoke-rc.d', 'aservice', 'restart']) self.assertTrue(DebianDistro.has_apt()) def test_pkg_by_file(self): with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.returncode = 0 PopenMock.communicate.return_value = ['apackage'] pkg = DebianDistro.pkg_by_file('afile') self.assertIsInstance(pkg, DebianPkg) self.assertEqual(pkg.name, 'apackage') PopenMock.communicate.assert_called_once_with() mock.assert_called_once_with(['dpkg-query', '-S', 'afile'], stderr=-1, stdout=-1) def test_pkg_by_file_failure(self): """Test if None is returned on subcommand erros""" with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.returncode = 1 PopenMock.communicate.return_value = ['apackage'] pkg = DebianDistro.pkg_by_file('afile') self.assertIsNone(pkg) PopenMock.communicate.assert_called_once_with() mock.assert_called_once_with(['dpkg-query', '-S', 'afile'], stderr=-1, stdout=-1) def test_read_apt_pipeline(self): """Test our interaction with the apt pipeline""" class AptPipelineMock(object): def __init__(self): self.iter = self.lines() def lines(self): for line in ['VERSION 2', 'Whatmaps::Enable-Restart=1', '\n']: yield line def readlines(self): return ['pkg1 0.0 c 1.0 **CONFIGURE**', 'pkg2 - c 1.0 **CONFIGURE**', ''] def readline(self): return next(self.iter) with patch('sys.stdin', new_callable=AptPipelineMock): pkgs = DebianDistro.read_apt_pipeline() self.assertEqual(len(pkgs), 1) self.assertIn('pkg1', pkgs) self.assertTrue(pkgs['pkg1'].name, 'pkg1') @patch('apt_pkg.init') @patch('apt_pkg.Acquire') @unittest.skipUnless(have_apt_pkg, "apt_pkg not installed") @unittest.skipUnless(have_lsb_release, "lsb_release not installed") def test_filter_security_updates(self, apt_pkg_acquire, apt_pkg_init): pkgs = {'pkg1': DebianPkg('pkg1'), 'pkg2': DebianPkg('pkg2'), } with patch('apt_pkg.Cache'): DebianDistro.filter_security_updates(pkgs) apt_pkg_init.assert_called_once_with() apt_pkg_acquire.assert_called_once_with() whatmaps-0.0.14/tests/test_debianpkg.py000066400000000000000000000022161464147427500201360ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch from whatmaps.debianpkg import DebianPkg class TestDebianPkg(unittest.TestCase): def test_services(self): with patch('whatmaps.pkg.Pkg._get_contents') as mock: mock.return_value = ['/etc/init.d/aservice', '/usr/bin/afile'] p = DebianPkg('doesnotmatter') self.assertEqual(p.services, ['aservice']) whatmaps-0.0.14/tests/test_distro.py000066400000000000000000000053501464147427500175200ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch try: import lsb_release # noqa: F401 have_lsb_release = True except ImportError: have_lsb_release = False from whatmaps.distro import Distro, detect class Pkg(object): name = 'doesnotmatter' class TestDistro(unittest.TestCase): def test_abstract(self): """Check abstract method signatures""" # Variables self.assertEqual(Distro.service_blacklist, set()) self.assertIsNone(Distro.id) # Pure virtual methods self.assertRaises(NotImplementedError, Distro.pkg, None) self.assertRaises(NotImplementedError, Distro.pkg_by_file, None) self.assertRaises(NotImplementedError, Distro.restart_service_cmd, None) self.assertRaises(NotImplementedError, Distro.restart_service, None) # Lookup methods self.assertEqual(Distro.pkg_services(Pkg), []) self.assertEqual(Distro.pkg_service_blacklist(Pkg), []) self.assertFalse(Distro.has_apt()) @unittest.skipUnless(have_lsb_release, "lsb_release not installed") def test_detect_via_lsb_release_module(self): "Detect distro via lsb_release" with patch('lsb_release.get_distro_information', return_value={'ID': 'Debian'}): # Make sure we don't use the fallback with patch('os.path.exists', return_value=False): d = detect() self.assertEqual(d.id, 'Debian') def test_filter_services_empty(self): d = Distro() self.assertEqual(set(['foo', 'bar']), d.filter_services(['foo', 'bar'])) def test_filter_services_one(self): d = Distro() d.service_blacklist_re = ['^f..'] self.assertEqual(set(['bar']), d.filter_services(['foo', 'bar'])) def test_filter_services_all(self): d = Distro() d.service_blacklist_re = ['^f..', '^b..'] self.assertEqual(set(), d.filter_services(['foo', 'bar'])) whatmaps-0.0.14/tests/test_pkg.py000066400000000000000000000074351464147427500170030ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch from whatmaps.pkg import Pkg, PkgError from . import context class TestPkg(unittest.TestCase): def setUp(self): self.tmpdir = context.new_tmpdir(__name__) def test_abstract(self): """Check abstract method signatures""" self.assertIsNone(Pkg.type) self.assertIsNone(Pkg.services) def test_repr(self): p = Pkg('apckage') self.assertEqual(str(p), "") def test_list_contents(self): with patch('subprocess.Popen') as mock: p = Pkg('doesnotmatter') p._list_contents = '/does/not/matter' PopenMock = mock.return_value PopenMock.communicate.return_value = [ b'/package/content', b'/more/package/content', ] PopenMock.returncode = 0 result = p._get_contents() self.assertIn('/package/content', result) self.assertNotIn('/more/package/content', result) # We want to check that we don't invoke Popen on # a second call so let it fail PopenMock.returncode = 1 result = p._get_contents() self.assertIn('/package/content', result) self.assertNotIn('/more/package/content', result) def test_shared_objects(self): """Test that we properly match shared objects""" with patch('subprocess.Popen') as mock: p = Pkg('doesnotmatter') p._list_contents = '/does/not/matter' PopenMock = mock.return_value PopenMock.communicate.return_value = [b'\n'.join([ b'/lib/foo.so.1', b'/lib/bar.so', b'/not/a/shared/object', b'/not/a/shared/object.soeither', ])] PopenMock.returncode = 0 result = p.shared_objects self.assertIn('/lib/foo.so.1', result) self.assertIn('/lib/bar.so', result) self.assertNotIn('/not/a/shred/object', result) self.assertNotIn('/not/a/shred/object.soeither', result) # We want to check that we don't invoke Popen on # a second call so let it fail. PopenMock.returncode = 1 result = p._get_contents() self.assertIn('/lib/foo.so.1', result) self.assertNotIn('/not/a/shred/object', result) def test_shared_object_error(self): """Test that we raise PkgError""" with patch('subprocess.Popen') as mock: p = Pkg('doesnotmatter') p._list_contents = '/does/not/matter' PopenMock = mock.return_value PopenMock.communicate.return_value = [''] PopenMock.returncode = 1 try: p.shared_objects self.fail("PkgError exception not raised") except PkgError: pass except Exception as e: self.fail("Raised '%s is not PkgError" % e) def tearDown(self): context.teardown() whatmaps-0.0.14/tests/test_process.py000066400000000000000000000111271464147427500176710ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import os import unittest import random from whatmaps.process import Process from . import context class TestWhatmapsProcess(unittest.TestCase): def setUp(self): self.tmpdir = context.new_tmpdir(__name__) self.procfs = str(self.tmpdir) self.pid = random.randint(1, 65535) self.piddir = os.path.join(self.procfs, str(self.pid)) os.mkdir(self.piddir) self.exe = os.path.join(self.piddir, 'exe') self.cmdline = os.path.join(self.piddir, 'cmdline') self.maps = os.path.join(self.piddir, 'maps') self._write_cmdline('doesnotmatter') # Write at least an empty cmdline self._write_exe_symlink('acommand') self._write_maps([['f32b43221000-7f32b4522000', '---p', '00020000', 'fe:02', '1704011', '/lib/x86_64-linux-gnu/libselinux.so.1'], ['7f32b4521000-7f32b4623000', 'r--p', '00020000', 'fe:02', '1704011', '/lib/x86_64-linux-gnu/libselinux.so.1'], ]) def _write_exe_symlink(self, name): exe = os.path.join(str(self.tmpdir), name) os.symlink(exe, self.exe) def _write_cmdline(self, text=''): f = open(self.cmdline, 'w') f.write(text) f.close() def _write_maps(self, data): f = open(self.maps, 'w') f.write('\n'.join([' '.join(r) for r in data])) f.close() def test_nonexistent(self): """No exe link should create an 'empty' object""" os.unlink(self.exe) p = Process(self.pid, self.procfs) self.assertIsNone(p.exe) self.assertIsNone(p.cmdline) def test_deleted(self): """Handle symlink to deleted binaries""" exe = '/does/not/matter' os.unlink(self.exe) os.symlink(os.path.join(self.piddir, '%s (deleted)' % exe), self.exe) p = Process(self.pid, procfs=self.procfs) self.assertEqual(p.exe, exe) self.assertTrue(p.deleted, True) self.assertEqual(p.cmdline, 'doesnotmatter') def test_existing(self): p = Process(self.pid, procfs=self.procfs) exe = os.path.join(str(self.tmpdir), 'acommand') self.assertEqual(p.exe, exe) self.assertEqual(p.cmdline, 'doesnotmatter') self.assertFalse(p.deleted) self.assertEqual(str(p), "" % self.pid) def test_maps(self): """Check whether the process maps a shared object at path""" p = Process(self.pid, procfs=self.procfs) self.assertFalse(p.maps('/does/not/exist')) self.assertTrue(p.maps('/lib/x86_64-linux-gnu/libselinux.so.1')) def test_no_maps(self): """Check if we don't fail if the process went away""" os.unlink(self.maps) p = Process(self.pid, procfs=self.procfs) self.assertFalse(p.maps('/does/not/exist')) self.assertFalse(p.maps('/lib/x86_64-linux-gnu/libselinux.so.1')) def test_broken_maps(self): """Continue on unparseable map file""" # Provoke index error by to few items in line self._write_maps([['do', 'few', 'items']]) p = Process(self.pid, procfs=self.procfs) self.assertFalse(p.maps('/does/not/exist')) self.assertFalse(p.maps('/lib/x86_64-linux-gnu/libselinux.so.1')) @unittest.skipIf(os.getuid() == 0, "Skip if root") def test_broken_unreadable_map(self): """Raise error if map file is unreadable""" os.chmod(self.maps, 0) p = Process(self.pid, procfs=self.procfs) self.assertRaises(IOError, p.maps, '/does/not/exist') def tearDown(self): context.teardown() whatmaps-0.0.14/tests/test_redhatdistro.py000066400000000000000000000047661464147427500207220ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch from whatmaps.redhatdistro import RedHatDistro from whatmaps.rpmpkg import RpmPkg class TestRedHatDistro(unittest.TestCase): def test_vars(self): """Check RedHat distro vars""" self.assertEqual(RedHatDistro.id, None) self.assertIsNotNone(RedHatDistro._pkg_services) self.assertIsNotNone(RedHatDistro._pkg_service_blacklist) self.assertIsNotNone(RedHatDistro.service_blacklist) self.assertEqual(RedHatDistro.restart_service_cmd('aservice'), ['service', 'aservice', 'restart']) self.assertFalse(RedHatDistro.has_apt()) def test_pkg_by_file(self): with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.returncode = 0 PopenMock.communicate.return_value = ['apackage'] pkg = RedHatDistro.pkg_by_file('afile') self.assertIsInstance(pkg, RpmPkg) self.assertEqual(pkg.name, 'apackage') PopenMock.communicate.assert_called_once_with() mock.assert_called_once_with(['rpm', '-qf', 'afile'], stderr=-1, stdout=-1) def test_pkg_by_file_failure(self): """Test if None is returned on subcommand erros""" with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.returncode = 1 PopenMock.communicate.return_value = ['apackage'] pkg = RedHatDistro.pkg_by_file('afile') self.assertIsNone(pkg) PopenMock.communicate.assert_called_once_with() mock.assert_called_once_with(['rpm', '-qf', 'afile'], stderr=-1, stdout=-1) whatmaps-0.0.14/tests/test_rpmpkg.py000066400000000000000000000022071464147427500175120ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch from whatmaps.rpmpkg import RpmPkg class TestRpmPkg(unittest.TestCase): def test_services(self): with patch('whatmaps.pkg.Pkg._get_contents') as mock: mock.return_value = ['/etc/rc.d/init.d/aservice', '/usr/bin/afile'] p = RpmPkg('doesnotmatter') self.assertEqual(p.services, ['aservice']) whatmaps-0.0.14/tests/test_systemd.py000066400000000000000000000071011464147427500177000ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # (C) 2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Test L{whatmaps.process} config""" import unittest from unittest.mock import patch from whatmaps.systemd import Systemd class Process(object): def __init__(self, pid): self.pid = pid class TestSystemd(unittest.TestCase): def test_is_init(self): """Check if we create a systemd object if systemd is the init system in use""" with patch('os.path.exists', return_value=True): self.assertIsNotNone(Systemd()) def test_is_not_init(self): """Check if we raise an exception if systemd isn't tthe init system in use""" with patch('os.path.exists', return_value=False): self.assertRaises(ValueError, Systemd) def test_process_to_unit(self): p = Process(952) output = """libvirt-bin.service - Virtualization daemon Loaded: loaded (/lib/systemd/system/libvirt-bin.service; enabled) Active: active (running) since Fr 2014-07-11 16:10:55 CEST; 50min ago Docs: man:libvirtd(8) http://libvirt.org Main PID: 952 (libvirtd) CGroup: name=systemd:/system/libvirt-bin.service ├─ 952 /usr/sbin/libvirtd └─1355 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf """.encode('utf-8') with patch('os.path.exists', return_value=True): with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.communicate.return_value = [output] PopenMock.returncode = 0 result = Systemd().process_to_unit(p) self.assertEqual(result, 'libvirt-bin.service') PopenMock.returncode = 1 result = Systemd().process_to_unit(p) self.assertIsNone(result) def test_process_user_session(self): p = Process(952) output = """● session-8762.scope - Session 8762 of user root Loaded: loaded Drop-In: /run/systemd/system/session-8762.scope.d └─50-After-systemd-logind\x2eservice.conf, 50-After-systemd-user-sessions\x2eservice.conf, 50-Description.conf, 50-SendSIGHUP.conf, 50-Slice.conf Active: active (running) since Thu 2016-04-07 20:53:52 CEST; 19min ago CGroup: /user.slice/user-0.slice/session-8762.scope ├─21150 sshd: root@pts/0 ├─21155 -bash ├─23956 /usr/bin/python /usr/bin/whatmaps --debug libc6 └─23962 systemctl status 21155 """.encode('utf-8') with patch('os.path.exists', return_value=True): with patch('subprocess.Popen') as mock: PopenMock = mock.return_value PopenMock.communicate.return_value = [output] PopenMock.returncode = 0 with self.assertRaisesRegex(ValueError, "Can't parse service name from session-8762.scope - Session 8762 of user root"): Systemd().process_to_unit(p) whatmaps-0.0.14/tox.ini000066400000000000000000000005301464147427500147470ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py39, py312 [testenv] commands = python setup.py nosetests deps = nose mock whatmaps-0.0.14/whatmaps/000077500000000000000000000000001464147427500152625ustar00rootroot00000000000000whatmaps-0.0.14/whatmaps/__init__.py000066400000000000000000000000001464147427500173610ustar00rootroot00000000000000whatmaps-0.0.14/whatmaps/command.py000077500000000000000000000205411464147427500172570ustar00rootroot00000000000000#!/usr/bin/python3 -u # vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import errno import glob import os import logging import sys from optparse import OptionParser from . process import Process from . distro import Distro from . pkg import PkgError from . systemd import Systemd def check_maps(procs, shared_objects): restart_procs = {} for proc in procs: for so in shared_objects: if proc.maps(so): if proc.exe in restart_procs: restart_procs[proc.exe] += [proc] else: restart_procs[proc.exe] = [proc] break return restart_procs def get_all_pids(): processes = [] paths = glob.glob('/proc/[0-9]*') for path in paths: p = Process(int(path.rsplit('/')[-1])) processes.append(p) return processes def write_cmd_file(services, cmd_file, distro): "Write out commands needed to restart the services to a file" out = open(cmd_file, 'w') print('#! /bin/sh', file=out) for service in services: logging.info("Need to restart '%s'", service) print(" ".join(distro.restart_service_cmd(service)), file=out) out.close() os.chmod(cmd_file, 0o755) def find_pkgs(procs, distro): """ Find packages that contain the binaries of the given processes """ pkgs = {} for proc in procs: pkg = distro.pkg_by_file(proc) if not pkg: logging.warning("No package found for '%s' - restart manually" % proc) else: if pkg.name in pkgs: pkgs[pkg.name].procs.append(proc) else: pkg.procs = [proc] pkgs[pkg.name] = pkg if pkgs: logging.info("Packages that ship the affected binaries:") for pkg in list(pkgs.values()): logging.info(" Pkg: %s, binaries: %s" % (pkg.name, pkg.procs)) return pkgs def find_services(pkgs, distro): """ Determine the services in pkgs honoring distro specific mappings and blacklists """ all_services = set() for pkg in list(pkgs.values()): services = set(pkg.services + distro.pkg_services(pkg)) services -= set(distro.pkg_service_blacklist(pkg)) if not services: logging.warning("No service script found in '%s' for '%s' " "- restart manually" % (pkg.name, pkg.procs)) else: all_services.update(services) all_services -= distro.service_blacklist return all_services def find_systemd_units(procmap, distro): """Find systemd units that contain the given processes""" units = set() for dummy, procs in list(procmap.items()): for proc in procs: try: unit = Systemd.process_to_unit(proc) except ValueError as e: logging.warning("No systemd unit found for '%s': %s " "- restart manually" % (proc.exe, e)) continue if not unit: logging.warning("No systemd unit found for '%s' " "- restart manually" % proc.exe) else: units.add(unit) units -= set([service + '.service' for service in distro.service_blacklist]) return units def filter_services(distro, services): filtered = distro.filter_services(services) diff = services - filtered if len(diff): logging.warning("Filtered out blacklisted service%s %s - restart manually", 's' if len(diff) > 1 else '', ', '.join(diff)) return filtered def main(argv): shared_objects = [] services = None ret = 0 parser = OptionParser(usage='%prog [options] pkg1 [pkg2 pkg3 pkg4]') parser.add_option("--debug", action="store_true", dest="debug", default=False, help="enable debug output") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="enable verbose output") parser.add_option("--restart", action="store_true", dest="restart", default=False, help="Restart services") parser.add_option("--print-cmds", dest="print_cmds", help="Output restart commands to file instead of restarting") parser.add_option("--apt", action="store_true", dest="apt", default=False, help="Use in apt pipeline") (options, args) = parser.parse_args(argv[1:]) if options.debug: level = logging.DEBUG elif options.verbose: level = logging.INFO else: level = logging.WARNING logging.basicConfig(level=level, format='%(levelname)s: %(message)s') distro = Distro.detect()() if not distro: logging.error("Unsupported Distribution") return 1 else: logging.debug("Detected distribution: '%s'", distro.id) if args: pkgs = [distro.pkg(arg) for arg in args] elif options.apt and distro.has_apt(): try: pkgs = distro.read_apt_pipeline() except PkgError: logging.error("Can't read apt pipeline") return 1 if not pkgs: return 0 pkgs, notfound = distro.filter_security_updates(pkgs) if notfound: logging.warning("Pkgs %s not found in apt cache" % ", ".join(notfound)) logging.debug("Security Upgrades: %s" % pkgs) else: parser.print_help() return 1 # Find shared objects of updated packages for pkg in pkgs: try: shared_objects += pkg.shared_objects except PkgError as e: logging.error("%s - skipping package %s" % (e, pkg.name)) ret = 1 logging.debug("Found shared objects:") for so in shared_objects: logging.debug(" %s", so) # Find processes that map them try: restart_procs = check_maps(get_all_pids(), shared_objects) except IOError as e: if e.errno == errno.EACCES: logging.error("Can't open process maps in '/proc//maps', are you root?") return 1 else: raise logging.debug("Processes that map them:") for exe, pids in list(restart_procs.items()): logging.debug(" Exe: %s Pids: %s", exe, pids), if Systemd.is_running(): logging.debug("Detected Systemd") services = find_systemd_units(restart_procs, distro) else: # Find the packages that contain the binaries the processes are # executing pkgs = find_pkgs(restart_procs, distro) # Find the services in these packages honoring distro specific # mappings and blacklists try: services = find_services(pkgs, distro) except NotImplementedError: if level > logging.INFO: logging.error("Getting Service listing not implemented " "for distribution %s - rerun with --verbose to see a list" "of binaries that map a shared objects from %s", distro.id, args) return 1 else: return 0 services = filter_services(distro, services) if options.restart: if options.print_cmds and services: write_cmd_file(services, options.print_cmds, distro) else: for service in services: logging.info("Restarting '%s'" % service) distro.restart_service(service) elif services: print("Services that possibly need to be restarted:") for s in services: print(s) return ret def run(): return main(sys.argv) # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: whatmaps-0.0.14/whatmaps/debiandistro.py000066400000000000000000000141671464147427500203140ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public Licnese for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # try: import apt_pkg except ImportError: apt_pkg = None import logging import os import subprocess import sys import string from . distro import Distro from . debianpkg import DebianPkg from . pkg import PkgError from . systemd import Systemd class DebianDistro(Distro): "Debian (dpkg) based distribution" id = 'Debian' _pkg_services = { 'apache2-mpm-worker': ['apache2'], 'apache2-mpm-prefork': ['apache2'], 'apache2.2-bin': ['apache2'], 'apache2-bin': ['apache2'], 'dovecot-imapd': ['dovecot'], 'dovecot-pop3d': ['dovecot'], 'exim4-daemon-light': ['exim4'], 'exim4-daemon-heavy': ['exim4'], 'libvirt-daemon': ['libvirtd'], 'openjdk-6-jre-headless': ['jenkins', 'tomcat7'], 'openjdk-7-jre-headless': ['jenkins', 'tomcat7'], 'qemu-system-x86_64': ['libvirt-guests'], } # Per package blacklist _pkg_service_blacklist = {'libvirt-bin': ['libvirt-guests']} # Per distro blacklist service_blacklist = set(['kvm', 'qemu-kvm', 'qemu-system-x86']) # Per distro regex filter service_blacklist_re = set([ # Restarting these aborts the users session "^dbus(.service)?$", "^user@[0-9]+.service$", "^systemd-logind.service$", "^(g|k|light|no|sd|w|x)dm(.service)?$", ]) @classmethod def pkg(klass, name): return DebianPkg(name) @classmethod def pkg_by_file(klass, path): find_file = subprocess.Popen(['dpkg-query', '-S', path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = find_file.communicate()[0] if find_file.returncode: return None pkg = output.split(':')[0] return DebianPkg(pkg) @classmethod def restart_service_cmd(klass, service): """The command that should be used to start a service""" if Systemd.is_running() and service.endswith('.service'): name = service[:-len('.service')] else: name = service return ['invoke-rc.d', name, 'restart'] @classmethod def is_service_installed(klass, name): """Whether the system has this service""" return os.path.exists('/etc/init.d/%s' % name) @classmethod def has_apt(klass): return True @staticmethod def read_apt_pipeline(): whatmaps_enabled = False version = sys.stdin.readline().rstrip() if version != "VERSION 2": err = "Wrong or missing VERSION from apt pipeline" logging.error("%s\n" "(is Dpkg::Tools::Options::/usr/bin/whatmaps::Version set to 2?)" % err) raise PkgError(err) while 1: aptconfig = sys.stdin.readline() if not aptconfig or aptconfig == '\n': break if (aptconfig.startswith('Whatmaps::Enable-Restart=') and aptconfig.strip().split('=', 1)[1].lower() in ["true", "1"]): logging.debug("Service restarts enabled") whatmaps_enabled = True if not whatmaps_enabled: return None pkgs = {} for line in sys.stdin.readlines(): if not line: break (pkgname, oldversion, compare, newversion, filename) = line.split() if filename == '**CONFIGURE**': if oldversion != '-': # Updates only pkgs[pkgname] = DebianPkg(pkgname) pkgs[pkgname].version = newversion return pkgs @classmethod def _security_update_origins(klass): "Determine security update origins from apt configuration" with open("/etc/debian_version") as f: codename = f.read().strip() def _subst(line): mapping = {'distro_codename': codename, 'distro_id': klass.id, } return string.Template(line).substitute(mapping) origins = [] for s in apt_pkg.config.value_list('Whatmaps::Security-Update-Origins'): (distro_id, distro_codename) = s.split() origins.append((_subst(distro_id), _subst(distro_codename))) logging.debug("Security Update Origins: %s", origins) return origins @classmethod def filter_security_updates(klass, pkgs): """Filter on security updates""" if apt_pkg is None: raise PkgError("apt_pkg not installed, can't determine security updates") apt_pkg.init() apt_pkg.Acquire() cache = apt_pkg.Cache() security_update_origins = klass._security_update_origins() security_updates = {} notfound = [] for pkg in list(pkgs.values()): try: cache_pkg = cache[pkg.name] except KeyError: notfound.append(pkg) continue for cache_version in cache_pkg.version_list: if pkg.version == cache_version.ver_str: for pfile, _ in cache_version.file_list: for origin in security_update_origins: if pfile.origin == origin[0] and \ pfile.archive == origin[1]: security_updates[pkg] = pkg break return (security_updates, notfound) whatmaps-0.0.14/whatmaps/debianpkg.py000066400000000000000000000025751464147427500175710ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import re from . pkg import Pkg class DebianPkg(Pkg): type = 'Debian' _init_script_re = re.compile(r'/etc/init.d/[\w\-\.]') _list_contents = ['dpkg-query', '-L', '${pkg_name}'] def __init__(self, name): Pkg.__init__(self, name) @property def services(self): if self._services is not None: return self._services self._services = [] contents = self._get_contents() # Only supports sysvinit so far: for line in contents: if self._init_script_re.match(line): self._services.append(os.path.basename(line.strip())) return self._services whatmaps-0.0.14/whatmaps/distro.py000066400000000000000000000112071464147427500171410ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import logging import os import re import subprocess try: import lsb_release except ImportError: lsb_release = None class Distro(object): """ A distribution @cvar id: distro id as returned by lsb-release @cvar service_blacklist: services that should never be restarted @cvar service_blacklist_re: regex list of services that should never be restartet @cvar _pkg_services: A C{dict} that maps packages to services. In case we find binaries that match a key in this hash restart the services listed in values. @cvar _pkg_service_blacklist: if we find binaries in the package listed as key don't restart services listed in values """ id = None service_blacklist = set() service_blacklist_re = set() _pkg_services = {} _pkg_blacklist = {} _pkg_service_blacklist = {} @classmethod def pkg(klass, name): """Return package object named name""" raise NotImplementedError @classmethod def pkg_by_file(klass, path): """Return package object that contains path""" raise NotImplementedError @classmethod def restart_service_cmd(klass, service): """Command to restart service""" raise NotImplementedError @classmethod def restart_service(klass, service): """Restart a service""" subprocess.call(klass.restart_service_cmd(service)) @classmethod def is_service_installed(klass, service): """Check wether a service exists on the system""" return True @classmethod def pkg_services(klass, pkg): """ List of services that package pkg needs restarted that aren't part of pkg itself """ return [s for s in klass._pkg_services.get(pkg.name, []) if klass.is_service_installed(s)] @classmethod def pkg_service_blacklist(klass, pkg): """ List of services in pkg that we don't want to be restarted even when a binary from this package maps a shared lib that changed. """ return klass._pkg_service_blacklist.get(pkg.name, []) def filter_services(self, services): """ Filter out servies that match service_blacklist_re """ ret = [] matchers = [re.compile(b) for b in self.service_blacklist_re] for s in services: if not any([m.match(s) for m in matchers]): ret.append(s) return set(ret) @classmethod def has_apt(klass): """Does the distribution use apt""" return False @staticmethod def detect(): return detect() import whatmaps.debiandistro # noqa: E402 import whatmaps.redhatdistro # noqa: E402 def detect(): """ Detect the distribution we run on. Returns C{None} if the distribution is unknown. """ id = None if lsb_release: id = lsb_release.get_distro_information()['ID'] else: try: lsb_cmd = subprocess.Popen(['lsb_release', '--id', '-s'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) output = lsb_cmd.communicate()[0] if not lsb_cmd.returncode: id = output.decode().split('\n')[0].strip() except OSError: # id is None in this case pass if id == whatmaps.debiandistro.DebianDistro.id: return whatmaps.debiandistro.DebianDistro elif id == whatmaps.redhatdistro.FedoraDistro.id: return whatmaps.redhatdistro.FedoraDistro else: if os.path.exists('/usr/bin/dpkg'): logging.warning("Unknown distro but dpkg found, assuming Debian") return whatmaps.debiandistro.DebianDistro elif os.path.exists('/bin/rpm'): logging.warning("Unknown distro but rpm found, assuming Fedora") return whatmaps.debiandistro.FedoraDistro else: return None whatmaps-0.0.14/whatmaps/pkg.py000066400000000000000000000054401464147427500164200ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import re import string import subprocess class PkgError(Exception): pass class Pkg(object): """ A package in a distribution @var services: list of services provided by package @var shared_objects: list of shared objects shipped in this package @cvar type: package type (e.g. RPM or Debian) @cvar _so_regex: regex that matches shared objects in the list returned by _get_contents @cvar _list_contents: command to list contents of a package, will be passed to subprocess. "$pkg_name" will be replaced by the package name. """ type = None services = None _so_regex = re.compile(r'(?P/.*\.so(\.[^/]*)?$)') _list_contents = None def __init__(self, name): self.name = name self._services = None self._shared_objects = None self._contents = None def __repr__(self): return "<%s Pkg object name:'%s'>" % (self.type, self.name) def _get_contents(self): """List of files in the package""" if self._contents: return self._contents else: cmd = [string.Template(arg).substitute(arg, pkg_name=self.name) for arg in self._list_contents] list_contents = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = list_contents.communicate()[0] if list_contents.returncode: raise PkgError("Failed to list package contents for '%s'" % self.name) self._contents = output.decode('utf-8').split('\n') return self._contents @property def shared_objects(self): if self._shared_objects is not None: return self._shared_objects self._shared_objects = [] contents = self._get_contents() for line in contents: m = self._so_regex.match(line) if m: self._shared_objects.append(m.group('so')) return self._shared_objects whatmaps-0.0.14/whatmaps/process.py000066400000000000000000000050031464147427500173100ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import errno import logging import os import re class Process(object): """A process - Linux only so far, needs /proc mounted""" deleted_re = re.compile(r"(?P.*) \(deleted\)$") def __init__(self, pid, procfs=None): self.procfs = procfs or '/proc' self.pid = pid self.mapped = [] self.deleted = False try: self.exe = os.readlink(self._procpath(str(self.pid), 'exe')) m = self.deleted_re.match(self.exe) if m: self.exe = m.group('exe') self.deleted = True logging.debug("Using deleted exe %s", self.exe) if not os.path.exists(self.exe): logging.debug("%s doesn't exist", self.exe) self.cmdline = open(self._procpath('%d/cmdline' % self.pid)).read() except OSError: self.exe = None self.cmdline = None def _procpath(self, *args): """ Return a path relative to the current procfs bsae """ return os.path.join(self.procfs, *args) def _read_maps(self): """Read the SOs from /proc//maps""" try: f = open(self._procpath('%d/maps' % self.pid)) except IOError as e: # ignore killed process if e.errno != errno.ENOENT: raise return for line in f: try: so = line.split()[5].strip() self.mapped.append(so) except IndexError: pass def maps(self, path): """Check if the process maps the object at path""" if not self.mapped: self._read_maps() return True if path in self.mapped else False def __repr__(self): return "" % self.pid whatmaps-0.0.14/whatmaps/redhatdistro.py000066400000000000000000000033221464147427500203300ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import re import subprocess from . distro import Distro from . rpmpkg import RpmPkg class RedHatDistro(Distro): """RPM based distribution""" _pkg_re = re.compile(r'(?P[\w\-\+]+)-(?P[\w\.]+)' r'-(?P[\w\.]+)\.(?P.+)') @classmethod def pkg(klass, name): return RpmPkg(name) @classmethod def pkg_by_file(klass, path): find_file = subprocess.Popen(['rpm', '-qf', path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = find_file.communicate()[0] if find_file.returncode: return None m = klass._pkg_re.match(output.strip()) if m: pkg = m.group('pkg') else: pkg = output.strip() return RpmPkg(pkg) @classmethod def restart_service_cmd(klass, name): return ['service', name, 'restart'] class FedoraDistro(RedHatDistro): id = 'Fedora' whatmaps-0.0.14/whatmaps/rpmpkg.py000066400000000000000000000025641464147427500171430ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2010,2014 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import re from . pkg import Pkg class RpmPkg(Pkg): type = 'RPM' _init_script_re = re.compile(r'/etc/rc.d/init.d/[\w\-\.]') _list_contents = ['rpm', '-ql', '$pkg_name'] def __init__(self, name): Pkg.__init__(self, name) @property def services(self): if self._services is not None: return self._services self._services = [] contents = self._get_contents() # Only supports sysvinit so far: for line in contents: if self._init_script_re.match(line): self._services.append(os.path.basename(line.strip())) return self._services whatmaps-0.0.14/whatmaps/systemd.py000066400000000000000000000035371464147427500173340ustar00rootroot00000000000000# vim: set fileencoding=utf-8 : # # (C) 2014,2015 Guido Günther # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import subprocess class Systemd(object): """Systemd init system""" def __init__(self): if not self.is_running(): raise ValueError("Systemd not running") @staticmethod def is_running(): return os.path.exists("/run/systemd/system") @staticmethod def process_to_unit(process): cmd = ['systemctl', 'status', "%d" % process.pid] systemctl_status = subprocess.Popen(cmd, stdout=subprocess.PIPE) output = systemctl_status.communicate()[0] if systemctl_status.returncode: return None else: parts = output.decode('utf-8').split() if parts[0].endswith('.service'): return parts[0] elif parts[1].endswith('.service'): return parts[1] elif parts[1].startswith('session-') and parts[1].endswith('.scope'): msg = output.decode('utf-8').split('\n')[0][2:] raise ValueError("Can't parse service name from %s" % msg) else: raise ValueError("Can't parse service name from: (%s %s)" % (parts[0], parts[1]))