--[[ LuCI - Lua Configuration Interface Copyright 2019 lisaac ]]-- local docker = require "luci.model.docker" local m, s, o local dk = docker.new() local cmd_line = table.concat(arg, '/') local create_body = {} local images = dk.images:list().body local networks = dk.networks:list().body local containers = dk.containers:list({ query = { all=true } }).body local is_quot_complete = function(str) local num = 0, w require "math" if not str then return true end local num = 0, w for w in str:gmatch("\"") do num = num + 1 end if math.fmod(num, 2) ~= 0 then return false end num = 0 for w in str:gmatch("\'") do num = num + 1 end if math.fmod(num, 2) ~= 0 then return false end return true end function contains(list, x) for _, v in pairs(list) do if v == x then return true end end return false end local resolve_cli = function(cmd_line) local config = { advance = 1 } local key_no_val = { 't', 'd', 'i', 'tty', 'rm', 'read_only', 'interactive', 'init', 'help', 'detach', 'privileged', 'P', 'publish_all', } local key_with_val = { 'sysctl', 'add_host', 'a', 'attach', 'blkio_weight_device', 'cap_add', 'cap_drop', 'device', 'device_cgroup_rule', 'device_read_bps', 'device_read_iops', 'device_write_bps', 'device_write_iops', 'dns', 'dns_option', 'dns_search', 'e', 'env', 'env_file', 'expose', 'group_add', 'l', 'label', 'label_file', 'link', 'link_local_ip', 'log_driver', 'log_opt', 'network_alias', 'p', 'publish', 'security_opt', 'storage_opt', 'tmpfs', 'v', 'volume', 'volumes_from', 'blkio_weight', 'cgroup_parent', 'cidfile', 'cpu_period', 'cpu_quota', 'cpu_rt_period', 'cpu_rt_runtime', 'c', 'cpu_shares', 'cpus', 'cpuset_cpus', 'cpuset_mems', 'detach_keys', 'disable_content_trust', 'domainname', 'entrypoint', 'gpus', 'health_cmd', 'health_interval', 'health_retries', 'health_start_period', 'health_timeout', 'h', 'hostname', 'ip', 'ip6', 'ipc', 'isolation', 'kernel_memory', 'log_driver', 'mac_address', 'm', 'memory', 'memory_reservation', 'memory_swap', 'memory_swappiness', 'mount', 'name', 'network', 'no_healthcheck', 'oom_kill_disable', 'oom_score_adj', 'pid', 'pids_limit', 'restart', 'runtime', 'shm_size', 'sig_proxy', 'stop_signal', 'stop_timeout', 'ulimit', 'u', 'user', 'userns', 'uts', 'volume_driver', 'w', 'workdir' } local key_abb = { net='network', a='attach', c='cpu-shares', d='detach', e='env', h='hostname', i='interactive', l='label', m='memory', p='publish', P='publish_all', t='tty', u='user', v='volume', w='workdir' } local key_with_list = { 'sysctl', 'add_host', 'a', 'attach', 'blkio_weight_device', 'cap_add', 'cap_drop', 'device', 'device_cgroup_rule', 'device_read_bps', 'device_read_iops', 'device_write_bps', 'device_write_iops', 'dns', 'dns_optiondns_search', 'e', 'env', 'env_file', 'expose', 'group_add', 'l', 'label', 'label_file', 'link', 'link_local_ip', 'log_driver', 'log_opt', 'network_alias', 'p', 'publish', 'security_opt', 'storage_opt', 'tmpfs', 'v', 'volume', 'volumes_from', } local key = nil local _key = nil local val = nil local is_cmd = false cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)") for w in cmd_line:gmatch("[^%s]+") do if w =='\\' then elseif not key and not _key and not is_cmd then --key=val key, val = w:match("^%-%-([%lP%-]-)=(.+)") if not key then --key val key = w:match("^%-%-([%lP%-]+)") if not key then -- -v val key = w:match("^%-([%lP%-]+)") if key then -- for -dit if key:match("i") or key:match("t") or key:match("d") then if key:match("i") then config[key_abb["i"]] = true key:gsub("i", "") end if key:match("t") then config[key_abb["t"]] = true key:gsub("t", "") end if key:match("d") then config[key_abb["d"]] = true key:gsub("d", "") end if key:match("P") then config[key_abb["P"]] = true key:gsub("P", "") end if key == "" then key = nil end end end end end if key then key = key:gsub("-","_") key = key_abb[key] or key if contains(key_no_val, key) then config[key] = true val = nil key = nil elseif contains(key_with_val, key) then -- if key == "cap_add" then config.privileged = true end else key = nil val = nil end else config.image = w key = nil val = nil is_cmd = true end elseif (key or _key) and not is_cmd then if key == "mount" then -- we need resolve mount options here -- type=bind,source=/source,target=/app local _type = w:match("^type=([^,]+),") or "bind" local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or "" local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or "" local ro = w:match(",readonly") and "ro" or nil if source and target then if _type ~= "tmpfs" then local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or "")) else local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil key = "tmpfs" val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "") if not config[key] then config[key] = {} end table.insert( config[key], val ) key = nil val = nil end end else val = w end elseif is_cmd then config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w end if (key or _key) and val then key = _key or key if contains(key_with_list, key) then if not config[key] then config[key] = {} end if _key then config[key][#config[key]] = config[key][#config[key]] .. " " .. w else table.insert( config[key], val ) end if is_quot_complete(config[key][#config[key]]) then config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "") _key = nil else _key = key end else config[key] = (config[key] and (config[key] .. " ") or "") .. val if is_quot_complete(config[key]) then config[key] = config[key]:gsub("[\"\']", "") _key = nil else _key = key end end key = nil val = nil end end return config end local default_config = {} if cmd_line and cmd_line:match("^DOCKERCLI.+") then default_config = resolve_cli(cmd_line) elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then local container_id = cmd_line:match("^duplicate/(.+)") create_body = dk:containers_duplicate_config({id = container_id}) or {} if not create_body.HostConfig then create_body.HostConfig = {} end if next(create_body) ~= nil then default_config.name = nil default_config.image = create_body.Image default_config.hostname = create_body.Hostname default_config.tty = create_body.Tty and true or false default_config.interactive = create_body.OpenStdin and true or false default_config.privileged = create_body.HostConfig.Privileged and true or false default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil default_config.link = create_body.HostConfig.Links default_config.env = create_body.Env default_config.dns = create_body.HostConfig.Dns default_config.volume = create_body.HostConfig.Binds default_config.cap_add = create_body.HostConfig.CapAdd default_config.publish_all = create_body.HostConfig.PublishAllPorts if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then default_config.sysctl = {} for k, v in pairs(create_body.HostConfig.Sysctls) do table.insert( default_config.sysctl, k.."="..v ) end end if create_body.HostConfig.LogConfig and create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then default_config.log_opt = {} for k, v in pairs(create_body.HostConfig.LogConfig.Config) do table.insert( default_config.log_opt, k.."="..v ) end end if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then default_config.publish = {} for k, v in pairs(create_body.HostConfig.PortBindings) do table.insert( default_config.publish, v[1].HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") ) end end default_config.user = create_body.User or nil default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil default_config.advance = 1 default_config.cpus = create_body.HostConfig.NanoCPUs default_config.cpu_shares = create_body.HostConfig.CpuShares default_config.memory = create_body.HostConfig.Memory default_config.blkio_weight = create_body.HostConfig.BlkioWeight if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then default_config.device = {} for _, v in ipairs(create_body.HostConfig.Devices) do table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") ) end end if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then default_config.tmpfs = {} for k, v in pairs(create_body.HostConfig.Tmpfs) do table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v ) end end end end m = SimpleForm("docker", translate("Docker")) m.redirect = luci.dispatcher.build_url("admin", "docker", "containers") s = m:section(SimpleSection) s.template = "dockerman/apply_widget" s.err=docker:read_status() s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") if s.err then docker:clear_status() end s = m:section(SimpleSection, translate("New Container")) s.addremove = true s.anonymous = true o = s:option(DummyValue,"cmd_line", translate("Resolve CLI")) o.rawhtml = true o.template = "dockerman/newcontainer_resolve" o = s:option(Value, "name", translate("Container Name")) o.rmempty = true o.default = default_config.name or nil o = s:option(Flag, "interactive", translate("Interactive (-i)")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = default_config.interactive and 1 or 0 o = s:option(Flag, "tty", translate("TTY (-t)")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = default_config.tty and 1 or 0 o = s:option(Value, "image", translate("Docker Image")) o.rmempty = true o.default = default_config.image or nil for _, v in ipairs (images) do if v.RepoTags then o:value(v.RepoTags[1], v.RepoTags[1]) end end o = s:option(Flag, "_force_pull", translate("Always pull image first")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = 0 o = s:option(Flag, "privileged", translate("Privileged")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = default_config.privileged and 1 or 0 o = s:option(ListValue, "restart", translate("Restart Policy")) o.rmempty = true o:value("no", "No") o:value("unless-stopped", "Unless stopped") o:value("always", "Always") o:value("on-failure", "On failure") o.default = default_config.restart or "unless-stopped" local d_network = s:option(ListValue, "network", translate("Networks")) d_network.rmempty = true d_network.default = default_config.network or "bridge" local d_ip = s:option(Value, "ip", translate("IPv4 Address")) d_ip.datatype="ip4addr" d_ip:depends("network", "nil") d_ip.default = default_config.ip or nil o = s:option(DynamicList, "link", translate("Links with other containers")) o.placeholder = "container_name:alias" o.rmempty = true o:depends("network", "bridge") o.default = default_config.link or nil o = s:option(DynamicList, "dns", translate("Set custom DNS servers")) o.placeholder = "8.8.8.8" o.rmempty = true o.default = default_config.dns or nil o = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])")) o.placeholder = "1000:1000" o.rmempty = true o.default = default_config.user or nil o = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container")) o.placeholder = "TZ=Asia/Shanghai" o.rmempty = true o.default = default_config.env or nil o = s:option(DynamicList, "volume", translate("Bind Mount(-v)"), translate("Bind mount a volume")) o.placeholder = "/media:/media:slave" o.rmempty = true o.default = default_config.volume or nil local d_publish = s:option(DynamicList, "publish", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host")) d_publish.placeholder = "2200:22/tcp" d_publish.rmempty = true d_publish.default = default_config.publish or nil o = s:option(Value, "command", translate("Run command")) o.placeholder = "/bin/sh init.sh" o.rmempty = true o.default = default_config.command or nil o = s:option(Flag, "advance", translate("Advance")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = default_config.advance or 0 o = s:option(Value, "hostname", translate("Host Name"), translate("The hostname to use for the container")) o.rmempty = true o.default = default_config.hostname or nil o:depends("advance", 1) o = s:option(Flag, "publish_all", translate("Exposed All Ports(-P)"), translate("Allocates an ephemeral host port for all of a container's exposed ports")) o.rmempty = true o.disabled = 0 o.enabled = 1 o.default = default_config.publish_all and 1 or 0 o:depends("advance", 1) o = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container")) o.placeholder = "/dev/sda:/dev/xvdc:rwm" o.rmempty = true o:depends("advance", 1) o.default = default_config.device or nil o = s:option(DynamicList, "tmpfs", translate("Tmpfs(--tmpfs)"), translate("Mount tmpfs directory")) o.placeholder = "/run:rw,noexec,nosuid,size=65536k" o.rmempty = true o:depends("advance", 1) o.default = default_config.tmpfs or nil o = s:option(DynamicList, "sysctl", translate("Sysctl(--sysctl)"), translate("Sysctls (kernel parameters) options")) o.placeholder = "net.ipv4.ip_forward=1" o.rmempty = true o:depends("advance", 1) o.default = default_config.sysctl or nil o = s:option(DynamicList, "cap_add", translate("CAP-ADD(--cap-add)"), translate("A list of kernel capabilities to add to the container")) o.placeholder = "NET_ADMIN" o.rmempty = true o:depends("advance", 1) o.default = default_config.cap_add or nil o = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit")) o.placeholder = "1.5" o.rmempty = true o:depends("advance", 1) o.datatype="ufloat" o.default = default_config.cpus or nil o = s:option(Value, "cpu_shares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024")) o.placeholder = "1024" o.rmempty = true o:depends("advance", 1) o.datatype="uinteger" o.default = default_config.cpu_shares or nil o = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: []). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M")) o.placeholder = "128m" o.rmempty = true o:depends("advance", 1) o.default = default_config.memory or nil o = s:option(Value, "blkio_weight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000")) o.placeholder = "500" o.rmempty = true o:depends("advance", 1) o.datatype="uinteger" o.default = default_config.blkio_weight or nil o = s:option(DynamicList, "log_opt", translate("Log driver options"), translate("The logging configuration for this container")) o.placeholder = "max-size=1m" o.rmempty = true o:depends("advance", 1) o.default = default_config.log_opt or nil for _, v in ipairs (networks) do if v.Name then local parent = v.Options and v.Options.parent or nil local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "") d_network:value(v.Name, network_name) if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then d_ip:depends("network", v.Name) end if v.Driver == "bridge" then d_publish:depends("network", v.Name) end end end m.handle = function(self, state, data) if state ~= FORM_VALID then return end local tmp local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S")) local hostname = data.hostname local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false local image = data.image local user = data.user if image and not image:match(".-:.+") then image = image .. ":latest" end local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false local restart = data.restart local env = data.env local dns = data.dns local cap_add = data.cap_add local sysctl = {} tmp = data.sysctl if type(tmp) == "table" then for i, v in ipairs(tmp) do local k,v1 = v:match("(.-)=(.+)") if k and v1 then sysctl[k]=v1 end end end local log_opt = {} tmp = data.log_opt if type(tmp) == "table" then for i, v in ipairs(tmp) do local k,v1 = v:match("(.-)=(.+)") if k and v1 then log_opt[k]=v1 end end end local network = data.network local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil local volume = data.volume local memory = data.memory or 0 local cpu_shares = data.cpu_shares or 0 local cpus = data.cpus or 0 local blkio_weight = data.blkio_weight or 500 local portbindings = {} local exposedports = {} local tmpfs = {} tmp = data.tmpfs if type(tmp) == "table" then for i, v in ipairs(tmp)do local k= v:match("([^:]+)") local v1 = v:match(".-:([^:]+)") or "" if k then tmpfs[k]=v1 end end end local device = {} tmp = data.device if type(tmp) == "table" then for i, v in ipairs(tmp) do local t = {} local _,_, h, c, p = v:find("(.-):(.-):(.+)") if h and c then t['PathOnHost'] = h t['PathInContainer'] = c t['CgroupPermissions'] = p or "rwm" else local _,_, h, c = v:find("(.-):(.+)") if h and c then t['PathOnHost'] = h t['PathInContainer'] = c t['CgroupPermissions'] = "rwm" else t['PathOnHost'] = v t['PathInContainer'] = v t['CgroupPermissions'] = "rwm" end end if next(t) ~= nil then table.insert( device, t ) end end end tmp = data.publish or {} for i, v in ipairs(tmp) do for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do local _,_,p= v2:find("^%d+/(%w+)") if p == nil then v2=v2..'/tcp' end portbindings[v2] = {{HostPort=v1}} exposedports[v2] = {HostPort=v1} end end local link = data.link tmp = data.command local command = {} if tmp ~= nil then for v in string.gmatch(tmp, "[^%s]+") do command[#command+1] = v end end if memory ~= 0 then _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)") if n then unit = unit and unit:sub(1,1):upper() or "B" if unit == "M" then memory = tonumber(n) * 1024 * 1024 elseif unit == "G" then memory = tonumber(n) * 1024 * 1024 * 1024 elseif unit == "K" then memory = tonumber(n) * 1024 else memory = tonumber(n) end end end create_body.Hostname = network ~= "host" and (hostname or name) or nil create_body.Tty = tty and true or false create_body.OpenStdin = interactive and true or false create_body.User = user create_body.Cmd = command create_body.Env = env create_body.Image = image create_body.ExposedPorts = exposedports create_body.HostConfig = create_body.HostConfig or {} create_body.HostConfig.Dns = dns create_body.HostConfig.Binds = volume create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 } create_body.HostConfig.Privileged = privileged and true or false create_body.HostConfig.PortBindings = portbindings create_body.HostConfig.Memory = tonumber(memory) create_body.HostConfig.CpuShares = tonumber(cpu_shares) create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9 create_body.HostConfig.BlkioWeight = tonumber(blkio_weight) create_body.HostConfig.PublishAllPorts = publish_all if create_body.HostConfig.NetworkMode ~= network then create_body.NetworkingConfig = nil end create_body.HostConfig.NetworkMode = network if ip then if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then v.IPAMConfig.IPv4Address = ip else create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } end break end else create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } } end elseif not create_body.NetworkingConfig then create_body.NetworkingConfig = nil end create_body["HostConfig"]["Tmpfs"] = tmpfs create_body["HostConfig"]["Devices"] = device create_body["HostConfig"]["Sysctls"] = sysctl create_body["HostConfig"]["CapAdd"] = cap_add create_body["HostConfig"]["LogConfig"] = next(log_opt) ~= nil and { Config = log_opt } or nil if network == "bridge" then create_body["HostConfig"]["Links"] = link end local pull_image = function(image) local json_stringify = luci.jsonc and luci.jsonc.stringify docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n") local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb) if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then docker:append_status("done\n") else res.code = (res.code == 200) and 500 or res.code docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n") luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer")) end end docker:clear_status() local exist_image = false if image then for _, v in ipairs (images) do if v.RepoTags and v.RepoTags[1] == image then exist_image = true break end end if not exist_image then pull_image(image) elseif data._force_pull == 1 then pull_image(image) end end create_body = docker.clear_empty_tables(create_body) docker:append_status("Container: " .. "create" .. " " .. name .. "...") local res = dk.containers:create({name = name, body = create_body}) if res and res.code == 201 then docker:clear_status() luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers")) else docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer")) end end return m