dockerman.lua 13 KB

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