a_13_updater.lua 6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
--[[
Copyright 2016, CZ.NIC z.s.p.o. (http://www.nic.cz/)

This file is part of the turris updater.

Updater is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Updater is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Updater.  If not, see <http://www.gnu.org/licenses/>.
]]--

20
local next = next
21 22
local error = error
local ipairs = ipairs
23
local pcall = pcall
24
local table = table
25
local WARN = WARN
26
local INFO = INFO
27
local DIE = DIE
28
local sha256 = sha256
29
local reexec = reexec
30 31 32 33
local LS_CONF = LS_CONF
local LS_PLAN = LS_PLAN
local LS_DOWN = LS_DOWN
local update_state = update_state
34
local utils = require "utils"
35
local syscnf = require "syscnf"
36 37 38 39 40 41 42 43 44 45
local sandbox = require "sandbox"
local uri = require "uri"
local postprocess = require "postprocess"
local planner = require "planner"
local requests = require "requests"
local backend = require "backend"
local transaction = require "transaction"

module "updater"

46 47 48 49
-- luacheck: globals tasks prepare no_tasks tasks_to_transaction pre_cleanup cleanup disable_replan approval_hash task_report

-- Prepared tasks
tasks = {}
50 51 52 53 54

local allow_replan = true
function disable_replan()
	allow_replan = false
end
55

56
local function required_pkgs(entrypoint)
57
	-- Get the top-level script
58 59 60 61 62
	local entry_chunk, entry_uri = utils.uri_content(entrypoint, nil, {})
	local merge = {
		-- Note: See requests.script for usage of this value
		["parent_script_uri"] = entry_uri
	}
63
	update_state(LS_CONF)
64
	local err = sandbox.run_sandboxed(entry_chunk, entrypoint, 'Full', nil, merge)
65
	if err and err.tp == 'error' then error(err) end
66
	update_state(LS_PLAN)
67 68
	-- Go through all the requirements and decide what we need
	postprocess.run()
69 70 71 72
	return planner.required_pkgs(postprocess.available_packages, requests.content_requests)
end

function prepare(entrypoint)
73 74 75
	if not entrypoint then
		entrypoint = "file://" .. syscnf.root_dir .. "etc/updater/conf.lua"
	end
76
	local required = required_pkgs(entrypoint)
77
	local run_state = backend.run_state()
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
	tasks = planner.filter_required(run_state.status, required, allow_replan)

	for _, task in ipairs(tasks) do
		if task.action == "require" then
			-- TODO downgrade and so on?
			INFO("Queue install of " .. task.name .. "/" .. task.package.repo.name .. "/" .. task.package.Version)
		elseif task.action == "remove" then
			INFO("Queue removal of " .. task.name)
		else
			DIE("Unknown action " .. task.action)
		end
	end
end

-- Check if we have some tasks
function no_tasks()
	return not next(tasks)
end

-- Download all packages and push tasks to transaction
function tasks_to_transaction()
	INFO("Downloading packages")
100
	update_state(LS_DOWN)
101
	-- Start packages download
102
	local uri_master = uri:new()
103
	for _, task in ipairs(tasks) do
104
		if task.action == "require" then
105 106 107 108
			task.file = syscnf.pkg_download_dir .. task.name .. '-' .. task.package.Version .. '.ipk'
			task.real_uri = uri_master:to_file(task.package.Filename, task.file, task.package.repo.index_uri)
			task.real_uri:add_pubkey() -- do not verify signatures (there are none)
			-- TODO on failure: log_event('D', task.name .. " " .. task.package.Version)
109 110
		end
	end
111
	uri_master:download() -- TODO what if error?
112
	-- Now push all data into the transaction
113
	utils.mkdirp(syscnf.pkg_download_dir)
114 115
	for _, task in ipairs(tasks) do
		if task.action == "require" then
116 117 118 119
			local ok, err = pcall(function() task.real_uri:finish() end)
			if not ok then error(err) end
			-- TODO check hash
			--[[
120 121 122 123
			if task.package.MD5Sum then
				local sum = md5(data)
				if sum ~= task.package.MD5Sum then
					error(utils.exception("corruption", "The md5 sum of " .. task.name .. " does not match"))
124
				end
125
			end
126 127 128 129 130 131
			if task.package.SHA256Sum then
				local sum = sha256(data)
				if sum ~= task.package.SHA256Sum then
					error(utils.exception("corruption", "The sha256 sum of " .. task.name .. " does not match"))
				end
			end
132 133
			]]
			transaction.queue_install_downloaded(task.file, task.name, task.package.Version, task.modifier)
134 135 136 137 138 139 140 141
		elseif task.action == "remove" then
			transaction.queue_remove(task.name)
		else
			DIE("Unknown action " .. task.action)
		end
	end
end

142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
local function queued_tasks(extensive)
	return utils.map(tasks, function (i, task)
		local d = {task.action, utils.multi_index(task, "package", "Version") or '-', task.name}
		if d[1] == "require" then
			d[1] = "install"
		elseif d[1] == "remove" then
			d[2] = '-'
		end -- Just to be backward compatible require=install and remove does not have version
		if extensive then
			table.insert(d, utils.multi_index(task, "modifier", "reboot") or '-')
		end
		return i, table.concat(d, '	') .. "\n"
	end)
end

-- Compute the approval hash of the queued operations
function approval_hash()
	-- Convert the tasks into formatted lines, sort them and hash it.
	local reqs = queued_tasks(true)
	table.sort(reqs)
	return sha256(table.concat(reqs))
end

-- Provide a human-readable report of the queued tasks
function task_report(prefix, extensive)
	prefix = prefix or ''
	return table.concat(utils.map(queued_tasks(extensive), function (i, str) return i, prefix .. str end))
end

171 172 173 174 175 176 177 178 179 180 181 182 183 184
-- Only cleanup actions that we want to give chance to program to react on
function pre_cleanup()
	local reboot_delayed = false
	local reboot_finished = false
	if transaction.cleanup_actions.reboot == "delayed" then
		WARN("Restart your device to apply all changes.")
		reboot_delayed = true
	elseif transaction.cleanup_actions.reboot == "finished" then
		reboot_finished = true
	end
	return reboot_delayed, reboot_finished
end

-- Note: This function don't have to return
Karel Koci's avatar
Karel Koci committed
185
function cleanup(reboot_finished)
186
	if transaction.cleanup_actions.reexec and allow_replan then
187 188 189 190 191
		if reboot_finished then
			reexec('--reboot-finished')
		else
			reexec()
		end
192 193 194
	end
end

195
return _M