luci-app-lxc: fix "plain-vanilla" integration, part 2

I've tried to get the lxc app in a more usable state. Tested with mips
and amd64 targets.

* check /etc/config/lxc in controller, not in cbi
* more controller cleanups
* remove unused 'fork_exec' function
* check path before container creation
* check space requirements before container creation
  * support new uci options 'min_space' and 'min_temp',
    default for both is 100000 KB
  * both options are configurable via LuCI CBI template
* write messages to log in case of an error
* validate the container name during creation,
  automatically remove invalid chars
* inform the user that only a stopped container can be destroyed
* add experimental ssl support (untested, disabled by default)

Signed-off-by: Dirk Brenken <dev@brenken.org>
This commit is contained in:
Dirk Brenken 2018-05-26 19:32:39 +02:00
parent fa4dc6be91
commit d9b6c5dd78
3 changed files with 92 additions and 74 deletions

View file

@ -14,73 +14,43 @@ Author: Petar Koretic <petar.koretic@sartura.hr>
]]-- ]]--
module("luci.controller.lxc", package.seeall)
local uci = require "luci.model.uci".cursor() local uci = require "luci.model.uci".cursor()
local util = require "luci.util" local util = require "luci.util"
local fs = require "nixio" local fs = require "nixio"
module("luci.controller.lxc", package.seeall)
function fork_exec(command)
local pid = fs.fork()
if pid > 0 then
return
elseif pid == 0 then
-- change to root dir
fs.chdir("/")
-- patch stdin, out, err to /dev/null
local null = fs.open("/dev/null", "w+")
if null then
fs.dup(null, fs.stderr)
fs.dup(null, fs.stdout)
fs.dup(null, fs.stdin)
if null:fileno() > 2 then
null:close()
end
end
-- replace with target command
fs.exec("/bin/sh", "-c", command)
end
end
function index() function index()
if not nixio.fs.access("/etc/config/lxc") then
return
end
page = node("admin", "services", "lxc") page = node("admin", "services", "lxc")
page.target = cbi("lxc") page.target = cbi("lxc")
page.title = _("LXC Containers") page.title = _("LXC Containers")
page.order = 70 page.order = 70
page = entry({"admin", "services", "lxc_create"}, call("lxc_create"), nil) entry({"admin", "services", "lxc_create"}, call("lxc_create"), nil).leaf = true
page.leaf = true entry({"admin", "services", "lxc_action"}, call("lxc_action"), nil).leaf = true
entry({"admin", "services", "lxc_get_downloadable"}, call("lxc_get_downloadable"), nil).leaf = true
page = entry({"admin", "services", "lxc_action"}, call("lxc_action"), nil) entry({"admin", "services", "lxc_configuration_get"}, call("lxc_configuration_get"), nil).leaf = true
page.leaf = true entry({"admin", "services", "lxc_configuration_set"}, call("lxc_configuration_set"), nil).leaf = true
page = entry({"admin", "services", "lxc_get_downloadable"}, call("lxc_get_downloadable"), nil)
page.leaf = true
page = entry({"admin", "services", "lxc_configuration_get"}, call("lxc_configuration_get"), nil)
page.leaf = true
page = entry({"admin", "services", "lxc_configuration_set"}, call("lxc_configuration_set"), nil)
page.leaf = true
end end
function lxc_get_downloadable() function lxc_get_downloadable()
local target = lxc_get_arch_target() local target = lxc_get_arch_target()
local templates = {} local templates = {}
local ssl_status = lxc_get_ssl_status()
local f = io.popen('sh /usr/share/lxc/templates/lxc-download --list --no-validate --server %s 2>/dev/null' local f = io.popen('sh /usr/share/lxc/templates/lxc-download --list %s --server %s 2>/dev/null'
% util.shellquote(uci:get("lxc", "lxc", "url")), 'r') %{ ssl_status, util.shellquote(uci:get("lxc", "lxc", "url")) }, 'r')
local line local line
for line in f:lines() do for line in f:lines() do
local dist, version, dist_target = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+default%s+%S+$") local dist, version, dist_target = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+default%s+%S+$")
if dist and version and dist_target == target then if dist and version and dist_target and dist_target == target then
templates[#templates+1] = "%s:%s" %{ dist, version } templates[#templates+1] = "%s:%s" %{ dist, version }
end end
end end
f:close() f:close()
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
@ -90,23 +60,26 @@ end
function lxc_create(lxc_name, lxc_template) function lxc_create(lxc_name, lxc_template)
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
if not pcall(dofile, "/etc/openwrt_release") then local check = lxc_get_config_path()
return luci.http.write("1") if not check then
return
end end
local lxc_dist, lxc_release = lxc_template:match("^(.+):(.+)$") local ssl_status = lxc_get_ssl_status()
local src_err
local lxc_dist, lxc_release = lxc_template:match("^(.+):(.+)$")
luci.http.write(util.ubus("lxc", "create", { luci.http.write(util.ubus("lxc", "create", {
name = lxc_name, name = lxc_name,
template = "download", template = "download",
args = { args = {
"--server", uci:get("lxc", "lxc", "url"), "--server", uci:get("lxc", "lxc", "url"),
"--no-validate",
"--dist", lxc_dist, "--dist", lxc_dist,
"--release", lxc_release, "--release", lxc_release,
"--arch", lxc_get_arch_target() "--arch", lxc_get_arch_target(),
ssl_status
} }
})) }), src_err)
end end
function lxc_action(lxc_action, lxc_name) function lxc_action(lxc_action, lxc_name)
@ -123,9 +96,25 @@ function lxc_get_config_path()
local ret = content:match('^%s*lxc.lxcpath%s*=%s*([^%s]*)') local ret = content:match('^%s*lxc.lxcpath%s*=%s*([^%s]*)')
if ret then if ret then
return ret .. "/" if nixio.fs.access(ret) then
local min_space = tonumber(uci:get("lxc", "lxc", "min_space")) or 100000
local free_space = tonumber(util.exec("df " ..ret.. " | awk '{if(NR==2)print $4}'"))
if free_space and free_space >= min_space then
local min_temp = tonumber(uci:get("lxc", "lxc", "min_temp")) or 100000
local free_temp = tonumber(util.exec("df /tmp | awk '{if(NR==2)print $4}'"))
if free_temp and free_temp >= min_temp then
return ret .. "/"
else
util.perror("lxc error: not enough temporary space (< " ..min_temp.. " KB)")
end
else
util.perror("lxc error: not enough space (< " ..min_space.. " KB)")
end
else
util.perror("lxc error: directory not found")
end
else else
return "/srv/lxc/" util.perror("lxc error: config path is empty")
end end
end end
@ -143,14 +132,15 @@ function lxc_configuration_set(lxc_name)
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
local lxc_configuration = luci.http.formvalue("lxc_configuration") local lxc_configuration = luci.http.formvalue("lxc_configuration")
if lxc_configuration == nil then if lxc_configuration == nil then
return luci.http.write("1") util.perror("lxc error: config formvalue is empty")
return
end end
local f, err = io.open(lxc_get_config_path() .. lxc_name .. "/config","w+") local f, err = io.open(lxc_get_config_path() .. lxc_name .. "/config","w+")
if not f then if not f then
return luci.http.write("2") util.perror("lxc error: config file not found")
return
end end
f:write(lxc_configuration) f:write(lxc_configuration)
@ -168,13 +158,21 @@ function lxc_get_arch_target()
armv8 = "arm64", armv8 = "arm64",
x86_64 = "amd64" x86_64 = "amd64"
} }
local k, v local k, v
for k, v in pairs(target_map) do for k, v in pairs(target_map) do
if target:find(k) then if target:find("^" ..k.. "$") then
return v return v
end end
end end
return target return target
end end
function lxc_get_ssl_status()
local ssl_enabled = uci:get("lxc", "lxc", "ssl_enabled")
local ssl_status = "--no-validate"
if ssl_enabled and ssl_enabled == "1" then
ssl_status = ""
end
return ssl_status
end

View file

@ -14,20 +14,35 @@ Author: Petar Koretic <petar.koretic@sartura.hr>
]]-- ]]--
local fs = require "nixio.fs"
m = Map("lxc", translate("LXC Containers"), m = Map("lxc", translate("LXC Containers"),
translate("<b>Please note:</b> For LXC Containers you need a custom OpenWrt image.<br />") translate("<b>Please note:</b> For LXC Containers you need a custom OpenWrt image.<br />")
.. translate("The image should include at least support for 'kernel cgroups', 'kernel namespaces' and 'miscellaneous LXC related options'.")) .. translate("The image should include at least support for 'kernel cgroups', 'kernel namespaces' and 'miscellaneous LXC related options'."))
m:section(SimpleSection).template = "lxc"
if fs.access("/etc/config/lxc") then s = m:section(TypedSection, "lxc", translate("Options"))
m:section(SimpleSection).template = "lxc" s.anonymous = true
s = m:section(TypedSection, "lxc", translate("Options")) o1 = s:option(Value, "url", translate("Containers URL"))
s.anonymous = true o1:value("images.linuxcontainers.org")
s.addremove = false o1:value("repo.turris.cz/lxc", "repo.turris.cz/lxc (SSL req.)")
o1.default = "images.linuxcontainers.org"
o1.rmempty = false
s:option(Value, "url", translate("Containers URL")) o2 = s:option(Flag, "ssl_enabled", translate("Enable SSL"),
end translate("Enable optional SSL encryption support. This requires additional packages like 'wget', 'ca-certificates', 'gnupg' and 'gnupg-utils'."))
o2.default = o2.disabled
o2.rmempty = false
o3 = s:option(Value, "min_space", translate("Free Space Threshold"),
translate("Minimum required free space for LXC Container creation in KB"))
o3.default = "100000"
o3.datatype = "min(50000)"
o3.rmempty = false
o4 = s:option(Value, "min_temp", translate("Free Temp Threshold"),
translate("Minimum required free temp space for LXC Container creation in KB"))
o4.default = "100000"
o4.datatype = "min(50000)"
o4.rmempty = false
return m return m

View file

@ -107,9 +107,9 @@ table.cbi-section-table td,
function lxc_create(tr) function lxc_create(tr)
{ {
var lxc_name = tr.querySelector("#tx_name").value.replace(/\s/g,'') var lxc_name = tr.querySelector("#tx_name").value.replace(/[\s!@#$%^&*()+=\[\]{};':"\\|,<>\/?]/g,'')
var lxc_template = tr.querySelector("#s_template").value var lxc_template = tr.querySelector("#s_template").value
var bt_create = tr.querySelector("#bt_create") var bt_create = tr.querySelector("#bt_create")
if (t_lxc_list.querySelector("[data-id='" + lxc_name + "']") != null) if (t_lxc_list.querySelector("[data-id='" + lxc_name + "']") != null)
return info_message(output_add, "Container with that name already exists!", 4000) return info_message(output_add, "Container with that name already exists!", 4000)
@ -224,6 +224,14 @@ table.cbi-section-table td,
else if (action == "destroy") else if (action == "destroy")
{ {
var tr = self.parentNode.parentNode
var img = tr.querySelector('img')
if (img.getAttribute('src') != window.img["red"])
{
bt_action.disabled = false
return info_message(output_list,"Container is still running!")
}
if (!confirm("This will completely remove a stopped LXC container from disk. Are you sure?")) if (!confirm("This will completely remove a stopped LXC container from disk. Are you sure?"))
return return
@ -389,9 +397,6 @@ table.cbi-section-table td,
function set_empty_template() function set_empty_template()
{ {
if (document.getElementById('tr_holder') !== null)
return
var row_count = t_lxc_create.rows.length; var row_count = t_lxc_create.rows.length;
while(--row_count) t_lxc_create.deleteRow(row_count); while(--row_count) t_lxc_create.deleteRow(row_count);
@ -399,7 +404,7 @@ table.cbi-section-table td,
row.id = 'tr_holder' row.id = 'tr_holder'
var cell = row.insertCell(0); var cell = row.insertCell(0);
cell.colSpan = 3; cell.colSpan = 3;
cell.innerHTML = '<em><br />There are no templates for your architecture (<%=target%>) available, please select another Containers URL.</em>'; cell.innerHTML = '<em><br />There are no templates for your architecture (<%=target%>) available, please select another containers URL.</em>';
} }
function lxc_list_update() function lxc_list_update()