dockerman.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. --[[
  2. LuCI - Lua Configuration Interface
  3. Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
  4. ]]--
  5. require "luci.util"
  6. local docker = require "luci.model.docker"
  7. -- local uci = require "luci.model.uci"
  8. module("luci.controller.dockerman",package.seeall)
  9. function index()
  10. local e = entry({"admin", "docker"}, firstchild(), "Docker", 40)
  11. e.dependent = false
  12. e.acl_depends = { "luci-app-dockerman" }
  13. entry({"admin","docker","overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true
  14. local remote = luci.model.uci.cursor():get("dockerman", "local", "remote_endpoint")
  15. if remote == nil then
  16. local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path")
  17. if socket and not nixio.fs.access(socket) then return end
  18. elseif remote == "true" then
  19. local host = luci.model.uci.cursor():get("dockerman", "local", "remote_host")
  20. local port = luci.model.uci.cursor():get("dockerman", "local", "remote_port")
  21. if not host or not port then return end
  22. end
  23. if (require "luci.model.docker").new():_ping().code ~= 200 then return end
  24. entry({"admin","docker","containers"},form("dockerman/containers"),_("Containers"),1).leaf=true
  25. entry({"admin","docker","images"},form("dockerman/images"),_("Images"),2).leaf=true
  26. entry({"admin","docker","networks"},form("dockerman/networks"),_("Networks"),3).leaf=true
  27. entry({"admin","docker","volumes"},form("dockerman/volumes"),_("Volumes"),4).leaf=true
  28. entry({"admin","docker","events"},call("action_events"),_("Events"),5)
  29. entry({"admin","docker","newcontainer"},form("dockerman/newcontainer")).leaf=true
  30. entry({"admin","docker","newnetwork"},form("dockerman/newnetwork")).leaf=true
  31. entry({"admin","docker","container"},form("dockerman/container")).leaf=true
  32. entry({"admin","docker","container_stats"},call("action_get_container_stats")).leaf=true
  33. entry({"admin","docker","container_get_archive"},call("download_archive")).leaf=true
  34. entry({"admin","docker","container_put_archive"},call("upload_archive")).leaf=true
  35. entry({"admin","docker","images_save"},call("save_images")).leaf=true
  36. entry({"admin","docker","images_load"},call("load_images")).leaf=true
  37. entry({"admin","docker","images_import"},call("import_images")).leaf=true
  38. entry({"admin","docker","images_get_tags"},call("get_image_tags")).leaf=true
  39. entry({"admin","docker","images_tag"},call("tag_image")).leaf=true
  40. entry({"admin","docker","images_untag"},call("untag_image")).leaf=true
  41. entry({"admin","docker","confirm"},call("action_confirm")).leaf=true
  42. end
  43. function action_events()
  44. local logs = ""
  45. local dk = docker.new()
  46. local query ={}
  47. query["until"] = os.time()
  48. local events = dk:events({query = query})
  49. if events.code == 200 then
  50. for _, v in ipairs(events.body) do
  51. if v and v.Type == "container" then
  52. logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. (v.Action or "null") .. " Container ID:".. (v.Actor.ID or "null") .. " Container Name:" .. (v.Actor.Attributes.name or "null")
  53. elseif v.Type == "network" then
  54. logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Container ID:"..( v.Actor.Attributes.container or "null" ) .. " Network Name:" .. (v.Actor.Attributes.name or "null") .. " Network type:".. v.Actor.Attributes.type or ""
  55. elseif v.Type == "image" then
  56. logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Image:".. (v.Actor.ID or "null").. " Image Name:" .. (v.Actor.Attributes.name or "null")
  57. end
  58. end
  59. end
  60. luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
  61. end
  62. local calculate_cpu_percent = function(d)
  63. if type(d) ~= "table" then return end
  64. cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
  65. cpu_percent = 0.0
  66. cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
  67. system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) - tonumber(d["precpu_stats"]["system_cpu_usage"])
  68. if system_delta > 0.0 then
  69. cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
  70. end
  71. -- return cpu_percent .. "%"
  72. return cpu_percent
  73. end
  74. local get_memory = function(d)
  75. if type(d) ~= "table" then return end
  76. -- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024)
  77. -- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024)
  78. -- return usage .. "MB / " .. limit.. "MB"
  79. local limit =tonumber(d["memory_stats"]["limit"])
  80. local usage = tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])
  81. return usage, limit
  82. end
  83. local get_rx_tx = function(d)
  84. if type(d) ~="table" then return end
  85. -- local data
  86. -- if type(d["networks"]) == "table" then
  87. -- for e, v in pairs(d["networks"]) do
  88. -- data = (data and (data .. "<br>") or "") .. e .. " Total Tx:" .. string.format("%.2f",(tonumber(v.tx_bytes)/1024/1024)) .. "MB Total Rx: ".. string.format("%.2f",(tonumber(v.rx_bytes)/1024/1024)) .. "MB"
  89. -- end
  90. -- end
  91. local data = {}
  92. if type(d["networks"]) == "table" then
  93. for e, v in pairs(d["networks"]) do
  94. data[e] = {
  95. bw_tx = tonumber(v.tx_bytes),
  96. bw_rx = tonumber(v.rx_bytes)
  97. }
  98. end
  99. end
  100. return data
  101. end
  102. function action_get_container_stats(container_id)
  103. if container_id then
  104. local dk = docker.new()
  105. local response = dk.containers:inspect({id = container_id})
  106. if response.code == 200 and response.body.State.Running then
  107. response = dk.containers:stats({id = container_id, query = {stream = false}})
  108. if response.code == 200 then
  109. local container_stats = response.body
  110. local cpu_percent = calculate_cpu_percent(container_stats)
  111. local mem_useage, mem_limit = get_memory(container_stats)
  112. local bw_rxtx = get_rx_tx(container_stats)
  113. luci.http.status(response.code, response.body.message)
  114. luci.http.prepare_content("application/json")
  115. luci.http.write_json({
  116. cpu_percent = cpu_percent,
  117. memory = {
  118. mem_useage = mem_useage,
  119. mem_limit = mem_limit
  120. },
  121. bw_rxtx = bw_rxtx
  122. })
  123. else
  124. luci.http.status(response.code, response.body.message)
  125. luci.http.prepare_content("text/plain")
  126. luci.http.write(response.body.message)
  127. end
  128. else
  129. if response.code == 200 then
  130. luci.http.status(500, "container "..container_id.." not running")
  131. luci.http.prepare_content("text/plain")
  132. luci.http.write("Container "..container_id.." not running")
  133. else
  134. luci.http.status(response.code, response.body.message)
  135. luci.http.prepare_content("text/plain")
  136. luci.http.write(response.body.message)
  137. end
  138. end
  139. else
  140. luci.http.status(404, "No container name or id")
  141. luci.http.prepare_content("text/plain")
  142. luci.http.write("No container name or id")
  143. end
  144. end
  145. function action_confirm()
  146. local data = docker:read_status()
  147. if data then
  148. data = data:gsub("\n","<br>"):gsub(" ","&nbsp;")
  149. code = 202
  150. msg = data
  151. else
  152. code = 200
  153. msg = "finish"
  154. data = "finish"
  155. end
  156. luci.http.status(code, msg)
  157. luci.http.prepare_content("application/json")
  158. luci.http.write_json({info = data})
  159. end
  160. function download_archive()
  161. local id = luci.http.formvalue("id")
  162. local path = luci.http.formvalue("path")
  163. local dk = docker.new()
  164. local first
  165. local cb = function(res, chunk)
  166. if res.code == 200 then
  167. if not first then
  168. first = true
  169. luci.http.header('Content-Disposition', 'inline; filename="archive.tar"')
  170. luci.http.header('Content-Type', 'application\/x-tar')
  171. end
  172. luci.ltn12.pump.all(chunk, luci.http.write)
  173. else
  174. if not first then
  175. first = true
  176. luci.http.prepare_content("text/plain")
  177. end
  178. luci.ltn12.pump.all(chunk, luci.http.write)
  179. end
  180. end
  181. local res = dk.containers:get_archive({id = id, query = {path = path}}, cb)
  182. end
  183. function upload_archive(container_id)
  184. local path = luci.http.formvalue("upload-path")
  185. local dk = docker.new()
  186. local ltn12 = require "luci.ltn12"
  187. local rec_send = function(sinkout)
  188. luci.http.setfilehandler(function (meta, chunk, eof)
  189. if chunk then
  190. ltn12.pump.step(ltn12.source.string(chunk), sinkout)
  191. end
  192. end)
  193. end
  194. local res = dk.containers:put_archive({id = container_id, query = {path = path}, body = rec_send})
  195. local msg = res and res.body and res.body.message or nil
  196. luci.http.status(res.code, msg)
  197. luci.http.prepare_content("application/json")
  198. luci.http.write_json({message = msg})
  199. end
  200. function save_images(container_id)
  201. local names = luci.http.formvalue("names")
  202. local dk = docker.new()
  203. local first
  204. local cb = function(res, chunk)
  205. if res.code == 200 then
  206. if not first then
  207. first = true
  208. luci.http.status(res.code, res.message)
  209. luci.http.header('Content-Disposition', 'inline; filename="images.tar"')
  210. luci.http.header('Content-Type', 'application\/x-tar')
  211. end
  212. luci.ltn12.pump.all(chunk, luci.http.write)
  213. else
  214. if not first then
  215. first = true
  216. luci.http.prepare_content("text/plain")
  217. end
  218. luci.ltn12.pump.all(chunk, luci.http.write)
  219. end
  220. end
  221. docker:write_status("Images: saving" .. " " .. container_id .. "...")
  222. local res = dk.images:get({id = container_id, query = {names = names}}, cb)
  223. docker:clear_status()
  224. local msg = res and res.body and res.body.message or nil
  225. luci.http.status(res.code, msg)
  226. luci.http.prepare_content("application/json")
  227. luci.http.write_json({message = msg})
  228. end
  229. function load_images()
  230. local path = luci.http.formvalue("upload-path")
  231. local dk = docker.new()
  232. local ltn12 = require "luci.ltn12"
  233. local rec_send = function(sinkout)
  234. luci.http.setfilehandler(function (meta, chunk, eof)
  235. if chunk then
  236. ltn12.pump.step(ltn12.source.string(chunk), sinkout)
  237. end
  238. end)
  239. end
  240. docker:write_status("Images: loading...")
  241. local res = dk.images:load({body = rec_send})
  242. -- res.body = {"stream":"Loaded image ID: sha256:1399d3d81f80d68832e85ed6ba5f94436ca17966539ba715f661bd36f3caf08f\n"}
  243. local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error)or nil
  244. if res.code == 200 and msg and msg:match("Loaded image ID") then
  245. docker:clear_status()
  246. luci.http.status(res.code, msg)
  247. else
  248. docker:append_status("code:" .. res.code.." ".. msg)
  249. luci.http.status(300, msg)
  250. end
  251. luci.http.prepare_content("application/json")
  252. luci.http.write_json({message = msg})
  253. end
  254. function import_images()
  255. local src = luci.http.formvalue("src")
  256. local itag = luci.http.formvalue("tag")
  257. local dk = docker.new()
  258. local ltn12 = require "luci.ltn12"
  259. local rec_send = function(sinkout)
  260. luci.http.setfilehandler(function (meta, chunk, eof)
  261. if chunk then
  262. ltn12.pump.step(ltn12.source.string(chunk), sinkout)
  263. end
  264. end)
  265. end
  266. docker:write_status("Images: importing".. " ".. itag .."...\n")
  267. local repo = itag and itag:match("^([^:]+)")
  268. local tag = itag and itag:match("^[^:]-:([^:]+)")
  269. local res = dk.images:create({query = {fromSrc = src or "-", repo = repo or nil, tag = tag or nil }, body = not src and rec_send or nil}, docker.import_image_show_status_cb)
  270. local msg = res and res.body and ( res.body.message )or nil
  271. if not msg and #res.body == 0 then
  272. -- res.body = {"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
  273. msg = res.body.status or res.body.error
  274. elseif not msg and #res.body >= 1 then
  275. -- res.body = [...{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}]
  276. msg = res.body[#res.body].status or res.body[#res.body].error
  277. end
  278. if res.code == 200 and msg and msg:match("sha256:") then
  279. docker:clear_status()
  280. else
  281. docker:append_status("code:" .. res.code.." ".. msg)
  282. end
  283. luci.http.status(res.code, msg)
  284. luci.http.prepare_content("application/json")
  285. luci.http.write_json({message = msg})
  286. end
  287. function get_image_tags(image_id)
  288. if not image_id then
  289. luci.http.status(400, "no image id")
  290. luci.http.prepare_content("application/json")
  291. luci.http.write_json({message = "no image id"})
  292. return
  293. end
  294. local dk = docker.new()
  295. local res = dk.images:inspect({id = image_id})
  296. local msg = res and res.body and res.body.message or nil
  297. luci.http.status(res.code, msg)
  298. luci.http.prepare_content("application/json")
  299. if res.code == 200 then
  300. local tags = res.body.RepoTags
  301. luci.http.write_json({tags = tags})
  302. else
  303. local msg = res and res.body and res.body.message or nil
  304. luci.http.write_json({message = msg})
  305. end
  306. end
  307. function tag_image(image_id)
  308. local src = luci.http.formvalue("tag")
  309. local image_id = image_id or luci.http.formvalue("id")
  310. if type(src) ~= "string" or not image_id then
  311. luci.http.status(400, "no image id or tag")
  312. luci.http.prepare_content("application/json")
  313. luci.http.write_json({message = "no image id or tag"})
  314. return
  315. end
  316. local repo = src:match("^([^:]+)")
  317. local tag = src:match("^[^:]-:([^:]+)")
  318. local dk = docker.new()
  319. local res = dk.images:tag({id = image_id, query={repo=repo, tag=tag}})
  320. local msg = res and res.body and res.body.message or nil
  321. luci.http.status(res.code, msg)
  322. luci.http.prepare_content("application/json")
  323. if res.code == 201 then
  324. local tags = res.body.RepoTags
  325. luci.http.write_json({tags = tags})
  326. else
  327. local msg = res and res.body and res.body.message or nil
  328. luci.http.write_json({message = msg})
  329. end
  330. end
  331. function untag_image(tag)
  332. local tag = tag or luci.http.formvalue("tag")
  333. if not tag then
  334. luci.http.status(400, "no tag name")
  335. luci.http.prepare_content("application/json")
  336. luci.http.write_json({message = "no tag name"})
  337. return
  338. end
  339. local dk = docker.new()
  340. local res = dk.images:inspect({name = tag})
  341. if res.code == 200 then
  342. local tags = res.body.RepoTags
  343. if #tags > 1 then
  344. local r = dk.images:remove({name = tag})
  345. local msg = r and r.body and r.body.message or nil
  346. luci.http.status(r.code, msg)
  347. luci.http.prepare_content("application/json")
  348. luci.http.write_json({message = msg})
  349. else
  350. luci.http.status(500, "Cannot remove the last tag")
  351. luci.http.prepare_content("application/json")
  352. luci.http.write_json({message = "Cannot remove the last tag"})
  353. end
  354. else
  355. local msg = res and res.body and res.body.message or nil
  356. luci.http.status(res.code, msg)
  357. luci.http.prepare_content("application/json")
  358. luci.http.write_json({message = msg})
  359. end
  360. end