Sunil Mohan Adapa 2dd00a8f08
*: Fix all typing hint related errors
- Try to mark class variables in component classes.

- Leave typing hints generic, such as 'list' and 'dict' where content is usually
not filled, too complex, or context is unimportant.

- backups: Handle failure for tarfile extraction so that methods are not called
on potentially None valued variables.

- backups: Prevent potentially passing a keyword argument twice.

- dynamicdns: Deal properly with outcome of urlparsing.

- ejabberd: Deal with failed regex match

- email: Fix a mypy compliant when iterating a filtered list.

- tor: Don't reuse variables for different typed values.

- tor: Don't reuse variables for different typed values.

- operation: Return None explicitly.

- operation: Ensure that keyword argument is not repeated.

Tests:

- Where only typing hints were modified and no syntax error came up, additional
testing was not done.

- `mypy --ignore-missing-imports .` run successfully.

- Generate developer documentation.

- Service runs without errors upon start up.

- backups: Listing and restoring specific apps from a backup works.

- backups: Mounting a remote backup repository works.

- NOT TESTED: dynamicdns: Migrating from old style configuration works.

- ejabberd: Verify that setting coturn configuration works.

- email: Test that showing configuration from postfix works.

- tor: Orport value is properly shown.

- transmission: Configuration values are properly set.

- users: Running unit tests as root works.

- operation: Operation status messages are show properly during app install.

- ./setup.py install runs

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2023-09-25 20:03:24 -04:00

131 lines
3.5 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Set and get postfix configuration using postconf.
See: http://www.postfix.org/postconf.1.html
See: http://www.postfix.org/master.5.html
See: http://www.postfix.org/postconf.5.html
"""
import re
import subprocess
from dataclasses import dataclass
@dataclass
class Service: # NOQA, pylint: disable=too-many-instance-attributes
"""Representation of a postfix daemon and its options."""
service: str
type_: str
private: str
unpriv: str
chroot: str
wakeup: str
maxproc: str
command: str
options: dict[str, str]
def __str__(self) -> str:
parts = [
self.service, self.type_, self.private, self.unpriv, self.chroot,
self.wakeup, self.maxproc, self.command
]
for key, value in self.options.items():
_validate_key(key)
_validate_value(value)
parts.extend(['-o', f'{key}={value}'])
return ' '.join(parts)
def get_config(keys: list) -> dict:
"""Get postfix configuration using the postconf command."""
for key in keys:
_validate_key(key)
args = ['/sbin/postconf']
for key in keys:
args.append(key)
output = _run(args)
result = {}
lines: list[str] = list(filter(None, output.split('\n')))
for line in lines:
key, sep, value = line.partition('=')
if not sep:
raise ValueError('Invalid output detected')
result[key.strip()] = value.strip()
if set(keys) != set(result.keys()):
raise ValueError('Some keys were missing from the output')
return result
def set_config(config: dict, flag=None):
"""Set postfix configuration using the postconf command."""
if not config:
return
for key, value in config.items():
_validate_key(key)
_validate_value(value)
args = ['/sbin/postconf']
if flag:
args.append(flag)
for key, value in config.items():
args.append('{}={}'.format(key, value))
_run(args)
def set_master_config(service: Service):
"""Set daemons and their options in postfix master.cf."""
service_key = service.service + '/' + service.type_
set_config({service_key: str(service)}, '-M')
def parse_maps(raw_value):
"""Parse postfix configuration values that are maps."""
if '{' in raw_value or '}' in raw_value:
raise ValueError('Unsupported map list format')
value_list = []
for segment in raw_value.split(','):
for sub_segment in segment.strip().split(' '):
sub_segment = sub_segment.strip()
if sub_segment:
value_list.append(sub_segment)
return value_list
def _run(args):
"""Run process. Capture and return standard output as a string.
Raise a RuntimeError on non-zero exit codes.
"""
try:
result = subprocess.run(args, check=True, capture_output=True)
return result.stdout.decode()
except subprocess.SubprocessError as subprocess_error:
raise RuntimeError('Subprocess failed') from subprocess_error
except UnicodeDecodeError as unicode_error:
raise RuntimeError('Unicode decoding failed') from unicode_error
def _validate_key(key):
"""Validate postconf key format or raise ValueError."""
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_/]*$', key):
raise ValueError('Invalid postconf key format')
def _validate_value(value):
"""Validate postconf value format or raise ValueError."""
for char in value:
if ord(char) < 32:
raise ValueError('Value contains control characters')