gluon-packages/admin/autoupdater/files/usr/sbin/autoupdater

370 lines
9.5 KiB
Lua
Executable File

#!/usr/bin/lua
local nixio = require('nixio')
local fs = require('nixio.fs')
local platform_info = require('platform_info')
local uci = require('uci').cursor()
local autoupdater_util = require('autoupdater.util')
local autoupdater_version = require('autoupdater.version')
if not platform_info.get_image_name() then
io.stderr:write("The autoupdater doesn't support this hardware model.\n")
os.exit(1)
end
autoupdater_util.randomseed()
local settings = uci:get_all('autoupdater', 'settings')
local branch_name = settings.branch
local version_file = io.open(settings.version_file)
local old_version = version_file and version_file:read('*l') or ''
version_file:close()
-- If force is true the updater will perform an upgrade regardless of
-- the priority and even when it is disabled in uci
local force = false
-- If fallback is true the updater will perform an update only if the
-- timespan given by the priority and another 24h have passed
local fallback = false
local mirrors = {}
local function parse_args()
local i = 1
while arg[i] do
if arg[i] == '-f' then
force = true
elseif arg[i] == '--fallback' then
fallback = true
elseif arg[i] == '-b' then
i = i+1
if not arg[i] then
io.stderr:write("Error parsing command line: expected branch name\n")
os.exit(1)
end
branch_name = arg[i]
elseif arg[i]:sub(0, 1) == '-' then
io.stderr:write("Error parsing command line: unexpected argument '" .. arg[i] .. "'\n")
os.exit(1)
else
table.insert(mirrors, arg[i])
end
i = i+1
end
end
parse_args()
local branch = uci:get_all('autoupdater', branch_name)
if not branch then
io.stderr:write("Can't find configuration for branch '" .. branch_name .. "'\n")
os.exit(1)
end
if settings.enabled ~= '1' and not force then
io.stderr:write('autoupdater is disabled.\n')
os.exit(0)
end
-- Verifies a file given as a list of lines with a list of signatures using ecdsaverify
local function verify_lines(lines, sigs)
local command = {'ecdsaverify', '-n', tostring(branch.good_signatures)}
-- Build command line from sigs and branch.pubkey
for _, sig in ipairs(sigs) do
if sig:match('^' .. string.rep('%x', 128) .. '$') then
table.insert(command, '-s')
table.insert(command, sig)
end
end
for _, key in ipairs(branch.pubkey) do
if key:match('^' .. string.rep('%x', 64) .. '$') then
table.insert(command, '-p')
table.insert(command, key)
end
end
-- Call ecdsautils
local pid, f = autoupdater_util.popen(true, unpack(command))
for _, line in ipairs(lines) do
f:write(line)
f:write('\n')
end
f:close()
local wpid, status, code = nixio.waitpid(pid)
return wpid and status == 'exited' and code == 0
end
-- Downloads, parses and verifies the update manifest from a mirror
-- Returns a table with the fields version, checksum and filename if everything is ok, nil otherwise
local function read_manifest(mirror)
local sep = false
local lines = {}
local sigs = {}
local branch_ok = false
local ret = {}
-- Remove potential trailing slash
mirror = mirror:gsub('/$', '')
local starttime = os.time()
local pid, manifest_loader = autoupdater_util.popen(false, 'wget', '-T', '120', '-O-', string.format('%s/%s.manifest', mirror, branch.name))
local data = ''
-- Read all lines from the manifest
-- The upper part is saved to lines, the lower part to sigs
while true do
-- If the manifest download takes more than 5 minutes, we don't really
-- have a chance to download a whole image
local timeout = starttime+300 - os.time()
if timeout < 0 or not nixio.poll({{fd = manifest_loader, events = nixio.poll_flags('in')}}, timeout * 1000) then
io.stderr:write("Timeout while reading manifest.\n")
nixio.kill(pid, nixio.const.SIGTERM)
manifest_loader:close()
return nil
end
local r = manifest_loader:read(1024)
if not r or r == '' then
break
end
data = data .. r
while data:match('\n') do
local line, rest = data:match('^([^\n]*)\n(.*)$')
data = rest
if not sep then
if line == '---' then
sep = true
else
table.insert(lines, line)
if line == ('BRANCH=' .. branch.name) then
branch_ok = true
end
local date = line:match('^DATE=(.+)$')
local priority = line:match('^PRIORITY=([%d%.]+)$')
local model, version, checksum, filename = line:match('^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)$')
if date then
ret.date = autoupdater_util.parse_date(date)
elseif priority then
ret.priority = tonumber(priority)
elseif model == platform_info.get_image_name() and #checksum == 64 then
ret.version = version
ret.checksum = checksum
ret.filename = filename
end
end
else
table.insert(sigs, line)
end
end
end
manifest_loader:close()
-- Do some very basic checks before checking the signatures
-- (as the signature verification is computationally expensive)
if not sep then
io.stderr:write('There seems to have gone something wrong downloading the manifest from ' .. mirror .. '\n')
return nil
end
if not ret.date or not ret.priority then
io.stderr:write('The manifest downloaded from ' .. mirror .. ' is invalid (DATE or PRIORITY missing)\n')
return nil
end
if not branch_ok then
io.stderr:write('Wrong branch. We are on ', branch.name, '.\n')
return nil
end
if not ret.version then
io.stderr:write('No matching firmware found (model ' .. platform_info.get_image_name() .. ')\n')
return nil
end
if not verify_lines(lines, sigs) then
io.stderr:write('Not enough valid signatures!\n')
return nil
end
return ret
end
-- Downloads the firmware image from a mirror to a given output file
local function fetch_firmware(mirror, filename, output)
-- Let's give the image download 30 minutes, hopefully more than enough
if autoupdater_util.exec(1800, 'wget', '-T', '120', '-O', output, mirror .. '/' .. filename) ~= 0 then
io.stderr:write('Error downloading the image from ' .. mirror .. '\n')
return false
end
return true
end
-- Returns the computed update probability
local function get_probability(date, priority)
local seconds = priority * 86400
local diff = os.difftime(os.time(), date)
if diff < 0 then
-- When the difference is negative, there are two possibilities: The manifest contains a wrong date, or our own clock is wrong.
-- As there isn't anything useful to do for an incorrect manifest, we'll assume the latter case and update anyways as we
-- can't do anything better
io.stderr:write('Warning: clock seems to be incorrect.\n')
if tonumber(fs.readfile('/proc/uptime'):match('^([^ ]+) ')) < 600 then
-- If the uptime is very low, it's possible we just didn't get the time over NTP yet, so we'll just wait until the next time the updater runs
return 0
else
-- Will give 1 when priority == 0, and lower probabilities the higher the priority value is
-- (similar to the old static probability system)
return 0.75^priority
end
elseif fallback then
if diff >= seconds + 86400 then
return 1
else
return 0
end
elseif diff >= seconds then
return 1
else
local x = diff/seconds
-- This is the most simple polynomial with value 0 at 0, 1 at 1, and whose first derivative is 0 at both 0 and 1
-- (we all love continuously differentiable functions, right?)
return (-2)*x^3 + 3*x^2
end
end
-- Tries to perform an update from a given mirror
local function autoupdate(mirror)
local download_d_dir = '/usr/lib/autoupdater/download.d'
local abort_d_dir = '/usr/lib/autoupdater/abort.d'
local upgrade_d_dir = '/usr/lib/autoupdater/upgrade.d'
local manifest = read_manifest(mirror)
if not manifest then
return false
end
if not autoupdater_version.newer_than(manifest.version, old_version) then
io.stderr:write('No new firmware available.\n')
return true
end
io.stderr:write('New version available.\n')
if not force and math.random() >= get_probability(manifest.date, manifest.priority) then
io.stderr:write('No autoupdate this time. Use -f to override.\n')
return true
end
autoupdater_util.run_dir(download_d_dir)
collectgarbage()
local image = os.tmpname()
if not fetch_firmware(mirror, manifest.filename, image) then
autoupdater_util.run_dir(abort_d_dir)
return false
end
local popen = io.popen(string.format("exec sha256sum '%s'", image))
local checksum = popen:read('*l'):match('^%x+')
popen:close()
if checksum ~= manifest.checksum then
io.stderr:write('Invalid image checksum!\n')
os.remove(image)
autoupdater_util.run_dir(abort_d_dir)
return false
end
autoupdater_util.run_dir(upgrade_d_dir)
io.stderr:write('Upgrading firmware...\n')
local null = nixio.open('/dev/null', 'w+')
if null then
nixio.dup(null, nixio.stdin)
nixio.dup(null, nixio.stderr)
if null:fileno() > 2 then
null:close()
end
end
nixio.exec('/sbin/sysupgrade', image)
-- This should never be reached as nixio.exec replaces the autoupdater process unless /sbin/sysupgrade can't be executed
-- We output the error message through stdout as stderr isn't available anymore
io.write('Failed to call sysupgrade?\n')
os.remove(image)
autoupdater_util.run_dir(abort_d_dir)
os.exit(1)
end
local lockfile = '/var/lock/autoupdater.lock'
local lockfd = nixio.open(lockfile, 'w', 'rw-------')
if not lockfd:lock('tlock') then
io.stderr:write(string.format(
"Unable to lock file %s. Make sure there is no other instance of the autoupdater running.\n",
lockfile, err
))
os.exit(1)
end
if #mirrors == 0 then
while #branch.mirror > 0 do
table.insert(mirrors, table.remove(branch.mirror, math.random(#branch.mirror)))
end
end
for k, mirror in ipairs(mirrors) do
if autoupdate(mirror) then
os.exit(0)
end
end
io.stderr:write('No usable mirror found.\n')
os.exit(1)