62f00e71004b9179f310e87672a863ed27faa299
[oweals/luci.git] / applications / luci-app-dockerman / luasrc / model / cbi / dockerman / container.lua
1 --[[
2 LuCI - Lua Configuration Interface
3 Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
4 ]]--
5
6 require "luci.util"
7 local docker = require "luci.model.docker"
8 local dk = docker.new()
9 container_id = arg[1]
10 local action = arg[2] or "info"
11
12 local images, networks, container_info
13 if not container_id then return end
14 local res = dk.containers:inspect({id = container_id})
15 if res.code < 300 then container_info = res.body else return end
16 res = dk.networks:list()
17 if res.code < 300 then networks = res.body else return end
18
19 local get_ports = function(d)
20   local data
21   if d.HostConfig and d.HostConfig.PortBindings then
22     for inter, out in pairs(d.HostConfig.PortBindings) do
23       data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
24     end
25   end
26   return data
27 end
28
29 local get_env = function(d)
30   local data
31   if d.Config and d.Config.Env then
32     for _,v in ipairs(d.Config.Env) do
33       data = (data and (data .. "<br>") or "") .. v
34     end
35   end
36   return data
37 end
38
39 local get_command = function(d)
40   local data
41   if d.Config and d.Config.Cmd then
42     for _,v in ipairs(d.Config.Cmd) do
43       data = (data and (data .. " ") or "") .. v
44     end
45   end
46   return data
47 end
48
49 local get_mounts = function(d)
50   local data
51   if d.Mounts then
52     for _,v in ipairs(d.Mounts) do
53       local v_sorce_d, v_dest_d
54       local v_sorce = ""
55       local v_dest = ""
56       for v_sorce_d in v["Source"]:gmatch('[^/]+') do
57         if v_sorce_d and #v_sorce_d > 12 then
58           v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..."
59         else
60           v_sorce = v_sorce .."/".. v_sorce_d
61         end
62       end
63       for v_dest_d in v["Destination"]:gmatch('[^/]+') do
64         if v_dest_d and #v_dest_d > 12 then
65           v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..."
66         else
67           v_dest = v_dest .."/".. v_dest_d
68         end
69       end
70       data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
71     end
72   end
73   return data
74 end
75
76 local get_device = function(d)
77   local data
78   if d.HostConfig and d.HostConfig.Devices then
79     for _,v in ipairs(d.HostConfig.Devices) do
80       data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
81     end
82   end
83   return data
84 end
85
86 local get_links = function(d)
87   local data
88   if d.HostConfig and d.HostConfig.Links then
89     for _,v in ipairs(d.HostConfig.Links) do
90       data = (data and (data .. "<br>") or "") .. v
91     end
92   end
93   return data
94 end
95
96 local get_tmpfs = function(d)
97   local data
98   if d.HostConfig and d.HostConfig.Tmpfs then
99     for k, v in pairs(d.HostConfig.Tmpfs) do
100       data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
101     end
102   end
103   return data
104 end
105
106 local get_dns = function(d)
107   local data
108   if d.HostConfig and d.HostConfig.Dns then
109     for _, v in ipairs(d.HostConfig.Dns) do
110       data = (data and (data .. "<br>") or "") .. v
111     end
112   end
113   return data
114 end
115
116 local get_sysctl = function(d)
117   local data
118   if d.HostConfig and d.HostConfig.Sysctls then
119     for k, v in pairs(d.HostConfig.Sysctls) do
120       data = (data and (data .. "<br>") or "") .. k..":"..v
121     end
122   end
123   return data
124 end
125
126 local get_networks = function(d)
127   local data={}
128   if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
129     for k,v in pairs(d.NetworkSettings.Networks) do
130       data[k] = v.IPAddress or ""
131     end
132   end
133   return data
134 end
135
136
137 local start_stop_remove = function(m, cmd)
138   docker:clear_status()
139   docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
140   local res
141   if cmd ~= "upgrade" then
142     res = dk.containers[cmd](dk, {id = container_id})
143   else
144     res = dk.containers_upgrade(dk, {id = container_id})
145   end
146   if res and res.code >= 300 then
147     docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
148     luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
149   else
150     docker:clear_status()
151     if cmd ~= "remove" and cmd ~= "upgrade" then
152       luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
153     else
154       luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
155     end
156   end
157 end
158
159 m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") )
160 m.redirect = luci.dispatcher.build_url("admin/docker/containers")
161 -- m:append(Template("dockerman/container"))
162 docker_status = m:section(SimpleSection)
163 docker_status.template = "dockerman/apply_widget"
164 docker_status.err=docker:read_status()
165 docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
166 if docker_status.err then docker:clear_status() end
167
168
169 action_section = m:section(Table,{{}})
170 action_section.notitle=true
171 action_section.rowcolors=false
172 action_section.template = "cbi/nullsection"
173
174 btnstart=action_section:option(Button, "_start")
175 btnstart.template = "dockerman/cbi/inlinebutton"
176 btnstart.inputtitle=translate("Start")
177 btnstart.inputstyle = "apply"
178 btnstart.forcewrite = true
179 btnrestart=action_section:option(Button, "_restart")
180 btnrestart.template = "dockerman/cbi/inlinebutton"
181 btnrestart.inputtitle=translate("Restart")
182 btnrestart.inputstyle = "reload"
183 btnrestart.forcewrite = true
184 btnstop=action_section:option(Button, "_stop")
185 btnstop.template = "dockerman/cbi/inlinebutton"
186 btnstop.inputtitle=translate("Stop")
187 btnstop.inputstyle = "reset"
188 btnstop.forcewrite = true
189 btnkill=action_section:option(Button, "_kill")
190 btnkill.template = "dockerman/cbi/inlinebutton"
191 btnkill.inputtitle=translate("Kill")
192 btnkill.inputstyle = "reset"
193 btnkill.forcewrite = true
194 btnupgrade=action_section:option(Button, "_upgrade")
195 btnupgrade.template = "dockerman/cbi/inlinebutton"
196 btnupgrade.inputtitle=translate("Upgrade")
197 btnupgrade.inputstyle = "reload"
198 btnstop.forcewrite = true
199 btnduplicate=action_section:option(Button, "_duplicate")
200 btnduplicate.template = "dockerman/cbi/inlinebutton"
201 btnduplicate.inputtitle=translate("Duplicate/Edit")
202 btnduplicate.inputstyle = "add"
203 btnstop.forcewrite = true
204 btnremove=action_section:option(Button, "_remove")
205 btnremove.template = "dockerman/cbi/inlinebutton"
206 btnremove.inputtitle=translate("Remove")
207 btnremove.inputstyle = "remove"
208 btnremove.forcewrite = true
209
210 btnstart.write = function(self, section)
211   start_stop_remove(m,"start")
212 end
213 btnrestart.write = function(self, section)
214   start_stop_remove(m,"restart")
215 end
216 btnupgrade.write = function(self, section)
217   start_stop_remove(m,"upgrade")
218 end
219 btnremove.write = function(self, section)
220   start_stop_remove(m,"remove")
221 end
222 btnstop.write = function(self, section)
223   start_stop_remove(m,"stop")
224 end
225 btnkill.write = function(self, section)
226   start_stop_remove(m,"kill")
227 end
228 btnduplicate.write = function(self, section)
229   luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
230 end
231
232 tab_section = m:section(SimpleSection)
233 tab_section.template = "dockerman/container"
234
235 if action == "info" then 
236   m.submit = false
237   m.reset  = false
238   table_info = {
239     ["01name"] = {_key = translate("Name"),  _value = container_info.Name:sub(2)  or "-", _button=translate("Update")},
240     ["02id"] = {_key = translate("ID"),  _value = container_info.Id  or "-"},
241     ["03image"] = {_key = translate("Image"),  _value = container_info.Config.Image .. "<br>" .. container_info.Image},
242     ["04status"] = {_key = translate("Status"),  _value = container_info.State and container_info.State.Status  or "-"},
243     ["05created"] = {_key = translate("Created"),  _value = container_info.Created  or "-"},
244   }
245   table_info["06start"] = container_info.State.Status == "running" and {_key = translate("Start Time"),  _value = container_info.State and container_info.State.StartedAt or "-"} or {_key = translate("Finish Time"),  _value = container_info.State and container_info.State.FinishedAt or "-"}
246   table_info["07healthy"] = {_key = translate("Healthy"),  _value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"}
247   table_info["08restart"] = {_key = translate("Restart Policy"),  _value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-", _button=translate("Update")}
248   table_info["081user"] = {_key = translate("User"),  _value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"}
249   table_info["09mount"] = {_key = translate("Mount/Volume"),  _value = get_mounts(container_info)  or "-"}
250   table_info["10cmd"] = {_key = translate("Command"),  _value = get_command(container_info) or "-"}
251   table_info["11env"] = {_key = translate("Env"),  _value = get_env(container_info)  or "-"}
252   table_info["12ports"] = {_key = translate("Ports"),  _value = get_ports(container_info) or "-"}
253   table_info["13links"] = {_key = translate("Links"),  _value = get_links(container_info)  or "-"}
254   table_info["14device"] = {_key = translate("Device"),  _value = get_device(container_info)  or "-"}
255   table_info["15tmpfs"] = {_key = translate("Tmpfs"),  _value = get_tmpfs(container_info)  or "-"}
256   table_info["16dns"] = {_key = translate("DNS"),  _value = get_dns(container_info)  or "-"}
257   table_info["17sysctl"] = {_key = translate("Sysctl"),  _value = get_sysctl(container_info)  or "-"}
258   info_networks = get_networks(container_info)
259   list_networks = {}
260   for _, v in ipairs (networks) do
261     if v.Name then
262       local parent = v.Options and v.Options.parent or nil
263       local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
264       ipv6 =  v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
265       local network_name = v.Name .. " | " .. v.Driver  .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
266       list_networks[v.Name] = network_name
267     end
268   end
269
270   if type(info_networks)== "table" then
271     for k,v in pairs(info_networks) do
272       table_info["14network"..k] = {
273         _key = translate("Network"),  _value = k.. (v~="" and (" | ".. v) or ""), _button=translate("Disconnect")
274       }
275       list_networks[k]=nil
276     end
277   end
278
279   table_info["15connect"] = {_key = translate("Connect Network"),  _value = list_networks ,_opts = "", _button=translate("Connect")}
280
281
282   d_info = m:section(Table,table_info)
283   d_info.nodescr=true
284   d_info.formvalue=function(self, section)
285     return table_info
286   end
287   dv_key = d_info:option(DummyValue, "_key", translate("Info"))
288   dv_key.width = "20%"
289   dv_value = d_info:option(ListValue, "_value")
290   dv_value.render = function(self, section, scope)
291     if table_info[section]._key == translate("Name") then
292       self:reset_values()
293       self.template = "cbi/value"
294       self.size = 30
295       self.keylist = {}
296       self.vallist = {}
297       self.default=table_info[section]._value
298       Value.render(self, section, scope)
299     elseif table_info[section]._key == translate("Restart Policy") then
300       self.template = "cbi/lvalue"
301       self:reset_values()
302       self.size = nil
303       self:value("no", "No")
304       self:value("unless-stopped", "Unless stopped")
305       self:value("always", "Always")
306       self:value("on-failure", "On failure")
307       self.default=table_info[section]._value
308       ListValue.render(self, section, scope)
309     elseif table_info[section]._key == translate("Connect Network") then
310       self.template = "cbi/lvalue"
311       self:reset_values()
312       self.size = nil
313       for k,v in pairs(list_networks) do
314         if k ~= "host" then
315           self:value(k,v)
316         end
317       end
318       self.default=table_info[section]._value
319       ListValue.render(self, section, scope)
320     else
321       self:reset_values()
322       self.rawhtml=true
323       self.template = "cbi/dvalue"
324       self.default=table_info[section]._value
325       DummyValue.render(self, section, scope)
326     end
327   end
328   dv_value.forcewrite = true -- for write function using simpleform 
329   dv_value.write = function(self, section, value)
330     table_info[section]._value=value
331   end
332   dv_value.validate = function(self, value)
333     return value
334   end
335   dv_opts = d_info:option(Value, "_opts")
336   dv_opts.forcewrite = true -- for write function using simpleform 
337   dv_opts.write = function(self, section, value)
338
339     table_info[section]._opts=value
340   end
341   dv_opts.validate = function(self, value)
342     return value
343   end
344   dv_opts.render = function(self, section, scope)
345     if table_info[section]._key==translate("Connect Network") then
346       self.template = "cbi/value"
347       self.keylist = {}
348       self.vallist = {}
349       self.placeholder = "10.1.1.254"
350       self.datatype = "ip4addr"
351       self.default=table_info[section]._opts
352       Value.render(self, section, scope)
353     else
354       self.rawhtml=true
355       self.template = "cbi/dvalue"
356       self.default=table_info[section]._opts
357       DummyValue.render(self, section, scope)
358     end
359   end
360   btn_update = d_info:option(Button, "_button")
361   btn_update.forcewrite = true
362   btn_update.render = function(self, section, scope)
363     if table_info[section]._button and table_info[section]._value ~= nil then
364       btn_update.inputtitle=table_info[section]._button
365       self.template = "cbi/button"
366       self.inputstyle = "edit"
367       Button.render(self, section, scope)
368     else 
369       self.template = "cbi/dvalue"
370       self.default=""
371       DummyValue.render(self, section, scope)
372     end
373   end
374   btn_update.write = function(self, section, value)
375     local res
376     docker:clear_status()
377     if section == "01name" then
378       docker:append_status("Containers: rename " .. container_id .. "...")
379       local new_name = table_info[section]._value
380       res = dk.containers:rename({id = container_id, query = {name=new_name}})
381     elseif section == "08restart" then
382       docker:append_status("Containers: update " .. container_id .. "...")
383       local new_restart = table_info[section]._value
384       res = dk.containers:update({id = container_id, body = {RestartPolicy = {Name = new_restart}}})
385     elseif table_info[section]._key == translate("Network") then
386       local _,_,leave_network = table_info[section]._value:find("(.-) | .+")
387       leave_network = leave_network or table_info[section]._value
388       docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
389       res = dk.networks:disconnect({name = leave_network, body = {Container = container_id}})
390     elseif section == "15connect" then
391       local connect_network = table_info[section]._value
392       local network_opiton
393       if connect_network ~= "none" and connect_network ~= "bridge" and connect_network ~= "host" then
394         network_opiton = table_info[section]._opts ~= "" and {
395             IPAMConfig={
396               IPv4Address=table_info[section]._opts
397             }
398         } or nil
399       end
400       docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
401       res = dk.networks:connect({name = connect_network, body = {Container = container_id, EndpointConfig= network_opiton}})
402     end
403     if res and res.code > 300 then
404       docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
405     else
406       docker:clear_status()
407     end
408     luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
409   end
410   
411 -- info end
412 elseif action == "resources" then
413   local resources_section= m:section(SimpleSection)
414   d = resources_section:option( Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
415   d.placeholder = "1.5"
416   d.rmempty = true
417   d.datatype="ufloat"
418   d.default = container_info.HostConfig.NanoCpus / (10^9)
419
420   d = resources_section:option(Value, "cpushares", 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."))
421   d.placeholder = "1024"
422   d.rmempty = true
423   d.datatype="uinteger"
424   d.default = container_info.HostConfig.CpuShares
425
426   d = resources_section:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
427   d.placeholder = "128m"
428   d.rmempty = true
429   d.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
430
431   d = resources_section:option(Value, "blkioweight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
432   d.placeholder = "500"
433   d.rmempty = true
434   d.datatype="uinteger"
435   d.default = container_info.HostConfig.BlkioWeight
436
437   m.handle = function(self, state, data)
438     if state == FORM_VALID then
439       local memory = data.memory
440       if memory and memory ~= 0 then
441         _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
442         if n then
443           unit = unit and unit:sub(1,1):upper() or "B"
444           if  unit == "M" then
445             memory = tonumber(n) * 1024 * 1024
446           elseif unit == "G" then
447             memory = tonumber(n) * 1024 * 1024 * 1024
448           elseif unit == "K" then
449             memory = tonumber(n) * 1024
450           else
451             memory = tonumber(n)
452           end
453         end
454       end
455       request_body = {
456         BlkioWeight = tonumber(data.blkioweight),
457         NanoCPUs = tonumber(data.cpus)*10^9,
458         Memory = tonumber(memory),
459         CpuShares = tonumber(data.cpushares)
460         }
461       docker:write_status("Containers: update " .. container_id .. "...")
462       local res = dk.containers:update({id = container_id, body = request_body})
463       if res and res.code >= 300 then
464         docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
465       else
466         docker:clear_status()
467       end
468       luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
469     end
470   end
471 elseif action == "file" then
472   local filesection= m:section(SimpleSection)
473   m.submit = false
474   m.reset  = false
475   filesection.template = "dockerman/container_file"
476   filesection.container = container_id
477 elseif action == "inspect" then
478   local inspectsection= m:section(SimpleSection)
479   inspectsection.syslog = luci.jsonc.stringify(container_info, true)
480   inspectsection.title = translate("Container Inspect")
481   inspectsection.template = "dockerman/logs"
482   m.submit = false
483   m.reset  = false
484 elseif action == "logs" then
485   local logsection= m:section(SimpleSection)
486   local logs = ""
487   local query ={
488     stdout = 1,
489     stderr = 1,
490     tail = 1000
491   }
492   local logs = dk.containers:logs({id = container_id, query = query})
493   if logs.code == 200 then
494     logsection.syslog=logs.body
495   else
496     logsection.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
497   end
498   logsection.title=translate("Container Logs")
499   logsection.template = "dockerman/logs"
500   m.submit = false
501   m.reset  = false
502 elseif action == "console" then
503   m.submit = false
504   m.reset  = false
505   local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
506   local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
507   if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
508     local consolesection= m:section(SimpleSection)
509     local cmd = "/bin/sh"
510     local uid
511     local vcommand = consolesection:option(Value, "command", translate("Command"))
512     vcommand:value("/bin/sh", "/bin/sh")
513     vcommand:value("/bin/ash", "/bin/ash")
514     vcommand:value("/bin/bash", "/bin/bash")
515     vcommand.default = "/bin/sh"
516     vcommand.forcewrite = true
517     vcommand.write = function(self, section, value)
518       cmd = value
519     end
520     local vuid = consolesection:option(Value, "uid", translate("UID"))
521     vuid.forcewrite = true
522     vuid.write = function(self, section, value)
523       uid = value
524     end
525     local btn_connect = consolesection:option(Button, "connect")
526     btn_connect.render = function(self, section, scope)
527       self.inputstyle = "add"
528       self.title = " "
529       self.inputtitle = translate("Connect")
530       Button.render(self, section, scope)
531     end
532     btn_connect.write = function(self, section)
533       local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
534       local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
535       if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then return end
536       local kill_ttyd = 'netstat -lnpt | grep ":7682[ \t].*ttyd$" | awk \'{print $NF}\' | awk -F\'/\' \'{print "kill -9 " $1}\' | sh > /dev/null'
537       luci.util.exec(kill_ttyd)
538       local hosts
539       local uci = (require "luci.model.uci").cursor()
540       local remote = uci:get("dockerman", "local", "remote_endpoint")
541       local socket_path = (remote == "false" or not remote) and  uci:get("dockerman", "local", "socket_path") or nil
542       local host = (remote == "true") and uci:get("dockerman", "local", "remote_host") or nil
543       local port = (remote == "true") and uci:get("dockerman", "local", "remote_port") or nil
544       if remote and host and port then
545         hosts = host .. ':'.. port
546       elseif socket_path then
547         hosts = "unix://" .. socket_path
548       else
549         return
550       end
551       local start_cmd = cmd_ttyd .. ' -d 2 --once -p 7682 '.. cmd_docker .. ' -H "'.. hosts ..'" exec -it ' .. (uid and uid ~= "" and (" -u ".. uid  .. ' ') or "").. container_id .. ' ' .. cmd .. ' &'
552       os.execute(start_cmd)
553       local console = consolesection:option(DummyValue, "console")
554       console.container_id = container_id
555       console.template = "dockerman/container_console"
556     end
557   end
558 elseif action == "stats" then
559   local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
560   local container_top
561   if response.code == 200 then
562     container_top=response.body
563   else
564     response = dk.containers:top({id = container_id})
565     if response.code == 200 then
566       container_top=response.body
567     end
568   end
569
570   if type(container_top) == "table" then
571     container_top=response.body
572     stat_section = m:section(SimpleSection)
573     stat_section.container_id = container_id
574     stat_section.template = "dockerman/container_stats"
575     table_stats = {cpu={key=translate("CPU Useage"),value='-'},memory={key=translate("Memory Useage"),value='-'}}
576     stat_section = m:section(Table, table_stats, translate("Stats"))
577     stat_section:option(DummyValue, "key", translate("Stats")).width="33%"
578     stat_section:option(DummyValue, "value")
579     top_section= m:section(Table, container_top.Processes, translate("TOP"))
580     for i, v in ipairs(container_top.Titles) do
581       top_section:option(DummyValue, i, translate(v))
582   end
583 end
584 m.submit = false
585 m.reset  = false
586 end
587
588 return m