├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug---.md │ └── bug_report.md └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── applications └── luci-app-dockerman │ ├── Makefile │ ├── depends.lst │ ├── htdocs │ └── luci-static │ │ └── resources │ │ └── dockerman │ │ ├── containers.svg │ │ ├── file-icon.png │ │ ├── file-manager.css │ │ ├── folder-icon.png │ │ ├── images.svg │ │ ├── link-icon.png │ │ ├── networks.svg │ │ ├── tar.min.js │ │ └── volumes.svg │ ├── luasrc │ ├── controller │ │ └── dockerman.lua │ ├── model │ │ ├── cbi │ │ │ └── dockerman │ │ │ │ ├── configuration.lua │ │ │ │ ├── container.lua │ │ │ │ ├── containers.lua │ │ │ │ ├── images.lua │ │ │ │ ├── networks.lua │ │ │ │ ├── newcontainer.lua │ │ │ │ ├── newnetwork.lua │ │ │ │ ├── overview.lua │ │ │ │ └── volumes.lua │ │ └── docker.lua │ └── view │ │ └── dockerman │ │ ├── apply_widget.htm │ │ ├── cbi │ │ ├── inlinebutton.htm │ │ ├── inlinevalue.htm │ │ ├── namedsection.htm │ │ └── xfvalue.htm │ │ ├── container.htm │ │ ├── container_console.htm │ │ ├── container_file_manager.htm │ │ ├── container_stats.htm │ │ ├── containers_running_stats.htm │ │ ├── images_import.htm │ │ ├── images_load.htm │ │ ├── logs.htm │ │ ├── newcontainer_resolve.htm │ │ ├── overview.htm │ │ └── volume_size.htm │ ├── po │ ├── templates │ │ └── dockerman.pot │ ├── zh-cn │ │ └── dockerman.po │ └── zh_Hans │ ├── postinst │ └── root │ ├── etc │ ├── init.d │ │ └── dockerman │ └── uci-defaults │ │ └── luci-app-dockerman │ └── usr │ └── share │ └── rpcd │ └── acl.d │ └── luci-app-dockerman.json └── doc ├── container_edit.png ├── container_info.png ├── container_logs.png ├── container_stats.png ├── containers.png ├── images.png ├── networks.png ├── new_container.png └── new_network.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: lisaac 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 报告 3 | about: 报告 bug 以使改进 dockerman 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **问题描述** 11 | 12 | 使用 dockerman 时遇到的问题 13 | 14 | **使用命令行** 15 | 同样的操作使用命令行的结果 16 | 17 | 18 | **版本信息:** 19 | - openwrt 版本: 20 | - luci 版本: 21 | - docker daemon 版本: 22 | - dockerman 版本: 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Use CLI** 14 | The result of using the command line 15 | 16 | **Version information** 17 | - openwrt version: 18 | - luci version: 19 | - docker daemon version: 20 | - dockerman version: 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - '*' # Tag events 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout codes 15 | uses: actions/checkout@v1 16 | - name: build 17 | id: build_ipk 18 | run: | 19 | TAG=${GITHUB_REF#refs/tags/} 20 | echo ::set-output name=tag_name::${TAG} 21 | mkdir -p /tmp/luci-app-dockerman/usr/lib/lua/luci /tmp/luci-app-dockerman/www/ 22 | [ -d $GITHUB_WORKSPACE/applications/luci-app-dockerman/luasrc ] && cp -R $GITHUB_WORKSPACE/applications/luci-app-dockerman/luasrc/* /tmp/luci-app-dockerman/usr/lib/lua/luci/ 23 | [ -d $GITHUB_WORKSPACE/applications/luci-app-dockerman/root ] && cp -R $GITHUB_WORKSPACE/applications/luci-app-dockerman/root/* /tmp/luci-app-dockerman/ 24 | chmod +x /tmp/luci-app-dockerman/etc/init.d/* >/dev/null 2>&1 25 | chmod +x /tmp/luci-app-dockerman/etc/uci-defaults/* >/dev/null 2>&1 26 | [ -d $GITHUB_WORKSPACE/applications/luci-app-dockerman/htdocs ] && cp -R $GITHUB_WORKSPACE/applications/luci-app-dockerman/htdocs/* /tmp/luci-app-dockerman/www/ 27 | [ -d $GITHUB_WORKSPACE/applications/luci-app-dockerman/po ] && sudo -E apt-get -y install gcc make && \ 28 | mkdir -p /tmp/po2lmo && mkdir -p /tmp/luci-app-dockerman/usr/lib/lua/luci/i18n/ && \ 29 | wget -O /tmp/po2lmo/po2lmo.c https://raw.githubusercontent.com/openwrt/luci/openwrt-18.06/modules/luci-base/src/po2lmo.c && \ 30 | wget -O /tmp/po2lmo/Makefile https://raw.githubusercontent.com/openwrt/luci/openwrt-18.06/modules/luci-base/src/Makefile && \ 31 | wget -O /tmp/po2lmo/template_lmo.h https://raw.githubusercontent.com/openwrt/luci/openwrt-18.06/modules/luci-base/src/template_lmo.h && \ 32 | wget -O /tmp/po2lmo/template_lmo.c https://raw.githubusercontent.com/openwrt/luci/openwrt-18.06/modules/luci-base/src/template_lmo.c && \ 33 | cd /tmp/po2lmo && make po2lmo && ./po2lmo $GITHUB_WORKSPACE/applications/luci-app-dockerman/po/zh-cn/dockerman.po /tmp/luci-app-dockerman/usr/lib/lua/luci/i18n/dockerman.zh-cn.lmo 34 | mkdir -p /tmp/luci-app-dockerman/CONTROL 35 | cat >/tmp/luci-app-dockerman/CONTROL/control < 41 | Section: luci 42 | Priority: optional 43 | Description: Simple Docker manager interface 44 | Source: http://github.com/lisaac/luci-app-dockerman 45 | EOF 46 | cat >/tmp/luci-app-dockerman/CONTROL/postinst < 9 | 10 | ## Docker Manager for LuCI / 适用于 LuCI 的 Docker 管理插件 11 | - 一个用于管理 Docker 容器、镜像、网络、存储卷的 Openwrt 插件 12 | - 同时也适用于 [Openwrt-in-docker](https://github.com/lisaac/openwrt-in-docker) 或 [LuCI-in-docker](https://github.com/lisaac/luci-in-docker) 13 | - [Download / 下载 ipk](https://github.com/lisaac/luci-app-dockerman/releases) 14 | 15 | ## Depends / 依赖 16 | - [luci-lib-docker](https://github.com/lisaac/luci-lib-docker) 17 | - dockerd (optional, since you can use it as a docker client) 18 | - luci-lib-jsonc 19 | - ttyd (optional, use for container console) 20 | - docker (optional, use for container console) 21 | 22 | ## Compile / 编译 23 | ```bash 24 | 25 | #compile package only 26 | make package/luci-lib-docker/compile v=99 27 | make package/luci-app-dockerman/compile v=99 28 | 29 | #compile 30 | make menuconfig 31 | #choose Utilities ---> <*> docker....................................... Docker Community Edition 32 | #choose Kernel features for Docker which you want 33 | #choose LuCI ---> 3. Applications ---> <*> luci-app-dockerman..... Docker Manager interface for LuCI ----> save 34 | make V=99 35 | ``` 36 | 37 | ## Screenshot / 截图 38 | - Containers 39 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/containers.png) 40 | - Container Info 41 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/container_info.png) 42 | - Container Edit 43 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/container_edit.png) 44 | - Container Stats 45 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/container_stats.png) 46 | - Container Logs 47 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/container_logs.png) 48 | - New Container 49 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/new_container.png) 50 | - Images 51 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/images.png) 52 | - Networks 53 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/networks.png) 54 | - New Network 55 | ![](https://raw.githubusercontent.com/lisaac/luci-app-dockerman/master/doc/new_network.png) 56 | 57 | ## Thanks / 谢致 58 | - Chinese translation by [401626436](https://www.right.com.cn/forum/space-uid-382335.html) -------------------------------------------------------------------------------- /applications/luci-app-dockerman/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | LUCI_TITLE:=LuCI Support for docker 4 | LUCI_DEPENDS:=@(aarch64||arm||x86_64) \ 5 | +luci-compat \ 6 | +luci-lib-docker \ 7 | +luci-lib-ip \ 8 | +docker \ 9 | +dockerd \ 10 | +ttyd 11 | LUCI_PKGARCH:=all 12 | 13 | PKG_LICENSE:=AGPL-3.0 14 | PKG_MAINTAINER:=lisaac \ 15 | Florian Eckert 16 | 17 | PKG_VERSION:=v0.5.26 18 | 19 | include $(TOPDIR)/feeds/luci/luci.mk 20 | 21 | # call BuildPackage - OpenWrt buildroot signature 22 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/depends.lst: -------------------------------------------------------------------------------- 1 | ttyd docker-cli -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/containers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Docker icon 6 | 7 | 8 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-icon.png -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-manager.css: -------------------------------------------------------------------------------- 1 | .fb-container { 2 | margin-top: 1rem; 3 | } 4 | .fb-container .cbi-button { 5 | height: 1.8rem; 6 | } 7 | .fb-container .cbi-input-text { 8 | margin-bottom: 1rem; 9 | width: 100%; 10 | } 11 | .fb-container .panel-title { 12 | padding-bottom: 0; 13 | width: 50%; 14 | border-bottom: none; 15 | } 16 | .fb-container .panel-container { 17 | display: flex; 18 | align-items: center; 19 | justify-content: space-between; 20 | padding-bottom: 1rem; 21 | border-bottom: 1px solid #eee; 22 | } 23 | .fb-container .upload-container { 24 | display: none; 25 | margin: 1rem 0; 26 | } 27 | .fb-container .upload-file { 28 | margin-right: 2rem; 29 | } 30 | .fb-container .cbi-value-field { 31 | text-align: left; 32 | } 33 | .fb-container .parent-icon strong { 34 | margin-left: 1rem; 35 | } 36 | .fb-container td[class$="-icon"] { 37 | cursor: pointer; 38 | } 39 | .fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon { 40 | position: relative; 41 | } 42 | .fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before { 43 | display: inline-block; 44 | width: 1.5rem; 45 | height: 1.5rem; 46 | content: ''; 47 | background-size: contain; 48 | margin: 0 0.5rem 0 1rem; 49 | vertical-align: middle; 50 | } 51 | .fb-container .file-icon:before { 52 | background-image: url(file-icon.png); 53 | } 54 | .fb-container .folder-icon:before { 55 | background-image: url(folder-icon.png); 56 | } 57 | .fb-container .link-icon:before { 58 | background-image: url(link-icon.png); 59 | } 60 | @media screen and (max-width: 480px) { 61 | .fb-container .upload-file { 62 | width: 14.6rem; 63 | } 64 | .fb-container .cbi-value-owner, 65 | .fb-container .cbi-value-perm { 66 | display: none; 67 | } 68 | } 69 | 70 | .cbi-section-table { 71 | width: 100%; 72 | } 73 | 74 | .cbi-section-table-cell { 75 | text-align: right; 76 | } 77 | 78 | .cbi-button-install { 79 | border-color: #c44; 80 | color: #c44; 81 | margin-left: 3px; 82 | } 83 | 84 | .cbi-value-field { 85 | padding: 10px 0; 86 | } 87 | 88 | .parent-icon { 89 | height: 1.8rem; 90 | padding: 10px 0; 91 | } -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/folder-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/folder-icon.png -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/images.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/link-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/link-icon.png -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/networks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js: -------------------------------------------------------------------------------- 1 | // https://github.com/thiscouldbebetter/TarFileExplorer 2 | class TarFileTypeFlag 3 | {constructor(value,name) 4 | {this.value=value;this.id="_"+this.value;this.name=name;} 5 | static _instances;static Instances() 6 | {if(TarFileTypeFlag._instances==null) 7 | {TarFileTypeFlag._instances=new TarFileTypeFlag_Instances();} 8 | return TarFileTypeFlag._instances;}} 9 | class TarFileTypeFlag_Instances 10 | {constructor() 11 | {this.Normal=new TarFileTypeFlag("0","Normal");this.HardLink=new TarFileTypeFlag("1","Hard Link");this.SymbolicLink=new TarFileTypeFlag("2","Symbolic Link");this.CharacterSpecial=new TarFileTypeFlag("3","Character Special");this.BlockSpecial=new TarFileTypeFlag("4","Block Special");this.Directory=new TarFileTypeFlag("5","Directory");this.FIFO=new TarFileTypeFlag("6","FIFO");this.ContiguousFile=new TarFileTypeFlag("7","Contiguous File");this.LongFilePath=new TarFileTypeFlag("L","././@LongLink");this._All=[this.Normal,this.HardLink,this.SymbolicLink,this.CharacterSpecial,this.BlockSpecial,this.Directory,this.FIFO,this.ContiguousFile,this.LongFilePath,];for(var i=0;ia+=String.fromCharCode(b),"");entryNext.header.fileName=entryNext.header.fileName.replace(/\0/g,"");entries.splice(i,1);i--;}}} 97 | downloadAs(fileNameToSaveAs) 98 | {return FileHelper.saveBytesAsFile 99 | (this.toBytes(),fileNameToSaveAs)} 100 | entriesForDirectories() 101 | {return this.entries.filter(x=>x.header.typeFlag.name==TarFileTypeFlag.Instances().Directory);} 102 | toBytes() 103 | {this.toBytes_PrependLongPathEntriesAsNeeded();var fileAsBytes=[];var entriesAsByteArrays=this.entries.map(x=>x.toBytes());this.consolidateLongPathEntries();for(var i=0;imaxLength) 112 | {var entryFileNameAsBytes=entryFileName.split("").map(x=>x.charCodeAt(0));var entryContainingLongPathToPrepend=TarFileEntry.fileNew 113 | (typeFlagLongPath.name,entryFileNameAsBytes);entryContainingLongPathToPrepend.header.typeFlag=typeFlagLongPath;entryContainingLongPathToPrepend.header.timeModifiedInUnixFormat=entryHeader.timeModifiedInUnixFormat;entryContainingLongPathToPrepend.header.checksumCalculate();entryHeader.fileName=entryFileName.substr(0,maxLength)+String.fromCharCode(0);entries.splice(i,0,entryContainingLongPathToPrepend);i++;}}} 114 | toString() 115 | {var newline="\n";var returnValue="[TarFile]"+newline;for(var i=0;i{var fileLoadedAsBinaryString=fileLoadedEvent.target.result;var fileLoadedAsBytes=ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);callback(fileToLoad.name,fileLoadedAsBytes);} 133 | fileReader.readAsBinaryString(fileToLoad);} 134 | static loadFileAsText(fileToLoad,callback) 135 | {var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var textFromFileLoaded=fileLoadedEvent.target.result;callback(fileToLoad.name,textFromFileLoaded);};fileReader.readAsText(fileToLoad);} 136 | static saveBytesAsFile(bytesToWrite,fileNameToSaveAs) 137 | {var bytesToWriteAsArrayBuffer=new ArrayBuffer(bytesToWrite.length);var bytesToWriteAsUIntArray=new Uint8Array(bytesToWriteAsArrayBuffer);for(var i=0;i 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/controller/dockerman.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | -- local uci = (require "luci.model.uci").cursor() 8 | 9 | module("luci.controller.dockerman",package.seeall) 10 | 11 | function index() 12 | entry({"admin", "docker"}, 13 | firstchild(), 14 | _("Docker"), 15 | 40).acl_depends = { "luci-app-dockerman" } 16 | 17 | entry({"admin", "docker", "config"},cbi("dockerman/configuration"),_("Configuration"), 8).leaf=true 18 | 19 | -- local uci = (require "luci.model.uci").cursor() 20 | -- if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 21 | -- local host = uci:get("dockerd", "dockerman", "remote_host") 22 | -- local port = uci:get("dockerd", "dockerman", "remote_port") 23 | -- if not host or not port then 24 | -- return 25 | -- end 26 | -- else 27 | -- local socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock" 28 | -- if socket and not nixio.fs.access(socket) then 29 | -- return 30 | -- end 31 | -- end 32 | 33 | -- if (require "luci.model.docker").new():_ping().code ~= 200 then 34 | -- return 35 | -- end 36 | 37 | entry({"admin", "docker", "overview"}, form("dockerman/overview"),_("Overview"), 2).leaf=true 38 | entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"), 3).leaf=true 39 | entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"), 4).leaf=true 40 | entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"), 5).leaf=true 41 | entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"), 6).leaf=true 42 | entry({"admin", "docker", "events"}, call("action_events"), _("Events"), 7) 43 | 44 | entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true 45 | entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true 46 | entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true 47 | 48 | entry({"admin", "docker", "container_stats"}, call("action_get_container_stats")).leaf=true 49 | entry({"admin", "docker", "containers_stats"}, call("action_get_containers_stats")).leaf=true 50 | entry({"admin", "docker", "get_system_df"}, call("action_get_system_df")).leaf=true 51 | entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true 52 | entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true 53 | entry({"admin", "docker", "container_list_file"}, call("list_file")).leaf=true 54 | entry({"admin", "docker", "container_remove_file"}, call("remove_file")).leaf=true 55 | entry({"admin", "docker", "container_rename_file"}, call("rename_file")).leaf=true 56 | entry({"admin", "docker", "container_export"}, call("export_container")).leaf=true 57 | entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true 58 | entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true 59 | entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true 60 | entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true 61 | entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true 62 | entry({"admin", "docker", "images_untag"}, call("untag_image")).leaf=true 63 | entry({"admin", "docker", "confirm"}, call("action_confirm")).leaf=true 64 | end 65 | 66 | function action_get_system_df() 67 | local res = docker.new():df() 68 | luci.http.status(res.code, res.message) 69 | luci.http.prepare_content("application/json") 70 | luci.http.write_json(res.body) 71 | end 72 | 73 | function scandir(id, directory) 74 | local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil 75 | if not cmd_docker or cmd_docker:match("^%s+$") then 76 | return 77 | end 78 | local i, t, popen = 0, {}, io.popen 79 | local uci = (require "luci.model.uci").cursor() 80 | local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") 81 | local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil 82 | local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil 83 | local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil 84 | if remote and host and port then 85 | hosts = "tcp://" .. host .. ':'.. port 86 | elseif socket_path then 87 | hosts = "unix://" .. socket_path 88 | else 89 | return 90 | end 91 | local pfile = popen(cmd_docker .. ' -H "'.. hosts ..'" exec ' ..id .." ls -lh \""..directory.."\" | egrep -v '^total'") 92 | for fileinfo in pfile:lines() do 93 | i = i + 1 94 | t[i] = fileinfo 95 | end 96 | pfile:close() 97 | return t 98 | end 99 | 100 | function list_response(id, path, success) 101 | luci.http.prepare_content("application/json") 102 | local result 103 | if success then 104 | local rv = scandir(id, path) 105 | result = { 106 | ec = 0, 107 | data = rv 108 | } 109 | else 110 | result = { 111 | ec = 1 112 | } 113 | end 114 | luci.http.write_json(result) 115 | end 116 | 117 | function list_file(id) 118 | local path = luci.http.formvalue("path") 119 | list_response(id, path, true) 120 | end 121 | 122 | function rename_file(id) 123 | local filepath = luci.http.formvalue("filepath") 124 | local newpath = luci.http.formvalue("newpath") 125 | local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil 126 | if not cmd_docker or cmd_docker:match("^%s+$") then 127 | return 128 | end 129 | local uci = (require "luci.model.uci").cursor() 130 | local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") 131 | local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil 132 | local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil 133 | local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil 134 | if remote and host and port then 135 | hosts = "tcp://" .. host .. ':'.. port 136 | elseif socket_path then 137 | hosts = "unix://" .. socket_path 138 | else 139 | return 140 | end 141 | local success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' mv "'..filepath..'" "'..newpath..'"') 142 | list_response(nixio.fs.dirname(filepath), success) 143 | end 144 | 145 | function remove_file(id) 146 | local path = luci.http.formvalue("path") 147 | local isdir = luci.http.formvalue("isdir") 148 | local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil 149 | if not cmd_docker or cmd_docker:match("^%s+$") then 150 | return 151 | end 152 | local uci = (require "luci.model.uci").cursor() 153 | local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") 154 | local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil 155 | local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil 156 | local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil 157 | if remote and host and port then 158 | hosts = "tcp://" .. host .. ':'.. port 159 | elseif socket_path then 160 | hosts = "unix://" .. socket_path 161 | else 162 | return 163 | end 164 | path = path:gsub("<>", "/") 165 | path = path:gsub(" ", "\ ") 166 | local success 167 | if isdir then 168 | success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' rm -r "'..path..'"') 169 | else 170 | success = os.remove(path) 171 | end 172 | list_response(nixio.fs.dirname(path), success) 173 | end 174 | 175 | function action_events() 176 | local logs = "" 177 | local query ={} 178 | 179 | local dk = docker.new() 180 | query["until"] = os.time() 181 | local events = dk:events({query = query}) 182 | 183 | if events.code == 200 then 184 | for _, v in ipairs(events.body) do 185 | local date = "unknown" 186 | if v and v.time then 187 | date = os.date("%Y-%m-%d %H:%M:%S", v.time) 188 | end 189 | 190 | local name = v.Actor.Attributes.name or "unknown" 191 | local action = v.Action or "unknown" 192 | 193 | if v and v.Type == "container" then 194 | local id = v.Actor.ID or "unknown" 195 | logs = logs .. string.format("[%s] %s %s Container ID: %s Container Name: %s\n", date, v.Type, action, id, name) 196 | elseif v.Type == "network" then 197 | local container = v.Actor.Attributes.container or "unknown" 198 | local network = v.Actor.Attributes.type or "unknown" 199 | logs = logs .. string.format("[%s] %s %s Container ID: %s Network Name: %s Network type: %s\n", date, v.Type, action, container, name, network) 200 | elseif v.Type == "image" then 201 | local id = v.Actor.ID or "unknown" 202 | logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name) 203 | end 204 | end 205 | end 206 | 207 | luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}}) 208 | end 209 | 210 | local calculate_cpu_percent = function(d) 211 | if type(d) ~= "table" then 212 | return 213 | end 214 | 215 | local cpu_count = tonumber(d["cpu_stats"]["online_cpus"]) 216 | local cpu_percent = 0.0 217 | local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"]) 218 | local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) -- tonumber(d["precpu_stats"]["system_cpu_usage"]) 219 | if system_delta > 0.0 then 220 | cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count) 221 | end 222 | 223 | return cpu_percent 224 | end 225 | 226 | local get_memory = function(d) 227 | if type(d) ~= "table" then 228 | return 229 | end 230 | 231 | -- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024) 232 | -- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024) 233 | -- return usage .. "MB / " .. limit.. "MB" 234 | 235 | local limit =tonumber(d["memory_stats"]["limit"]) 236 | local usage = tonumber(d["memory_stats"]["usage"]) 237 | -- - tonumber(d["memory_stats"]["stats"]["total_cache"]) 238 | 239 | return usage, limit 240 | end 241 | 242 | local get_rx_tx = function(d) 243 | if type(d) ~="table" then 244 | return 245 | end 246 | 247 | local data = {} 248 | if type(d["networks"]) == "table" then 249 | for e, v in pairs(d["networks"]) do 250 | data[e] = { 251 | bw_tx = tonumber(v.tx_bytes), 252 | bw_rx = tonumber(v.rx_bytes) 253 | } 254 | end 255 | end 256 | 257 | return data 258 | end 259 | 260 | local function get_stat(container_id) 261 | if container_id then 262 | local dk = docker.new() 263 | local response = dk.containers:inspect({id = container_id}) 264 | if response.code == 200 and response.body.State.Running then 265 | response = dk.containers:stats({id = container_id, query = {stream = false, ["one-shot"] = true}}) 266 | if response.code == 200 then 267 | local container_stats = response.body 268 | local cpu_percent = calculate_cpu_percent(container_stats) 269 | local mem_useage, mem_limit = get_memory(container_stats) 270 | local bw_rxtx = get_rx_tx(container_stats) 271 | return response.code, response.body.message, { 272 | cpu_percent = cpu_percent, 273 | memory = { 274 | mem_useage = mem_useage, 275 | mem_limit = mem_limit 276 | }, 277 | bw_rxtx = bw_rxtx 278 | } 279 | else 280 | return response.code, response.body.message 281 | end 282 | else 283 | if response.code == 200 then 284 | return 500, "container "..container_id.." not running" 285 | else 286 | return response.code, response.body.message 287 | end 288 | end 289 | else 290 | return 404, "No container name or id" 291 | end 292 | end 293 | function action_get_container_stats(container_id) 294 | local code, msg, res = get_stat(container_id) 295 | luci.http.status(code, msg) 296 | luci.http.prepare_content("application/json") 297 | luci.http.write_json(res) 298 | end 299 | 300 | function action_get_containers_stats() 301 | local res = luci.http.formvalue(containers) or "" 302 | local stats = {} 303 | res = luci.jsonc.parse(res.containers) 304 | if res and type(res) == "table" then 305 | for i, v in ipairs(res) do 306 | _,_,stats[v] = get_stat(v) 307 | end 308 | end 309 | luci.http.status(200, "OK") 310 | luci.http.prepare_content("application/json") 311 | luci.http.write_json(stats) 312 | end 313 | 314 | function action_confirm() 315 | local data = docker:read_status() 316 | if data then 317 | data = data:gsub("\n","
"):gsub(" "," ") 318 | code = 202 319 | msg = data 320 | else 321 | code = 200 322 | msg = "finish" 323 | data = "finish" 324 | end 325 | 326 | luci.http.status(code, msg) 327 | luci.http.prepare_content("application/json") 328 | luci.http.write_json({info = data}) 329 | end 330 | 331 | function export_container(id) 332 | local dk = docker.new() 333 | local first 334 | 335 | local cb = function(res, chunk) 336 | if res.code == 200 then 337 | if not first then 338 | first = true 339 | luci.http.header('Content-Disposition', 'inline; filename="'.. id ..'.tar"') 340 | luci.http.header('Content-Type', 'application\/x-tar') 341 | end 342 | luci.ltn12.pump.all(chunk, luci.http.write) 343 | else 344 | if not first then 345 | first = true 346 | luci.http.prepare_content("text/plain") 347 | end 348 | luci.ltn12.pump.all(chunk, luci.http.write) 349 | end 350 | end 351 | 352 | local res = dk.containers:export({id = id}, cb) 353 | end 354 | 355 | function download_archive() 356 | local id = luci.http.formvalue("id") 357 | local path = luci.http.formvalue("path") 358 | local filename = luci.http.formvalue("filename") or "archive" 359 | local dk = docker.new() 360 | local first 361 | 362 | local cb = function(res, chunk) 363 | if res and res.code and res.code == 200 then 364 | if not first then 365 | first = true 366 | luci.http.header('Content-Disposition', 'inline; filename="'.. filename .. '.tar"') 367 | luci.http.header('Content-Type', 'application\/x-tar') 368 | end 369 | luci.ltn12.pump.all(chunk, luci.http.write) 370 | else 371 | if not first then 372 | first = true 373 | luci.http.status(res and res.code or 500, msg or "unknow") 374 | luci.http.prepare_content("text/plain") 375 | end 376 | luci.ltn12.pump.all(chunk, luci.http.write) 377 | end 378 | end 379 | 380 | local res = dk.containers:get_archive({ 381 | id = id, 382 | query = { 383 | path = luci.http.urlencode(path) 384 | } 385 | }, cb) 386 | end 387 | 388 | function upload_archive(container_id) 389 | local path = luci.http.formvalue("upload-path") 390 | local dk = docker.new() 391 | local ltn12 = require "luci.ltn12" 392 | 393 | local rec_send = function(sinkout) 394 | luci.http.setfilehandler(function (meta, chunk, eof) 395 | if chunk then 396 | ltn12.pump.step(ltn12.source.string(chunk), sinkout) 397 | end 398 | end) 399 | end 400 | 401 | local res = dk.containers:put_archive({ 402 | id = container_id, 403 | query = { 404 | path = luci.http.urlencode(path) 405 | }, 406 | body = rec_send 407 | }) 408 | 409 | local msg = res and res.message or res.body and res.body.message or nil 410 | luci.http.status(res and res.code or 500, msg or "unknow") 411 | luci.http.prepare_content("application/json") 412 | luci.http.write_json({message = msg or "unknow"}) 413 | end 414 | 415 | -- function save_images() 416 | -- local names = luci.http.formvalue("names") 417 | -- local dk = docker.new() 418 | -- local first 419 | 420 | -- local cb = function(res, chunk) 421 | -- if res.code == 200 then 422 | -- if not first then 423 | -- first = true 424 | -- luci.http.status(res.code, res.message) 425 | -- luci.http.header('Content-Disposition', 'inline; filename="'.. "images" ..'.tar"') 426 | -- luci.http.header('Content-Type', 'application\/x-tar') 427 | -- end 428 | -- luci.ltn12.pump.all(chunk, luci.http.write) 429 | -- else 430 | -- if not first then 431 | -- first = true 432 | -- luci.http.prepare_content("text/plain") 433 | -- end 434 | -- luci.ltn12.pump.all(chunk, luci.http.write) 435 | -- end 436 | -- end 437 | 438 | -- docker:write_status("Images: saving" .. " " .. names .. "...") 439 | -- local res = dk.images:get({ 440 | -- query = { 441 | -- names = luci.http.urlencode(names) 442 | -- } 443 | -- }, cb) 444 | -- docker:clear_status() 445 | 446 | -- local msg = res and res.body and res.body.message or nil 447 | -- luci.http.status(res.code, msg) 448 | -- luci.http.prepare_content("application/json") 449 | -- luci.http.write_json({message = msg}) 450 | -- end 451 | 452 | function load_images() 453 | local archive = luci.http.formvalue("upload-archive") 454 | local dk = docker.new() 455 | local ltn12 = require "luci.ltn12" 456 | 457 | local rec_send = function(sinkout) 458 | luci.http.setfilehandler(function (meta, chunk, eof) 459 | if chunk then 460 | ltn12.pump.step(ltn12.source.string(chunk), sinkout) 461 | end 462 | end) 463 | end 464 | 465 | docker:write_status("Images: loading...") 466 | local res = dk.images:load({body = rec_send}) 467 | local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil 468 | if res and res.code == 200 and msg and msg:match("Loaded image ID") then 469 | docker:clear_status() 470 | else 471 | docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow")) 472 | end 473 | 474 | luci.http.status(res and res.code or 500, msg or "unknow") 475 | luci.http.prepare_content("application/json") 476 | luci.http.write_json({message = msg or "unknow"}) 477 | end 478 | 479 | function import_images() 480 | local src = luci.http.formvalue("src") 481 | local itag = luci.http.formvalue("tag") 482 | local dk = docker.new() 483 | local ltn12 = require "luci.ltn12" 484 | 485 | local rec_send = function(sinkout) 486 | luci.http.setfilehandler(function (meta, chunk, eof) 487 | if chunk then 488 | ltn12.pump.step(ltn12.source.string(chunk), sinkout) 489 | end 490 | end) 491 | end 492 | 493 | docker:write_status("Images: importing".. " ".. itag .."...\n") 494 | local repo = itag and itag:match("^([^:]+)") 495 | local tag = itag and itag:match("^[^:]-:([^:]+)") 496 | local res = dk.images:create({ 497 | query = { 498 | fromSrc = luci.http.urlencode(src or "-"), 499 | repo = repo or nil, 500 | tag = tag or nil 501 | }, 502 | body = not src and rec_send or nil 503 | }, docker.import_image_show_status_cb) 504 | 505 | local msg = res and res.body and ( res.body.message )or nil 506 | if not msg and #res.body == 0 then 507 | msg = res.body.status or res.body.error 508 | elseif not msg and #res.body >= 1 then 509 | msg = res.body[#res.body].status or res.body[#res.body].error 510 | end 511 | 512 | if res.code == 200 and msg and msg:match("sha256:") then 513 | docker:clear_status() 514 | else 515 | docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow")) 516 | end 517 | 518 | luci.http.status(res and res.code or 500, msg or "unknow") 519 | luci.http.prepare_content("application/json") 520 | luci.http.write_json({message = msg or "unknow"}) 521 | end 522 | 523 | function get_image_tags(image_id) 524 | if not image_id then 525 | luci.http.status(400, "no image id") 526 | luci.http.prepare_content("application/json") 527 | luci.http.write_json({message = "no image id"}) 528 | return 529 | end 530 | 531 | local dk = docker.new() 532 | local res = dk.images:inspect({ 533 | id = image_id 534 | }) 535 | local msg = res and res.body and res.body.message or nil 536 | luci.http.status(res and res.code or 500, msg or "unknow") 537 | luci.http.prepare_content("application/json") 538 | 539 | if res.code == 200 then 540 | local tags = res.body.RepoTags 541 | luci.http.write_json({tags = tags}) 542 | else 543 | local msg = res and res.body and res.body.message or nil 544 | luci.http.write_json({message = msg or "unknow"}) 545 | end 546 | end 547 | 548 | function tag_image(image_id) 549 | local src = luci.http.formvalue("tag") 550 | local image_id = image_id or luci.http.formvalue("id") 551 | 552 | if type(src) ~= "string" or not image_id then 553 | luci.http.status(400, "no image id or tag") 554 | luci.http.prepare_content("application/json") 555 | luci.http.write_json({message = "no image id or tag"}) 556 | return 557 | end 558 | 559 | local repo = src:match("^([^:]+)") 560 | local tag = src:match("^[^:]-:([^:]+)") 561 | local dk = docker.new() 562 | local res = dk.images:tag({ 563 | id = image_id, 564 | query={ 565 | repo=repo, 566 | tag=tag 567 | } 568 | }) 569 | local msg = res and res.body and res.body.message or nil 570 | luci.http.status(res and res.code or 500, msg or "unknow") 571 | luci.http.prepare_content("application/json") 572 | 573 | if res.code == 201 then 574 | local tags = res.body.RepoTags 575 | luci.http.write_json({tags = tags}) 576 | else 577 | local msg = res and res.body and res.body.message or nil 578 | luci.http.write_json({message = msg or "unknow"}) 579 | end 580 | end 581 | 582 | function untag_image(tag) 583 | local tag = tag or luci.http.formvalue("tag") 584 | 585 | if not tag then 586 | luci.http.status(400, "no tag name") 587 | luci.http.prepare_content("application/json") 588 | luci.http.write_json({message = "no tag name"}) 589 | return 590 | end 591 | 592 | local dk = docker.new() 593 | local res = dk.images:inspect({name = tag}) 594 | 595 | if res.code == 200 then 596 | local tags = res.body.RepoTags 597 | if #tags > 1 then 598 | local r = dk.images:remove({name = tag}) 599 | local msg = r and r.body and r.body.message or nil 600 | luci.http.status(r.code, msg) 601 | luci.http.prepare_content("application/json") 602 | luci.http.write_json({message = msg}) 603 | else 604 | luci.http.status(500, "Cannot remove the last tag") 605 | luci.http.prepare_content("application/json") 606 | luci.http.write_json({message = "Cannot remove the last tag"}) 607 | end 608 | else 609 | local msg = res and res.body and res.body.message or nil 610 | luci.http.status(res and res.code or 500, msg or "unknow") 611 | luci.http.prepare_content("application/json") 612 | luci.http.write_json({message = msg or "unknow"}) 613 | end 614 | end 615 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2021 Florian Eckert 4 | Copyright 2021 lisaac 5 | ]]-- 6 | 7 | local uci = (require "luci.model.uci").cursor() 8 | 9 | local m, s, o 10 | 11 | m = Map("dockerd", 12 | translate("Docker - Configuration"), 13 | translate("DockerMan is a simple docker manager client for LuCI")) 14 | 15 | if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 16 | s = m:section(NamedSection, "globals", "section", translate("Docker Daemon settings")) 17 | 18 | o = s:option(Flag, "auto_start", translate("Auto start")) 19 | o.rmempty = false 20 | o.write = function(self, section, value) 21 | if value == "1" then 22 | luci.util.exec("/etc/init.d/dockerd enable") 23 | else 24 | luci.util.exec("/etc/init.d/dockerd disable") 25 | end 26 | m.uci:set("dockerd", "globals", "auto_start", value) 27 | end 28 | 29 | o = s:option(Value, "data_root", 30 | translate("Docker Root Dir")) 31 | o.placeholder = "/opt/docker/" 32 | o:depends("remote_endpoint", 0) 33 | 34 | o = s:option(Value, "bip", 35 | translate("Default bridge"), 36 | translate("Configure the default bridge network")) 37 | o.placeholder = "172.17.0.1/16" 38 | o.datatype = "ipaddr" 39 | o:depends("remote_endpoint", 0) 40 | 41 | o = s:option(DynamicList, "registry_mirrors", 42 | translate("Registry Mirrors"), 43 | translate("It replaces the daemon registry mirrors with a new set of registry mirrors")) 44 | o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com") 45 | o:depends("remote_endpoint", 0) 46 | o.forcewrite = true 47 | 48 | o = s:option(ListValue, "log_level", 49 | translate("Log Level"), 50 | translate('Set the logging level')) 51 | o:value("debug", translate("Debug")) 52 | o:value("", translate("Info")) -- This is the default debug level from the deamon is optin is not set 53 | o:value("warn", translate("Warning")) 54 | o:value("error", translate("Error")) 55 | o:value("fatal", translate("Fatal")) 56 | o.rmempty = true 57 | o:depends("remote_endpoint", 0) 58 | 59 | o = s:option(DynamicList, "hosts", 60 | translate("Client connection"), 61 | translate('Specifies where the Docker daemon will listen for client connections (default: unix:///var/run/docker.sock)')) 62 | o:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock") 63 | o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375") 64 | o.rmempty = true 65 | o:depends("remote_endpoint", 0) 66 | end 67 | 68 | s = m:section(NamedSection, "dockerman", "section", translate("DockerMan settings")) 69 | s:tab("ac", translate("Access Control")) 70 | s:tab("dockerman", translate("DockerMan")) 71 | 72 | o = s:taboption("dockerman", Flag, "remote_endpoint", 73 | translate("Remote Endpoint"), 74 | translate("Connect to remote docker endpoint")) 75 | o.rmempty = false 76 | o.validate = function(self, value, sid) 77 | local res = luci.http.formvaluetable("cbid.dockerd") 78 | if res["dockerman.remote_endpoint"] == "1" then 79 | if res["dockerman.remote_port"] and res["dockerman.remote_port"] ~= "" and res["dockerman.remote_host"] and res["dockerman.remote_host"] ~= "" then 80 | return 1 81 | else 82 | return nil, translate("Please input the PORT or HOST IP of remote docker instance!") 83 | end 84 | else 85 | if not res["dockerman.socket_path"] then 86 | return nil, translate("Please input the SOCKET PATH of docker daemon!") 87 | end 88 | end 89 | return 0 90 | end 91 | 92 | o = s:taboption("dockerman", Value, "socket_path", 93 | translate("Docker Socket Path")) 94 | o.default = "/var/run/docker.sock" 95 | o.placeholder = "/var/run/docker.sock" 96 | o:depends("remote_endpoint", 0) 97 | 98 | o = s:taboption("dockerman", Value, "remote_host", 99 | translate("Remote Host"), 100 | translate("Host or IP Address for the connection to a remote docker instance")) 101 | o.datatype = "host" 102 | o.placeholder = "10.1.1.2" 103 | o:depends("remote_endpoint", 1) 104 | 105 | o = s:taboption("dockerman", Value, "remote_port", 106 | translate("Remote Port")) 107 | o.placeholder = "2375" 108 | o.datatype = "port" 109 | o:depends("remote_endpoint", 1) 110 | 111 | -- o = s:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file")) 112 | -- o = s:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path")) 113 | -- o.enabled="true" 114 | -- o.disabled="false" 115 | -- o = s:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile")) 116 | 117 | if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 118 | o = s:taboption("ac", DynamicList, "ac_allowed_interface", translate("Allowed access interfaces"), translate("Which interface(s) can access containers under the bridge network, fill-in Interface Name")) 119 | local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {} 120 | for i, v in ipairs(interfaces) do 121 | o:value(v, v) 122 | end 123 | o = s:taboption("ac", DynamicList, "ac_allowed_ports", translate("Ports allowed to be accessed"), translate("Which Port(s) can be accessed, it's not restricted by the Allowed Access interfaces configuration. Use this configuration with caution!")) 124 | o.placeholder = "8080/tcp" 125 | local docker = require "luci.model.docker" 126 | local containers, res, lost_state 127 | local dk = docker.new() 128 | if dk:_ping().code ~= 200 then 129 | lost_state = true 130 | else 131 | lost_state = false 132 | res = dk.containers:list() 133 | if res and res.code and res.code < 300 then 134 | containers = res.body 135 | end 136 | end 137 | 138 | -- allowed_container.placeholder = "container name_or_id" 139 | if containers then 140 | for i, v in ipairs(containers) do 141 | if v.State == "running" and v.Ports then 142 | for _, port in ipairs(v.Ports) do 143 | if port.PublicPort and port.IP and not string.find(port.IP,":") then 144 | o:value(port.PublicPort.."/"..port.Type, v.Names[1]:sub(2) .. " | " .. port.PublicPort .. " | " .. port.Type) 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | 152 | return m 153 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | require "luci.util" 7 | 8 | local docker = require "luci.model.docker" 9 | local dk = docker.new() 10 | 11 | container_id = arg[1] 12 | local action = arg[2] or "info" 13 | 14 | local m, s, o 15 | local images, networks, container_info, res 16 | 17 | if not container_id then 18 | return 19 | end 20 | 21 | res = dk.containers:inspect({id = container_id}) 22 | if res.code < 300 then 23 | container_info = res.body 24 | else 25 | return 26 | end 27 | 28 | local get_ports = function(d) 29 | local data 30 | 31 | if d.HostConfig and d.HostConfig.PortBindings then 32 | for inter, out in pairs(d.HostConfig.PortBindings) do 33 | data = (data and (data .. "
") or "") .. out[1]["HostPort"] .. ":" .. inter 34 | end 35 | end 36 | 37 | return data 38 | end 39 | 40 | local get_env = function(d) 41 | local data 42 | 43 | if d.Config and d.Config.Env then 44 | for _,v in ipairs(d.Config.Env) do 45 | data = (data and (data .. "
") or "") .. v 46 | end 47 | end 48 | 49 | return data 50 | end 51 | 52 | local get_command = function(d) 53 | local data 54 | 55 | if d.Config and d.Config.Cmd then 56 | for _,v in ipairs(d.Config.Cmd) do 57 | data = (data and (data .. " ") or "") .. v 58 | end 59 | end 60 | 61 | return data 62 | end 63 | 64 | local get_mounts = function(d) 65 | local data 66 | 67 | if d.Mounts then 68 | for _,v in ipairs(d.Mounts) do 69 | local v_sorce_d, v_dest_d 70 | local v_sorce = "" 71 | local v_dest = "" 72 | for v_sorce_d in v["Source"]:gmatch('[^/]+') do 73 | if v_sorce_d and #v_sorce_d > 12 then 74 | v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..." 75 | else 76 | v_sorce = v_sorce .."/".. v_sorce_d 77 | end 78 | end 79 | for v_dest_d in v["Destination"]:gmatch('[^/]+') do 80 | if v_dest_d and #v_dest_d > 12 then 81 | v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..." 82 | else 83 | v_dest = v_dest .."/".. v_dest_d 84 | end 85 | end 86 | data = (data and (data .. "
") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "") 87 | end 88 | end 89 | 90 | return data 91 | end 92 | 93 | local get_device = function(d) 94 | local data 95 | 96 | if d.HostConfig and d.HostConfig.Devices then 97 | for _,v in ipairs(d.HostConfig.Devices) do 98 | data = (data and (data .. "
") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "") 99 | end 100 | end 101 | 102 | return data 103 | end 104 | 105 | local get_links = function(d) 106 | local data 107 | 108 | if d.HostConfig and d.HostConfig.Links then 109 | for _,v in ipairs(d.HostConfig.Links) do 110 | data = (data and (data .. "
") or "") .. v 111 | end 112 | end 113 | 114 | return data 115 | end 116 | 117 | local get_tmpfs = function(d) 118 | local data 119 | 120 | if d.HostConfig and d.HostConfig.Tmpfs then 121 | for k, v in pairs(d.HostConfig.Tmpfs) do 122 | data = (data and (data .. "
") or "") .. k .. (v~="" and ":" or "")..v 123 | end 124 | end 125 | 126 | return data 127 | end 128 | 129 | local get_dns = function(d) 130 | local data 131 | 132 | if d.HostConfig and d.HostConfig.Dns then 133 | for _, v in ipairs(d.HostConfig.Dns) do 134 | data = (data and (data .. "
") or "") .. v 135 | end 136 | end 137 | 138 | return data 139 | end 140 | 141 | local get_sysctl = function(d) 142 | local data 143 | 144 | if d.HostConfig and d.HostConfig.Sysctls then 145 | for k, v in pairs(d.HostConfig.Sysctls) do 146 | data = (data and (data .. "
") or "") .. k..":"..v 147 | end 148 | end 149 | 150 | return data 151 | end 152 | 153 | local get_networks = function(d) 154 | local data={} 155 | 156 | if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then 157 | for k,v in pairs(d.NetworkSettings.Networks) do 158 | data[k] = v.IPAddress or "" 159 | end 160 | end 161 | 162 | return data 163 | end 164 | 165 | 166 | local start_stop_remove = function(m, cmd) 167 | local res 168 | 169 | docker:clear_status() 170 | docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...") 171 | 172 | if cmd ~= "upgrade" then 173 | res = dk.containers[cmd](dk, {id = container_id}) 174 | else 175 | res = dk.containers_upgrade(dk, {id = container_id}) 176 | end 177 | 178 | if res and res.code >= 300 then 179 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) 180 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id)) 181 | else 182 | docker:clear_status() 183 | if cmd ~= "remove" and cmd ~= "upgrade" then 184 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id)) 185 | else 186 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers")) 187 | end 188 | end 189 | end 190 | 191 | local c_color 192 | if container_info.State.Status == 'running' then 193 | c_color = 'green' 194 | elseif container_info.State.Status == 'restarting' then 195 | c_color = 'yellow' 196 | else 197 | c_color = 'red' 198 | end 199 | 200 | m=SimpleForm("docker", 201 | translatef("Docker - Container (%s)", c_color, container_info.Name:sub(2)), 202 | translate("On this page, the selected container can be managed.")) 203 | m.redirect = luci.dispatcher.build_url("admin/docker/containers") 204 | 205 | s = m:section(SimpleSection) 206 | s.template = "dockerman/apply_widget" 207 | s.err=docker:read_status() 208 | s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") 209 | if s.err then 210 | docker:clear_status() 211 | end 212 | 213 | s = m:section(Table,{{}}) 214 | s.notitle=true 215 | s.rowcolors=false 216 | s.template = "cbi/nullsection" 217 | 218 | o = s:option(Button, "_start") 219 | o.template = "dockerman/cbi/inlinebutton" 220 | o.inputtitle=translate("Start") 221 | o.inputstyle = "apply" 222 | o.forcewrite = true 223 | o.write = function(self, section) 224 | start_stop_remove(m,"start") 225 | end 226 | 227 | o = s:option(Button, "_restart") 228 | o.template = "dockerman/cbi/inlinebutton" 229 | o.inputtitle=translate("Restart") 230 | o.inputstyle = "reload" 231 | o.forcewrite = true 232 | o.write = function(self, section) 233 | start_stop_remove(m,"restart") 234 | end 235 | 236 | o = s:option(Button, "_stop") 237 | o.template = "dockerman/cbi/inlinebutton" 238 | o.inputtitle=translate("Stop") 239 | o.inputstyle = "reset" 240 | o.forcewrite = true 241 | o.write = function(self, section) 242 | start_stop_remove(m,"stop") 243 | end 244 | 245 | o = s:option(Button, "_kill") 246 | o.template = "dockerman/cbi/inlinebutton" 247 | o.inputtitle=translate("Kill") 248 | o.inputstyle = "reset" 249 | o.forcewrite = true 250 | o.write = function(self, section) 251 | start_stop_remove(m,"kill") 252 | end 253 | 254 | o = s:option(Button, "_export") 255 | o.template = "dockerman/cbi/inlinebutton" 256 | o.inputtitle=translate("Export") 257 | o.inputstyle = "apply" 258 | o.forcewrite = true 259 | o.write = function(self, section) 260 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/container_export/"..container_id)) 261 | end 262 | 263 | o = s:option(Button, "_upgrade") 264 | o.template = "dockerman/cbi/inlinebutton" 265 | o.inputtitle=translate("Upgrade") 266 | o.inputstyle = "reload" 267 | o.forcewrite = true 268 | o.write = function(self, section) 269 | start_stop_remove(m,"upgrade") 270 | end 271 | 272 | o = s:option(Button, "_duplicate") 273 | o.template = "dockerman/cbi/inlinebutton" 274 | o.inputtitle=translate("Duplicate/Edit") 275 | o.inputstyle = "add" 276 | o.forcewrite = true 277 | o.write = function(self, section) 278 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id)) 279 | end 280 | 281 | o = s:option(Button, "_remove") 282 | o.template = "dockerman/cbi/inlinebutton" 283 | o.inputtitle=translate("Remove") 284 | o.inputstyle = "remove" 285 | o.forcewrite = true 286 | o.write = function(self, section) 287 | start_stop_remove(m,"remove") 288 | end 289 | 290 | s = m:section(SimpleSection) 291 | s.template = "dockerman/container" 292 | 293 | if action == "info" then 294 | res = dk.networks:list() 295 | if res.code < 300 then 296 | networks = res.body 297 | else 298 | return 299 | end 300 | m.submit = false 301 | m.reset = false 302 | table_info = { 303 | ["01name"] = { 304 | _key = translate("Name"), 305 | _value = container_info.Name:sub(2) or "-", 306 | _button=translate("Update") 307 | }, 308 | ["02id"] = { 309 | _key = translate("ID"), 310 | _value = container_info.Id or "-" 311 | }, 312 | ["03image"] = { 313 | _key = translate("Image"), 314 | _value = container_info.Config.Image .. "
" .. container_info.Image 315 | }, 316 | ["04status"] = { 317 | _key = translate("Status"), 318 | _value = container_info.State and container_info.State.Status or "-" 319 | }, 320 | ["05created"] = { 321 | _key = translate("Created"), 322 | _value = container_info.Created or "-" 323 | }, 324 | } 325 | 326 | if container_info.State.Status == "running" then 327 | table_info["06start"] = { 328 | _key = translate("Start Time"), 329 | _value = container_info.State and container_info.State.StartedAt or "-" 330 | } 331 | else 332 | table_info["06start"] = { 333 | _key = translate("Finish Time"), 334 | _value = container_info.State and container_info.State.FinishedAt or "-" 335 | } 336 | end 337 | 338 | table_info["07healthy"] = { 339 | _key = translate("Healthy"), 340 | _value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-" 341 | } 342 | table_info["08restart"] = { 343 | _key = translate("Restart Policy"), 344 | _value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-", 345 | _button=translate("Update") 346 | } 347 | table_info["081user"] = { 348 | _key = translate("User"), 349 | _value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-" 350 | } 351 | table_info["09mount"] = { 352 | _key = translate("Mount/Volume"), 353 | _value = get_mounts(container_info) or "-" 354 | } 355 | table_info["10cmd"] = { 356 | _key = translate("Command"), 357 | _value = get_command(container_info) or "-" 358 | } 359 | table_info["11env"] = { 360 | _key = translate("Env"), 361 | _value = get_env(container_info) or "-" 362 | } 363 | table_info["12ports"] = { 364 | _key = translate("Ports"), 365 | _value = get_ports(container_info) or "-" 366 | } 367 | table_info["13links"] = { 368 | _key = translate("Links"), 369 | _value = get_links(container_info) or "-" 370 | } 371 | table_info["14device"] = { 372 | _key = translate("Device"), 373 | _value = get_device(container_info) or "-" 374 | } 375 | table_info["15tmpfs"] = { 376 | _key = translate("Tmpfs"), 377 | _value = get_tmpfs(container_info) or "-" 378 | } 379 | table_info["16dns"] = { 380 | _key = translate("DNS"), 381 | _value = get_dns(container_info) or "-" 382 | } 383 | table_info["17sysctl"] = { 384 | _key = translate("Sysctl"), 385 | _value = get_sysctl(container_info) or "-" 386 | } 387 | 388 | info_networks = get_networks(container_info) 389 | list_networks = {} 390 | for _, v in ipairs (networks) do 391 | if v and v.Name then 392 | local parent = v.Options and v.Options.parent or nil 393 | local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil 394 | ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil 395 | local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "") 396 | list_networks[v.Name] = network_name 397 | end 398 | end 399 | 400 | if type(info_networks)== "table" then 401 | for k,v in pairs(info_networks) do 402 | table_info["14network"..k] = { 403 | _key = translate("Network"), 404 | _value = k.. (v~="" and (" | ".. v) or ""), 405 | _button=translate("Disconnect") 406 | } 407 | list_networks[k]=nil 408 | end 409 | end 410 | 411 | table_info["15connect"] = { 412 | _key = translate("Connect Network"), 413 | _value = list_networks ,_opts = "", 414 | _button=translate("Connect") 415 | } 416 | 417 | s = m:section(Table,table_info) 418 | s.nodescr=true 419 | s.formvalue=function(self, section) 420 | return table_info 421 | end 422 | 423 | o = s:option(DummyValue, "_key", translate("Info")) 424 | o.width = "20%" 425 | 426 | o = s:option(ListValue, "_value") 427 | o.render = function(self, section, scope) 428 | if table_info[section]._key == translate("Name") then 429 | self:reset_values() 430 | self.template = "cbi/value" 431 | self.size = 30 432 | self.keylist = {} 433 | self.vallist = {} 434 | self.default=table_info[section]._value 435 | Value.render(self, section, scope) 436 | elseif table_info[section]._key == translate("Restart Policy") then 437 | self.template = "cbi/lvalue" 438 | self:reset_values() 439 | self.size = nil 440 | self:value("no", "No") 441 | self:value("unless-stopped", "Unless stopped") 442 | self:value("always", "Always") 443 | self:value("on-failure", "On failure") 444 | self.default=table_info[section]._value 445 | ListValue.render(self, section, scope) 446 | elseif table_info[section]._key == translate("Connect Network") then 447 | self.template = "cbi/lvalue" 448 | self:reset_values() 449 | self.size = nil 450 | for k,v in pairs(list_networks) do 451 | if k ~= "host" then 452 | self:value(k,v) 453 | end 454 | end 455 | self.default=table_info[section]._value 456 | ListValue.render(self, section, scope) 457 | else 458 | self:reset_values() 459 | self.rawhtml=true 460 | self.template = "cbi/dvalue" 461 | self.default=table_info[section]._value 462 | DummyValue.render(self, section, scope) 463 | end 464 | end 465 | o.forcewrite = true 466 | o.write = function(self, section, value) 467 | table_info[section]._value=value 468 | end 469 | o.validate = function(self, value) 470 | return value 471 | end 472 | 473 | o = s:option(Value, "_opts") 474 | o.forcewrite = true 475 | o.write = function(self, section, value) 476 | table_info[section]._opts=value 477 | end 478 | o.validate = function(self, value) 479 | return value 480 | end 481 | o.render = function(self, section, scope) 482 | if table_info[section]._key==translate("Connect Network") then 483 | self.template = "cbi/value" 484 | self.keylist = {} 485 | self.vallist = {} 486 | self.placeholder = "10.1.1.254" 487 | self.datatype = "ip4addr" 488 | self.default=table_info[section]._opts 489 | Value.render(self, section, scope) 490 | else 491 | self.rawhtml=true 492 | self.template = "cbi/dvalue" 493 | self.default=table_info[section]._opts 494 | DummyValue.render(self, section, scope) 495 | end 496 | end 497 | 498 | o = s:option(Button, "_button") 499 | o.forcewrite = true 500 | o.render = function(self, section, scope) 501 | if table_info[section]._button and table_info[section]._value ~= nil then 502 | self.inputtitle=table_info[section]._button 503 | self.template = "cbi/button" 504 | self.inputstyle = "edit" 505 | Button.render(self, section, scope) 506 | else 507 | self.template = "cbi/dvalue" 508 | self.default="" 509 | DummyValue.render(self, section, scope) 510 | end 511 | end 512 | o.write = function(self, section, value) 513 | local res 514 | 515 | docker:clear_status() 516 | 517 | if section == "01name" then 518 | docker:append_status("Containers: rename " .. container_id .. "...") 519 | local new_name = table_info[section]._value 520 | res = dk.containers:rename({ 521 | id = container_id, 522 | query = { 523 | name=new_name 524 | } 525 | }) 526 | elseif section == "08restart" then 527 | docker:append_status("Containers: update " .. container_id .. "...") 528 | local new_restart = table_info[section]._value 529 | res = dk.containers:update({ 530 | id = container_id, 531 | body = { 532 | RestartPolicy = { 533 | Name = new_restart 534 | } 535 | } 536 | }) 537 | elseif table_info[section]._key == translate("Network") then 538 | local _,_,leave_network 539 | 540 | _, _, leave_network = table_info[section]._value:find("(.-) | .+") 541 | leave_network = leave_network or table_info[section]._value 542 | docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...") 543 | res = dk.networks:disconnect({ 544 | name = leave_network, 545 | body = { 546 | Container = container_id 547 | } 548 | }) 549 | elseif section == "15connect" then 550 | local connect_network = table_info[section]._value 551 | local network_opiton 552 | if connect_network ~= "none" 553 | and connect_network ~= "bridge" 554 | and connect_network ~= "host" then 555 | 556 | network_opiton = table_info[section]._opts ~= "" and { 557 | IPAMConfig={ 558 | IPv4Address=table_info[section]._opts 559 | } 560 | } or nil 561 | end 562 | docker:append_status("Network: connect " .. connect_network .. container_id .. "...") 563 | res = dk.networks:connect({ 564 | name = connect_network, 565 | body = { 566 | Container = container_id, 567 | EndpointConfig= network_opiton 568 | } 569 | }) 570 | end 571 | 572 | if res and res.code > 300 then 573 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) 574 | else 575 | docker:clear_status() 576 | end 577 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info")) 578 | end 579 | elseif action == "resources" then 580 | s = m:section(SimpleSection) 581 | o = s:option( Value, "cpus", 582 | translate("CPUs"), 583 | translate("Number of CPUs. Number is a fractional number. 0.000 means no limit.")) 584 | o.placeholder = "1.5" 585 | o.rmempty = true 586 | o.datatype="ufloat" 587 | o.default = container_info.HostConfig.NanoCpus / (10^9) 588 | 589 | o = s:option(Value, "cpushares", 590 | translate("CPU Shares Weight"), 591 | translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024.")) 592 | o.placeholder = "1024" 593 | o.rmempty = true 594 | o.datatype="uinteger" 595 | o.default = container_info.HostConfig.CpuShares 596 | 597 | o = s:option(Value, "memory", 598 | translate("Memory"), 599 | translate("Memory limit (format: []). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M.")) 600 | o.placeholder = "128m" 601 | o.rmempty = true 602 | o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0 603 | 604 | o = s:option(Value, "blkioweight", 605 | translate("Block IO Weight"), 606 | translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000.")) 607 | o.placeholder = "500" 608 | o.rmempty = true 609 | o.datatype="uinteger" 610 | o.default = container_info.HostConfig.BlkioWeight 611 | 612 | m.handle = function(self, state, data) 613 | if state == FORM_VALID then 614 | local memory = data.memory 615 | if memory and memory ~= 0 then 616 | _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)") 617 | if n then 618 | unit = unit and unit:sub(1,1):upper() or "B" 619 | if unit == "M" then 620 | memory = tonumber(n) * 1024 * 1024 621 | elseif unit == "G" then 622 | memory = tonumber(n) * 1024 * 1024 * 1024 623 | elseif unit == "K" then 624 | memory = tonumber(n) * 1024 625 | else 626 | memory = tonumber(n) 627 | end 628 | end 629 | end 630 | 631 | request_body = { 632 | BlkioWeight = tonumber(data.blkioweight), 633 | NanoCPUs = tonumber(data.cpus)*10^9, 634 | Memory = tonumber(memory), 635 | CpuShares = tonumber(data.cpushares) 636 | } 637 | 638 | docker:write_status("Containers: update " .. container_id .. "...") 639 | local res = dk.containers:update({id = container_id, body = request_body}) 640 | if res and res.code >= 300 then 641 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) 642 | else 643 | docker:clear_status() 644 | end 645 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources")) 646 | end 647 | end 648 | 649 | elseif action == "file" then 650 | m.submit = false 651 | m.reset = false 652 | s= m:section(SimpleSection) 653 | s.template = "dockerman/container_file_manager" 654 | s.container = container_id 655 | m.redirect = nil 656 | elseif action == "inspect" then 657 | s = m:section(SimpleSection) 658 | s.syslog = luci.jsonc.stringify(container_info, true) 659 | s.title = translate("Container Inspect") 660 | s.template = "dockerman/logs" 661 | m.submit = false 662 | m.reset = false 663 | elseif action == "logs" then 664 | local logs = "" 665 | local query ={ 666 | stdout = 1, 667 | stderr = 1, 668 | tail = 1000 669 | } 670 | 671 | s = m:section(SimpleSection) 672 | 673 | logs = dk.containers:logs({id = container_id, query = query}) 674 | if logs.code == 200 then 675 | s.syslog=logs.body 676 | else 677 | s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body 678 | end 679 | 680 | s.title=translate("Container Logs") 681 | s.template = "dockerman/logs" 682 | m.submit = false 683 | m.reset = false 684 | elseif action == "console" then 685 | m.submit = false 686 | m.reset = false 687 | local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil 688 | local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil 689 | 690 | if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then 691 | local cmd = "/bin/sh" 692 | local uid 693 | 694 | s = m:section(SimpleSection) 695 | 696 | o = s:option(Value, "command", translate("Command")) 697 | o:value("/bin/sh", "/bin/sh") 698 | o:value("/bin/ash", "/bin/ash") 699 | o:value("/bin/bash", "/bin/bash") 700 | o.default = "/bin/sh" 701 | o.forcewrite = true 702 | o.write = function(self, section, value) 703 | cmd = value 704 | end 705 | 706 | o = s:option(Value, "uid", translate("UID")) 707 | o.forcewrite = true 708 | o.write = function(self, section, value) 709 | uid = value 710 | end 711 | 712 | o = s:option(Button, "connect") 713 | o.render = function(self, section, scope) 714 | self.inputstyle = "add" 715 | self.title = " " 716 | self.inputtitle = translate("Connect") 717 | Button.render(self, section, scope) 718 | end 719 | o.write = function(self, section) 720 | local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil 721 | local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil 722 | 723 | if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then 724 | return 725 | end 726 | local uci = (require "luci.model.uci").cursor() 727 | 728 | local ttyd_ssl = uci:get("ttyd", "@ttyd[0]", "ssl") 729 | local ttyd_ssl_key = uci:get("ttyd", "@ttyd[0]", "ssl_key") 730 | local ttyd_ssl_cert = uci:get("ttyd", "@ttyd[0]", "ssl_cert") 731 | 732 | if ttyd_ssl == "1" and ttyd_ssl_cert and ttyd_ssl_key then 733 | cmd_ttyd = string.format('%s -S -C %s -K %s', cmd_ttyd, ttyd_ssl_cert, ttyd_ssl_key) 734 | end 735 | 736 | local pid = luci.util.trim(luci.util.exec("netstat -lnpt | grep :7682 | grep ttyd | tr -s ' ' | cut -d ' ' -f7 | cut -d'/' -f1")) 737 | if pid and pid ~= "" then 738 | luci.util.exec("kill -9 " .. pid) 739 | end 740 | 741 | local hosts 742 | local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") or false 743 | local host = nil 744 | local port = nil 745 | local socket = nil 746 | 747 | if remote then 748 | host = uci:get("dockerd", "dockerman", "remote_host") or nil 749 | port = uci:get("dockerd", "dockerman", "remote_port") or nil 750 | else 751 | socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock" 752 | end 753 | 754 | if remote and host and port then 755 | hosts = "tcp://" .. host .. ':'.. port 756 | elseif socket then 757 | hosts = "unix://" .. socket 758 | else 759 | return 760 | end 761 | 762 | if uid and uid ~= "" then 763 | uid = "-u " .. uid 764 | else 765 | uid = "" 766 | end 767 | 768 | local start_cmd = string.format('%s -d 2 --once -p 7682 %s -H "%s" exec -it %s %s %s&', cmd_ttyd, cmd_docker, hosts, uid, container_id, cmd) 769 | 770 | os.execute(start_cmd) 771 | 772 | o = s:option(DummyValue, "console") 773 | o.container_id = container_id 774 | o.template = "dockerman/container_console" 775 | end 776 | end 777 | elseif action == "stats" then 778 | local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}}) 779 | local container_top 780 | 781 | if response.code == 200 then 782 | container_top=response.body 783 | else 784 | response = dk.containers:top({id = container_id}) 785 | if response.code == 200 then 786 | container_top=response.body 787 | end 788 | end 789 | 790 | if type(container_top) == "table" then 791 | s = m:section(SimpleSection) 792 | s.container_id = container_id 793 | s.template = "dockerman/container_stats" 794 | table_stats = { 795 | cpu={ 796 | key=translate("CPU Useage"), 797 | value='-' 798 | }, 799 | memory={ 800 | key=translate("Memory Useage"), 801 | value='-' 802 | } 803 | } 804 | 805 | container_top = response.body 806 | s = m:section(Table, table_stats, translate("Stats")) 807 | s:option(DummyValue, "key", translate("Stats")).width="33%" 808 | s:option(DummyValue, "value") 809 | top_section = m:section(Table, container_top.Processes, translate("TOP")) 810 | for i, v in ipairs(container_top.Titles) do 811 | top_section:option(DummyValue, i, translate(v)) 812 | end 813 | end 814 | 815 | m.submit = false 816 | m.reset = false 817 | end 818 | 819 | return m 820 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local http = require "luci.http" 7 | local docker = require "luci.model.docker" 8 | 9 | local m, s, o 10 | local images, networks, containers, res, lost_state 11 | local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode 12 | local dk = docker.new() 13 | 14 | if dk:_ping().code ~= 200 then 15 | lost_state = true 16 | else 17 | res = dk.images:list() 18 | if res and res.code and res.code < 300 then 19 | images = res.body 20 | end 21 | 22 | res = dk.networks:list() 23 | if res and res.code and res.code < 300 then 24 | networks = res.body 25 | end 26 | 27 | res = dk.containers:list({ 28 | query = { 29 | all = true 30 | } 31 | }) 32 | if res and res.code and res.code < 300 then 33 | containers = res.body 34 | end 35 | end 36 | 37 | function get_containers() 38 | local data = {} 39 | if type(containers) ~= "table" then 40 | return nil 41 | end 42 | 43 | for i, v in ipairs(containers) do 44 | local index = (10^12 - v.Created) .. "_id_" .. v.Id 45 | 46 | data[index]={} 47 | data[index]["_selected"] = 0 48 | data[index]["_id"] = v.Id:sub(1,12) 49 | -- data[index]["name"] = v.Names[1]:sub(2) 50 | data[index]["_status"] = v.Status 51 | 52 | if v.Status:find("^Up") then 53 | data[index]["_name"] = ""..v.Names[1]:sub(2).."" 54 | data[index]["_status"] = "".. data[index]["_status"] .. "" .. "


" 55 | else 56 | data[index]["_name"] = ""..v.Names[1]:sub(2).."" 57 | data[index]["_status"] = ''.. data[index]["_status"] .. "" 58 | end 59 | 60 | if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then 61 | for networkname, netconfig in pairs(v.NetworkSettings.Networks) do 62 | data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "") 63 | end 64 | end 65 | 66 | -- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge" 67 | -- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil 68 | -- local _, _, image = v.Image:find("^sha256:(.+)") 69 | -- if image ~= nil then 70 | -- image=image:sub(1,12) 71 | -- end 72 | 73 | if v.Ports and next(v.Ports) ~= nil then 74 | data[index]["_ports"] = nil 75 | local ip = require "luci.ip" 76 | for _,v2 in ipairs(v.Ports) do 77 | -- display ipv4 only 78 | if ip.new(v2.IP or "0.0.0.0"):is4() then 79 | data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "") 80 | .. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('') or "") 81 | .. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "") 82 | .. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "" or "") 83 | end 84 | end 85 | end 86 | for ii,iv in ipairs(images) do 87 | if iv.Id == v.ImageID then 88 | data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (next(iv.RepoDigests) and (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")) or "" 89 | end 90 | end 91 | data[index]["_id_name"] = ''.. data[index]["_name"] .. "
ID: " .. data[index]["_id"] 92 | .. "

Image: " .. (data[index]["_image"] or "<none>") 93 | .. "
" 94 | 95 | if type(v.Mounts) == "table" and next(v.Mounts) then 96 | for _, v2 in pairs(v.Mounts) do 97 | if v2.Type ~= "volume" then 98 | local v_sorce_d, v_dest_d 99 | local v_sorce = "" 100 | local v_dest = "" 101 | for v_sorce_d in v2["Source"]:gmatch('[^/]+') do 102 | if v_sorce_d and #v_sorce_d > 12 then 103 | v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,8) .. ".." 104 | else 105 | v_sorce = v_sorce .."/".. v_sorce_d 106 | end 107 | end 108 | for v_dest_d in v2["Destination"]:gmatch('[^/]+') do 109 | if v_dest_d and #v_dest_d > 12 then 110 | v_dest = v_dest .. "/" .. v_dest_d:sub(1,8) .. ".." 111 | else 112 | v_dest = v_dest .."/".. v_dest_d 113 | end 114 | end 115 | data[index]["_mounts"] = (data[index]["_mounts"] and (data[index]["_mounts"] .. "
") or "") .. '' .. v_sorce .. "→" .. v_dest..'' 116 | end 117 | end 118 | end 119 | 120 | data[index]["_image_id"] = v.ImageID:sub(8,20) 121 | data[index]["_command"] = v.Command 122 | end 123 | return data 124 | end 125 | 126 | local container_list = not lost_state and get_containers() or {} 127 | 128 | m = SimpleForm("docker", 129 | translate("Docker - Containers"), 130 | translate("This page displays all containers that have been created on the connected docker host.")) 131 | m.submit=false 132 | m.reset=false 133 | m:append(Template("dockerman/containers_running_stats")) 134 | 135 | s = m:section(SimpleSection) 136 | s.template = "dockerman/apply_widget" 137 | s.err=docker:read_status() 138 | s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") 139 | if s.err then 140 | docker:clear_status() 141 | end 142 | 143 | s = m:section(Table, container_list, translate("Containers")) 144 | s.nodescr=true 145 | s.config="containers" 146 | 147 | o = s:option(Flag, "_selected","") 148 | o.disabled = 0 149 | o.enabled = 1 150 | o.default = 0 151 | o.width = "1%" 152 | o.write=function(self, section, value) 153 | container_list[section]._selected = value 154 | end 155 | 156 | -- o = s:option(DummyValue, "_id", translate("ID")) 157 | -- o.width="10%" 158 | 159 | -- o = s:option(DummyValue, "_name", translate("Container Name")) 160 | -- o.rawhtml = true 161 | 162 | o = s:option(DummyValue, "_id_name", translate("Container Info")) 163 | o.rawhtml = true 164 | o.width="15%" 165 | 166 | o = s:option(DummyValue, "_status", translate("Status")) 167 | o.width="15%" 168 | o.rawhtml=true 169 | 170 | o = s:option(DummyValue, "_network", translate("Network")) 171 | o.width="10%" 172 | 173 | o = s:option(DummyValue, "_ports", translate("Ports")) 174 | o.width="5%" 175 | o.rawhtml = true 176 | o = s:option(DummyValue, "_mounts", translate("Mounts")) 177 | o.width="25%" 178 | o.rawhtml = true 179 | 180 | -- o = s:option(DummyValue, "_image", translate("Image")) 181 | -- o.width="8%" 182 | 183 | o = s:option(DummyValue, "_command", translate("Command")) 184 | o.width="15%" 185 | 186 | local start_stop_remove = function(m, cmd) 187 | local container_selected = {} 188 | -- 遍历table中sectionid 189 | for k in pairs(container_list) do 190 | -- 得到选中项的名字 191 | if container_list[k]._selected == 1 then 192 | container_selected[#container_selected + 1] = container_list[k]["_id"] 193 | end 194 | end 195 | if #container_selected > 0 then 196 | local success = true 197 | 198 | docker:clear_status() 199 | for _, cont in ipairs(container_selected) do 200 | docker:append_status("Containers: " .. cmd .. " " .. cont .. "...") 201 | local res = dk.containers[cmd](dk, {id = cont}) 202 | if res and res.code and res.code >= 300 then 203 | success = false 204 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n") 205 | else 206 | docker:append_status("done\n") 207 | end 208 | end 209 | 210 | if success then 211 | docker:clear_status() 212 | end 213 | 214 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers")) 215 | end 216 | end 217 | 218 | s = m:section(Table,{{}}) 219 | s.notitle=true 220 | s.rowcolors=false 221 | s.template="cbi/nullsection" 222 | 223 | o = s:option(Button, "_new") 224 | o.inputtitle = translate("Add") 225 | o.template = "dockerman/cbi/inlinebutton" 226 | o.inputstyle = "add" 227 | o.forcewrite = true 228 | o.write = function(self, section) 229 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer")) 230 | end 231 | o.disable = lost_state 232 | 233 | o = s:option(Button, "_start") 234 | o.template = "dockerman/cbi/inlinebutton" 235 | o.inputtitle = translate("Start") 236 | o.inputstyle = "apply" 237 | o.forcewrite = true 238 | o.write = function(self, section) 239 | start_stop_remove(m,"start") 240 | end 241 | o.disable = lost_state 242 | 243 | o = s:option(Button, "_restart") 244 | o.template = "dockerman/cbi/inlinebutton" 245 | o.inputtitle = translate("Restart") 246 | o.inputstyle = "reload" 247 | o.forcewrite = true 248 | o.write = function(self, section) 249 | start_stop_remove(m,"restart") 250 | end 251 | o.disable = lost_state 252 | 253 | o = s:option(Button, "_stop") 254 | o.template = "dockerman/cbi/inlinebutton" 255 | o.inputtitle = translate("Stop") 256 | o.inputstyle = "reset" 257 | o.forcewrite = true 258 | o.write = function(self, section) 259 | start_stop_remove(m,"stop") 260 | end 261 | o.disable = lost_state 262 | 263 | o = s:option(Button, "_kill") 264 | o.template = "dockerman/cbi/inlinebutton" 265 | o.inputtitle = translate("Kill") 266 | o.inputstyle = "reset" 267 | o.forcewrite = true 268 | o.write = function(self, section) 269 | start_stop_remove(m,"kill") 270 | end 271 | o.disable = lost_state 272 | 273 | o = s:option(Button, "_remove") 274 | o.template = "dockerman/cbi/inlinebutton" 275 | o.inputtitle = translate("Remove") 276 | o.inputstyle = "remove" 277 | o.forcewrite = true 278 | o.write = function(self, section) 279 | start_stop_remove(m, "remove") 280 | end 281 | o.disable = lost_state 282 | 283 | return m 284 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | local dk = docker.new() 8 | 9 | local containers, images, res, lost_state 10 | local m, s, o 11 | 12 | if dk:_ping().code ~= 200 then 13 | lost_state = true 14 | else 15 | res = dk.images:list() 16 | if res and res.code and res.code < 300 then 17 | images = res.body 18 | end 19 | 20 | res = dk.containers:list({ query = { all = true } }) 21 | if res and res.code and res.code < 300 then 22 | containers = res.body 23 | end 24 | end 25 | 26 | function get_images() 27 | local data = {} 28 | 29 | for i, v in ipairs(images) do 30 | local index = v.Created .. v.Id 31 | 32 | data[index]={} 33 | data[index]["_selected"] = 0 34 | data[index]["id"] = v.Id:sub(8) 35 | data[index]["_id"] = '' .. v.Id:sub(8,20) .. '' 36 | 37 | if v.RepoTags and next(v.RepoTags)~=nil then 38 | for i, v1 in ipairs(v.RepoTags) do 39 | data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "
" )or "") .. ((v1:match("") or (#v.RepoTags == 1)) and v1 or ('' .. v1 .. '')) 40 | 41 | if not data[index]["tag"] then 42 | data[index]["tag"] = v1 43 | end 44 | end 45 | else 46 | data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+") 47 | data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "" ).. ":" 48 | end 49 | 50 | data[index]["_tags"] = data[index]["_tags"]:gsub("","<none>") 51 | for ci,cv in ipairs(containers) do 52 | if v.Id == cv.ImageID then 53 | data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "").. 54 | ''.. cv.Names[1]:sub(2).."" 55 | end 56 | end 57 | 58 | data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB" 59 | data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created) 60 | end 61 | 62 | return data 63 | end 64 | 65 | local image_list = not lost_state and get_images() or {} 66 | 67 | m = SimpleForm("docker", 68 | translate("Docker - Images"), 69 | translate("On this page all images are displayed that are available on the system and with which a container can be created.")) 70 | m.submit=false 71 | m.reset=false 72 | 73 | local pull_value={ 74 | _image_tag_name="", 75 | _registry="index.docker.io" 76 | } 77 | 78 | s = m:section(SimpleSection, 79 | translate("Pull Image"), 80 | translate("By entering a valid image name with the corresponding version, the docker image can be downloaded from the configured registry.")) 81 | s.template="cbi/nullsection" 82 | 83 | o = s:option(Value, "_image_tag_name") 84 | o.template = "dockerman/cbi/inlinevalue" 85 | o.placeholder="lisaac/luci:latest" 86 | o.write = function(self, section, value) 87 | local hastag = value:find(":") 88 | 89 | if not hastag then 90 | value = value .. ":latest" 91 | end 92 | pull_value["_image_tag_name"] = value 93 | end 94 | 95 | o = s:option(Button, "_pull") 96 | o.inputtitle= translate("Pull") 97 | o.template = "dockerman/cbi/inlinebutton" 98 | o.inputstyle = "add" 99 | o.disable = lost_state 100 | o.write = function(self, section) 101 | local tag = pull_value["_image_tag_name"] 102 | local json_stringify = luci.jsonc and luci.jsonc.stringify 103 | 104 | if tag and tag ~= "" then 105 | docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n") 106 | local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb) 107 | 108 | if res and res.code 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 ".. tag)) then 109 | docker:clear_status() 110 | else 111 | docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n") 112 | end 113 | else 114 | docker:append_status("code: 400 please input the name of image name!") 115 | end 116 | 117 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/images")) 118 | end 119 | 120 | s = m:section(SimpleSection, 121 | translate("Import Image"), 122 | translate("When pressing the Import button, both a local image can be loaded onto the system and a valid image tar can be downloaded from remote.")) 123 | 124 | o = s:option(DummyValue, "_image_import") 125 | o.template = "dockerman/images_import" 126 | o.disable = lost_state 127 | 128 | s = m:section(Table, image_list, translate("Images overview")) 129 | 130 | o = s:option(Flag, "_selected","") 131 | o.disabled = 0 132 | o.enabled = 1 133 | o.default = 0 134 | o.write = function(self, section, value) 135 | image_list[section]._selected = value 136 | end 137 | 138 | o = s:option(DummyValue, "_id", translate("ID")) 139 | o.rawhtml = true 140 | 141 | o = s:option(DummyValue, "_tags", translate("RepoTags")) 142 | o.rawhtml = true 143 | 144 | o = s:option(DummyValue, "_containers", translate("Containers")) 145 | o.rawhtml = true 146 | 147 | o = s:option(DummyValue, "_size", translate("Size")) 148 | 149 | o = s:option(DummyValue, "_created", translate("Created")) 150 | 151 | local remove_action = function(force) 152 | local image_selected = {} 153 | 154 | for k in pairs(image_list) do 155 | if image_list[k]._selected == 1 then 156 | image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("
") or image_list[k]["_tags"]:match("<none>")) and image_list[k].id or image_list[k].tag 157 | end 158 | end 159 | 160 | if next(image_selected) ~= nil then 161 | local success = true 162 | 163 | docker:clear_status() 164 | for _, img in ipairs(image_selected) do 165 | local query 166 | docker:append_status("Images: " .. "remove" .. " " .. img .. "...") 167 | 168 | if force then 169 | query = {force = true} 170 | end 171 | 172 | local msg = dk.images:remove({ 173 | id = img, 174 | query = query 175 | }) 176 | if msg and msg.code ~= 200 then 177 | docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") 178 | success = false 179 | else 180 | docker:append_status("done\n") 181 | end 182 | end 183 | 184 | if success then 185 | docker:clear_status() 186 | end 187 | 188 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/images")) 189 | end 190 | end 191 | 192 | s = m:section(SimpleSection) 193 | s.template = "dockerman/apply_widget" 194 | s.err = docker:read_status() 195 | s.err = s.err and s.err:gsub("\n","
"):gsub(" "," ") 196 | if s.err then 197 | docker:clear_status() 198 | end 199 | 200 | s = m:section(Table,{{}}) 201 | s.notitle=true 202 | s.rowcolors=false 203 | s.template="cbi/nullsection" 204 | 205 | o = s:option(Button, "remove") 206 | o.inputtitle= translate("Remove") 207 | o.template = "dockerman/cbi/inlinebutton" 208 | o.inputstyle = "remove" 209 | o.forcewrite = true 210 | o.write = function(self, section) 211 | remove_action() 212 | end 213 | o.disable = lost_state 214 | 215 | o = s:option(Button, "forceremove") 216 | o.inputtitle= translate("Force Remove") 217 | o.template = "dockerman/cbi/inlinebutton" 218 | o.inputstyle = "remove" 219 | o.forcewrite = true 220 | o.write = function(self, section) 221 | remove_action(true) 222 | end 223 | o.disable = lost_state 224 | 225 | o = s:option(Button, "save") 226 | o.inputtitle= translate("Save") 227 | o.template = "dockerman/cbi/inlinebutton" 228 | o.inputstyle = "edit" 229 | o.disable = lost_state 230 | o.forcewrite = true 231 | o.write = function (self, section) 232 | local image_selected = {} 233 | 234 | for k in pairs(image_list) do 235 | if image_list[k]._selected == 1 then 236 | image_selected[#image_selected + 1] = image_list[k].id 237 | end 238 | end 239 | 240 | if next(image_selected) ~= nil then 241 | local names, first, show_name 242 | 243 | for _, img in ipairs(image_selected) do 244 | names = names and (names .. "&names=".. img) or img 245 | end 246 | if #image_selected > 1 then 247 | show_name = "images" 248 | else 249 | show_name = image_selected[1] 250 | end 251 | local cb = function(res, chunk) 252 | if res and res.code and res.code == 200 then 253 | if not first then 254 | first = true 255 | luci.http.header('Content-Disposition', 'inline; filename="'.. show_name .. '.tar"') 256 | luci.http.header('Content-Type', 'application\/x-tar') 257 | end 258 | luci.ltn12.pump.all(chunk, luci.http.write) 259 | else 260 | if not first then 261 | first = true 262 | luci.http.prepare_content("text/plain") 263 | end 264 | luci.ltn12.pump.all(chunk, luci.http.write) 265 | end 266 | end 267 | 268 | docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...") 269 | local msg = dk.images:get({query = {names = names}}, cb) 270 | if msg and msg.code and msg.code ~= 200 then 271 | docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") 272 | else 273 | docker:clear_status() 274 | end 275 | end 276 | end 277 | 278 | o = s:option(Button, "load") 279 | o.inputtitle= translate("Load") 280 | o.template = "dockerman/images_load" 281 | o.inputstyle = "add" 282 | o.disable = lost_state 283 | 284 | return m 285 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | 8 | local m, s, o 9 | local networks, dk, res, lost_state 10 | 11 | dk = docker.new() 12 | 13 | if dk:_ping().code ~= 200 then 14 | lost_state = true 15 | else 16 | res = dk.networks:list() 17 | if res and res.code and res.code < 300 then 18 | networks = res.body 19 | end 20 | end 21 | 22 | local get_networks = function () 23 | local data = {} 24 | 25 | if type(networks) ~= "table" then 26 | return nil 27 | end 28 | 29 | for i, v in ipairs(networks) do 30 | local index = v.Created .. v.Id 31 | 32 | data[index]={} 33 | data[index]["_selected"] = 0 34 | data[index]["_id"] = v.Id:sub(1,12) 35 | data[index]["_name"] = v.Name 36 | data[index]["_driver"] = v.Driver 37 | 38 | if v.Driver == "bridge" then 39 | data[index]["_interface"] = v.Options["com.docker.network.bridge.name"] 40 | elseif v.Driver == "macvlan" then 41 | data[index]["_interface"] = v.Options.parent 42 | end 43 | 44 | data[index]["_subnet"] = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil 45 | data[index]["_gateway"] = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil 46 | end 47 | 48 | return data 49 | end 50 | 51 | local network_list = not lost_state and get_networks() or {} 52 | 53 | m = SimpleForm("docker", 54 | translate("Docker - Networks"), 55 | translate("This page displays all docker networks that have been created on the connected docker host.")) 56 | m.submit=false 57 | m.reset=false 58 | 59 | s = m:section(Table, network_list, translate("Networks overview")) 60 | s.nodescr=true 61 | 62 | o = s:option(Flag, "_selected","") 63 | o.template = "dockerman/cbi/xfvalue" 64 | o.disabled = 0 65 | o.enabled = 1 66 | o.default = 0 67 | o.render = function(self, section, scope) 68 | self.disable = 0 69 | if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then 70 | self.disable = 1 71 | end 72 | Flag.render(self, section, scope) 73 | end 74 | o.write = function(self, section, value) 75 | network_list[section]._selected = value 76 | end 77 | 78 | o = s:option(DummyValue, "_id", translate("ID")) 79 | 80 | o = s:option(DummyValue, "_name", translate("Network Name")) 81 | 82 | o = s:option(DummyValue, "_driver", translate("Driver")) 83 | 84 | o = s:option(DummyValue, "_interface", translate("Parent Interface")) 85 | 86 | o = s:option(DummyValue, "_subnet", translate("Subnet")) 87 | 88 | o = s:option(DummyValue, "_gateway", translate("Gateway")) 89 | 90 | s = m:section(SimpleSection) 91 | s.template = "dockerman/apply_widget" 92 | s.err = docker:read_status() 93 | s.err = s.err and s.err:gsub("\n","
"):gsub(" "," ") 94 | if s.err then 95 | docker:clear_status() 96 | end 97 | 98 | s = m:section(Table,{{}}) 99 | s.notitle=true 100 | s.rowcolors=false 101 | s.template="cbi/nullsection" 102 | 103 | o = s:option(Button, "_new") 104 | o.inputtitle= translate("New") 105 | o.template = "dockerman/cbi/inlinebutton" 106 | o.notitle=true 107 | o.inputstyle = "add" 108 | o.forcewrite = true 109 | o.disable = lost_state 110 | o.write = function(self, section) 111 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork")) 112 | end 113 | 114 | o = s:option(Button, "_remove") 115 | o.inputtitle= translate("Remove") 116 | o.template = "dockerman/cbi/inlinebutton" 117 | o.inputstyle = "remove" 118 | o.forcewrite = true 119 | o.disable = lost_state 120 | o.write = function(self, section) 121 | local network_selected = {} 122 | local network_name_selected = {} 123 | local network_driver_selected = {} 124 | 125 | for k in pairs(network_list) do 126 | if network_list[k]._selected == 1 then 127 | network_selected[#network_selected + 1] = network_list[k]._id 128 | network_name_selected[#network_name_selected + 1] = network_list[k]._name 129 | network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver 130 | end 131 | end 132 | 133 | if next(network_selected) ~= nil then 134 | local success = true 135 | docker:clear_status() 136 | 137 | for ii, net in ipairs(network_selected) do 138 | docker:append_status("Networks: " .. "remove" .. " " .. net .. "...") 139 | local res = dk.networks["remove"](dk, {id = net}) 140 | 141 | if res and res.code and res.code >= 300 then 142 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n") 143 | success = false 144 | else 145 | docker:append_status("done\n") 146 | if network_driver_selected[ii] == "macvlan" then 147 | docker.remove_macvlan_interface(network_name_selected[ii]) 148 | end 149 | end 150 | end 151 | 152 | if success then 153 | docker:clear_status() 154 | end 155 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks")) 156 | end 157 | end 158 | 159 | return m 160 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | 8 | local m, s, o 9 | 10 | local dk = docker.new() 11 | if dk:_ping().code ~= 200 then 12 | lost_state = true 13 | end 14 | 15 | m = SimpleForm("docker", translate("Docker - Network")) 16 | m.redirect = luci.dispatcher.build_url("admin", "docker", "networks") 17 | if lost_state then 18 | m.submit=false 19 | m.reset=false 20 | end 21 | 22 | 23 | s = m:section(SimpleSection) 24 | s.template = "dockerman/apply_widget" 25 | s.err=docker:read_status() 26 | s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") 27 | if s.err then 28 | docker:clear_status() 29 | end 30 | 31 | s = m:section(SimpleSection, translate("Create new docker network")) 32 | s.addremove = true 33 | s.anonymous = true 34 | 35 | o = s:option(Value, "name", 36 | translate("Network Name"), 37 | translate("Name of the network that can be selected during container creation")) 38 | o.rmempty = true 39 | 40 | o = s:option(ListValue, "driver", translate("Driver")) 41 | o.rmempty = true 42 | o:value("bridge", translate("Bridge device")) 43 | o:value("macvlan", translate("MAC VLAN")) 44 | o:value("ipvlan", translate("IP VLAN")) 45 | o:value("overlay", translate("Overlay network")) 46 | 47 | o = s:option(Value, "parent", translate("Base device")) 48 | o.rmempty = true 49 | o:depends("driver", "macvlan") 50 | local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {} 51 | for _, v in ipairs(interfaces) do 52 | o:value(v, v) 53 | end 54 | o.default="br-lan" 55 | o.placeholder="br-lan" 56 | 57 | o = s:option(ListValue, "macvlan_mode", translate("Mode")) 58 | o.rmempty = true 59 | o:depends("driver", "macvlan") 60 | o.default="bridge" 61 | o:value("bridge", translate("Bridge (Support direct communication between MAC VLANs)")) 62 | o:value("private", translate("Private (Prevent communication between MAC VLANs)")) 63 | o:value("vepa", translate("VEPA (Virtual Ethernet Port Aggregator)")) 64 | o:value("passthru", translate("Pass-through (Mirror physical device to single MAC VLAN)")) 65 | 66 | o = s:option(ListValue, "ipvlan_mode", translate("Ipvlan Mode")) 67 | o.rmempty = true 68 | o:depends("driver", "ipvlan") 69 | o.default="l3" 70 | o:value("l2", translate("L2 bridge")) 71 | o:value("l3", translate("L3 bridge")) 72 | 73 | o = s:option(Flag, "ingress", 74 | translate("Ingress"), 75 | translate("Ingress network is the network which provides the routing-mesh in swarm mode")) 76 | o.rmempty = true 77 | o.disabled = 0 78 | o.enabled = 1 79 | o.default = 0 80 | o:depends("driver", "overlay") 81 | 82 | o = s:option(DynamicList, "options", translate("Options")) 83 | o.rmempty = true 84 | o.placeholder="com.docker.network.driver.mtu=1500" 85 | 86 | o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network")) 87 | o.rmempty = true 88 | o:depends("driver", "overlay") 89 | o.disabled = 0 90 | o.enabled = 1 91 | o.default = 0 92 | 93 | if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then 94 | o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt")) 95 | o:depends("driver", "macvlan") 96 | o.disabled = 0 97 | o.enabled = 1 98 | o.default = 1 99 | end 100 | 101 | o = s:option(Value, "subnet", translate("Subnet")) 102 | o.rmempty = true 103 | o.placeholder="10.1.0.0/16" 104 | o.datatype="ip4addr" 105 | 106 | o = s:option(Value, "gateway", translate("Gateway")) 107 | o.rmempty = true 108 | o.placeholder="10.1.1.1" 109 | o.datatype="ip4addr" 110 | 111 | o = s:option(Value, "ip_range", translate("IP range")) 112 | o.rmempty = true 113 | o.placeholder="10.1.1.0/24" 114 | o.datatype="ip4addr" 115 | 116 | o = s:option(DynamicList, "aux_address", translate("Exclude IPs")) 117 | o.rmempty = true 118 | o.placeholder="my-route=10.1.1.1" 119 | 120 | o = s:option(Flag, "ipv6", translate("Enable IPv6")) 121 | o.rmempty = true 122 | o.disabled = 0 123 | o.enabled = 1 124 | o.default = 0 125 | 126 | o = s:option(Value, "subnet6", translate("IPv6 Subnet")) 127 | o.rmempty = true 128 | o.placeholder="fe80::/10" 129 | o.datatype="ip6addr" 130 | o:depends("ipv6", 1) 131 | 132 | o = s:option(Value, "gateway6", translate("IPv6 Gateway")) 133 | o.rmempty = true 134 | o.placeholder="fe80::1" 135 | o.datatype="ip6addr" 136 | o:depends("ipv6", 1) 137 | 138 | m.handle = function(self, state, data) 139 | if state == FORM_VALID then 140 | local name = data.name 141 | local driver = data.driver 142 | 143 | local internal = data.internal == 1 and true or false 144 | 145 | local subnet = data.subnet 146 | local gateway = data.gateway 147 | local ip_range = data.ip_range 148 | 149 | local aux_address = {} 150 | local tmp = data.aux_address or {} 151 | for i,v in ipairs(tmp) do 152 | _,_,k1,v1 = v:find("(.-)=(.+)") 153 | aux_address[k1] = v1 154 | end 155 | 156 | local options = {} 157 | tmp = data.options or {} 158 | for i,v in ipairs(tmp) do 159 | _,_,k1,v1 = v:find("(.-)=(.+)") 160 | options[k1] = v1 161 | end 162 | 163 | local ipv6 = data.ipv6 == 1 and true or false 164 | 165 | local create_body = { 166 | Name = name, 167 | Driver = driver, 168 | EnableIPv6 = ipv6, 169 | IPAM = { 170 | Driver= "default" 171 | }, 172 | Internal = internal 173 | } 174 | 175 | if subnet or gateway or ip_range then 176 | create_body["IPAM"]["Config"] = { 177 | { 178 | Subnet = subnet, 179 | Gateway = gateway, 180 | IPRange = ip_range, 181 | AuxAddress = aux_address, 182 | AuxiliaryAddresses = aux_address 183 | } 184 | } 185 | end 186 | 187 | if driver == "macvlan" then 188 | create_body["Options"] = { 189 | macvlan_mode = data.macvlan_mode, 190 | parent = data.parent 191 | } 192 | elseif driver == "ipvlan" then 193 | create_body["Options"] = { 194 | ipvlan_mode = data.ipvlan_mode 195 | } 196 | elseif driver == "overlay" then 197 | create_body["Ingress"] = data.ingerss == 1 and true or false 198 | end 199 | 200 | if ipv6 and data.subnet6 and data.subnet6 then 201 | if type(create_body["IPAM"]["Config"]) ~= "table" then 202 | create_body["IPAM"]["Config"] = {} 203 | end 204 | local index = #create_body["IPAM"]["Config"] 205 | create_body["IPAM"]["Config"][index+1] = { 206 | Subnet = data.subnet6, 207 | Gateway = data.gateway6 208 | } 209 | end 210 | 211 | if next(options) ~= nil then 212 | create_body["Options"] = create_body["Options"] or {} 213 | for k, v in pairs(options) do 214 | create_body["Options"][k] = v 215 | end 216 | end 217 | 218 | create_body = docker.clear_empty_tables(create_body) 219 | docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...") 220 | 221 | local res = dk.networks:create({ 222 | body = create_body 223 | }) 224 | 225 | if res and res.code == 201 then 226 | docker:write_status("Network: " .. "create macvlan interface...") 227 | res = dk.networks:inspect({ 228 | name = create_body.Name 229 | }) 230 | 231 | if driver == "macvlan" and 232 | data.op_macvlan ~= 0 and 233 | res and 234 | res.code and 235 | res.code == 200 and 236 | res.body and 237 | res.body.IPAM and 238 | res.body.IPAM.Config and 239 | res.body.IPAM.Config[1] and 240 | res.body.IPAM.Config[1].Gateway and 241 | res.body.IPAM.Config[1].Subnet then 242 | 243 | docker.create_macvlan_interface(data.name, 244 | data.parent, 245 | res.body.IPAM.Config[1].Gateway, 246 | res.body.IPAM.Config[1].Subnet) 247 | end 248 | 249 | docker:clear_status() 250 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks")) 251 | else 252 | docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n") 253 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork")) 254 | end 255 | end 256 | end 257 | 258 | return m 259 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | local uci = (require "luci.model.uci").cursor() 8 | 9 | local m, s, o, lost_state 10 | local dk = docker.new() 11 | 12 | if dk:_ping().code ~= 200 then 13 | lost_state = true 14 | end 15 | 16 | m = SimpleForm("dockerd", 17 | translate("Docker - Overview"), 18 | translate("An overview with the relevant data is displayed here with which the LuCI docker client is connected.") 19 | .. 20 | " " .. 21 | [[]] .. 22 | translate("Github") .. 23 | [[]]) 24 | m.submit=false 25 | m.reset=false 26 | 27 | local docker_info_table = {} 28 | -- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'} 29 | -- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'} 30 | -- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'} 31 | docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'} 32 | docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'} 33 | docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'} 34 | docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'} 35 | docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'} 36 | docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'} 37 | docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'} 38 | 39 | if nixio.fs.access("/usr/bin/dockerd") and not uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 40 | s = m:section(SimpleSection) 41 | s.template = "dockerman/apply_widget" 42 | s.err=docker:read_status() 43 | s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") 44 | if s.err then 45 | docker:clear_status() 46 | end 47 | s = m:section(Table,{{}}) 48 | s.notitle=true 49 | s.rowcolors=false 50 | s.template = "cbi/nullsection" 51 | 52 | o = s:option(Button, "_start") 53 | o.template = "dockerman/cbi/inlinebutton" 54 | o.inputtitle = lost_state and translate("Start") or translate("Stop") 55 | o.inputstyle = lost_state and "add" or "remove" 56 | o.forcewrite = true 57 | o.write = function(self, section) 58 | docker:clear_status() 59 | 60 | if lost_state then 61 | docker:append_status("Docker daemon: starting") 62 | luci.util.exec("/etc/init.d/dockerd start") 63 | luci.util.exec("sleep 5") 64 | luci.util.exec("/etc/init.d/dockerman start") 65 | 66 | else 67 | docker:append_status("Docker daemon: stopping") 68 | luci.util.exec("/etc/init.d/dockerd stop") 69 | end 70 | docker:clear_status() 71 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview")) 72 | end 73 | 74 | o = s:option(Button, "_restart") 75 | o.template = "dockerman/cbi/inlinebutton" 76 | o.inputtitle = translate("Restart") 77 | o.inputstyle = "reload" 78 | o.forcewrite = true 79 | o.write = function(self, section) 80 | docker:clear_status() 81 | docker:append_status("Docker daemon: restarting") 82 | luci.util.exec("/etc/init.d/dockerd restart") 83 | luci.util.exec("sleep 5") 84 | luci.util.exec("/etc/init.d/dockerman start") 85 | docker:clear_status() 86 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview")) 87 | end 88 | end 89 | 90 | s = m:section(Table, docker_info_table) 91 | s:option(DummyValue, "_key", translate("Info")) 92 | s:option(DummyValue, "_value") 93 | 94 | s = m:section(SimpleSection) 95 | s.template = "dockerman/overview" 96 | 97 | s.containers_running = '-' 98 | s.images_used = '-' 99 | s.containers_total = '-' 100 | s.images_total = '-' 101 | s.networks_total = '-' 102 | s.volumes_total = '-' 103 | 104 | -- local socket = luci.model.uci.cursor():get("dockerd", "dockerman", "socket_path") 105 | if not lost_state then 106 | local containers_list = dk.containers:list({query = {all=true}}).body 107 | local images_list = dk.images:list().body 108 | local vol = dk.volumes:list() 109 | local volumes_list = vol and vol.body and vol.body.Volumes or {} 110 | local networks_list = dk.networks:list().body or {} 111 | local docker_info = dk:info() 112 | 113 | -- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem 114 | -- docker_info_table['1Architecture']._value = docker_info.body.Architecture 115 | -- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion 116 | docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion 117 | docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"] 118 | docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU) 119 | docker_info_table['6MemTotal']._value = docker.byte_format(docker_info.body.MemTotal) 120 | if docker_info.body.DockerRootDir then 121 | local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir) 122 | local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0 123 | docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(docker.byte_format(size)) .. " " .. translate("Available") .. ")" 124 | end 125 | 126 | docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress 127 | if docker_info.body.RegistryConfig and docker_info.body.RegistryConfig.Mirrors then 128 | for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do 129 | docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v) 130 | end 131 | end 132 | 133 | s.images_used = 0 134 | for i, v in ipairs(images_list) do 135 | for ci,cv in ipairs(containers_list) do 136 | if v.Id == cv.ImageID then 137 | s.images_used = s.images_used + 1 138 | break 139 | end 140 | end 141 | end 142 | 143 | s.containers_running = tostring(docker_info.body.ContainersRunning) 144 | s.images_used = tostring(s.images_used) 145 | s.containers_total = tostring(docker_info.body.Containers) 146 | s.images_total = tostring(#images_list) 147 | s.networks_total = tostring(#networks_list) 148 | s.volumes_total = tostring(#volumes_list) 149 | else 150 | docker_info_table['3ServerVersion']._value = translate("Can NOT connect to docker daemon, please check!!") 151 | end 152 | 153 | return m 154 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.model.docker" 7 | local dk = docker.new() 8 | 9 | local m, s, o 10 | 11 | local res, containers, volumes, lost_state 12 | 13 | function get_volumes() 14 | local data = {} 15 | for i, v in ipairs(volumes) do 16 | local index = v.Name 17 | data[index]={} 18 | data[index]["_selected"] = 0 19 | data[index]["_nameraw"] = v.Name 20 | data[index]["_name"] = v.Name:sub(1,12) 21 | 22 | for ci,cv in ipairs(containers) do 23 | if cv.Mounts and type(cv.Mounts) ~= "table" then 24 | break 25 | end 26 | for vi, vv in ipairs(cv.Mounts) do 27 | if v.Name == vv.Name then 28 | data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "").. 29 | ''.. cv.Names[1]:sub(2)..'' 30 | end 31 | end 32 | end 33 | data[index]["_driver"] = v.Driver 34 | data[index]["_mountpoint"] = nil 35 | 36 | for v1 in v.Mountpoint:gmatch('[^/]+') do 37 | if v1 == index then 38 | data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..." 39 | else 40 | data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1 41 | end 42 | end 43 | data[index]["_created"] = v.CreatedAt 44 | data[index]["_size"] = "-" 45 | end 46 | 47 | return data 48 | end 49 | if dk:_ping().code ~= 200 then 50 | lost_state = true 51 | else 52 | res = dk.volumes:list() 53 | if res and res.code and res.code <300 then 54 | volumes = res.body.Volumes 55 | end 56 | 57 | res = dk.containers:list({ 58 | query = { 59 | all=true 60 | } 61 | }) 62 | if res and res.code and res.code <300 then 63 | containers = res.body 64 | end 65 | end 66 | 67 | local volume_list = not lost_state and get_volumes() or {} 68 | 69 | m = SimpleForm("docker", translate("Docker - Volumes")) 70 | m.submit=false 71 | m.reset=false 72 | m:append(Template("dockerman/volume_size")) 73 | 74 | s = m:section(Table, volume_list, translate("Volumes overview")) 75 | 76 | o = s:option(Flag, "_selected","") 77 | o.disabled = 0 78 | o.enabled = 1 79 | o.default = 0 80 | o.write = function(self, section, value) 81 | volume_list[section]._selected = value 82 | end 83 | 84 | o = s:option(DummyValue, "_name", translate("Name")) 85 | o = s:option(DummyValue, "_driver", translate("Driver")) 86 | o = s:option(DummyValue, "_containers", translate("Containers")) 87 | o.rawhtml = true 88 | o = s:option(DummyValue, "_mountpoint", translate("Mount Point")) 89 | o = s:option(DummyValue, "_size", translate("Size")) 90 | o.rawhtml = true 91 | o = s:option(DummyValue, "_created", translate("Created")) 92 | 93 | s = m:section(SimpleSection) 94 | s.template = "dockerman/apply_widget" 95 | s.err=docker:read_status() 96 | s.err=s.err and s.err:gsub("\n","
"):gsub(" "," ") 97 | if s.err then 98 | docker:clear_status() 99 | end 100 | 101 | s = m:section(Table,{{}}) 102 | s.notitle=true 103 | s.rowcolors=false 104 | s.template="cbi/nullsection" 105 | 106 | o = s:option(Button, "remove") 107 | o.inputtitle= translate("Remove") 108 | o.template = "dockerman/cbi/inlinebutton" 109 | o.inputstyle = "remove" 110 | o.forcewrite = true 111 | o.disable = lost_state 112 | o.write = function(self, section) 113 | local volume_selected = {} 114 | 115 | for k in pairs(volume_list) do 116 | if volume_list[k]._selected == 1 then 117 | volume_selected[#volume_selected+1] = k 118 | end 119 | end 120 | 121 | if next(volume_selected) ~= nil then 122 | local success = true 123 | docker:clear_status() 124 | for _,vol in ipairs(volume_selected) do 125 | docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...") 126 | local msg = dk.volumes["remove"](dk, {id = vol}) 127 | if msg and msg.code and msg.code ~= 204 then 128 | docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") 129 | success = false 130 | else 131 | docker:append_status("done\n") 132 | end 133 | end 134 | 135 | if success then 136 | docker:clear_status() 137 | end 138 | luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes")) 139 | end 140 | end 141 | 142 | return m 143 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/model/docker.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | LuCI - Lua Configuration Interface 3 | Copyright 2019 lisaac 4 | ]]-- 5 | 6 | local docker = require "luci.docker" 7 | local fs = require "nixio.fs" 8 | local uci = (require "luci.model.uci").cursor() 9 | 10 | local _docker = {} 11 | _docker.options = {} 12 | 13 | --pull image and return iamge id 14 | local update_image = function(self, image_name) 15 | local json_stringify = luci.jsonc and luci.jsonc.stringify 16 | _docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n") 17 | local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb) 18 | 19 | if res and res.code and res.code == 200 and (#res.body > 0 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_name)) then 20 | _docker:append_status("done\n") 21 | else 22 | res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message) 23 | end 24 | 25 | new_image_id = self.images:inspect({name = image_name}).body.Id 26 | return new_image_id, res 27 | end 28 | 29 | local table_equal = function(t1, t2) 30 | if not t1 then 31 | return true 32 | end 33 | 34 | if not t2 then 35 | return false 36 | end 37 | 38 | if #t1 ~= #t2 then 39 | return false 40 | end 41 | 42 | for i, v in ipairs(t1) do 43 | if t1[i] ~= t2[i] then 44 | return false 45 | end 46 | end 47 | 48 | return true 49 | end 50 | 51 | local table_subtract = function(t1, t2) 52 | if not t1 or next(t1) == nil then 53 | return nil 54 | end 55 | 56 | if not t2 or next(t2) == nil then 57 | return t1 58 | end 59 | 60 | local res = {} 61 | for _, v1 in ipairs(t1) do 62 | local found = false 63 | for _, v2 in ipairs(t2) do 64 | if v1 == v2 then 65 | found= true 66 | break 67 | end 68 | end 69 | if not found then 70 | table.insert(res, v1) 71 | end 72 | end 73 | 74 | return next(res) == nil and nil or res 75 | end 76 | 77 | local map_subtract = function(t1, t2) 78 | if not t1 or next(t1) == nil then 79 | return nil 80 | end 81 | 82 | if not t2 or next(t2) == nil then 83 | return t1 84 | end 85 | 86 | local res = {} 87 | for k1, v1 in pairs(t1) do 88 | local found = false 89 | for k2, v2 in ipairs(t2) do 90 | if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then 91 | found= true 92 | break 93 | end 94 | end 95 | 96 | if not found then 97 | res[k1] = v1 98 | end 99 | end 100 | 101 | return next(res) ~= nil and res or nil 102 | end 103 | 104 | _docker.clear_empty_tables = function ( t ) 105 | local k, v 106 | 107 | if next(t) == nil then 108 | t = nil 109 | else 110 | for k, v in pairs(t) do 111 | if type(v) == 'table' then 112 | t[k] = _docker.clear_empty_tables(v) 113 | if t[k] and next(t[k]) == nil then 114 | t[k] = nil 115 | end 116 | end 117 | end 118 | end 119 | 120 | return t 121 | end 122 | 123 | local get_config = function(container_config, image_config) 124 | local config = container_config.Config 125 | local old_host_config = container_config.HostConfig 126 | local old_network_setting = container_config.NetworkSettings.Networks or {} 127 | 128 | if config.WorkingDir == image_config.WorkingDir then 129 | config.WorkingDir = "" 130 | end 131 | 132 | if config.User == image_config.User then 133 | config.User = "" 134 | end 135 | 136 | if table_equal(config.Cmd, image_config.Cmd) then 137 | config.Cmd = nil 138 | end 139 | 140 | if table_equal(config.Entrypoint, image_config.Entrypoint) then 141 | config.Entrypoint = nil 142 | end 143 | 144 | if table_equal(config.ExposedPorts, image_config.ExposedPorts) then 145 | config.ExposedPorts = nil 146 | end 147 | 148 | config.Env = table_subtract(config.Env, image_config.Env) 149 | config.Labels = table_subtract(config.Labels, image_config.Labels) 150 | config.Volumes = map_subtract(config.Volumes, image_config.Volumes) 151 | 152 | if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then 153 | config.ExposedPorts = {} 154 | for p, v in pairs(old_host_config.PortBindings) do 155 | config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort } 156 | end 157 | end 158 | 159 | local network_setting = {} 160 | local multi_network = false 161 | local extra_network = {} 162 | 163 | for k, v in pairs(old_network_setting) do 164 | if multi_network then 165 | extra_network[k] = v 166 | else 167 | network_setting[k] = v 168 | end 169 | multi_network = true 170 | end 171 | 172 | local host_config = old_host_config 173 | host_config.Mounts = {} 174 | for i, v in ipairs(container_config.Mounts) do 175 | if v.Type == "volume" then 176 | table.insert(host_config.Mounts, { 177 | Type = v.Type, 178 | Target = v.Destination, 179 | Source = v.Source:match("([^/]+)\/_data"), 180 | BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil, 181 | ReadOnly = not v.RW 182 | }) 183 | end 184 | end 185 | 186 | local create_body = config 187 | create_body["HostConfig"] = host_config 188 | create_body["NetworkingConfig"] = {EndpointsConfig = network_setting} 189 | create_body = _docker.clear_empty_tables(create_body) or {} 190 | extra_network = _docker.clear_empty_tables(extra_network) or {} 191 | 192 | return create_body, extra_network 193 | end 194 | 195 | local upgrade = function(self, request) 196 | _docker:clear_status() 197 | 198 | local container_info = self.containers:inspect({id = request.id}) 199 | 200 | if container_info.code > 300 and type(container_info.body) == "table" then 201 | return container_info 202 | end 203 | 204 | local image_name = container_info.body.Config.Image 205 | if not image_name:match(".-:.+") then 206 | image_name = image_name .. ":latest" 207 | end 208 | 209 | local old_image_id = container_info.body.Image 210 | local container_name = container_info.body.Name:sub(2) 211 | 212 | local image_id, res = update_image(self, image_name) 213 | if res and res.code and res.code ~= 200 then 214 | return res 215 | end 216 | 217 | if image_id == old_image_id then 218 | return {code = 305, body = {message = "Already up to date"}} 219 | end 220 | 221 | local t = os.date("%Y%m%d%H%M%S") 222 | _docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old_".. t .. "...") 223 | res = self.containers:rename({name = container_name, query = { name = container_name .. "_old_" ..t }}) 224 | if res and res.code and res.code < 300 then 225 | _docker:append_status("done\n") 226 | else 227 | return res 228 | end 229 | 230 | local image_config = self.images:inspect({id = old_image_id}).body.Config 231 | local create_body, extra_network = get_config(container_info.body, image_config) 232 | 233 | -- create new container 234 | _docker:append_status("Container: Create" .. " " .. container_name .. "...") 235 | create_body = _docker.clear_empty_tables(create_body) 236 | res = self.containers:create({name = container_name, body = create_body}) 237 | if res and res.code and res.code > 300 then 238 | return res 239 | end 240 | _docker:append_status("done\n") 241 | 242 | -- extra networks need to network connect action 243 | for k, v in pairs(extra_network) do 244 | _docker:append_status("Networks: Connect" .. " " .. container_name .. "...") 245 | res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}}) 246 | if res and res.code and res.code > 300 then 247 | return res 248 | end 249 | _docker:append_status("done\n") 250 | end 251 | 252 | _docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "_old_".. t .. "...") 253 | res = self.containers:stop({name = container_name .. "_old_" ..t }) 254 | if res and res.code and res.code < 305 then 255 | _docker:append_status("done\n") 256 | else 257 | return res 258 | end 259 | 260 | _docker:append_status("Container: " .. "Start" .. " " .. container_name .. "...") 261 | res = self.containers:start({name = container_name}) 262 | if res and res.code and res.code < 305 then 263 | _docker:append_status("done\n") 264 | else 265 | return res 266 | end 267 | 268 | _docker:clear_status() 269 | return res 270 | end 271 | 272 | local duplicate_config = function (self, request) 273 | local container_info = self.containers:inspect({id = request.id}) 274 | if container_info.code > 300 and type(container_info.body) == "table" then 275 | return nil 276 | end 277 | 278 | local old_image_id = container_info.body.Image 279 | local image_config = self.images:inspect({id = old_image_id}).body.Config 280 | 281 | return get_config(container_info.body, image_config) 282 | end 283 | 284 | _docker.new = function() 285 | local host = nil 286 | local port = nil 287 | local socket_path = nil 288 | local debug_path = nil 289 | 290 | if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 291 | host = uci:get("dockerd", "dockerman", "remote_host") or nil 292 | port = uci:get("dockerd", "dockerman", "remote_port") or nil 293 | else 294 | socket_path = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock" 295 | end 296 | 297 | local debug = uci:get_bool("dockerd", "dockerman", "debug") 298 | if debug then 299 | debug_path = uci:get("dockerd", "dockerman", "debug_path") or "/tmp/.docker_debug" 300 | end 301 | 302 | local status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status" 303 | 304 | _docker.options = { 305 | host = host, 306 | port = port, 307 | socket_path = socket_path, 308 | debug = debug, 309 | debug_path = debug_path, 310 | status_path = status_path 311 | } 312 | 313 | local _new = docker.new(_docker.options) 314 | _new.containers_upgrade = upgrade 315 | _new.containers_duplicate_config = duplicate_config 316 | 317 | return _new 318 | end 319 | 320 | _docker.options.status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status" 321 | 322 | _docker.append_status=function(self,val) 323 | if not val then 324 | return 325 | end 326 | local file_docker_action_status=io.open(self.options.status_path, "a+") 327 | file_docker_action_status:write(val) 328 | file_docker_action_status:close() 329 | end 330 | 331 | _docker.write_status=function(self,val) 332 | if not val then 333 | return 334 | end 335 | local file_docker_action_status=io.open(self.options.status_path, "w+") 336 | file_docker_action_status:write(val) 337 | file_docker_action_status:close() 338 | end 339 | 340 | _docker.read_status=function(self) 341 | return fs.readfile(self.options.status_path) 342 | end 343 | 344 | _docker.clear_status=function(self) 345 | fs.remove(self.options.status_path) 346 | end 347 | 348 | local status_cb = function(res, source, handler) 349 | res.body = res.body or {} 350 | while true do 351 | local chunk = source() 352 | if chunk then 353 | --standard output to res.body 354 | table.insert(res.body, chunk) 355 | handler(chunk) 356 | else 357 | return 358 | end 359 | end 360 | end 361 | 362 | --{"status":"Pulling from library\/debian","id":"latest"} 363 | --{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"} 364 | --{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"} 365 | --{"status":"Download complete","progressDetail":[],"id":"50e431f79093"} 366 | --{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"} 367 | --{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"} 368 | --{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"} 369 | --{"status":"Status: Downloaded newer image for debian:latest"} 370 | _docker.pull_image_show_status_cb = function(res, source) 371 | return status_cb(res, source, function(chunk) 372 | local json_parse = luci.jsonc.parse 373 | local step = json_parse(chunk) 374 | if type(step) == "table" then 375 | local buf = _docker:read_status() 376 | local num = 0 377 | local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n" 378 | if step.id then 379 | buf, num = buf:gsub("\t"..step.id .. ": .-\n", str) 380 | end 381 | if num == 0 then 382 | buf = buf .. str 383 | end 384 | _docker:write_status(buf) 385 | end 386 | end) 387 | end 388 | 389 | --{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"} 390 | --{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"} 391 | --{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"} 392 | _docker.import_image_show_status_cb = function(res, source) 393 | return status_cb(res, source, function(chunk) 394 | local json_parse = luci.jsonc.parse 395 | local step = json_parse(chunk) 396 | if type(step) == "table" then 397 | local buf = _docker:read_status() 398 | local num = 0 399 | local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n" 400 | if step.status then 401 | buf, num = buf:gsub("\t"..step.status .. " .-\n", str) 402 | end 403 | if num == 0 then 404 | buf = buf .. str 405 | end 406 | _docker:write_status(buf) 407 | end 408 | end) 409 | end 410 | 411 | _docker.create_macvlan_interface = function(name, device, gateway, subnet) 412 | if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then 413 | return 414 | end 415 | 416 | if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 417 | return 418 | end 419 | 420 | local ip = require "luci.ip" 421 | local if_name = "docker_"..name 422 | local dev_name = "macvlan_"..name 423 | local net_mask = tostring(ip.new(subnet):mask()) 424 | local lan_interfaces 425 | 426 | -- add macvlan device 427 | uci:delete("network", dev_name) 428 | uci:set("network", dev_name, "device") 429 | uci:set("network", dev_name, "name", dev_name) 430 | uci:set("network", dev_name, "ifname", device) 431 | uci:set("network", dev_name, "type", "macvlan") 432 | uci:set("network", dev_name, "mode", "bridge") 433 | 434 | -- add macvlan interface 435 | uci:delete("network", if_name) 436 | uci:set("network", if_name, "interface") 437 | uci:set("network", if_name, "proto", "static") 438 | uci:set("network", if_name, "ifname", dev_name) 439 | uci:set("network", if_name, "ipaddr", gateway) 440 | uci:set("network", if_name, "netmask", net_mask) 441 | uci:foreach("firewall", "zone", function(s) 442 | if s.name == "lan" then 443 | local interfaces 444 | if type(s.network) == "table" then 445 | interfaces = table.concat(s.network, " ") 446 | uci:delete("firewall", s[".name"], "network") 447 | else 448 | interfaces = s.network and s.network or "" 449 | end 450 | interfaces = interfaces .. " " .. if_name 451 | interfaces = interfaces:gsub("%s+", " ") 452 | uci:set("firewall", s[".name"], "network", interfaces) 453 | end 454 | end) 455 | 456 | uci:commit("firewall") 457 | uci:commit("network") 458 | 459 | os.execute("ifup " .. if_name) 460 | end 461 | 462 | _docker.remove_macvlan_interface = function(name) 463 | if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then 464 | return 465 | end 466 | 467 | if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then 468 | return 469 | end 470 | 471 | local if_name = "docker_"..name 472 | local dev_name = "macvlan_"..name 473 | uci:foreach("firewall", "zone", function(s) 474 | if s.name == "lan" then 475 | local interfaces 476 | if type(s.network) == "table" then 477 | interfaces = table.concat(s.network, " ") 478 | else 479 | interfaces = s.network and s.network or "" 480 | end 481 | interfaces = interfaces and interfaces:gsub(if_name, "") 482 | interfaces = interfaces and interfaces:gsub("%s+", " ") 483 | uci:set("firewall", s[".name"], "network", interfaces) 484 | end 485 | end) 486 | 487 | uci:delete("network", dev_name) 488 | uci:delete("network", if_name) 489 | uci:commit("network") 490 | uci:commit("firewall") 491 | 492 | os.execute("ip link del " .. if_name) 493 | end 494 | 495 | _docker.byte_format = function (byte) 496 | if not byte then return 'NaN' end 497 | local suff = {"B", "KB", "MB", "GB", "TB"} 498 | for i=1, 5 do 499 | if byte > 1024 and i < 5 then 500 | byte = byte / 1024 501 | else 502 | return string.format("%.2f %s", byte, suff[i]) 503 | end 504 | end 505 | end 506 | 507 | return _docker 508 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm: -------------------------------------------------------------------------------- 1 | 44 | 45 | 148 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinebutton.htm: -------------------------------------------------------------------------------- 1 |
2 | <% if self:cfgvalue(section) ~= false then %> 3 | " type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> /> 4 | <% else %> 5 | - 6 | <% end %> 7 |
8 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm: -------------------------------------------------------------------------------- 1 |
2 | 9 | <%- if self.password then -%> 10 | /> 13 | <%- end -%> 14 | 0, "data-choices", { self.keylist, self.vallist }) 29 | %> /> 30 | <%- if self.password then -%> 31 |
32 | <% end %> 33 |
34 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/cbi/namedsection.htm: -------------------------------------------------------------------------------- 1 | <% if self:cfgvalue(self.section) then section = self.section %> 2 |
3 | <%+cbi/tabmenu%> 4 |
5 | <%+cbi/ucisection%> 6 |
7 |
8 | <% end %> 9 | 10 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm: -------------------------------------------------------------------------------- 1 | <%+cbi/valueheader%> 2 | /> 5 | disabled <% end %><%= 6 | attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) .. 7 | ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked") 8 | %> /> 9 | > 10 | <%+cbi/valuefooter%> 11 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/container.htm: -------------------------------------------------------------------------------- 1 |
2 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/container_console.htm: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 7 | 8 |
9 |
10 |
11 | 12 | 333 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm: -------------------------------------------------------------------------------- 1 | 82 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/containers_running_stats.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | disabled <% end %>/> 5 | 6 |
7 | 8 | 105 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/images_load.htm: -------------------------------------------------------------------------------- 1 |
2 | disabled <% end %>/> 3 | 4 |
5 | 41 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/logs.htm: -------------------------------------------------------------------------------- 1 | <% if self.title == "Events" then %> 2 | <%+header%> 3 |

<%:Docker - Events%>

4 |
5 |

<%:Events%>

6 | <% end %> 7 |
8 | 9 |
10 | <% if self.title == "Events" then %> 11 |
12 | <%+footer%> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm: -------------------------------------------------------------------------------- 1 | 52 | 53 | 98 | <%+cbi/valueheader%> 99 | 100 | 101 | 102 | <%+cbi/valuefooter%> 103 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm: -------------------------------------------------------------------------------- 1 | 123 | 124 |
125 |
126 |
127 |
128 |
129 | 130 |
131 |
132 |
133 |

<%:Containers%>

134 |

135 | <%- if self.containers_total ~= "-" then -%><%- end -%> 136 | <%=self.containers_running%> 137 | /<%=self.containers_total%> 138 | <%- if self.containers_total ~= "-" then -%><%- end -%> 139 |

140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 |
151 |

<%:Images%>

152 |

153 | <%- if self.images_total ~= "-" then -%><%- end -%> 154 | <%=self.images_used%> 155 | /<%=self.images_total%> 156 | <%- if self.images_total ~= "-" then -%><%- end -%> 157 |

158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | 166 |
167 |
168 |
169 |

<%:Networks%>

170 |

171 | <%- if self.networks_total ~= "-" then -%><%- end -%> 172 | <%=self.networks_total%> 173 | 174 | <%- if self.networks_total ~= "-" then -%><%- end -%> 175 |

176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | 184 |
185 |
186 |
187 |

<%:Volumes%>

188 |

189 | <%- if self.volumes_total ~= "-" then -%><%- end -%> 190 | <%=self.volumes_total%> 191 | 192 | <%- if self.volumes_total ~= "-" then -%><%- end -%> 193 |

194 |
195 |
196 |
197 |
198 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/luasrc/view/dockerman/volume_size.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/po/zh_Hans: -------------------------------------------------------------------------------- 1 | zh-cn -------------------------------------------------------------------------------- /applications/luci-app-dockerman/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /init.sh env 4 | touch /etc/config/dockerd 5 | uci set dockerd.dockerman=dockerman 6 | uci set dockerd.dockerman.socket_path=`uci get dockerd.dockerman.socket_path 2&> /dev/null || echo '/var/run/docker.sock'` 7 | uci set dockerd.dockerman.status_path=`uci get dockerd.dockerman.status_path 2&> /dev/null || echo '/tmp/.docker_action_status'` 8 | uci set dockerd.dockerman.debug=`uci get dockerd.dockerman.debug 2&> /dev/null || echo 'false'` 9 | uci set dockerd.dockerman.debug_path=`uci get dockerd.dockerman.debug_path 2&> /dev/null || echo '/tmp/.docker_debug'` 10 | uci set dockerd.dockerman.remote_port=`uci get dockerd.dockerman.remote_port 2&> /dev/null || echo '2375'` 11 | uci set dockerd.dockerman.remote_endpoint=`uci get dockerd.dockerman.remote_endpoint 2&> /dev/null || echo '0'` 12 | uci del_list dockerd.dockerman.ac_allowed_interface='br-lan' 13 | uci add_list dockerd.dockerman.ac_allowed_interface='br-lan' 14 | uci commit dockerd -------------------------------------------------------------------------------- /applications/luci-app-dockerman/root/etc/init.d/dockerman: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | USE_PROCD=1 5 | # PROCD_DEBUG=1 6 | config_load 'dockerd' 7 | # config_get daemon_ea "dockerman" daemon_ea 8 | _DOCKERD=/etc/init.d/dockerd 9 | 10 | docker_running(){ 11 | docker version > /dev/null 2>&1 12 | return $? 13 | } 14 | 15 | add_ports() { 16 | [ $# -eq 0 ] && return 17 | $($_DOCKERD running) && docker_running || return 1 18 | ids=$@ 19 | for id in $ids; do 20 | id=$(docker ps --filter "ID=$id" --quiet) 21 | [ -z "$id" ] && { 22 | echo "Docker containner not running"; 23 | return 1; 24 | } 25 | ports=$(docker ps --filter "ID=$id" --format "{{.Ports}}") 26 | # echo "$ports" 27 | for port in $ports; do 28 | echo "$port" | grep -qE "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:.*$" || continue; 29 | [ "${port: -1}" == "," ] && port="${port:0:-1}" 30 | local protocol="" 31 | [ "${port%tcp}" != "$port" ] && protocol="/tcp" 32 | [ "${port%udp}" != "$port" ] && protocol="/udp" 33 | [ "$protocol" == "" ] && continue 34 | port="${port%%->*}" 35 | port="${port##*:}" 36 | uci_add_list dockerd dockerman ac_allowed_ports "${port}${protocol}" 37 | done 38 | done 39 | uci_commit dockerd 40 | } 41 | 42 | 43 | convert() { 44 | _convert() { 45 | _id=$1 46 | _id=$(docker ps --all --filter "ID=$_id" --quiet) 47 | if [ -z "$_id" ]; then 48 | uci_remove_list dockerd dockerman ac_allowed_container "$1" 49 | return 50 | fi 51 | if /etc/init.d/dockerman add_ports "$_id"; then 52 | uci_remove_list dockerd dockerman ac_allowed_container "$_id" 53 | fi 54 | } 55 | config_list_foreach dockerman ac_allowed_container _convert 56 | uci_commit dockerd 57 | } 58 | 59 | iptables_append(){ 60 | # Wait for a maximum of 10 second per command, retrying every millisecond 61 | local iptables_wait_args="--wait 10 --wait-interval 1000" 62 | if ! iptables ${iptables_wait_args} --check $@ 2>/dev/null; then 63 | iptables ${iptables_wait_args} -A $@ 2>/dev/null 64 | fi 65 | } 66 | 67 | init_dockerman_chain(){ 68 | iptables -N DOCKER-MAN >/dev/null 2>&1 69 | iptables -F DOCKER-MAN >/dev/null 2>&1 70 | iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1 71 | iptables -I DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1 72 | } 73 | 74 | delete_dockerman_chain(){ 75 | iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1 76 | iptables -F DOCKER-MAN >/dev/null 2>&1 77 | iptables -X DOCKER-MAN >/dev/null 2>&1 78 | } 79 | 80 | add_allowed_interface(){ 81 | iptables_append DOCKER-MAN -i $1 -o docker0 -j RETURN 82 | } 83 | 84 | add_allowed_ports(){ 85 | port=$1 86 | if [ "${port%/tcp}" != "$port" ]; then 87 | iptables_append DOCKER-MAN -p tcp -m conntrack --ctorigdstport ${port%/tcp} --ctdir ORIGINAL -j RETURN 88 | elif [ "${port%/udp}" != "$port" ]; then 89 | iptables_append DOCKER-MAN -p udp -m conntrack --ctorigdstport ${port%/udp} --ctdir ORIGINAL -j RETURN 90 | fi 91 | } 92 | 93 | handle_allowed_ports(){ 94 | config_list_foreach "dockerman" "ac_allowed_ports" add_allowed_ports 95 | } 96 | 97 | handle_allowed_interface(){ 98 | config_list_foreach "dockerman" "ac_allowed_interface" add_allowed_interface 99 | iptables_append DOCKER-MAN -m conntrack --ctstate ESTABLISHED,RELATED -o docker0 -j RETURN >/dev/null 2>&1 100 | iptables_append DOCKER-MAN -m conntrack --ctstate NEW,INVALID -o docker0 -j DROP >/dev/null 2>&1 101 | iptables_append DOCKER-MAN -j RETURN >/dev/null 2>&1 102 | } 103 | 104 | start_service(){ 105 | [ -x "$_DOCKERD" ] && $($_DOCKERD enabled) || return 0 106 | delete_dockerman_chain 107 | $($_DOCKERD running) && docker_running || return 0 108 | init_dockerman_chain 109 | handle_allowed_ports 110 | handle_allowed_interface 111 | } 112 | 113 | stop_service(){ 114 | delete_dockerman_chain 115 | } 116 | 117 | service_triggers() { 118 | procd_add_reload_trigger 'dockerd' 119 | } 120 | 121 | reload_service() { 122 | start 123 | } 124 | 125 | boot() { 126 | sleep 5s 127 | start 128 | } 129 | 130 | extra_command "add_ports" "Add allowed ports based on the container ID(s)" 131 | extra_command "convert" "Convert Ac allowed container to AC allowed ports" 132 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . $IPKG_INSTROOT/lib/functions.sh 4 | 5 | [ -x "$(command -v dockerd)" ] && chmod +x /etc/init.d/dockerman && /etc/init.d/dockerman enable >/dev/null 2>&1 6 | sed -i 's/self:cfgvalue(section) or {}/self:cfgvalue(section) or self.default or {}/' /usr/lib/lua/luci/view/cbi/dynlist.htm 7 | /etc/init.d/uhttpd restart >/dev/null 2>&1 8 | rm -fr /tmp/luci-indexcache /tmp/luci-modulecache >/dev/null 2>&1 9 | touch /etc/config/dockerd 10 | ls /etc/rc.d/*dockerd &> /dev/null && uci -q set dockerd.globals.auto_start="1" || uci -q set dockerd.globals.auto_start="0" 11 | uci -q batch <<-EOF >/dev/null 12 | set uhttpd.main.script_timeout="3600" 13 | commit uhttpd 14 | set dockerd.dockerman=dockerman 15 | set dockerd.dockerman.socket_path='/var/run/docker.sock' 16 | set dockerd.dockerman.status_path='/tmp/.docker_action_status' 17 | set dockerd.dockerman.debug='false' 18 | set dockerd.dockerman.debug_path='/tmp/.docker_debug' 19 | set dockerd.dockerman.remote_endpoint='0' 20 | 21 | del_list dockerd.dockerman.ac_allowed_interface='br-lan' 22 | add_list dockerd.dockerman.ac_allowed_interface='br-lan' 23 | 24 | commit dockerd 25 | EOF 26 | # remove dockerd firewall 27 | config_load dockerd 28 | remove_firewall(){ 29 | cfg=${1} 30 | uci_remove dockerd ${1} 31 | } 32 | config_foreach remove_firewall firewall 33 | # Convert ac_allowed_container to ac_allowed_ports 34 | (sleep 30s && /etc/init.d/dockerman convert;/etc/init.d/dockerman restart) & 35 | 36 | /etc/init.d/dockerd restart & 37 | exit 0 38 | -------------------------------------------------------------------------------- /applications/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-dockerman": { 3 | "description": "Grant UCI access for luci-app-dockerman", 4 | "read": { 5 | "uci": [ "dockerd" ] 6 | }, 7 | "write": { 8 | "uci": [ "dockerd" ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /doc/container_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/container_edit.png -------------------------------------------------------------------------------- /doc/container_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/container_info.png -------------------------------------------------------------------------------- /doc/container_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/container_logs.png -------------------------------------------------------------------------------- /doc/container_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/container_stats.png -------------------------------------------------------------------------------- /doc/containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/containers.png -------------------------------------------------------------------------------- /doc/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/images.png -------------------------------------------------------------------------------- /doc/networks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/networks.png -------------------------------------------------------------------------------- /doc/new_container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/new_container.png -------------------------------------------------------------------------------- /doc/new_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisaac/luci-app-dockerman/7292955a1b415bb60fa2e403bb3a437b4b7f7846/doc/new_network.png --------------------------------------------------------------------------------