Verified Commit d04ed05a authored by Karel Koci's avatar Karel Koci 🤘
Browse files

General code refactor

This removes some left over Python 2 compatibility code. It also makes
sure that public modules do not "unpack" any modules. That is 'from foo
import fee` is pretty much not used in public modules.
parent 2381b0ec
......@@ -3,9 +3,9 @@ from . import autorun, const
from .utils import check_exclusive_lock as _check_exclusive_lock
from .utils import daemonize as _daemonize
from ._pidlock import pid_locked as _pid_locked
from .exceptions import UpdaterDisabledError
from ._supervisor import run as _run
from .prerun import wait_for_network as _wait_for_network
from .exceptions import UpdaterDisabledError
def opkg_lock() -> bool:
......
"""Helper to identify board we are running on.
"""
import functools
import distro
# These are all board
......@@ -14,13 +17,9 @@ BOARD_MAP = {
"Turris 1.x": "turris1x",
}
__board = None
@functools.lru_cache(maxsize=1)
def board() -> str:
"""Returns board name as expected by updater components of current board host.
"""
global __board
if __board is None:
__board = BOARD_MAP.get(distro.os_release_attr("openwrt_device_product"), "unknown")
return __board
return BOARD_MAP.get(distro.os_release_attr("openwrt_device_product"), "unknown")
"""This implements updater-supervisor pid file lock.
This ensures that only one instance of updater-supervisor is running and that
any other just spawned instance can send signal to this instance.
Signals are used in updater-supervisor for simple comunication between instance
holding lock and any other spawned instance.
This ensures that only one instance of updater-supervisor is running and that any other just spawned instance can send
signal to this instance.
Signals are used in updater-supervisor for simple comunication between instance holding lock and any other spawned
instance.
"""
import os
import fcntl
......@@ -67,6 +67,7 @@ class PidLock():
Note that there should be only once PidLock object used in single process
because it registers signal.
"""
def __init__(self):
self.file = None
self._sigusr_rec = False
......
"""This module is core of udpdater-supervisor. It runs and supervise updater
execution.
"""This module is core of udpdater-supervisor. It runs and supervise updater execution.
"""
from __future__ import print_function
import os
import sys
import subprocess
......@@ -9,17 +7,15 @@ import atexit
import signal
import errno
from threading import Thread, Lock
from . import autorun
from . import approvals
from . import notify
from . import hook
from . import autorun, approvals, notify, hook
from .utils import setup_alarm, report
from .const import PKGUPDATE_CMD, APPROVALS_ASK_FILE, PKGUPDATE_STATE
from ._pidlock import PidLock
class Supervisor:
"pkgupdate supervisor"
"Supervisor itself."
def __init__(self, verbose):
self.verbose = verbose
self.kill_timeout = 0
......
"""Access and control functions of update approvals.
"""
import os
import time
import typing
from . import const, autorun, notify
from .utils import report
from . import const, autorun, notify, utils
from .exceptions import UpdaterApproveInvalidError
......@@ -153,7 +154,7 @@ def _approved():
def _gen_new_stat(new_hash):
"Generate new stat file and send notification."
report('Generating new approval request')
utils.report('Generating new approval request')
# Write to stat file
with open(const.APPROVALS_STAT_FILE, 'w') as file:
file.write(' '.join((new_hash, 'asked', str(int(time.time())))))
......
"""Configuration of updater's automatic execution.
"""
import typing
from euci import EUci, UciExceptionNotFound
import euci
def enabled() -> typing.Optional[bool]:
......@@ -9,10 +11,10 @@ def enabled() -> typing.Optional[bool]:
configuration was set so it is possible to catch no configuration case.
Relevant uci configuration is: updater.autorun.enable
"""
with EUci() as uci:
with euci.EUci() as uci:
try:
return uci.get("updater", "autorun", "enable", dtype=bool)
except UciExceptionNotFound:
except euci.UciExceptionNotFound:
# No option means disabled but instead of False we return None to
# allow to handle no setting situation.
return None
......@@ -22,7 +24,7 @@ def set_enabled(enable: bool):
"""Set value that can be later received with enable function.
It sets uci configuration value: updater.autorun.enable
"""
with EUci() as uci:
with euci.EUci() as uci:
uci.set('updater', 'autorun', 'autorun')
uci.set('updater', 'autorun', 'enable', enable)
......@@ -31,7 +33,7 @@ def approvals() -> bool:
"""Returns True if updater approvals are enabled.
Relevant uci configuration is: updater.autorun.approvals
"""
with EUci() as uci:
with euci.EUci() as uci:
return uci.get("updater", "autorun", "approvals", dtype=bool, default=False)
......@@ -39,7 +41,7 @@ def set_approvals(enabled: bool):
"""Set value that can later be received by enabled function.
This is relevant to uci config: updater.autorun.approvals
"""
with EUci() as uci:
with euci.EUci() as uci:
uci.set('updater', 'autorun', 'autorun')
uci.set('updater', 'autorun', 'approvals', enabled)
......@@ -49,7 +51,7 @@ def auto_approve_time() -> typing.Optional[int]:
approval time is configured then this function returns None.
This is releavant to uci config: updater.autorun.auto_approve_time
"""
with EUci() as uci:
with euci.EUci() as uci:
value = uci.get("updater", "autorun", "auto_approve_time", dtype=int, default=0)
return value if value > 0 else None
......@@ -59,7 +61,7 @@ def set_auto_approve_time(approve_time: typing.Optional[int]):
or value that is less or equal to zero and in that case this feature is
disabled and if approvals are enabled only manual approve can be granted.
"""
with EUci() as uci:
with euci.EUci() as uci:
if approve_time and approve_time > 0:
uci.set('updater', 'autorun', 'autorun')
uci.set('updater', 'autorun', 'auto_approve_time', approve_time)
......
"""This module provides easy way to access mode and target of updates. That is version branch updater follow.
"""
import typing
from euci import EUci
import euci
def get_os_branch_or_version() -> typing.Tuple[str, str]:
"""Get OS branch or version from uci."""
with EUci() as uci:
with euci.EUci() as uci:
mode = uci.get("updater", "turris", "mode", dtype=str, default="branch")
value = uci.get("updater", "turris", mode, dtype=str, default="")
......
......@@ -33,11 +33,10 @@ PING_TIMEOUT = 10
APPROVALS_ASK_FILE = "/usr/share/updater/need_approval"
APPROVALS_STAT_FILE = "/usr/share/updater/approvals"
# Approvals notification message
NOTIFY_MESSAGE_CS = u"Updater žádá o autorizaci akcí. Autorizaci můžete" + \
u" přidělit v administračním rozhraní Foris v záložce 'Updater'."
NOTIFY_MESSAGE_EN = "Your approval is required to apply pending updates." + \
"You can grant it in the Foris administrative interface in the" + \
" 'Updater' menu."
NOTIFY_MESSAGE_CS = "Updater žádá o autorizaci akcí. Autorizaci můžete přidělit v administračním rozhraní Foris " + \
"v záložce 'Updater'."
NOTIFY_MESSAGE_EN = "Your approval is required to apply pending updates. You can grant it in the Foris " + \
"administrative interface in the 'Updater' menu."
# File containing l10n symbols as a list of supported ones
L10N_FILE = "/usr/share/updater/l10n_supported"
......
"""Support for hook commands executed at the end of updater execution. It is in general a way to get notification about
updater execution termination even if we are not the master of that process.
"""
import os
import sys
import fcntl
import errno
import subprocess
import typing
from threading import Thread
from .utils import report
from ._pidlock import pid_locked
from .const import POSTRUN_HOOK_FILE
import threading
from . import utils, const, _pidlock
from .exceptions import UpdaterInvalidHookCommandError
......@@ -17,21 +18,21 @@ def __run_command(command):
line = file.readline()
if not line:
break
report(line.decode(sys.getdefaultencoding()))
utils.report(line.decode(sys.getdefaultencoding()))
report('Running command: ' + command)
utils.report('Running command: ' + command)
process = subprocess.Popen(command, stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
shell=True)
tout = Thread(target=_fthread, args=(process.stdout,))
terr = Thread(target=_fthread, args=(process.stderr,))
tout = threading.Thread(target=_fthread, args=(process.stdout,))
terr = threading.Thread(target=_fthread, args=(process.stderr,))
tout.daemon = True
terr.daemon = True
tout.start()
terr.start()
exit_code = process.wait()
if exit_code != 0:
report('Command failed with exit code: ' + str(exit_code))
utils.report('Command failed with exit code: ' + str(exit_code))
def register(command: str):
......@@ -47,12 +48,12 @@ def register(command: str):
raise UpdaterInvalidHookCommandError(
"Argument register can be only single line string.")
# Open file for writing and take exclusive lock
file = os.open(POSTRUN_HOOK_FILE, os.O_WRONLY | os.O_CREAT | os.O_APPEND)
file = os.open(const.POSTRUN_HOOK_FILE, os.O_WRONLY | os.O_CREAT | os.O_APPEND)
fcntl.lockf(file, fcntl.LOCK_EX)
# Check if we are working with existing file
invalid = False
try:
if os.fstat(file).st_ino != os.stat(POSTRUN_HOOK_FILE).st_ino:
if os.fstat(file).st_ino != os.stat(const.POSTRUN_HOOK_FILE).st_ino:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
......@@ -62,7 +63,7 @@ def register(command: str):
os.close(file)
register(command)
return
if not pid_locked(): # Check if updater is running
if not _pidlock.pid_locked(): # Check if updater is running
os.close(file)
# If there is no running instance then just run given command
__run_command(command)
......@@ -72,7 +73,7 @@ def register(command: str):
# it seems that way)
with os.fdopen(file, 'w') as fhook:
fhook.write(command + '\n')
report('Postrun hook registered: ' + command)
utils.report('Postrun hook registered: ' + command)
def register_list(commands: typing.Iterable[str]):
......@@ -89,7 +90,7 @@ def _run():
"""
# Open file for reading and take exclusive lock
try:
file = os.open(POSTRUN_HOOK_FILE, os.O_RDWR)
file = os.open(const.POSTRUN_HOOK_FILE, os.O_RDWR)
except OSError as excp:
if excp.errno == errno.ENOENT:
return # No file means nothing to do
......@@ -101,4 +102,4 @@ def _run():
with os.fdopen(file, 'r') as fhook:
for line in fhook.readlines():
__run_command(line)
os.remove(POSTRUN_HOOK_FILE)
os.remove(const.POSTRUN_HOOK_FILE)
"""Language support functions.
"""
import os
import typing
from euci import EUci
from .const import L10N_FILE
import euci
from . import const
from .exceptions import UpdaterNoSuchLangError
......@@ -10,14 +12,14 @@ def languages() -> typing.Dict[str, bool]:
"""
result = dict()
if os.path.isfile(L10N_FILE): # Just to be sure
with open(L10N_FILE, 'r') as file:
if os.path.isfile(const.L10N_FILE): # Just to be sure
with open(const.L10N_FILE, 'r') as file:
for line in file.readlines():
if not line.strip():
continue # ignore empty lines
result[line.strip()] = False
with EUci() as uci:
with euci.EUci() as uci:
l10n_enabled = uci.get("updater", "l10n", "langs", list=True, default=[])
for lang in l10n_enabled:
result[lang] = True
......@@ -34,8 +36,8 @@ def update_languages(langs: typing.Iterable[str]):
"""
# Verify langs
expected = set()
if os.path.isfile(L10N_FILE): # Just to be sure
with open(L10N_FILE, 'r') as file:
if os.path.isfile(const.L10N_FILE): # Just to be sure
with open(const.L10N_FILE, 'r') as file:
for line in file.readlines():
expected.add(line.strip())
for lang in langs:
......@@ -44,6 +46,6 @@ def update_languages(langs: typing.Iterable[str]):
"Can't enable unsupported language code:" + str(lang))
# Set
with EUci() as uci:
with euci.EUci() as uci:
uci.set('updater', 'l10n', 'l10n')
uci.set('updater', 'l10n', 'langs', langs)
"""Package lists control functions.
"""
import os
import json
import gettext
import typing
from euci import EUci
from .const import PKGLISTS_FILE, PKGLISTS_LABELS_FILE
import euci
from . import const, _board
from .exceptions import UpdaterNoSuchListError, UpdaterNoSuchListOptionError
from . import _board
PkgListLabel = typing.Dict[str, str]
PkgListOption = typing.Dict[str, typing.Union[bool, str, None, PkgListLabel]]
......@@ -104,10 +105,10 @@ def pkglists(lang=None) -> typing.Dict[str, PkgListEntry]:
'pkglists',
languages=[lang] if lang is not None else None,
fallback=True)
known_lists = _load_json_dict(PKGLISTS_FILE)
known_labels = _load_json_dict(PKGLISTS_LABELS_FILE)
known_lists = _load_json_dict(const.PKGLISTS_FILE)
known_labels = _load_json_dict(const.PKGLISTS_LABELS_FILE)
with EUci() as uci:
with euci.EUci() as uci:
enabled_lists = uci.get('pkglists', 'pkglists', 'pkglist', list=True, default=[])
return {
name: {
......@@ -128,7 +129,7 @@ def update_pkglists(lists: typing.Dict[str, typing.Dict[str, bool]]):
options.
Anything omitted will be disabled.
"""
known_lists = _load_json_dict(PKGLISTS_FILE)
known_lists = _load_json_dict(const.PKGLISTS_FILE)
for name, options in lists.items():
if name not in known_lists:
......@@ -136,7 +137,7 @@ def update_pkglists(lists: typing.Dict[str, typing.Dict[str, bool]]):
for opt in options:
if opt not in known_lists[name]['options']:
raise UpdaterNoSuchListOptionError("Can't enable unknown package list option: {}: {}".format(name, opt))
with EUci() as uci:
with euci.EUci() as uci:
uci.set('pkglists', 'pkglists', 'pkglist', list(lists.keys()))
for name, options in lists.items():
uci.delete('pkglists', name)
......
"""Functions generating notifications about various events. These are notifications send to user using notification
system.
"""
import os
import sys
import subprocess
from .utils import report
from .const import PKGUPDATE_LOG, NOTIFY_MESSAGE_CS, NOTIFY_MESSAGE_EN
from .const import PKGUPDATE_ERROR_LOG, PKGUPDATE_CRASH_LOG
if sys.version_info < (3, 0):
import approvals
else:
from . import approvals
from . import utils, const, approvals
def clear_logs():
"""Remove files updater dumps when it detects failure.
"""
if os.path.isfile(PKGUPDATE_ERROR_LOG):
os.remove(PKGUPDATE_ERROR_LOG)
if os.path.isfile(PKGUPDATE_CRASH_LOG):
os.remove(PKGUPDATE_CRASH_LOG)
if os.path.isfile(const.PKGUPDATE_ERROR_LOG):
os.remove(const.PKGUPDATE_ERROR_LOG)
if os.path.isfile(const.PKGUPDATE_CRASH_LOG):
os.remove(const.PKGUPDATE_CRASH_LOG)
def failure(exit_code: int, trace: str):
"""Send notification about updater's failure
"""
if exit_code == 0 and not os.path.isfile(PKGUPDATE_ERROR_LOG):
if exit_code == 0 and not os.path.isfile(const.PKGUPDATE_ERROR_LOG):
return
msg_cs = "Updater selhal: "
msg_en = "Updater failed: "
if os.path.isfile(PKGUPDATE_ERROR_LOG):
with open(PKGUPDATE_ERROR_LOG, 'r') as file:
if os.path.isfile(const.PKGUPDATE_ERROR_LOG):
with open(const.PKGUPDATE_ERROR_LOG, 'r') as file:
content = '\n'.join(file.readlines())
msg_en += content
msg_cs += content
elif os.path.isfile(PKGUPDATE_CRASH_LOG):
with open(PKGUPDATE_CRASH_LOG, 'r') as file:
elif os.path.isfile(const.PKGUPDATE_CRASH_LOG):
with open(const.PKGUPDATE_CRASH_LOG, 'r') as file:
content = '\n'.join(file.readlines())
msg_en += content
msg_cs += content
......@@ -47,7 +44,7 @@ def failure(exit_code: int, trace: str):
if subprocess.call(['create_notification', '-s', 'error',
msg_cs, msg_en]) != 0:
report('Notification creation failed.')
utils.report('Notification creation failed.')
clear_logs()
......@@ -55,12 +52,12 @@ def failure(exit_code: int, trace: str):
def changes():
"""Send notification about changes.
"""
if not os.path.isfile(PKGUPDATE_LOG):
if not os.path.isfile(const.PKGUPDATE_LOG):
return
text_en = ""
text_cs = ""
with open(PKGUPDATE_LOG, 'r') as file:
with open(const.PKGUPDATE_LOG, 'r') as file:
for line in file.readlines():
pkg = line.split(' ')
if pkg[0].strip() == 'I':
......@@ -75,16 +72,16 @@ def changes():
# Ignore package downloads
pass
else:
report("Unknown log entry: " + line.strip())
utils.report("Unknown log entry: " + line.strip())
if text_en and text_cs:
if subprocess.call(['create_notification', '-s', 'update',
text_cs.encode(sys.getdefaultencoding()),
text_en.encode(sys.getdefaultencoding())
]) != 0:
report('Notification creation failed.')
utils.report('Notification creation failed.')
os.remove(PKGUPDATE_LOG)
os.remove(const.PKGUPDATE_LOG)
def approval():
......@@ -93,17 +90,17 @@ def approval():
apprv = approvals.current()
text = ""
for pkg in apprv['plan']:
text += u"\n • {0} {1} {2}".format(
text += "\n • {0} {1} {2}".format(
pkg['op'].title(), pkg['name'],
"" if pkg['new_ver'] is None else pkg['new_ver'])
if subprocess.call(['create_notification', '-s', 'update',
NOTIFY_MESSAGE_CS + text, NOTIFY_MESSAGE_EN + text]) \
const.NOTIFY_MESSAGE_CS + text, const.NOTIFY_MESSAGE_EN + text]) \
!= 0:
report('Notification creation failed.')
utils.report('Notification creation failed.')
def notifier():
"""This just calls notifier. It processes new notification and sends them together.
"""
if subprocess.call(['notifier']) != 0:
report('Notifier failed')
utils.report('Notifier failed')
......@@ -3,24 +3,25 @@ updater-supervisor to be suspended for random amount of time or it allows it to
wait for internet connection
"""
import os
import subprocess
import time
from random import randrange
from multiprocessing import Process
from .const import PING_ADDRESS
from .utils import report
import random
import typing
import subprocess
import multiprocessing
from . import const, utils
def random_sleep(max_seconds: int):
"Sleep random amount of seconds with maximum of max_seconds"
if max_seconds is None or max_seconds <= 0:
return # No sleep at all
suspend = randrange(max_seconds)
suspend = random.randrange(max_seconds)
if suspend > 0: # Just nice to have no print if we wait for 0 seconds
report("Suspending updater start for " + str(suspend) + " seconds")
utils.report("Suspending updater start for " + str(suspend) + " seconds")
time.sleep(suspend)
def ping(address: str = PING_ADDRESS, count: int = 1, deadline: int = 1) -> bool:
def ping(address: str = const.PING_ADDRESS, count: int = 1, deadline: int = 1) -> bool:
"""Ping address with given amount of pings and deadline.
Returns True on success and False if ping fails.
"""
......@@ -32,7 +33,8 @@ def ping(address: str = PING_ADDRESS, count: int = 1, deadline: int = 1) -> bool
stderr=devnull
) == 0
def wait_for_network(max_stall: int) -> bool:
def wait_for_network(max_stall: int) -> typing.Optional[bool]:
"""This tries to connect to repo.turris.cz to check if we can access it and
otherwise it stalls execution for given maximum number of seconds.
......@@ -47,8 +49,8 @@ def wait_for_network(max_stall: int) -> bool:
pass
if max_stall is None:
return # None means no stall
process = Process(target=network_test)
return None # None means no stall
process = multiprocessing.Process(target=network_test)
process.start()
process.join(max_stall)
if process.is_alive():
......
"""Various utility functions used in more than one other updater-supervisor
module.
"""
from __future__ import print_function
import os
import sys
import fcntl
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment