Verified Commit 91d4fd18 authored by Karel Koci's avatar Karel Koci 🤘

supervisor: update to allow autorun configuration

This adds possibility to completely configure updater (autorun) trough
supervisor. Advantage of this is that supervisor is the only place that
defines uci configuration for updater (at least if we are talking about
autorun) and because of that same API can be used on top of generally
different configuration. This is handy because of back compatibility
with Turris OS 3.x and introduced cleanup in Turris OS 4.x.
parent 3bc6a0c2
......@@ -22,12 +22,10 @@
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from .const import OPKG_LOCK, PING_TIMEOUT, PKGUPDATE_TIMEOUT
from .const import PKGUPDATE_TIMEOUT_KILL
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 .config import Config
from .exceptions import ExceptionUpdaterDisabled
from ._supervisor import run as _run
from .prerun import wait_for_network as _wait_for_network
......@@ -37,7 +35,7 @@ def opkg_lock():
"""Returns True if opkg lock is taken. It can be taken by any other
process. It doesn't have to be updater.
"""
return _check_exclusive_lock(OPKG_LOCK, False)
return _check_exclusive_lock(const.OPKG_LOCK, False)
def updater_supervised():
......@@ -48,8 +46,8 @@ def updater_supervised():
return _pid_locked()
def run(wait_for_network=False, ensure_run=False, timeout=PKGUPDATE_TIMEOUT,
timeout_kill=PKGUPDATE_TIMEOUT_KILL, hooklist=None):
def run(wait_for_network=False, ensure_run=False, timeout=const.PKGUPDATE_TIMEOUT,
timeout_kill=const.PKGUPDATE_TIMEOUT_KILL, hooklist=None):
"""Run updater.
This call will spawn daemon process and returns. But be aware that at first
it checks if some other supervisor is not running and it takes file lock
......@@ -60,17 +58,16 @@ def run(wait_for_network=False, ensure_run=False, timeout=PKGUPDATE_TIMEOUT,
exits.
You can pass hooks (single line shell scripts) to be run after updater.
"""
with Config() as cnf:
if cnf.disable():
raise ExceptionUpdaterDisabled(
"Can't run. Updater is configured to be disabled.")
if not autorun.enabled():
raise ExceptionUpdaterDisabled(
"Can't run. Updater is configured to be disabled.")
# Fork to daemon
if _daemonize():
return
# Wait for network if configured
if wait_for_network:
if type(wait_for_network == bool):
wait_for_network = PING_TIMEOUT
wait_for_network = const.PING_TIMEOUT
_wait_for_network(wait_for_network)
# And run updater
_run(
......
......@@ -24,11 +24,14 @@
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import time
from .config import Config
from .const import APPROVALS_ASK_FILE, APPROVALS_STAT_FILE
from uci import Uci, UciExceptionNotFound
from . import const, autorun, notify
from .utils import report
from .exceptions import ExceptionUpdaterApproveInvalid
from . import notify
# TODO do we want to have list of packages that are auto approved?
# This would be beneficial for packages such as base-files that are updated
# very often but do not change at all.
def current():
......@@ -63,22 +66,21 @@ def current():
update. Note that this is forced immediate reboot.
"""
# Both files have to exists otherwise it is invalid approval request
with Config() as cnf:
if not os.path.isfile(APPROVALS_ASK_FILE) or \
not os.path.isfile(APPROVALS_STAT_FILE) or \
not cnf.approvals_need():
return None
if not os.path.isfile(const.APPROVALS_ASK_FILE) or \
not os.path.isfile(const.APPROVALS_STAT_FILE) or \
not autorun.approvals():
return None
result = dict()
result['reboot'] = False
with open(APPROVALS_STAT_FILE, 'r') as file:
with open(const.APPROVALS_STAT_FILE, 'r') as file:
cols = file.readline().split(' ')
result['hash'] = cols[0].strip()
result['status'] = cols[1].strip()
result['time'] = int(cols[2].strip())
with open(APPROVALS_ASK_FILE, 'r') as file:
with open(const.APPROVALS_ASK_FILE, 'r') as file:
# First line contains hash. We have has from stat file so just compare
if file.readline().strip() != result['hash']:
return None # Invalid request
......@@ -104,16 +106,15 @@ def current():
def _set_stat(status, hsh, allowed):
"Set given status to APPROVALS_STAT_FILE if hsh matches current hash"
# Both files have to exists otherwise it is invalid approval request
with Config() as cnf:
if not os.path.isfile(APPROVALS_ASK_FILE) or \
not os.path.isfile(APPROVALS_STAT_FILE) or \
not cnf.approvals_need():
return
if not os.path.isfile(const.APPROVALS_ASK_FILE) or \
not os.path.isfile(const.APPROVALS_STAT_FILE) or \
not autorun.approvals():
return
# TODO locks (we should lock stat file before doing this)
# Read current stat
cols = list()
with open(APPROVALS_STAT_FILE, 'r') as file:
with open(const.APPROVALS_STAT_FILE, 'r') as file:
cols.extend(file.readline().split(' '))
if hsh is not None and cols[0].strip() != hsh:
......@@ -125,7 +126,7 @@ def _set_stat(status, hsh, allowed):
# Write new stat
cols[1] = status
with open(APPROVALS_STAT_FILE, 'w') as file:
with open(const.APPROVALS_STAT_FILE, 'w') as file:
file.write(' '.join(cols))
......@@ -150,27 +151,24 @@ def _approved():
"""This returns hash of approved plan. If there is no approved plan then it
returns None.
"""
with Config() as cnf:
if not os.path.isfile(APPROVALS_ASK_FILE) or \
not os.path.isfile(APPROVALS_STAT_FILE) or \
not cnf.approvals_need():
return None
with open(APPROVALS_STAT_FILE, 'r') as file:
cols = file.readline().split(' ')
auto_grant = cnf.approvals_auto_grant_seconds()
if cols[1].strip() == 'granted' or \
(auto_grant is not None and
int(cols[2]) < (time.time() - auto_grant)):
return cols[0]
return None
if not os.path.isfile(const.APPROVALS_ASK_FILE) or \
not os.path.isfile(const.APPROVALS_STAT_FILE) or \
not autorun.approvals():
return None
with open(const.APPROVALS_STAT_FILE, 'r') as file:
cols = file.readline().split(' ')
auto_grant_time = autorun.auto_approve_time()
if cols[1].strip() == 'granted' or (auto_grant_time is not None and int(cols[2]) < (time.time() - (auto_grant_time * 3600))):
return cols[0]
return None
def _gen_new_stat(new_hash):
"Generate new stat file and send notification."
report('Generating new approval request')
# Write to stat file
with open(APPROVALS_STAT_FILE, 'w') as file:
with open(const.APPROVALS_STAT_FILE, 'w') as file:
file.write(' '.join((new_hash, 'asked', str(int(time.time())))))
# Send notification
notify.approval()
......@@ -183,25 +181,23 @@ def _update_stat():
When new plan is presented then it also sends notification to user about
it.
"""
with Config() as cnf:
if not os.path.isfile(APPROVALS_ASK_FILE) or \
not cnf.approvals_need():
# Drop any existing stat file
if os.path.isfile(APPROVALS_STAT_FILE):
os.remove(APPROVALS_STAT_FILE)
return
if not os.path.isfile(const.APPROVALS_ASK_FILE) or not autorun.approvals():
# Drop any existing stat file
if os.path.isfile(const.APPROVALS_STAT_FILE):
os.remove(const.APPROVALS_STAT_FILE)
return
new_hash = ''
with open(APPROVALS_ASK_FILE, 'r') as file:
with open(const.APPROVALS_ASK_FILE, 'r') as file:
new_hash = file.readline().strip()
if not os.path.isfile(APPROVALS_STAT_FILE):
if not os.path.isfile(const.APPROVALS_STAT_FILE):
# No previous stat file so just generate it
_gen_new_stat(new_hash)
return
# For existing stat file compare hashes and if they differ then generate
with open(APPROVALS_STAT_FILE, 'r') as file:
with open(const.APPROVALS_STAT_FILE, 'r') as file:
cols = file.readline().split(' ')
if cols[0].strip() != new_hash:
_gen_new_stat(new_hash)
......@@ -24,60 +24,71 @@
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from uci import Uci, UciExceptionNotFound
def enabled():
"""Returns True if updater can be automatically started by various system
utils. This includes automatic periodic execution, after-boot recovery and
other tools call to configuration aplication.
Relevant uci configuration is: updater.autorun.enable
"""
with Uci() as uci:
try:
# TODO use EUci instead of this retype (as this is not perfect)
return bool(int(uci.get("updater", "autorun", "enabled")))
except UciExceptionNotFound:
return False # No option means disabled
class Config:
"Updater's configuration wrapper."
def __init__(self):
self.uci = None
def __enter__(self):
self.uci = Uci()
return self
def set_enabled(enabled):
"""Set value that can be later received with enabled function.
It sets uci configuration value: updater.autorun.enable
"""
with Uci() as uci:
uci.set('updater', 'autorun', 'autorun')
uci.set('updater', 'autorun', 'enabled', int(bool(enabled)))
def __exit__(self, *args):
del self.uci
self.uci = None
def _get(self, package, section, option, req_type):
"Internal uci_get function"
if self.uci is None:
return None
def approvals():
"""Returns True if updater approvals are enabled.
Relevant uci configuration is: updater.autorun.approvals
"""
with Uci() as uci:
try:
value = self.uci.get(package, section, option)
# TODO use EUci instead of this retype (as this is not perfect)
return bool(int(uci.get("updater", "autorun", "approvals")))
except UciExceptionNotFound:
return None
# TODO use EUci instead of this retype (as this is not perfect)
if req_type == bool:
return bool(int(value))
return req_type(value)
return False # No option means disabled
def disable(self):
"""Returns True if updater is set to be disabled.
This is config: updater.override.disable
"""
return self._get("updater", "override", "disable", bool)
def branch(self):
"""Return name of configured branch. But on top of that if nothing is
configured then it returns deploy instead of empty string.
This is config: updater.override.branch
"""
branch = self._get("updater", "override", "branch", str)
if not branch:
branch = "deploy"
return branch
def set_approvals(enabled):
"""Set value that can later be received by enabled function.
This is relevant to uci config: updater.autorun.approvals
"""
with Uci() as uci:
uci.set('updater', 'autorun', 'approvals')
uci.set('updater', 'autorun', 'approvals', int(bool(enabled)))
def approvals_need(self):
"""Returns True if updater.approvals.need is set.
"""
return self._get("updater", "approvals", "need", bool)
def approvals_auto_grant_seconds(self):
"""Returns number of seconds after which should be approval
automatically granted. In configuration it's a
updater.approvals.auto_grant_seconds option. It returns None if
approvals are disabled or if such option is not set.
"""
if not self.approvals_need():
def auto_approve_time():
"""Returns number of hours before automatic approval is granted. If no
approval time is configured then this function returns None.
This is releavant to uci config: updater.autorun.auto_approve_time
"""
with Uci() as uci:
try:
value = int(uci.get("updater", "autorun", "auto_approve_time"))
return value if value > 0 else None
except UciExceptionNotFound:
return None
return self._get("updater", "approvals", "auto_grant_seconds", int)
def set_auto_approve_time(approve_time):
"""Sets time in hours after which approval is granted. You can provide None
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 Uci() as uci:
if approve_time > 0:
uci.set('updater', 'autorun', 'autorun')
uci.set('updater', 'autorun', 'auto_approve_time', int(approve_time))
else:
uci.delete('updater', 'autorun', 'auto_approve_time')
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