Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Extend diagnostic information's #1318

Merged
merged 1 commit into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 84 additions & 39 deletions common/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""
import sys
import os
import pathlib
from pathlib import Path
import pwd
import platform
import locale
Expand All @@ -26,10 +26,6 @@ def collect_diagnostics():
"""
result = {}

# Replace home folder user names with this dummy name
# for privacy reasons
USER_REPLACED = 'UsernameReplaced'

pwd_struct = pwd.getpwuid(os.getuid())

# === BACK IN TIME ===
Expand All @@ -44,11 +40,11 @@ def collect_diagnostics():
'version': config.Config.VERSION,
'latest-config-version': config.Config.CONFIG_VERSION,
'local-config-file': cfg._LOCAL_CONFIG_PATH,
'local-config-file-found': os.path.exists(cfg._LOCAL_CONFIG_PATH),
'local-config-file-found': Path(cfg._LOCAL_CONFIG_PATH).exists(),
'global-config-file': cfg._GLOBAL_CONFIG_PATH,
'global-config-file-found': os.path.exists(cfg._GLOBAL_CONFIG_PATH),
'global-config-file-found': Path(cfg._GLOBAL_CONFIG_PATH).exists(),
'distribution-package': str(distro_path),
'started-from': str(pathlib.Path(config.__file__).parent),
'started-from': str(Path(config.__file__).parent),
'running-as-root': pwd_struct.pw_name == 'root',
'user-callback': cfg.takeSnapshotUserCallback()
}
Expand All @@ -67,28 +63,10 @@ def collect_diagnostics():
'platform': platform.platform(),
# OS Version (and maybe name)
'system': '{} {}'.format(platform.system(), platform.version()),
}


# content of /etc/os-release
try:
osrelease = platform.freedesktop_os_release() # since Python 3.10

except AttributeError: # refactor: when we drop Python 3.9 support
# read and parse the os-release file ourself
fp = pathlib.Path('/etc') / 'os-release'

try:
with fp.open('r') as handle:
osrelease = handle.read()

except FileNotFoundError:
osrelease = '(os-release file not found)'
# OS Release name (prettier)
'os-release': _get_os_release()

else:
osrelease = re.findall('PRETTY_NAME=\"(.*)\"', osrelease)[0]

result['host-setup']['os-release'] = osrelease
}

# Display system (X11 or Wayland)
# This doesn't catch all edge cases.
Expand All @@ -102,6 +80,10 @@ def collect_diagnostics():
# PATH environment variable
result['host-setup']['PATH'] = os.environ.get('PATH', '($PATH unknown)')

# RSYNC environment variables
for var in ['RSYNC_OLD_ARGS', 'RSYNC_PROTECT_ARGS']:
result['host-setup'][var] = os.environ.get(var, '(not set)')

# === PYTHON setup ===
python = '{} {} {} {}'.format(
platform.python_version(),
Expand All @@ -118,11 +100,22 @@ def collect_diagnostics():
if rev:
python = '{} rev: {}'.format(python, rev)

python_executable = Path(sys.executable)

# Python interpreter
result['python-setup'] = {
'python': python,
'sys.path': sys.path,
'python-executable': str(python_executable),
'python-executable-symlink': python_executable.is_symlink(),
}

# Real interpreter path if it is used via a symlink
if result['python-setup']['python-executable-symlink']:
result['python-setup']['python-executable-resolved'] \
= str(python_executable.resolve())

result['python-setup']['sys.path'] = sys.path

# Qt
try:
import PyQt5.QtCore
Expand Down Expand Up @@ -179,11 +172,11 @@ def collect_diagnostics():

if shell != SHELL_ERR_MSG:
shell_version = _get_extern_versions([shell, '--version'])
result['external-programs']['shell-version'] = shell_version.split('\n')[0]
result['external-programs']['shell-version'] \
= shell_version.split('\n')[0]

result = json.loads(
json.dumps(result).replace(pwd_struct.pw_name, USER_REPLACED)
)
result = _replace_username_paths(result=result,
username=pwd_struct.pw_name)

return result

Expand Down Expand Up @@ -257,16 +250,16 @@ def get_git_repository_info(path=None):
Credits: https://stackoverflow.com/a/51224861/4865723

Args:
path (pathlib.Path): Path with '.git' folder in (default is
current working directory).
path (Path): Path with '.git' folder in (default is
current working directory).

Returns:
(dict): Dict with keys "branch" and "hash" if it is a git repo,
otherwise an `None`.
"""

if not path:
path = pathlib.Path.cwd()
path = Path.cwd()

git_folder = path / '.git'

Expand Down Expand Up @@ -296,6 +289,31 @@ def get_git_repository_info(path=None):
return result


def _get_os_release():
"""Extract infos from os-release file.

Returns:
(str): OS name e.g. "Debian GNU/Linux 11 (bullseye)"
"""

try:
# content of /etc/os-release
return platform.freedesktop_os_release() # since Python 3.10
except AttributeError: # refactor: when we drop Python 3.9 support
pass

# read and parse the os-release file ourself
fp = Path('/etc') / 'os-release'

try:
with fp.open('r') as handle:
osrelease = handle.read()
except FileNotFoundError:
return '(os-release file not found)'

return re.findall('PRETTY_NAME=\"(.*)\"', osrelease)[0]


def _determine_distro_package_folder():
"""Return the projects root folder.

Expand All @@ -307,12 +325,39 @@ def _determine_distro_package_folder():
"""

# "current" folder
path = pathlib.Path(__file__)
path = Path(__file__)

# level of highest folder named "backintime"
bit_idx = path.parts.index('backintime')

# cut the path to that folder
path = pathlib.Path(*(path.parts[:bit_idx+1]))
path = Path(*(path.parts[:bit_idx+1]))

return path


def _replace_username_paths(result, username):
"""User's homepath and the username is replaced because of security
reasons.

Args:
result (dict): Dict possibily containing the username and its home
path.
username (str). The user login name to look for.

Returns:
(str): String with replacements.
"""

# Replace home folder user names with this dummy name
# for privacy reasons
USER_REPLACED = 'UsernameReplaced'

# JSON to string
result = json.dumps(result)

result = result.replace(f'/home/{username}', f'/home/{USER_REPLACED}')
result = result.replace(f'~/{username}', f'~/{USER_REPLACED}')

# string to JSON
return json.loads(result)
26 changes: 23 additions & 3 deletions common/test/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def test_minimal(self):
self.assertIn(key, result['backintime'], key)

# 2nd level "host-setup"
minimal_keys = ['platform', 'system', 'display-system',
'locale', 'PATH']
minimal_keys = ['platform', 'system', 'display-system', 'locale',
'PATH', 'RSYNC_OLD_ARGS', 'RSYNC_PROTECT_ARGS']
for key in minimal_keys:
self.assertIn(key, result['host-setup'], key)

Expand All @@ -43,7 +43,6 @@ def test_minimal(self):
for key in minimal_keys:
self.assertIn(key, result['external-programs'], key)


def test_no_ressource_warning(self):
"""No ResourceWarning's.

Expand All @@ -70,6 +69,27 @@ def test_no_extern_version(self):
'(no fooXbar)'
)

def test_replace_user_path(self):
"""Replace users path."""

d = {
'foo': '/home/rsync',
'bar': '~/rsync'
}

self.assertEqual(
diagnostics._replace_username_paths(d, 'rsync'),
{
'foo': '/home/UsernameReplaced',
'bar': '~/UsernameReplaced'
}
)

self.assertEqual(
diagnostics._replace_username_paths(d, 'user'),
d
)


class Diagnostics_FakeFS(pyfakefs_ut.TestCase):
"""Tests using a fake filesystem.
Expand Down