...
 
Commits (3)
This diff is collapsed.
--[[
This file is part of updater-ng-localrepo. Don't edit it.
]]
-- Add local repositories (might be missing if not in use)
script_path = root_dir .. "usr/share/updater/localrepo/localrepo.lua"
if stat(script_path) then
Script("file://" .. script_path)
end
#!/bin/sh
# Copyright (c) 2016-2017, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF 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.
# Automatic updater configuration file
UPDATER_CONFIG='/etc/updater/conf.d/opkg-auto.lua'
# Path to opkg
OPKG='/bin/opkg-cl'
ARGS=""
EXC=0
OPERATION="n"
while [ "$#" -gt 0 ]; do
case "$1" in
install)
OPERATION="i"
break
;;
remove)
OPERATION="r"
break
;;
upgrade)
echo "You should not be using opkg upgrade as it is known to break stuff. You have been warned." >&2
echo "You should be using pkgupdate instead!" >&2
echo "If you know what you are doing then type YES and continue.." >&2
read -r iknow
[ "$iknow" = "YES" ] || exit
ARGS="$ARGS upgrade"
break
;;
*)
ARGS="$ARGS $1"
;;
esac
shift
done
shift
if [ "$OPERATION" = "n" ]; then
# Not install or remove operation
"$OPKG" $ARGS "$@"
exit $?
fi
PACKAGES=""
while [ "$#" -gt 0 ]; do
# ignore options
if echo "$1" | grep -q '^-'; then
ARGS="$ARGS $1"
else
PACKAGES="$PACKAGES
$1"
fi
shift
done
# delete first empty line
PACKAGES="$( echo "$PACKAGES" | sed '1d' )"
# As first takes name of package to be added to config file.
config_add_pkg() {
echo "Install(\"$1\")" >>"$UPDATER_CONFIG"
}
# Takes single argument, name of package to be removed from config.
config_clean_pkg() {
sed -e "/^Install\((\| \)\"$1\"/d" -i "$UPDATER_CONFIG"
}
# Checks if package is specified in auto.lua config
config_pkg_in() {
grep -qe "^Install\((\| \)\"$1\"" "$UPDATER_CONFIG"
}
# Checks if given package is installed
pkg_installed() {
[ -n "$("$OPKG" $ARGS list-installed "$1")" ]
}
install() {
local PKG="$1"
local OPKG_PKG="$PKG"
if echo "$PKG" | grep -q -E '.*(\.ipk|\.deb)$'; then
# Source package name from package it self
local PKGCONTENT=$PKG
local TMPDIR1=`mktemp -d /tmp/updater-XXXXXX`
tar -xzf "$PKGCONTENT" -C "$TMPDIR1"
local TMPDIR2=`mktemp -d /tmp/updater-XXXXXX`
tar -xzf "$TMPDIR1/control.tar.gz" -C "$TMPDIR2"
PKG=`sed -ne 's/^Package:[ ]*\([^ ]*\).*/\1/p' "$TMPDIR2/control"`
rm -rf "$TMPDIR1" "$TMPDIR2"
fi
pkg_installed "$PKG"
local WASINSTALLED=$?
if ! "$OPKG" $ARGS install "$OPKG_PKG"; then
# opkg failed, probably some problem with package. Lets not change anything
EXC=255
return
fi
if [ -n "$PKGCONTENT" ]; then # We have local file (*.ipk or *.deb)
# Add package file to local repository
localrepo add --repo auto "$PKGCONTENT"
fi
# Add package to config file but only if we really installed it
if pkg_installed "$PKG" && [ $WASINSTALLED -ne 0 ]; then
config_clean_pkg "$PKG"
config_add_pkg "$PKG"
fi
}
remove() {
pkg_installed "$1"
local WASINSTALLED=$?
if ! "$OPKG" $ARGS remove "$1"; then
# opkg failed. But continue anyway
EXC=255
fi
if ! pkg_installed "$1"; then
if ! config_pkg_in "$1" && [ $WASINSTALLED -eq 0 ]; then
# We show this message only if package was really removed
echo "WARNING: You probably just removed a package that was installed as part of a user list or the basic system. This package will return durring the next updater run. We suggest you disable the user list instead." >&2
fi
config_clean_pkg "$1"
localrepo clean --repo auto
fi
}
# Check if $UPDATER_CONFIG exists. Otherwise create.
if [ ! -f "$UPDATER_CONFIG" ]; then
echo "-- This is automatically generated file managed by opkg wrapper script. Please don't edit!" > "$UPDATER_CONFIG"
fi
echo "$PACKAGES" | while read P; do
case "$OPERATION" in
i) # Install
install "$P"
;;
r) # Remove
remove "$P"
;;
esac
done
exit $EXC
--[[
This file is part of updater-ng-opkg. Don't edit it.
]]
-- Repositories configured in opkg configuration.
-- We read only customfeeds.conf as that should be only file where user should add additional repositories to
local custom_feed = io.open(root_dir .. "etc/opkg/customfeeds.conf")
if custom_feed then
-- Prepare list of custom keys added to opkg
local pubkeys = {}
for f in pairs(ls(root_dir .. "etc/opkg/keys")) do
table.insert(pubkeys, "file://" .. root_dir .. "etc/opkg/keys/" .. f)
end
-- Read ignore expressions
local ignore_regs = {}
for f in pairs(ls(root_dir .. "etc/updater/opkg-ignore")) do
local ignore_f = io.open(root_dir .. "etc/updater/opkg-ignore/" .. f)
for line in ignore_f:lines() do
if not line:match('^#') then
ignore_regs[line] = true
end
end
end
-- Read opkg feeds and register them to updater
for line in custom_feed:lines() do
if line:match('^%s*src/gz ') then
local not_ignored = true
for reg in pairs(ignore_regs) do
if line:match(reg) then
not_ignored = false
break
end
end
if not_ignored then
local name, feed_uri = line:match('src/gz[%s]+([^%s]+)[%s]+([^%s]+)')
if name and feed_uri then
DBG("Adding custom opkg feed " .. name .. " (" .. feed_uri .. ")")
Repository(name, feed_uri, {pubkey = pubkeys, ignore = {"missing"}})
else
WARN("Malformed line in customfeeds.conf:\n" .. line)
end
else
DBG("Line from customfeeds.conf ignored:\n" .. line)
end
end
end
custom_feed:close()
else
ERROR("No " .. root_dir .. "etc/opkg/customfeeds.conf file. No opkg feeds are included.")
end
MAILTO=""
0 0-23/4 * * * root /usr/bin/updater-supervisor -d --rand-sleep
#!/bin/sh
# Updater it self is not aware of approvals. Those are completely handled in
# updater-supervisor. But when updater does something to system it's most probable
# that it invalidated current approval request. So this script just removes it
# every time update proceeds.
rm -f /usr/share/updater/approvals
#!/bin/sh /etc/rc.common
# This script handles updater-ng immediate reboot recovery. After an immediate
# reboot there is going to be a journal and we should continue execution from it
# as soon as possible.
START=85
status() {
return 0
}
start() {
# Recover updater's journal if it exists
if [ -e "/usr/share/updater/journal" ]; then
# Note: supervisor runs pkgupdate that recovers run from journal and later
# checks for update. This update is required because there could have been
# replan planned after reboot and this ensures that we do replan as soon
# as possible.
updater-supervisor
fi
}
stop() {
:
}
restart() {
:
}
reload() {
:
}
#!/usr/bin/env python3
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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 setuptools import setup
setup(
name='svupdater',
version='0.1',
description="Supervising application and library for updater-ng.",
url="https://gitlab.labs.nic.cz/turris/updater",
author="CZ.NIC, z. s. p. o.",
author_email="karel.koci@nic.cz",
license="MIT",
packages=['svupdater']
)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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 . 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 ExceptionUpdaterDisabled
from ._supervisor import run as _run
from .prerun import wait_for_network as _wait_for_network
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(const.OPKG_LOCK, False)
def updater_supervised():
"""This returns True if there is running updater-supervisor instance.
(Running means as a running process not as a library in some other process)
"""
# This is in reality a wrapper on top of pidlock
return _pid_locked()
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
because of that. If someone messed up that lock then it won't return
immediately. Calling this with timeout is advised for time sensitive
applications.
If there is already running daemon then it just sends signal to it and
exits.
You can pass hooks (single line shell scripts) to be run after updater.
"""
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 = const.PING_TIMEOUT
_wait_for_network(wait_for_network)
# And run updater
_run(
ensure_run=ensure_run,
timeout=timeout,
timeout_kill=timeout_kill,
verbose=False,
hooklist=hooklist)
exit()
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
"""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.
"""
import os
import fcntl
import errno
import signal
from .const import PID_FILE_PATH
from .utils import report, check_exclusive_lock
from .exceptions import ExceptionUpdaterPidLockFailure
def pid_locked():
"""Check if someone holds pid lock. It won't check if process holding the
lock is alive. But it can potentially also catch such situation as in that
case we would manage to get exclusive lock. But we can't ensure that
because some other instance can have shared lock at the same time because
it wants to read content.
"""
return check_exclusive_lock(PID_FILE_PATH, True)
def pid_lock_content():
"""Get content of our pid lock. That is, it returns pid (as an integer).
If there is no lock or its content is invalid then it returns None.
"""
file = None
try:
file = os.open(PID_FILE_PATH, os.O_RDONLY)
except IOError as excp:
# There is no such file
if excp.errno == errno.EACCES:
return None
raise
# TODO timeout
fcntl.flock(file, fcntl.LOCK_SH) # Lock for shared read
# Check if we are reading existing file (if it wasn't unlinked)
invalid = False
try:
if os.fstat(file).st_ino != os.stat(PID_FILE_PATH).st_ino:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
invalid = True
raise
if invalid: # Otherwise try again
os.close(file)
return pid_lock_content()
val = None
with os.fdopen(file, 'r') as filed:
try:
val = int(filed.readline())
except ValueError:
pass # Failed to convert for us means that pid is invalid (None)
# Note: file is closed when we leave fdopen closure
return val
class PidLock():
"""Supervisor pid file to ensure that only one instance is running and that
that specific instance can receive SIGUSR1 to inform it that it should run
pkgupdate once again. This functionality is exported using sigusr property.
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
signal.signal(signal.SIGUSR1, self._sigusr1)
def __del__(self):
if self.file is not None:
self.free()
def _sigusr1(self, *_):
self._sigusr_rec = True
@property
def sigusr1(self):
"If SIGUSR1 was receiver. Reading this sets it back to False"
val = self._sigusr_rec
self._sigusr_rec = False
return val
def _take(self, overtake):
"Take lock if possible"
flags = os.O_WRONLY | os.O_SYNC | os.O_CREAT | \
(os.O_EXCL if not overtake else 0)
while True:
try:
self.file = os.open(PID_FILE_PATH, flags)
fcntl.flock(self.file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as excp:
# File exists or lock couldn't been acquired
if excp.errno == errno.EEXIST or \
excp.errno == errno.EWOULDBLOCK:
return False
raise
# There is possible race condition when file is removed before we
# lock it. This ensures that we have file that is on FS
invalid = False
try:
if os.fstat(self.file).st_ino == os.stat(PID_FILE_PATH).st_ino:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
invalid = True
raise
if invalid:
os.ftruncate(self.file, 0)
os.write(self.file, str.encode(str(os.getpid())))
os.fsync(self.file)
return True
# File was removed before we were able to acquire lock. Try again.
os.close(self.file)
def acquire(self, send_signal):
"""Try to take supervisor pid lock. Returns boolean signaling if lock
was taken successfully.
"""
if self._take(False):
return True # We have lock so return
pid = pid_lock_content()
if pid is None:
report("Taking lock for PID file failed but no pid loaded. Trying to lock pid again.")
if self._take(True):
return True
pid = pid_lock_content() # Second attempt
if pid is None:
report("Second attempt failed too. Giving up.")
return False
# Here we have loaded pid
sig = signal.SIGUSR1 if send_signal else 0
try:
os.kill(pid, sig)
except OSError as excp:
if excp.errno != errno.ESRCH:
raise
# It doesn't runs
report("There is no running process with stored pid. Overtaking it.")
if self._take(True):
return True
report("Pid file overtake failed. Giving up.")
return False
# Signal sent successfully
if send_signal:
report("Another instance is already running. It was notified to run pkgupdate again.")
else:
report("Another instance of supervisor is already running.")
return False
def free(self):
"""Free pid lock if we have it at the moment
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't free not taken pidlock")
file = self.file
self.file = None
# TODO timeout
fcntl.flock(file, fcntl.LOCK_EX)
os.remove(PID_FILE_PATH)
os.close(file)
file = None
def block(self):
"""Block read access to pid lock.
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't block not taken pidlock")
# TODO timeout
fcntl.flock(self.file, fcntl.LOCK_EX)
def unblock(self):
"""Unblock previously blocked read access. Note that in default when
lock is acquired it is blocking read access.
"""
if self.file is None:
raise ExceptionUpdaterPidLockFailure(
"Can't block not taken pidlock")
# TODO timeout
fcntl.flock(self.file, fcntl.LOCK_SH)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
"""This module is core of udpdater-supervisor. It runs and supervise updater
execution.
"""
from __future__ import print_function
import os
import sys
import subprocess
import atexit
import signal
import errno
from threading import Thread, Lock
from . import autorun
from . import approvals
from . import notify
from . import 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"
def __init__(self, verbose):
self.verbose = verbose
self.kill_timeout = 0
self.process = None
self.trace = None
self.trace_lock = Lock()
self._devnull = open(os.devnull, 'w')
self._stdout_thread = Thread(
target=self._stdout,
name="pkgupdate-stdout")
self._stderr_thread = Thread(
target=self._stderr,
name="pkgupdate-stderr")
atexit.register(self._at_exit)
def run(self):
"Run pkgupdate"
if self.process is not None:
raise Exception("Only one call to Supervisor.run is allowed.")
self.trace = ""
# Create state directory
try:
os.mkdir(PKGUPDATE_STATE)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
# Prepare command to be run
cmd = list(PKGUPDATE_CMD)
if autorun.approvals():
cmd.append('--ask-approval=' + APPROVALS_ASK_FILE)
approved = approvals._approved()
if approved is not None:
cmd.append('--approve=' + approved)
# Clear old dump files
notify.clear_logs()
# Open process
self.process = subprocess.Popen(
cmd,
stdin=self._devnull,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
self._stdout_thread.start()
self._stderr_thread.start()
def join(self, timeout, killtimeout):
"Join pkgupdate execution and return exit code."
self.kill_timeout = killtimeout
# Wait for pkgupdate to exit (with timeout)
setup_alarm(self._timeout, timeout)
exit_code = self.process.wait()
signal.alarm(0)
# Wait untill we process all output
self._stdout_thread.join()
self._stderr_thread.join()
# Dump process
self.process = None
# Return exit code
return exit_code
def _stdout(self):
while True:
line = self.process.stdout.readline().decode(sys.getdefaultencoding())
self.trace_lock.acquire()
self.trace += line
self.trace_lock.release()
if not line:
break
if self.verbose:
print(line.decode(sys.getdefaultencoding()), end='')
sys.stdout.flush()
def _stderr(self):
while True:
line = self.process.stderr.readline().decode(sys.getdefaultencoding())
self.trace_lock.acquire()
self.trace += line
self.trace_lock.release()
if not line:
break
if self.verbose:
print(line, end='', file=sys.stderr)
sys.stderr.flush()
def _at_exit(self):
if self.process is not None:
self.process.terminate()
def _timeout(self):
report("Timeout run out. Terminating pkgupdate.")
self.process.terminate()
setup_alarm(self._kill_timeout, self.kill_timeout)
self.process.wait()
signal.alarm(0)
def _kill_timeout(self):
report("Kill timeout run out. Killing pkgupdate.")
self.process.kill()
def run(ensure_run, timeout, timeout_kill, verbose, hooklist=None):
"""Run updater
"""
pidlock = PidLock()
plown = pidlock.acquire(ensure_run)
hook.register_list(hooklist)
if not plown:
sys.exit(1)
exit_code = 0
while True:
pidlock.unblock()
supervisor = Supervisor(verbose=verbose)
report("Running pkgupdate")
supervisor.run()
exit_code = supervisor.join(timeout, timeout_kill)
if exit_code != 0:
report("pkgupdate exited with: " + str(exit_code))
notify.failure(exit_code, supervisor.trace)
else:
report("pkgupdate reported no errors")
del supervisor # To clean signals and more
approvals._update_stat()
notify.changes()
pidlock.block()
if pidlock.sigusr1:
report("Rerunning pkgupdate as requested.")
else:
break
hook._run()
notify.notifier()
# Note: pid_lock is freed using atexit
return exit_code
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
import os
import time
from . import const, autorun, notify
from .utils import report
from .exceptions import ExceptionUpdaterApproveInvalid
# 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():
"""Returns currently existing aprroval request. If there is no approval
request pending then it returns None.
Existing approval is returned as a dictonary with following keys:
"hash": This contains string identifying current request. This is used for
updater's plan identification, most probably you won't need it.
"status": This contains one following strings:
"asked" if request was created and user have not decided yet.
"granted" if request was approved.
"denied" if request was denied and should be hidden.
"time": This is time of request creation. It's number, a Unix time.
"plan": This contains exact plan to be approved.
"name": Name of package.
"op": string identifying operation. One of following are allowed:
"install" if package should be installed. Because of backward
compatibility this also means upgrade or downgrade of package.
"upgrade" if package should be upgraded (install newer version).
"downgrade" if package should be downgraded (install older
version).
"remove" if package should be removed from system.
"cur_ver: Currently installed version of package as a string. This can
be None if it makes no sense for given operation (such as install).
This can also be None when such information wasn't provided (should
be expected because of compatibility reasons).
"new_ver": Target version of packages as a string. This is None if it
makes no sense for given operation (such as remove). This can also
be None when such information wasn't provided.
"reboot": This is boolean value informing if reboot will be done durring
update. Note that this is forced immediate reboot.
"""
# Both files have to exists otherwise it is invalid approval request
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(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(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
# Rest of the lines contains operations
result['plan'] = list()
for line in file.readlines():
cols = line.split('\t')
pkg = dict()
pkg['op'] = cols[0].strip()
if cols[1] != '-':
pkg['new_ver'] = cols[1].strip()
else:
pkg['new_ver'] = None
pkg['cur_ver'] = None
pkg['name'] = cols[2].strip()
result['plan'].append(pkg)
result['reboot'] = result['reboot'] or \
cols[3].strip() == 'immediate'
return result
def _set_stat(status, hsh):
"Set given status to APPROVALS_STAT_FILE if hsh matches current hash"
# Both files have to exists otherwise it is invalid approval request
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(const.APPROVALS_STAT_FILE, 'r') as file:
cols.extend(file.readline().split(' '))
if hsh is not None and cols[0].strip() != hsh:
raise ExceptionUpdaterApproveInvalid("Not matching hash passed")
# Write new stat
cols[1] = status
with open(const.APPROVALS_STAT_FILE, 'w') as file:
file.write(' '.join(cols))
def approve(hsh):
"""Approve current plan. Passed hash should match with hash returned from
current(). If it doesn't match then ExceptionUpdaterApproveInvalid is
thrown. You can pass None to skip this check.
"""
_set_stat('granted', hsh)
def deny(hsh):
"""Deny current plan. This makes it effectively never timeout
(automatically installed). Passed hash should be same as the one returned
from current(). If it doesn't match then ExceptionUpdaterApproveInvalid is
thrown. You can pass None to skip this check.
"""
_set_stat('denied', hsh)
def _approved():
"""This returns hash of approved plan. If there is no approved plan then it
returns 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 > 0 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(const.APPROVALS_STAT_FILE, 'w') as file:
file.write(' '.join((new_hash, 'asked', str(int(time.time())))))
# Send notification
notify.approval()
def _update_stat():
"""This function should be run when updater finished its execution. It
checks if stat file is consistent with plan. If there is new plan (old was
replaced with some newer one) then it updates stat file.
When new plan is presented then it also sends notification to user about
it.
"""
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(const.APPROVALS_ASK_FILE, 'r') as file:
new_hash = file.readline().strip()
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(const.APPROVALS_STAT_FILE, 'r') as file:
cols = file.readline().split(' ')
if cols[0].strip() != new_hash:
_gen_new_stat(new_hash)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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 uci import UciExceptionNotFound
from euci import EUci
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 EUci() as uci:
try:
# TODO use EUci instead of this retype (as this is not perfect)
return uci.get_boolean("updater", "autorun", "enabled")
except UciExceptionNotFound:
return False # No option means disabled
def set_enabled(enabled):
"""Set value that can be later received with enabled function.
It sets uci configuration value: updater.autorun.enable
"""
with EUci() as uci:
uci.set('updater', 'autorun', 'autorun')
uci.set_boolean('updater', 'autorun', 'enabled', enabled)
def approvals():
"""Returns True if updater approvals are enabled.
Relevant uci configuration is: updater.autorun.approvals
"""
with EUci() as uci:
try:
# TODO use EUci instead of this retype (as this is not perfect)
return uci.get_boolean("updater", "autorun", "approvals")
except UciExceptionNotFound:
return False # No option means disabled
def set_approvals(enabled):
"""Set value that can later be received by enabled function.
This is relevant to uci config: updater.autorun.approvals
"""
with EUci() as uci:
uci.set('updater', 'autorun', 'autorun')
uci.set_boolean('updater', 'autorun', 'approvals', enabled)
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 EUci() as uci:
try:
value = uci.get_integer("updater", "autorun", "auto_approve_time")
return value if value > 0 else None
except UciExceptionNotFound:
return 0
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 EUci() as uci:
if approve_time > 0:
uci.set('updater', 'autorun', 'autorun')
uci.set_integer('updater', 'autorun', 'auto_approve_time', approve_time)
else:
uci.delete('updater', 'autorun', 'auto_approve_time')
# coding=utf-8
"""This just holds some constants used in updater-supervisor
"""
# Path where we should found supervisor pid lock file
PID_FILE_PATH = "/tmp/updater-supervisor.pid"
# Path where failure dumps are dumped
FAIL_DUMP_PATH = "/var/log/updater-dump"
# This is path to opkg lock
OPKG_LOCK = "/var/lock/opkg.lock"
PKGUPDATE_STATE = "/tmp/update-state"
# File containing log of changes done on system
PKGUPDATE_LOG = PKGUPDATE_STATE + "/log2"
# File with latest error dumped from pkgupdate
PKGUPDATE_ERROR_LOG = PKGUPDATE_STATE + "/last_error"
# File containing stack trace from Lua
PKGUPDATE_CRASH_LOG = "/tmp/updater_crash.log"
# Updater run command
# TODO get rid of state log
PKGUPDATE_CMD = ['pkgupdate', '--batch', '--state-log',
'--task-log=/usr/share/updater/updater-log']
# pkgupdate default timeout
PKGUPDATE_TIMEOUT = 3000
# pkgupdate default kill timeout
PKGUPDATE_TIMEOUT_KILL = 60
# Address we ping to check if we have Internet connection
PING_ADDRESS = "repo.turris.cz"
# Maximum number of secomds we wait for network (testing if we can ping
# PING_ADDRESS)
PING_TIMEOUT = 10
# Files used for approvals handling.
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."
# File containing l10n symbols as a list of supported ones
L10N_FILE = "/usr/share/updater/l10n_supported"
# File containing list of known pkglists in json
PKGLISTS_FILE = "/usr/share/updater/pkglists.json"
# Hooks file containing commands to be run after updater execution finished.
POSTRUN_HOOK_FILE = "/tmp/updater-postrun-hook"
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
"""updater-supervisor specific exceptions.
"""
class ExceptionUpdaterDisabled(Exception):
"""This exception is thrown when you try to run updater when it's
configured to be disabled.
"""
pass
class ExceptionUpdaterApproveInvalid(Exception):
"""Exception thrown from either approve.approve() or approve.deny() when
given hash doesn't match the one from approve.current().
"""
pass
class ExceptionUpdaterPidLockFailure(Exception):
"""This exception is thrown when we encounter some invalid usage of
pidlock.
"""
pass
class ExceptionUpdaterNoSuchList(Exception):
"""Exception thrown from lists.update when non-existent list is given.
"""
pass
class ExceptionUpdaterNoSuchLang(Exception):
"""Exception thrown from l10n.update when unsupported language code is
given.
"""
pass
class ExceptionUpdaterInvalidHookCommand(Exception):
"""Thrown from hook.register when argument command contains more than one
line.
"""
pass
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
import os
import sys
import fcntl
import errno
import subprocess
from threading import Thread
from .utils import report
from ._pidlock import pid_locked
from .const import POSTRUN_HOOK_FILE
from .exceptions import ExceptionUpdaterInvalidHookCommand
def __run_command(command):
def _fthread(file):
while True:
line = file.readline()
if not line:
break
report(line.decode(sys.getdefaultencoding()))
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.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))
def register(command):
"""Add given command (format is expected to be same as if you call
subprocess.run) to be executed when updater exits. Note that this hook is
executed no matter if updater passed or failed or even if it just requested
user's approval. In all of those cases when updater exits this hook is
executed.
"commands" has to be single line shell script.
"""
if '\n' in command:
raise ExceptionUpdaterInvalidHookCommand(
"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)
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:
invalid = True
except OSError as excp:
if excp.errno == errno.ENOENT:
invalid = True
raise
if invalid: # File was removed before we locked it
os.close(file)
register(command)
return
if not pid_locked(): # Check if updater is running
os.close(file)
# If there is no running instance then just run given command
__run_command(command)
return
# Append given arguments to file
# Note: This takes ownership of file and automatically closes it. (at least
# it seems that way)
with os.fdopen(file, 'w') as fhook:
fhook.write(command + '\n')
report('Postrun hook registered: ' + command)
def register_list(commands):
"""Same as register but it allows multiple commands to be registered at
once.
"""
if commands is not None:
for cmd in commands:
register(cmd)
def _run():
"""Run all registered commands.
"""
# Open file for reading and take exclusive lock
try:
file = os.open(POSTRUN_HOOK_FILE, os.O_RDWR)
except OSError as excp:
if excp.errno == errno.ENOENT:
return # No file means nothing to do
raise
fcntl.lockf(file, fcntl.LOCK_EX)
# Note: nobody except us should be able to remove this file (because we
# should hold pidlock) so we don't have to check if file we opened is still
# on FS.
with os.fdopen(file, 'r') as fhook:
for line in fhook.readlines():
__run_command(line)
os.remove(POSTRUN_HOOK_FILE)
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
import os
from uci import Uci, UciExceptionNotFound
from .const import L10N_FILE
from .exceptions import ExceptionUpdaterNoSuchLang
def languages():
"""Returns dict with all available l10n translations for system packages.
"""
result = dict()
if os.path.isfile(L10N_FILE): # Just to be sure
with open(L10N_FILE, 'r') as file:
for line in file.readlines():
if not line.strip():
continue # ignore empty lines
result[line.strip()] = False
with Uci() as uci:
try:
l10n_enabled = uci.get("updater", "l10n", "langs")
except (UciExceptionNotFound, KeyError):
# If we fail to get that section then just ignore
return result
for lang in l10n_enabled:
result[lang] = True
return result
def update_languages(langs):
"""Updates what languages should be installed to system.
langs is expected to be a list of strings where values are ISO languages
codes.
Note that this doesn't verifies that those languages are specified as
supported in appropriate file.
"""
# Verify langs
expected = set()
if os.path.isfile(L10N_FILE): # Just to be sure
with open(L10N_FILE, 'r') as file:
for line in file.readlines():
expected.add(line.strip())
for lang in langs:
if lang not in expected:
raise ExceptionUpdaterNoSuchLang(
"Can't enable unsupported language code:" + str(lang))
# Set
with Uci() as uci:
uci.set('updater', 'l10n', 'l10n')
uci.set('updater', 'l10n', 'langs', tuple(langs))
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
import os
import json
import gettext
from uci import Uci, UciExceptionNotFound
from .const import PKGLISTS_FILE
from .exceptions import ExceptionUpdaterNoSuchList
def pkglists(lang=None):
"""Returns dict of pkglists.
Argument lang is expected to be a string containing language code. This
code is then used for gettext translations of titles and descriptions of
messages.
Return pkglists are in dictionary where key is name of pkglist and value is
another dictionary with following content:
"enabled": This is boolean value containing info if pkglist is enabled.
"hidden": This is boolean value specifying if pkglist is pkg visible.
"title": This is title text describing pkglist (human readable name). This
field can be None if "hidden" field is set to True.
"message": This is human readable description of given pkglist. This can be
None if "hidden" is set to True.
"""
result = dict()
trans = gettext.translation(
'pkglists',
languages=[lang] if lang is not None else None,
fallback=True)
if os.path.isfile(PKGLISTS_FILE): # Just to be sure
with open(PKGLISTS_FILE, 'r') as file:
ldul = json.load(file)
for name, lst in ldul.items():
visible = lst['visible']
result[name] = {
"title": trans.gettext(lst['title']) if 'title' in lst else None,
"message": trans.gettext(lst['description']) if 'description' in lst else None,
"enabled": False,
"hidden": not visible
}
with Uci() as uci:
try:
lists = uci.get("updater", "turris", "pkglists")
except (UciExceptionNotFound, KeyError):
# If we fail to get that section then just ignore
return result
for lst in lists:
if lst in result:
result[lst]['enabled'] = True
# Ignore any unknown but enabled lists
return result
def update_pkglists(lists):
"""
List is expected to be a array of strings (list ids) that should be
enabled. Anything omitted will be disabled.
"""
expected = set()
if os.path.isfile(PKGLISTS_FILE): # Just to be sure
with open(PKGLISTS_FILE, 'r') as file:
ldul = json.load(file)
for name in ldul:
expected.add(name)
for lst in lists:
if lst not in expected:
raise ExceptionUpdaterNoSuchList(
"Can't enable unknown package list:" + str(lst))
# Set
with Uci() as uci:
uci.set('updater', 'turris', 'turris')
uci.set('updater', 'turris', 'pkglists', tuple(lists))
def userlists(lang=None):
"""Backward compatibility API. Please use pkglists instead."""
return pkglists(lang)
def update_userlists(lists):
"""Backward compatibility API. Please use update_pkglists instead."""
update_pkglists(lists)
# coding=utf-8
# Copyright (c) 2018, CZ.NIC, z.s.p.o. (http://www.nic.cz/)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the CZ.NIC nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL CZ.NIC BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# 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.
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
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)
def failure(exit_code, trace):
"""Send notification about updater's failure
"""
if exit_code == 0 and not os.path.isfile(PKGUPDATE_ERROR_LOG):
return
msg_en = "Updater selhal: "
msg_cs = "Updater failed: "
if os.path.isfile(PKGUPDATE_ERROR_LOG):
with open(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:
content = '\n'.join(file.readlines())
msg_en += content
msg_cs += content
elif trace is not None:
msg_en += trace + "\n\nExit code: " + str(exit_code)
msg_cs += trace + "\n\nNávratový kód: " + str(exit_code)
else:
msg_en += "Unknown error"
msg_cs += "Neznámá chyba"
if subprocess.call(['create_notification', '-s', 'error',
msg_cs, msg_en]) != 0:
report('Notification creation failed.')
clear_logs()
def changes():
"""Send notification about changes.
"""
if not os.path.isfile(PKGUPDATE_LOG):
return
text_en = ""
text_cs = ""
with open(PKGUPDATE_LOG, 'r') as file:
for line in file.readlines():
pkg = line.split(' ')
if pkg[0].strip() ==