fix
21
luci-app-dockerman/Makefile
Normal file
|
@ -0,0 +1,21 @@
|
|||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI Support for docker
|
||||
LUCI_DEPENDS:=@(aarch64||arm||x86_64) \
|
||||
+luci-compat \
|
||||
+luci-lib-docker \
|
||||
+luci-lib-ip \
|
||||
+docker \
|
||||
+dockerd \
|
||||
+ttyd
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0
|
||||
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com> \
|
||||
Florian Eckert <fe@dev.tdt.de>
|
||||
|
||||
PKG_VERSION:=v0.5.25
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
1
luci-app-dockerman/depends.lst
Normal file
|
@ -0,0 +1 @@
|
|||
ttyd docker-cli
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>Docker icon</title>
|
||||
<path d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,91 @@
|
|||
.fb-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.fb-container .cbi-button {
|
||||
height: 1.8rem;
|
||||
}
|
||||
.fb-container .cbi-input-text {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.fb-container .panel-title {
|
||||
padding-bottom: 0;
|
||||
width: 50%;
|
||||
border-bottom: none;
|
||||
}
|
||||
.fb-container .panel-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.fb-container .upload-container {
|
||||
display: none;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.fb-container .upload-file {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
.fb-container .cbi-value-field {
|
||||
text-align: left;
|
||||
}
|
||||
.fb-container .parent-icon strong {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.fb-container td[class$="-icon"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon {
|
||||
position: relative;
|
||||
}
|
||||
.fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
content: '';
|
||||
background-size: contain;
|
||||
margin: 0 0.5rem 0 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.fb-container .file-icon:before {
|
||||
background-image: url(file-icon.png);
|
||||
}
|
||||
.fb-container .folder-icon:before {
|
||||
background-image: url(folder-icon.png);
|
||||
}
|
||||
.fb-container .link-icon:before {
|
||||
background-image: url(link-icon.png);
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
.fb-container .upload-file {
|
||||
width: 14.6rem;
|
||||
}
|
||||
.fb-container .cbi-value-owner,
|
||||
.fb-container .cbi-value-perm {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-section-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cbi-section-table-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cbi-button-install {
|
||||
border-color: #c44;
|
||||
color: #c44;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.cbi-value-field {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.parent-icon {
|
||||
height: 1.8rem;
|
||||
padding: 10px 0;
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%">
|
||||
<path d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
|
||||
id="Fill-1"></path>
|
||||
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
|
||||
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 3 KiB |
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve">
|
||||
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619 c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721 c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0 h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
|
||||
<path d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
|
||||
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
|
||||
<path d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
|
||||
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
|
||||
<path d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
|
||||
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
185
luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js
vendored
Normal file
|
@ -0,0 +1,185 @@
|
|||
// https://github.com/thiscouldbebetter/TarFileExplorer
|
||||
class TarFileTypeFlag
|
||||
{constructor(value,name)
|
||||
{this.value=value;this.id="_"+this.value;this.name=name;}
|
||||
static _instances;static Instances()
|
||||
{if(TarFileTypeFlag._instances==null)
|
||||
{TarFileTypeFlag._instances=new TarFileTypeFlag_Instances();}
|
||||
return TarFileTypeFlag._instances;}}
|
||||
class TarFileTypeFlag_Instances
|
||||
{constructor()
|
||||
{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;i<this._All.length;i++)
|
||||
{var item=this._All[i];this._All[item.id]=item;}}}
|
||||
class TarFileEntryHeader
|
||||
{constructor
|
||||
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix)
|
||||
{this.fileName=fileName;this.fileMode=fileMode;this.userIDOfOwner=userIDOfOwner;this.userIDOfGroup=userIDOfGroup;this.fileSizeInBytes=fileSizeInBytes;this.timeModifiedInUnixFormat=timeModifiedInUnixFormat;this.checksum=checksum;this.typeFlag=typeFlag;this.nameOfLinkedFile=nameOfLinkedFile;this.uStarIndicator=uStarIndicator;this.uStarVersion=uStarVersion;this.userNameOfOwner=userNameOfOwner;this.groupNameOfOwner=groupNameOfOwner;this.deviceNumberMajor=deviceNumberMajor;this.deviceNumberMinor=deviceNumberMinor;this.filenamePrefix=filenamePrefix;}
|
||||
static FileNameMaxLength=99;static SizeInBytes=500;static default()
|
||||
{var now=new Date();var unixEpoch=new Date(1970,1,1);var millisecondsSinceUnixEpoch=now-unixEpoch;var secondsSinceUnixEpoch=Math.floor
|
||||
(millisecondsSinceUnixEpoch/1000);var secondsSinceUnixEpochAsStringOctal=secondsSinceUnixEpoch.toString(8).padRight(12,"\0");var timeModifiedInUnixFormat=[];for(var i=0;i<secondsSinceUnixEpochAsStringOctal.length;i++)
|
||||
{var digitAsASCIICode=secondsSinceUnixEpochAsStringOctal.charCodeAt(i);timeModifiedInUnixFormat.push(digitAsASCIICode);}
|
||||
var returnValue=new TarFileEntryHeader
|
||||
("".padRight(100,"\0"),"0100777","0000000","0000000",0,timeModifiedInUnixFormat,0,TarFileTypeFlag.Instances().Normal,"","ustar","00","","","","","");return returnValue;};static directoryNew(directoryName)
|
||||
{var header=TarFileEntryHeader.default();header.fileName=directoryName;header.typeFlag=TarFileTypeFlag.Instances().Directory;header.fileSizeInBytes=0;header.checksumCalculate();return header;};static fileNew(fileName,fileContentsAsBytes)
|
||||
{var header=TarFileEntryHeader.default();header.fileName=fileName;header.typeFlag=TarFileTypeFlag.Instances().Normal;header.fileSizeInBytes=fileContentsAsBytes.length;header.checksumCalculate();return header;};static fromBytes(bytes)
|
||||
{var reader=new ByteStream(bytes);var fileName=reader.readString(100).trim();var fileMode=reader.readString(8);var userIDOfOwner=reader.readString(8);var userIDOfGroup=reader.readString(8);var fileSizeInBytesAsStringOctal=reader.readString(12);var timeModifiedInUnixFormat=reader.readBytes(12);var checksumAsStringOctal=reader.readString(8);var typeFlagValue=reader.readString(1);var nameOfLinkedFile=reader.readString(100);var uStarIndicator=reader.readString(6);var uStarVersion=reader.readString(2);var userNameOfOwner=reader.readString(32);var groupNameOfOwner=reader.readString(32);var deviceNumberMajor=reader.readString(8);var deviceNumberMinor=reader.readString(8);var filenamePrefix=reader.readString(155);var reserved=reader.readBytes(12);var fileSizeInBytes=parseInt
|
||||
(fileSizeInBytesAsStringOctal.trim(),8);var checksum=parseInt
|
||||
(checksumAsStringOctal,8);var typeFlags=TarFileTypeFlag.Instances()._All;var typeFlagID="_"+typeFlagValue;var typeFlag=typeFlags[typeFlagID];var returnValue=new TarFileEntryHeader
|
||||
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix);return returnValue;};checksumCalculate()
|
||||
{var thisAsBytes=this.toBytes();var offsetOfChecksumInBytes=148;var numberOfBytesInChecksum=8;var presumedValueOfEachChecksumByte=" ".charCodeAt(0);for(var i=0;i<numberOfBytesInChecksum;i++)
|
||||
{var offsetOfByte=offsetOfChecksumInBytes+i;thisAsBytes[offsetOfByte]=presumedValueOfEachChecksumByte;}
|
||||
var checksumSoFar=0;for(var i=0;i<thisAsBytes.length;i++)
|
||||
{var byteToAdd=thisAsBytes[i];checksumSoFar+=byteToAdd;}
|
||||
this.checksum=checksumSoFar;return this.checksum;};toBytes()
|
||||
{var headerAsBytes=[];var writer=new ByteStream(headerAsBytes);var fileSizeInBytesAsStringOctal=(this.fileSizeInBytes.toString(8)+"\0").padLeft(12,"0")
|
||||
var checksumAsStringOctal=(this.checksum.toString(8)+"\0 ").padLeft(8,"0");writer.writeString(this.fileName,100);writer.writeString(this.fileMode,8);writer.writeString(this.userIDOfOwner,8);writer.writeString(this.userIDOfGroup,8);writer.writeString(fileSizeInBytesAsStringOctal,12);writer.writeBytes(this.timeModifiedInUnixFormat);writer.writeString(checksumAsStringOctal,8);writer.writeString(this.typeFlag.value,1);writer.writeString(this.nameOfLinkedFile,100);writer.writeString(this.uStarIndicator,6);writer.writeString(this.uStarVersion,2);writer.writeString(this.userNameOfOwner,32);writer.writeString(this.groupNameOfOwner,32);writer.writeString(this.deviceNumberMajor,8);writer.writeString(this.deviceNumberMinor,8);writer.writeString(this.filenamePrefix,155);writer.writeString("".padRight(12,"\0"));return headerAsBytes;};toString()
|
||||
{var newline="\n";var returnValue="[TarFileEntryHeader "
|
||||
+"fileName='"+this.fileName+"' "
|
||||
+"typeFlag='"+(this.typeFlag==null?"err":this.typeFlag.name)+"' "
|
||||
+"fileSizeInBytes='"+this.fileSizeInBytes+"' "
|
||||
+"]"
|
||||
+newline;return returnValue;};}
|
||||
class TarFileEntry
|
||||
{constructor(header,dataAsBytes)
|
||||
{this.header=header;this.dataAsBytes=dataAsBytes;}
|
||||
static directoryNew(directoryName)
|
||||
{var header=TarFileEntryHeader.directoryNew(directoryName);var entry=new TarFileEntry(header,[]);return entry;};static fileNew(fileName,fileContentsAsBytes)
|
||||
{var header=TarFileEntryHeader.fileNew(fileName,fileContentsAsBytes);var entry=new TarFileEntry(header,fileContentsAsBytes);return entry;};static fromBytes(chunkAsBytes,reader)
|
||||
{var chunkSize=TarFile.ChunkSize;var header=TarFileEntryHeader.fromBytes
|
||||
(chunkAsBytes);var sizeOfDataEntryInBytesUnpadded=header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
|
||||
(sizeOfDataEntryInBytesUnpadded/chunkSize)
|
||||
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var dataAsBytes=reader.readBytes
|
||||
(sizeOfDataEntryInBytesPadded).slice
|
||||
(0,sizeOfDataEntryInBytesUnpadded);var entry=new TarFileEntry(header,dataAsBytes);return entry;};static manyFromByteArrays
|
||||
(fileNamePrefix,fileNameSuffix,entriesAsByteArrays)
|
||||
{var returnValues=[];for(var i=0;i<entriesAsByteArrays.length;i++)
|
||||
{var entryAsBytes=entriesAsByteArrays[i];var entry=TarFileEntry.fileNew
|
||||
(fileNamePrefix+i+fileNameSuffix,entryAsBytes);returnValues.push(entry);}
|
||||
return returnValues;};download(event)
|
||||
{FileHelper.saveBytesAsFile
|
||||
(this.dataAsBytes,this.header.fileName);};remove(event)
|
||||
{alert("Not yet implemented!");};toBytes()
|
||||
{var entryAsBytes=[];var chunkSize=TarFile.ChunkSize;var headerAsBytes=this.header.toBytes();entryAsBytes=entryAsBytes.concat(headerAsBytes);entryAsBytes=entryAsBytes.concat(this.dataAsBytes);var sizeOfDataEntryInBytesUnpadded=this.header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
|
||||
(sizeOfDataEntryInBytesUnpadded/chunkSize)
|
||||
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var numberOfBytesOfPadding=sizeOfDataEntryInBytesPadded-sizeOfDataEntryInBytesUnpadded;for(var i=0;i<numberOfBytesOfPadding;i++)
|
||||
{entryAsBytes.push(0);}
|
||||
return entryAsBytes;};toString()
|
||||
{var newline="\n";headerAsString=this.header.toString();var dataAsHexadecimalString=ByteHelper.bytesToStringHexadecimal
|
||||
(this.dataAsBytes);var returnValue="[TarFileEntry]"+newline
|
||||
+headerAsString
|
||||
+"[Data]"
|
||||
+dataAsHexadecimalString
|
||||
+"[/Data]"+newline
|
||||
+"[/TarFileEntry]"
|
||||
+newline;return returnValue}}
|
||||
class TarFile
|
||||
{constructor(fileName,entries)
|
||||
{this.fileName=fileName;this.entries=entries;}
|
||||
static ChunkSize=512;static fromBytes(fileName,bytes)
|
||||
{var reader=new ByteStream(bytes);var entries=[];var chunkSize=TarFile.ChunkSize;var numberOfConsecutiveZeroChunks=0;while(reader.hasMoreBytes()==true)
|
||||
{var chunkAsBytes=reader.readBytes(chunkSize);var areAllBytesInChunkZeroes=true;for(var b=0;b<chunkAsBytes.length;b++)
|
||||
{if(chunkAsBytes[b]!=0)
|
||||
{areAllBytesInChunkZeroes=false;break;}}
|
||||
if(areAllBytesInChunkZeroes==true)
|
||||
{numberOfConsecutiveZeroChunks++;if(numberOfConsecutiveZeroChunks==2)
|
||||
{break;}}
|
||||
else
|
||||
{numberOfConsecutiveZeroChunks=0;var entry=TarFileEntry.fromBytes(chunkAsBytes,reader);entries.push(entry);}}
|
||||
var returnValue=new TarFile(fileName,entries);returnValue.consolidateLongPathEntries();return returnValue;}
|
||||
static create(fileName)
|
||||
{return new TarFile
|
||||
(fileName,[]);}
|
||||
consolidateLongPathEntries()
|
||||
{var typeFlagLongPathName=TarFileTypeFlag.Instances().LongFilePath.name;var entries=this.entries;for(var i=0;i<entries.length;i++)
|
||||
{var entry=entries[i];if(entry.header.typeFlag.name==typeFlagLongPathName)
|
||||
{var entryNext=entries[i+1];entryNext.header.fileName=entry.dataAsBytes.reduce
|
||||
((a,b)=>a+=String.fromCharCode(b),"");entryNext.header.fileName=entryNext.header.fileName.replace(/\0/g,"");entries.splice(i,1);i--;}}}
|
||||
downloadAs(fileNameToSaveAs)
|
||||
{return FileHelper.saveBytesAsFile
|
||||
(this.toBytes(),fileNameToSaveAs)}
|
||||
entriesForDirectories()
|
||||
{return this.entries.filter(x=>x.header.typeFlag.name==TarFileTypeFlag.Instances().Directory);}
|
||||
toBytes()
|
||||
{this.toBytes_PrependLongPathEntriesAsNeeded();var fileAsBytes=[];var entriesAsByteArrays=this.entries.map(x=>x.toBytes());this.consolidateLongPathEntries();for(var i=0;i<entriesAsByteArrays.length;i++)
|
||||
{var entryAsBytes=entriesAsByteArrays[i];fileAsBytes=fileAsBytes.concat(entryAsBytes);}
|
||||
var chunkSize=TarFile.ChunkSize;var numberOfZeroChunksToWrite=2;for(var i=0;i<numberOfZeroChunksToWrite;i++)
|
||||
{for(var b=0;b<chunkSize;b++)
|
||||
{fileAsBytes.push(0);}}
|
||||
return fileAsBytes;}
|
||||
toBytes_PrependLongPathEntriesAsNeeded()
|
||||
{var typeFlagLongPath=TarFileTypeFlag.Instances().LongFilePath;var maxLength=TarFileEntryHeader.FileNameMaxLength;var entries=this.entries;for(var i=0;i<entries.length;i++)
|
||||
{var entry=entries[i];var entryHeader=entry.header;var entryFileName=entryHeader.fileName;if(entryFileName.length>maxLength)
|
||||
{var entryFileNameAsBytes=entryFileName.split("").map(x=>x.charCodeAt(0));var entryContainingLongPathToPrepend=TarFileEntry.fileNew
|
||||
(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++;}}}
|
||||
toString()
|
||||
{var newline="\n";var returnValue="[TarFile]"+newline;for(var i=0;i<this.entries.length;i++)
|
||||
{var entry=this.entries[i];var entryAsString=entry.toString();returnValue+=entryAsString;}
|
||||
returnValue+="[/TarFile]"+newline;return returnValue;}}
|
||||
function StringExtensions()
|
||||
{}
|
||||
{String.prototype.padLeft=function(lengthToPadTo,charToPadWith)
|
||||
{var returnValue=this;while(returnValue.length<lengthToPadTo)
|
||||
{returnValue=charToPadWith+returnValue;}
|
||||
return returnValue;}
|
||||
String.prototype.padRight=function(lengthToPadTo,charToPadWith)
|
||||
{var returnValue=this;while(returnValue.length<lengthToPadTo)
|
||||
{returnValue+=charToPadWith;}
|
||||
return returnValue;}}
|
||||
class Globals
|
||||
{static Instance=new Globals();}
|
||||
class FileHelper
|
||||
{static loadFileAsBytes(fileToLoad,callback)
|
||||
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var fileLoadedAsBinaryString=fileLoadedEvent.target.result;var fileLoadedAsBytes=ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);callback(fileToLoad.name,fileLoadedAsBytes);}
|
||||
fileReader.readAsBinaryString(fileToLoad);}
|
||||
static loadFileAsText(fileToLoad,callback)
|
||||
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var textFromFileLoaded=fileLoadedEvent.target.result;callback(fileToLoad.name,textFromFileLoaded);};fileReader.readAsText(fileToLoad);}
|
||||
static saveBytesAsFile(bytesToWrite,fileNameToSaveAs)
|
||||
{var bytesToWriteAsArrayBuffer=new ArrayBuffer(bytesToWrite.length);var bytesToWriteAsUIntArray=new Uint8Array(bytesToWriteAsArrayBuffer);for(var i=0;i<bytesToWrite.length;i++)
|
||||
{bytesToWriteAsUIntArray[i]=bytesToWrite[i];}
|
||||
var bytesToWriteAsBlob=new Blob
|
||||
([bytesToWriteAsArrayBuffer],{type:"application/type"});
|
||||
return bytesToWriteAsBlob
|
||||
// var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=window.URL.createObjectURL(bytesToWriteAsBlob);downloadLink.click();
|
||||
}
|
||||
static saveTextAsFile(textToSave,fileNameToSaveAs)
|
||||
{var textToSaveAsBlob=new Blob([textToSave],{type:"text/plain"});var textToSaveAsURL=window.URL.createObjectURL(textToSaveAsBlob);var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=textToSaveAsURL;downloadLink.click();}}
|
||||
class ByteStream
|
||||
{constructor(bytes)
|
||||
{this.bytes=bytes;this.byteIndexCurrent=0;}
|
||||
static BitsPerByte=8;static BitsPerByteTimesTwo=ByteStream.BitsPerByte*2;static BitsPerByteTimesThree=ByteStream.BitsPerByte*3;hasMoreBytes()
|
||||
{return(this.byteIndexCurrent<this.bytes.length);}
|
||||
readBytes(numberOfBytesToRead)
|
||||
{var returnValue=new Array(numberOfBytesToRead);for(var b=0;b<numberOfBytesToRead;b++)
|
||||
{returnValue[b]=this.readByte();}
|
||||
return returnValue;}
|
||||
readByte()
|
||||
{var returnValue=this.bytes[this.byteIndexCurrent];this.byteIndexCurrent++;return returnValue;}
|
||||
readString(lengthOfString)
|
||||
{var returnValue="";for(var i=0;i<lengthOfString;i++)
|
||||
{var byte=this.readByte();if(byte!=0)
|
||||
{var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar;}}
|
||||
return returnValue;}
|
||||
writeBytes(bytesToWrite)
|
||||
{for(var b=0;b<bytesToWrite.length;b++)
|
||||
{this.bytes.push(bytesToWrite[b]);}
|
||||
this.byteIndexCurrent=this.bytes.length;}
|
||||
writeByte(byteToWrite)
|
||||
{this.bytes.push(byteToWrite);this.byteIndexCurrent++;}
|
||||
writeString(stringToWrite,lengthPadded)
|
||||
{for(var i=0;i<stringToWrite.length;i++)
|
||||
{var charAsByte=stringToWrite.charCodeAt(i);this.writeByte(charAsByte);}
|
||||
var numberOfPaddingChars=lengthPadded-stringToWrite.length;for(var i=0;i<numberOfPaddingChars;i++)
|
||||
{this.writeByte(0);}}}
|
||||
class ByteHelper
|
||||
{static stringUTF8ToBytes(stringToConvert)
|
||||
{var bytes=[];for(var i=0;i<stringToConvert.length;i++)
|
||||
{var byte=stringToConvert.charCodeAt(i);bytes.push(byte);}
|
||||
return bytes;}
|
||||
static bytesToStringUTF8(bytesToConvert)
|
||||
{var returnValue="";for(var i=0;i<bytesToConvert.length;i++)
|
||||
{var byte=bytesToConvert[i];var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar}
|
||||
return returnValue;}}
|
||||
function ArrayExtensions()
|
||||
{}
|
||||
{Array.prototype.remove=function(elementToRemove)
|
||||
{this.splice(this.indexOf(elementToRemove),1);}}
|
After Width: | Height: | Size: 6.4 KiB |
614
luci-app-dockerman/luasrc/controller/dockerman.lua
Normal file
|
@ -0,0 +1,614 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
-- local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
module("luci.controller.dockerman",package.seeall)
|
||||
|
||||
function index()
|
||||
entry({"admin", "docker"},
|
||||
alias("admin", "docker", "config"),
|
||||
_("Docker"),
|
||||
40).acl_depends = { "luci-app-dockerman" }
|
||||
|
||||
entry({"admin", "docker", "config"},cbi("dockerman/configuration"),_("Configuration"), 8).leaf=true
|
||||
|
||||
-- local uci = (require "luci.model.uci").cursor()
|
||||
-- if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
-- local host = uci:get("dockerd", "dockerman", "remote_host")
|
||||
-- local port = uci:get("dockerd", "dockerman", "remote_port")
|
||||
-- if not host or not port then
|
||||
-- return
|
||||
-- end
|
||||
-- else
|
||||
-- local socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
-- if socket and not nixio.fs.access(socket) then
|
||||
-- return
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- if (require "luci.model.docker").new():_ping().code ~= 200 then
|
||||
-- return
|
||||
-- end
|
||||
|
||||
entry({"admin", "docker", "overview"}, form("dockerman/overview"),_("Overview"), 2).leaf=true
|
||||
entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"), 3).leaf=true
|
||||
entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"), 4).leaf=true
|
||||
entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"), 5).leaf=true
|
||||
entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"), 6).leaf=true
|
||||
entry({"admin", "docker", "events"}, call("action_events"), _("Events"), 7)
|
||||
|
||||
entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true
|
||||
entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true
|
||||
entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true
|
||||
|
||||
entry({"admin", "docker", "container_stats"}, call("action_get_container_stats")).leaf=true
|
||||
entry({"admin", "docker", "containers_stats"}, call("action_get_containers_stats")).leaf=true
|
||||
entry({"admin", "docker", "get_system_df"}, call("action_get_system_df")).leaf=true
|
||||
entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true
|
||||
entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true
|
||||
entry({"admin", "docker", "container_list_file"}, call("list_file")).leaf=true
|
||||
entry({"admin", "docker", "container_remove_file"}, call("remove_file")).leaf=true
|
||||
entry({"admin", "docker", "container_rename_file"}, call("rename_file")).leaf=true
|
||||
entry({"admin", "docker", "container_export"}, call("export_container")).leaf=true
|
||||
entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true
|
||||
entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true
|
||||
entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true
|
||||
entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true
|
||||
entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true
|
||||
entry({"admin", "docker", "images_untag"}, call("untag_image")).leaf=true
|
||||
entry({"admin", "docker", "confirm"}, call("action_confirm")).leaf=true
|
||||
end
|
||||
|
||||
function action_get_system_df()
|
||||
local res = docker.new():df()
|
||||
luci.http.status(res.code, res.message)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(res.body)
|
||||
end
|
||||
|
||||
function scandir(id, directory)
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local i, t, popen = 0, {}, io.popen
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
local pfile = popen(cmd_docker .. ' -H "'.. hosts ..'" exec ' ..id .." ls -lh \""..directory.."\" | egrep -v '^total'")
|
||||
for fileinfo in pfile:lines() do
|
||||
i = i + 1
|
||||
t[i] = fileinfo
|
||||
end
|
||||
pfile:close()
|
||||
return t
|
||||
end
|
||||
|
||||
function list_response(id, path, success)
|
||||
luci.http.prepare_content("application/json")
|
||||
local result
|
||||
if success then
|
||||
local rv = scandir(id, path)
|
||||
result = {
|
||||
ec = 0,
|
||||
data = rv
|
||||
}
|
||||
else
|
||||
result = {
|
||||
ec = 1
|
||||
}
|
||||
end
|
||||
luci.http.write_json(result)
|
||||
end
|
||||
|
||||
function list_file(id)
|
||||
local path = luci.http.formvalue("path")
|
||||
list_response(id, path, true)
|
||||
end
|
||||
|
||||
function rename_file(id)
|
||||
local filepath = luci.http.formvalue("filepath")
|
||||
local newpath = luci.http.formvalue("newpath")
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
local success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' mv "'..filepath..'" "'..newpath..'"')
|
||||
list_response(nixio.fs.dirname(filepath), success)
|
||||
end
|
||||
|
||||
function remove_file(id)
|
||||
local path = luci.http.formvalue("path")
|
||||
local isdir = luci.http.formvalue("isdir")
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
path = path:gsub("<>", "/")
|
||||
path = path:gsub(" ", "\ ")
|
||||
local success
|
||||
if isdir then
|
||||
success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' rm -r "'..path..'"')
|
||||
else
|
||||
success = os.remove(path)
|
||||
end
|
||||
list_response(nixio.fs.dirname(path), success)
|
||||
end
|
||||
|
||||
function action_events()
|
||||
local logs = ""
|
||||
local query ={}
|
||||
|
||||
local dk = docker.new()
|
||||
query["until"] = os.time()
|
||||
local events = dk:events({query = query})
|
||||
|
||||
if events.code == 200 then
|
||||
for _, v in ipairs(events.body) do
|
||||
local date = "unknown"
|
||||
if v and v.time then
|
||||
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
|
||||
end
|
||||
|
||||
local name = v.Actor.Attributes.name or "unknown"
|
||||
local action = v.Action or "unknown"
|
||||
|
||||
if v and v.Type == "container" then
|
||||
local id = v.Actor.ID or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Container ID: %s Container Name: %s\n", date, v.Type, action, id, name)
|
||||
elseif v.Type == "network" then
|
||||
local container = v.Actor.Attributes.container or "unknown"
|
||||
local network = v.Actor.Attributes.type or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Container ID: %s Network Name: %s Network type: %s\n", date, v.Type, action, container, name, network)
|
||||
elseif v.Type == "image" then
|
||||
local id = v.Actor.ID or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
|
||||
end
|
||||
|
||||
local calculate_cpu_percent = function(d)
|
||||
if type(d) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
local cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
|
||||
local cpu_percent = 0.0
|
||||
local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
|
||||
local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) -- tonumber(d["precpu_stats"]["system_cpu_usage"])
|
||||
if system_delta > 0.0 then
|
||||
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
|
||||
end
|
||||
|
||||
return cpu_percent
|
||||
end
|
||||
|
||||
local get_memory = function(d)
|
||||
if type(d) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
-- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024)
|
||||
-- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024)
|
||||
-- return usage .. "MB / " .. limit.. "MB"
|
||||
|
||||
local limit =tonumber(d["memory_stats"]["limit"])
|
||||
local usage = tonumber(d["memory_stats"]["usage"])
|
||||
-- - tonumber(d["memory_stats"]["stats"]["total_cache"])
|
||||
|
||||
return usage, limit
|
||||
end
|
||||
|
||||
local get_rx_tx = function(d)
|
||||
if type(d) ~="table" then
|
||||
return
|
||||
end
|
||||
|
||||
local data = {}
|
||||
if type(d["networks"]) == "table" then
|
||||
for e, v in pairs(d["networks"]) do
|
||||
data[e] = {
|
||||
bw_tx = tonumber(v.tx_bytes),
|
||||
bw_rx = tonumber(v.rx_bytes)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function get_stat(container_id)
|
||||
if container_id then
|
||||
local dk = docker.new()
|
||||
local response = dk.containers:inspect({id = container_id})
|
||||
if response.code == 200 and response.body.State.Running then
|
||||
response = dk.containers:stats({id = container_id, query = {stream = false, ["one-shot"] = true}})
|
||||
if response.code == 200 then
|
||||
local container_stats = response.body
|
||||
local cpu_percent = calculate_cpu_percent(container_stats)
|
||||
local mem_useage, mem_limit = get_memory(container_stats)
|
||||
local bw_rxtx = get_rx_tx(container_stats)
|
||||
return response.code, response.body.message, {
|
||||
cpu_percent = cpu_percent,
|
||||
memory = {
|
||||
mem_useage = mem_useage,
|
||||
mem_limit = mem_limit
|
||||
},
|
||||
bw_rxtx = bw_rxtx
|
||||
}
|
||||
else
|
||||
return response.code, response.body.message
|
||||
end
|
||||
else
|
||||
if response.code == 200 then
|
||||
return 500, "container "..container_id.." not running"
|
||||
else
|
||||
return response.code, response.body.message
|
||||
end
|
||||
end
|
||||
else
|
||||
return 404, "No container name or id"
|
||||
end
|
||||
end
|
||||
function action_get_container_stats(container_id)
|
||||
local code, msg, res = get_stat(container_id)
|
||||
luci.http.status(code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(res)
|
||||
end
|
||||
|
||||
function action_get_containers_stats()
|
||||
local res = luci.http.formvalue(containers) or ""
|
||||
local stats = {}
|
||||
res = luci.jsonc.parse(res.containers)
|
||||
if res and type(res) == "table" then
|
||||
for i, v in ipairs(res) do
|
||||
_,_,stats[v] = get_stat(v)
|
||||
end
|
||||
end
|
||||
luci.http.status(200, "OK")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(stats)
|
||||
end
|
||||
|
||||
function action_confirm()
|
||||
local data = docker:read_status()
|
||||
if data then
|
||||
data = data:gsub("\n","<br>"):gsub(" "," ")
|
||||
code = 202
|
||||
msg = data
|
||||
else
|
||||
code = 200
|
||||
msg = "finish"
|
||||
data = "finish"
|
||||
end
|
||||
|
||||
luci.http.status(code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({info = data})
|
||||
end
|
||||
|
||||
function export_container(id)
|
||||
local dk = docker.new()
|
||||
local first
|
||||
|
||||
local cb = function(res, chunk)
|
||||
if res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. id ..'.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
local res = dk.containers:export({id = id}, cb)
|
||||
end
|
||||
|
||||
function download_archive()
|
||||
local id = luci.http.formvalue("id")
|
||||
local path = luci.http.formvalue("path")
|
||||
local filename = luci.http.formvalue("filename") or "archive"
|
||||
local dk = docker.new()
|
||||
local first
|
||||
|
||||
local cb = function(res, chunk)
|
||||
if res and res.code and res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. filename .. '.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
local res = dk.containers:get_archive({
|
||||
id = id,
|
||||
query = {
|
||||
path = luci.http.urlencode(path)
|
||||
}
|
||||
}, cb)
|
||||
end
|
||||
|
||||
function upload_archive(container_id)
|
||||
local path = luci.http.formvalue("upload-path")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local res = dk.containers:put_archive({
|
||||
id = container_id,
|
||||
query = {
|
||||
path = luci.http.urlencode(path)
|
||||
},
|
||||
body = rec_send
|
||||
})
|
||||
|
||||
local msg = res and res.message or res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
-- function save_images()
|
||||
-- local names = luci.http.formvalue("names")
|
||||
-- local dk = docker.new()
|
||||
-- local first
|
||||
|
||||
-- local cb = function(res, chunk)
|
||||
-- if res.code == 200 then
|
||||
-- if not first then
|
||||
-- first = true
|
||||
-- luci.http.status(res.code, res.message)
|
||||
-- luci.http.header('Content-Disposition', 'inline; filename="'.. "images" ..'.tar"')
|
||||
-- luci.http.header('Content-Type', 'application\/x-tar')
|
||||
-- end
|
||||
-- luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
-- else
|
||||
-- if not first then
|
||||
-- first = true
|
||||
-- luci.http.prepare_content("text/plain")
|
||||
-- end
|
||||
-- luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- docker:write_status("Images: saving" .. " " .. names .. "...")
|
||||
-- local res = dk.images:get({
|
||||
-- query = {
|
||||
-- names = luci.http.urlencode(names)
|
||||
-- }
|
||||
-- }, cb)
|
||||
-- docker:clear_status()
|
||||
|
||||
-- local msg = res and res.body and res.body.message or nil
|
||||
-- luci.http.status(res.code, msg)
|
||||
-- luci.http.prepare_content("application/json")
|
||||
-- luci.http.write_json({message = msg})
|
||||
-- end
|
||||
|
||||
function load_images()
|
||||
local archive = luci.http.formvalue("upload-archive")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
docker:write_status("Images: loading...")
|
||||
local res = dk.images:load({body = rec_send})
|
||||
local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil
|
||||
if res and res.code == 200 and msg and msg:match("Loaded image ID") then
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
|
||||
end
|
||||
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
function import_images()
|
||||
local src = luci.http.formvalue("src")
|
||||
local itag = luci.http.formvalue("tag")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
docker:write_status("Images: importing".. " ".. itag .."...\n")
|
||||
local repo = itag and itag:match("^([^:]+)")
|
||||
local tag = itag and itag:match("^[^:]-:([^:]+)")
|
||||
local res = dk.images:create({
|
||||
query = {
|
||||
fromSrc = luci.http.urlencode(src or "-"),
|
||||
repo = repo or nil,
|
||||
tag = tag or nil
|
||||
},
|
||||
body = not src and rec_send or nil
|
||||
}, docker.import_image_show_status_cb)
|
||||
|
||||
local msg = res and res.body and ( res.body.message )or nil
|
||||
if not msg and #res.body == 0 then
|
||||
msg = res.body.status or res.body.error
|
||||
elseif not msg and #res.body >= 1 then
|
||||
msg = res.body[#res.body].status or res.body[#res.body].error
|
||||
end
|
||||
|
||||
if res.code == 200 and msg and msg:match("sha256:") then
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
|
||||
end
|
||||
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
function get_image_tags(image_id)
|
||||
if not image_id then
|
||||
luci.http.status(400, "no image id")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no image id"})
|
||||
return
|
||||
end
|
||||
|
||||
local dk = docker.new()
|
||||
local res = dk.images:inspect({
|
||||
id = image_id
|
||||
})
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
if res.code == 200 then
|
||||
local tags = res.body.RepoTags
|
||||
luci.http.write_json({tags = tags})
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
||||
|
||||
function tag_image(image_id)
|
||||
local src = luci.http.formvalue("tag")
|
||||
local image_id = image_id or luci.http.formvalue("id")
|
||||
|
||||
if type(src) ~= "string" or not image_id then
|
||||
luci.http.status(400, "no image id or tag")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no image id or tag"})
|
||||
return
|
||||
end
|
||||
|
||||
local repo = src:match("^([^:]+)")
|
||||
local tag = src:match("^[^:]-:([^:]+)")
|
||||
local dk = docker.new()
|
||||
local res = dk.images:tag({
|
||||
id = image_id,
|
||||
query={
|
||||
repo=repo,
|
||||
tag=tag
|
||||
}
|
||||
})
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
if res.code == 201 then
|
||||
local tags = res.body.RepoTags
|
||||
luci.http.write_json({tags = tags})
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
||||
|
||||
function untag_image(tag)
|
||||
local tag = tag or luci.http.formvalue("tag")
|
||||
|
||||
if not tag then
|
||||
luci.http.status(400, "no tag name")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no tag name"})
|
||||
return
|
||||
end
|
||||
|
||||
local dk = docker.new()
|
||||
local res = dk.images:inspect({name = tag})
|
||||
|
||||
if res.code == 200 then
|
||||
local tags = res.body.RepoTags
|
||||
if #tags > 1 then
|
||||
local r = dk.images:remove({name = tag})
|
||||
local msg = r and r.body and r.body.message or nil
|
||||
luci.http.status(r.code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg})
|
||||
else
|
||||
luci.http.status(500, "Cannot remove the last tag")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "Cannot remove the last tag"})
|
||||
end
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
152
luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua
Normal file
|
@ -0,0 +1,152 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2021 Florian Eckert <fe@dev.tdt.de>
|
||||
Copyright 2021 lisaac <lisaac.cn@gmail.com>
|
||||
]]--
|
||||
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local m, s, o
|
||||
|
||||
m = Map("dockerd",
|
||||
translate("Docker - Configuration"),
|
||||
translate("DockerMan is a simple docker manager client for LuCI"))
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
s = m:section(NamedSection, "globals", "section", translate("Docker Daemon settings"))
|
||||
|
||||
o = s:option(Flag, "auto_start", translate("Auto start"))
|
||||
o.rmempty = false
|
||||
o.write = function(self, section, value)
|
||||
if value == "1" then
|
||||
luci.util.exec("/etc/init.d/dockerd enable")
|
||||
else
|
||||
luci.util.exec("/etc/init.d/dockerd disable")
|
||||
end
|
||||
m.uci:set("dockerd", "globals", "auto_start", value)
|
||||
end
|
||||
|
||||
o = s:option(Value, "data_root",
|
||||
translate("Docker Root Dir"))
|
||||
o.placeholder = "/opt/docker/"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(Value, "bip",
|
||||
translate("Default bridge"),
|
||||
translate("Configure the default bridge network"))
|
||||
o.placeholder = "172.17.0.1/16"
|
||||
o.datatype = "ipaddr"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(DynamicList, "registry_mirrors",
|
||||
translate("Registry Mirrors"),
|
||||
translate("It replaces the daemon registry mirrors with a new set of registry mirrors"))
|
||||
o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
|
||||
o:depends("remote_endpoint", 0)
|
||||
o.forcewrite = true
|
||||
|
||||
o = s:option(ListValue, "log_level",
|
||||
translate("Log Level"),
|
||||
translate('Set the logging level'))
|
||||
o:value("debug", translate("Debug"))
|
||||
o:value("", translate("Info")) -- This is the default debug level from the deamon is optin is not set
|
||||
o:value("warn", translate("Warning"))
|
||||
o:value("error", translate("Error"))
|
||||
o:value("fatal", translate("Fatal"))
|
||||
o.rmempty = true
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(DynamicList, "hosts",
|
||||
translate("Client connection"),
|
||||
translate('Specifies where the Docker daemon will listen for client connections (default: unix:///var/run/docker.sock)'))
|
||||
o:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
|
||||
o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
|
||||
o.rmempty = true
|
||||
o:depends("remote_endpoint", 0)
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, "dockerman", "section", translate("DockerMan settings"))
|
||||
s:tab("ac", translate("Access Control"))
|
||||
s:tab("dockerman", translate("DockerMan"))
|
||||
|
||||
o = s:taboption("dockerman", Flag, "remote_endpoint",
|
||||
translate("Remote Endpoint"),
|
||||
translate("Connect to remote docker endpoint"))
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value, sid)
|
||||
local res = luci.http.formvaluetable("cbid.dockerd")
|
||||
if res["dockerman.remote_endpoint"] == "1" then
|
||||
if res["dockerman.remote_port"] and res["dockerman.remote_port"] ~= "" and res["dockerman.remote_host"] and res["dockerman.remote_host"] ~= "" then
|
||||
return 1
|
||||
else
|
||||
return nil, translate("Please input the PORT or HOST IP of remote docker instance!")
|
||||
end
|
||||
else
|
||||
if not res["dockerman.socket_path"] then
|
||||
return nil, translate("Please input the SOCKET PATH of docker daemon!")
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
o = s:taboption("dockerman", Value, "socket_path",
|
||||
translate("Docker Socket Path"))
|
||||
o.default = "/var/run/docker.sock"
|
||||
o.placeholder = "/var/run/docker.sock"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:taboption("dockerman", Value, "remote_host",
|
||||
translate("Remote Host"),
|
||||
translate("Host or IP Address for the connection to a remote docker instance"))
|
||||
o.datatype = "host"
|
||||
o.placeholder = "10.1.1.2"
|
||||
o:depends("remote_endpoint", 1)
|
||||
|
||||
o = s:taboption("dockerman", Value, "remote_port",
|
||||
translate("Remote Port"))
|
||||
o.placeholder = "2375"
|
||||
o.datatype = "port"
|
||||
o:depends("remote_endpoint", 1)
|
||||
|
||||
-- o = s:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
|
||||
-- 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"))
|
||||
-- o.enabled="true"
|
||||
-- o.disabled="false"
|
||||
-- o = s:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
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"))
|
||||
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
|
||||
for i, v in ipairs(interfaces) do
|
||||
o:value(v, v)
|
||||
end
|
||||
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!"))
|
||||
o.placeholder = "8080/tcp"
|
||||
local docker = require "luci.model.docker"
|
||||
local containers, res, lost_state
|
||||
local dk = docker.new()
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
lost_state = false
|
||||
res = dk.containers:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
-- allowed_container.placeholder = "container name_or_id"
|
||||
if containers then
|
||||
for i, v in ipairs(containers) do
|
||||
if v.State == "running" and v.Ports then
|
||||
for _, port in ipairs(v.Ports) do
|
||||
if port.PublicPort and port.IP and not string.find(port.IP,":") then
|
||||
o:value(port.PublicPort.."/"..port.Type, v.Names[1]:sub(2) .. " | " .. port.PublicPort .. " | " .. port.Type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
810
luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua
Normal file
|
@ -0,0 +1,810 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
container_id = arg[1]
|
||||
local action = arg[2] or "info"
|
||||
|
||||
local m, s, o
|
||||
local images, networks, container_info, res
|
||||
|
||||
if not container_id then
|
||||
return
|
||||
end
|
||||
|
||||
res = dk.containers:inspect({id = container_id})
|
||||
if res.code < 300 then
|
||||
container_info = res.body
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
local get_ports = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.PortBindings then
|
||||
for inter, out in pairs(d.HostConfig.PortBindings) do
|
||||
data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_env = function(d)
|
||||
local data
|
||||
|
||||
if d.Config and d.Config.Env then
|
||||
for _,v in ipairs(d.Config.Env) do
|
||||
data = (data and (data .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_command = function(d)
|
||||
local data
|
||||
|
||||
if d.Config and d.Config.Cmd then
|
||||
for _,v in ipairs(d.Config.Cmd) do
|
||||
data = (data and (data .. " ") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_mounts = function(d)
|
||||
local data
|
||||
|
||||
if d.Mounts then
|
||||
for _,v in ipairs(d.Mounts) do
|
||||
local v_sorce_d, v_dest_d
|
||||
local v_sorce = ""
|
||||
local v_dest = ""
|
||||
for v_sorce_d in v["Source"]:gmatch('[^/]+') do
|
||||
if v_sorce_d and #v_sorce_d > 12 then
|
||||
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..."
|
||||
else
|
||||
v_sorce = v_sorce .."/".. v_sorce_d
|
||||
end
|
||||
end
|
||||
for v_dest_d in v["Destination"]:gmatch('[^/]+') do
|
||||
if v_dest_d and #v_dest_d > 12 then
|
||||
v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..."
|
||||
else
|
||||
v_dest = v_dest .."/".. v_dest_d
|
||||
end
|
||||
end
|
||||
data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_device = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Devices then
|
||||
for _,v in ipairs(d.HostConfig.Devices) do
|
||||
data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_links = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Links then
|
||||
for _,v in ipairs(d.HostConfig.Links) do
|
||||
data = (data and (data .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_tmpfs = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Tmpfs then
|
||||
for k, v in pairs(d.HostConfig.Tmpfs) do
|
||||
data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_dns = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Dns then
|
||||
for _, v in ipairs(d.HostConfig.Dns) do
|
||||
data = (data and (data .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_sysctl = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Sysctls then
|
||||
for k, v in pairs(d.HostConfig.Sysctls) do
|
||||
data = (data and (data .. "<br>") or "") .. k..":"..v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_networks = function(d)
|
||||
local data={}
|
||||
|
||||
if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
|
||||
for k,v in pairs(d.NetworkSettings.Networks) do
|
||||
data[k] = v.IPAddress or ""
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
|
||||
local start_stop_remove = function(m, cmd)
|
||||
local res
|
||||
|
||||
docker:clear_status()
|
||||
docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
|
||||
|
||||
if cmd ~= "upgrade" then
|
||||
res = dk.containers[cmd](dk, {id = container_id})
|
||||
else
|
||||
res = dk.containers_upgrade(dk, {id = container_id})
|
||||
end
|
||||
|
||||
if res and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
|
||||
else
|
||||
docker:clear_status()
|
||||
if cmd ~= "remove" and cmd ~= "upgrade" then
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m=SimpleForm("docker",
|
||||
translatef("Docker - Container (%s)", container_info.Name:sub(2)),
|
||||
translate("On this page, the selected container can be managed."))
|
||||
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template = "cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Start")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"start")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"restart")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_stop")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Stop")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"stop")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_kill")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Kill")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"kill")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_export")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Export")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container_export/"..container_id))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_upgrade")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Upgrade")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"upgrade")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_duplicate")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Duplicate/Edit")
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Remove")
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"remove")
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/container"
|
||||
|
||||
if action == "info" then
|
||||
res = dk.networks:list()
|
||||
if res.code < 300 then
|
||||
networks = res.body
|
||||
else
|
||||
return
|
||||
end
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
table_info = {
|
||||
["01name"] = {
|
||||
_key = translate("Name"),
|
||||
_value = container_info.Name:sub(2) or "-",
|
||||
_button=translate("Update")
|
||||
},
|
||||
["02id"] = {
|
||||
_key = translate("ID"),
|
||||
_value = container_info.Id or "-"
|
||||
},
|
||||
["03image"] = {
|
||||
_key = translate("Image"),
|
||||
_value = container_info.Config.Image .. "<br>" .. container_info.Image
|
||||
},
|
||||
["04status"] = {
|
||||
_key = translate("Status"),
|
||||
_value = container_info.State and container_info.State.Status or "-"
|
||||
},
|
||||
["05created"] = {
|
||||
_key = translate("Created"),
|
||||
_value = container_info.Created or "-"
|
||||
},
|
||||
}
|
||||
|
||||
if container_info.State.Status == "running" then
|
||||
table_info["06start"] = {
|
||||
_key = translate("Start Time"),
|
||||
_value = container_info.State and container_info.State.StartedAt or "-"
|
||||
}
|
||||
else
|
||||
table_info["06start"] = {
|
||||
_key = translate("Finish Time"),
|
||||
_value = container_info.State and container_info.State.FinishedAt or "-"
|
||||
}
|
||||
end
|
||||
|
||||
table_info["07healthy"] = {
|
||||
_key = translate("Healthy"),
|
||||
_value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"
|
||||
}
|
||||
table_info["08restart"] = {
|
||||
_key = translate("Restart Policy"),
|
||||
_value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-",
|
||||
_button=translate("Update")
|
||||
}
|
||||
table_info["081user"] = {
|
||||
_key = translate("User"),
|
||||
_value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"
|
||||
}
|
||||
table_info["09mount"] = {
|
||||
_key = translate("Mount/Volume"),
|
||||
_value = get_mounts(container_info) or "-"
|
||||
}
|
||||
table_info["10cmd"] = {
|
||||
_key = translate("Command"),
|
||||
_value = get_command(container_info) or "-"
|
||||
}
|
||||
table_info["11env"] = {
|
||||
_key = translate("Env"),
|
||||
_value = get_env(container_info) or "-"
|
||||
}
|
||||
table_info["12ports"] = {
|
||||
_key = translate("Ports"),
|
||||
_value = get_ports(container_info) or "-"
|
||||
}
|
||||
table_info["13links"] = {
|
||||
_key = translate("Links"),
|
||||
_value = get_links(container_info) or "-"
|
||||
}
|
||||
table_info["14device"] = {
|
||||
_key = translate("Device"),
|
||||
_value = get_device(container_info) or "-"
|
||||
}
|
||||
table_info["15tmpfs"] = {
|
||||
_key = translate("Tmpfs"),
|
||||
_value = get_tmpfs(container_info) or "-"
|
||||
}
|
||||
table_info["16dns"] = {
|
||||
_key = translate("DNS"),
|
||||
_value = get_dns(container_info) or "-"
|
||||
}
|
||||
table_info["17sysctl"] = {
|
||||
_key = translate("Sysctl"),
|
||||
_value = get_sysctl(container_info) or "-"
|
||||
}
|
||||
|
||||
info_networks = get_networks(container_info)
|
||||
list_networks = {}
|
||||
for _, v in ipairs (networks) do
|
||||
if v and v.Name then
|
||||
local parent = v.Options and v.Options.parent or nil
|
||||
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
|
||||
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
|
||||
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
|
||||
list_networks[v.Name] = network_name
|
||||
end
|
||||
end
|
||||
|
||||
if type(info_networks)== "table" then
|
||||
for k,v in pairs(info_networks) do
|
||||
table_info["14network"..k] = {
|
||||
_key = translate("Network"),
|
||||
_value = k.. (v~="" and (" | ".. v) or ""),
|
||||
_button=translate("Disconnect")
|
||||
}
|
||||
list_networks[k]=nil
|
||||
end
|
||||
end
|
||||
|
||||
table_info["15connect"] = {
|
||||
_key = translate("Connect Network"),
|
||||
_value = list_networks ,_opts = "",
|
||||
_button=translate("Connect")
|
||||
}
|
||||
|
||||
s = m:section(Table,table_info)
|
||||
s.nodescr=true
|
||||
s.formvalue=function(self, section)
|
||||
return table_info
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_key", translate("Info"))
|
||||
o.width = "20%"
|
||||
|
||||
o = s:option(ListValue, "_value")
|
||||
o.render = function(self, section, scope)
|
||||
if table_info[section]._key == translate("Name") then
|
||||
self:reset_values()
|
||||
self.template = "cbi/value"
|
||||
self.size = 30
|
||||
self.keylist = {}
|
||||
self.vallist = {}
|
||||
self.default=table_info[section]._value
|
||||
Value.render(self, section, scope)
|
||||
elseif table_info[section]._key == translate("Restart Policy") then
|
||||
self.template = "cbi/lvalue"
|
||||
self:reset_values()
|
||||
self.size = nil
|
||||
self:value("no", "No")
|
||||
self:value("unless-stopped", "Unless stopped")
|
||||
self:value("always", "Always")
|
||||
self:value("on-failure", "On failure")
|
||||
self.default=table_info[section]._value
|
||||
ListValue.render(self, section, scope)
|
||||
elseif table_info[section]._key == translate("Connect Network") then
|
||||
self.template = "cbi/lvalue"
|
||||
self:reset_values()
|
||||
self.size = nil
|
||||
for k,v in pairs(list_networks) do
|
||||
if k ~= "host" then
|
||||
self:value(k,v)
|
||||
end
|
||||
end
|
||||
self.default=table_info[section]._value
|
||||
ListValue.render(self, section, scope)
|
||||
else
|
||||
self:reset_values()
|
||||
self.rawhtml=true
|
||||
self.template = "cbi/dvalue"
|
||||
self.default=table_info[section]._value
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
table_info[section]._value=value
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "_opts")
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
table_info[section]._opts=value
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
return value
|
||||
end
|
||||
o.render = function(self, section, scope)
|
||||
if table_info[section]._key==translate("Connect Network") then
|
||||
self.template = "cbi/value"
|
||||
self.keylist = {}
|
||||
self.vallist = {}
|
||||
self.placeholder = "10.1.1.254"
|
||||
self.datatype = "ip4addr"
|
||||
self.default=table_info[section]._opts
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.rawhtml=true
|
||||
self.template = "cbi/dvalue"
|
||||
self.default=table_info[section]._opts
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Button, "_button")
|
||||
o.forcewrite = true
|
||||
o.render = function(self, section, scope)
|
||||
if table_info[section]._button and table_info[section]._value ~= nil then
|
||||
self.inputtitle=table_info[section]._button
|
||||
self.template = "cbi/button"
|
||||
self.inputstyle = "edit"
|
||||
Button.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
self.default=""
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
local res
|
||||
|
||||
docker:clear_status()
|
||||
|
||||
if section == "01name" then
|
||||
docker:append_status("Containers: rename " .. container_id .. "...")
|
||||
local new_name = table_info[section]._value
|
||||
res = dk.containers:rename({
|
||||
id = container_id,
|
||||
query = {
|
||||
name=new_name
|
||||
}
|
||||
})
|
||||
elseif section == "08restart" then
|
||||
docker:append_status("Containers: update " .. container_id .. "...")
|
||||
local new_restart = table_info[section]._value
|
||||
res = dk.containers:update({
|
||||
id = container_id,
|
||||
body = {
|
||||
RestartPolicy = {
|
||||
Name = new_restart
|
||||
}
|
||||
}
|
||||
})
|
||||
elseif table_info[section]._key == translate("Network") then
|
||||
local _,_,leave_network
|
||||
|
||||
_, _, leave_network = table_info[section]._value:find("(.-) | .+")
|
||||
leave_network = leave_network or table_info[section]._value
|
||||
docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
|
||||
res = dk.networks:disconnect({
|
||||
name = leave_network,
|
||||
body = {
|
||||
Container = container_id
|
||||
}
|
||||
})
|
||||
elseif section == "15connect" then
|
||||
local connect_network = table_info[section]._value
|
||||
local network_opiton
|
||||
if connect_network ~= "none"
|
||||
and connect_network ~= "bridge"
|
||||
and connect_network ~= "host" then
|
||||
|
||||
network_opiton = table_info[section]._opts ~= "" and {
|
||||
IPAMConfig={
|
||||
IPv4Address=table_info[section]._opts
|
||||
}
|
||||
} or nil
|
||||
end
|
||||
docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
|
||||
res = dk.networks:connect({
|
||||
name = connect_network,
|
||||
body = {
|
||||
Container = container_id,
|
||||
EndpointConfig= network_opiton
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
if res and res.code > 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
|
||||
end
|
||||
elseif action == "resources" then
|
||||
s = m:section(SimpleSection)
|
||||
o = s:option( Value, "cpus",
|
||||
translate("CPUs"),
|
||||
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
|
||||
o.placeholder = "1.5"
|
||||
o.rmempty = true
|
||||
o.datatype="ufloat"
|
||||
o.default = container_info.HostConfig.NanoCpus / (10^9)
|
||||
|
||||
o = s:option(Value, "cpushares",
|
||||
translate("CPU Shares Weight"),
|
||||
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
|
||||
o.placeholder = "1024"
|
||||
o.rmempty = true
|
||||
o.datatype="uinteger"
|
||||
o.default = container_info.HostConfig.CpuShares
|
||||
|
||||
o = s:option(Value, "memory",
|
||||
translate("Memory"),
|
||||
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
|
||||
o.placeholder = "128m"
|
||||
o.rmempty = true
|
||||
o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
|
||||
|
||||
o = s:option(Value, "blkioweight",
|
||||
translate("Block IO Weight"),
|
||||
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
|
||||
o.placeholder = "500"
|
||||
o.rmempty = true
|
||||
o.datatype="uinteger"
|
||||
o.default = container_info.HostConfig.BlkioWeight
|
||||
|
||||
m.handle = function(self, state, data)
|
||||
if state == FORM_VALID then
|
||||
local memory = data.memory
|
||||
if memory and memory ~= 0 then
|
||||
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
|
||||
if n then
|
||||
unit = unit and unit:sub(1,1):upper() or "B"
|
||||
if unit == "M" then
|
||||
memory = tonumber(n) * 1024 * 1024
|
||||
elseif unit == "G" then
|
||||
memory = tonumber(n) * 1024 * 1024 * 1024
|
||||
elseif unit == "K" then
|
||||
memory = tonumber(n) * 1024
|
||||
else
|
||||
memory = tonumber(n)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
request_body = {
|
||||
BlkioWeight = tonumber(data.blkioweight),
|
||||
NanoCPUs = tonumber(data.cpus)*10^9,
|
||||
Memory = tonumber(memory),
|
||||
CpuShares = tonumber(data.cpushares)
|
||||
}
|
||||
|
||||
docker:write_status("Containers: update " .. container_id .. "...")
|
||||
local res = dk.containers:update({id = container_id, body = request_body})
|
||||
if res and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
|
||||
end
|
||||
end
|
||||
|
||||
elseif action == "file" then
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
s= m:section(SimpleSection)
|
||||
s.template = "dockerman/container_file_manager"
|
||||
s.container = container_id
|
||||
m.redirect = nil
|
||||
elseif action == "inspect" then
|
||||
s = m:section(SimpleSection)
|
||||
s.syslog = luci.jsonc.stringify(container_info, true)
|
||||
s.title = translate("Container Inspect")
|
||||
s.template = "dockerman/logs"
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
elseif action == "logs" then
|
||||
local logs = ""
|
||||
local query ={
|
||||
stdout = 1,
|
||||
stderr = 1,
|
||||
tail = 1000
|
||||
}
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
|
||||
logs = dk.containers:logs({id = container_id, query = query})
|
||||
if logs.code == 200 then
|
||||
s.syslog=logs.body
|
||||
else
|
||||
s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
|
||||
end
|
||||
|
||||
s.title=translate("Container Logs")
|
||||
s.template = "dockerman/logs"
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
elseif action == "console" then
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
|
||||
|
||||
if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
|
||||
local cmd = "/bin/sh"
|
||||
local uid
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
|
||||
o = s:option(Value, "command", translate("Command"))
|
||||
o:value("/bin/sh", "/bin/sh")
|
||||
o:value("/bin/ash", "/bin/ash")
|
||||
o:value("/bin/bash", "/bin/bash")
|
||||
o.default = "/bin/sh"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
cmd = value
|
||||
end
|
||||
|
||||
o = s:option(Value, "uid", translate("UID"))
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
uid = value
|
||||
end
|
||||
|
||||
o = s:option(Button, "connect")
|
||||
o.render = function(self, section, scope)
|
||||
self.inputstyle = "add"
|
||||
self.title = " "
|
||||
self.inputtitle = translate("Connect")
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
o.write = function(self, section)
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
|
||||
|
||||
if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local ttyd_ssl = uci:get("ttyd", "@ttyd[0]", "ssl")
|
||||
local ttyd_ssl_key = uci:get("ttyd", "@ttyd[0]", "ssl_key")
|
||||
local ttyd_ssl_cert = uci:get("ttyd", "@ttyd[0]", "ssl_cert")
|
||||
|
||||
if ttyd_ssl == "1" and ttyd_ssl_cert and ttyd_ssl_key then
|
||||
cmd_ttyd = string.format('%s -S -C %s -K %s', cmd_ttyd, ttyd_ssl_cert, ttyd_ssl_key)
|
||||
end
|
||||
|
||||
local pid = luci.util.trim(luci.util.exec("netstat -lnpt | grep :7682 | grep ttyd | tr -s ' ' | cut -d ' ' -f7 | cut -d'/' -f1"))
|
||||
if pid and pid ~= "" then
|
||||
luci.util.exec("kill -9 " .. pid)
|
||||
end
|
||||
|
||||
local hosts
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") or false
|
||||
local host = nil
|
||||
local port = nil
|
||||
local socket = nil
|
||||
|
||||
if remote then
|
||||
host = uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
port = uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
else
|
||||
socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
end
|
||||
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket then
|
||||
hosts = "unix://" .. socket
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
if uid and uid ~= "" then
|
||||
uid = "-u " .. uid
|
||||
else
|
||||
uid = ""
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
os.execute(start_cmd)
|
||||
|
||||
o = s:option(DummyValue, "console")
|
||||
o.container_id = container_id
|
||||
o.template = "dockerman/container_console"
|
||||
end
|
||||
end
|
||||
elseif action == "stats" then
|
||||
local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
|
||||
local container_top
|
||||
|
||||
if response.code == 200 then
|
||||
container_top=response.body
|
||||
else
|
||||
response = dk.containers:top({id = container_id})
|
||||
if response.code == 200 then
|
||||
container_top=response.body
|
||||
end
|
||||
end
|
||||
|
||||
if type(container_top) == "table" then
|
||||
s = m:section(SimpleSection)
|
||||
s.container_id = container_id
|
||||
s.template = "dockerman/container_stats"
|
||||
table_stats = {
|
||||
cpu={
|
||||
key=translate("CPU Useage"),
|
||||
value='-'
|
||||
},
|
||||
memory={
|
||||
key=translate("Memory Useage"),
|
||||
value='-'
|
||||
}
|
||||
}
|
||||
|
||||
container_top = response.body
|
||||
s = m:section(Table, table_stats, translate("Stats"))
|
||||
s:option(DummyValue, "key", translate("Stats")).width="33%"
|
||||
s:option(DummyValue, "value")
|
||||
top_section = m:section(Table, container_top.Processes, translate("TOP"))
|
||||
for i, v in ipairs(container_top.Titles) do
|
||||
top_section:option(DummyValue, i, translate(v))
|
||||
end
|
||||
end
|
||||
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
end
|
||||
|
||||
return m
|
284
luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua
Normal file
|
@ -0,0 +1,284 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local http = require "luci.http"
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
local images, networks, containers, res, lost_state
|
||||
local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
|
||||
local dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.images:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
images = res.body
|
||||
end
|
||||
|
||||
res = dk.networks:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
networks = res.body
|
||||
end
|
||||
|
||||
res = dk.containers:list({
|
||||
query = {
|
||||
all = true
|
||||
}
|
||||
})
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
function get_containers()
|
||||
local data = {}
|
||||
if type(containers) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i, v in ipairs(containers) do
|
||||
local index = (10^12 - v.Created) .. "_id_" .. v.Id
|
||||
|
||||
data[index]={}
|
||||
data[index]["_selected"] = 0
|
||||
data[index]["_id"] = v.Id:sub(1,12)
|
||||
-- data[index]["name"] = v.Names[1]:sub(2)
|
||||
data[index]["_status"] = v.Status
|
||||
|
||||
if v.Status:find("^Up") then
|
||||
data[index]["_name"] = "<font color='green'>"..v.Names[1]:sub(2).."</font>"
|
||||
data[index]["_status"] = "<a href='"..luci.dispatcher.build_url("admin/docker/container/"..v.Id).."/stats'><font color='green'>".. data[index]["_status"] .. "</font>" .. "<br><font color='#9f9f9f' class='container_cpu_status'></font><br><font color='#9f9f9f' class='container_mem_status'></font><br><font color='#9f9f9f' class='container_network_status'></font></a>"
|
||||
else
|
||||
data[index]["_name"] = "<font color='red'>"..v.Names[1]:sub(2).."</font>"
|
||||
data[index]["_status"] = '<font class="container_not_running" color="red">'.. data[index]["_status"] .. "</font>"
|
||||
end
|
||||
|
||||
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
|
||||
for networkname, netconfig in pairs(v.NetworkSettings.Networks) do
|
||||
data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "")
|
||||
end
|
||||
end
|
||||
|
||||
-- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge"
|
||||
-- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil
|
||||
-- local _, _, image = v.Image:find("^sha256:(.+)")
|
||||
-- if image ~= nil then
|
||||
-- image=image:sub(1,12)
|
||||
-- end
|
||||
|
||||
if v.Ports and next(v.Ports) ~= nil then
|
||||
data[index]["_ports"] = nil
|
||||
local ip = require "luci.ip"
|
||||
for _,v2 in ipairs(v.Ports) do
|
||||
-- display ipv4 only
|
||||
if ip.new(v2.IP or "0.0.0.0"):is4() then
|
||||
data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
|
||||
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
|
||||
.. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
|
||||
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for ii,iv in ipairs(images) do
|
||||
if iv.Id == v.ImageID then
|
||||
data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")
|
||||
end
|
||||
end
|
||||
data[index]["_id_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. data[index]["_name"] .. "<br><font color='#9f9f9f'>ID: " .. data[index]["_id"]
|
||||
.. "</font></a><br>Image: " .. (data[index]["_image"] or "<none>")
|
||||
.. "<br><font color='#9f9f9f' class='container_size_".. v.Id .."'></font>"
|
||||
|
||||
if type(v.Mounts) == "table" and next(v.Mounts) then
|
||||
for _, v2 in pairs(v.Mounts) do
|
||||
if v2.Type ~= "volume" then
|
||||
local v_sorce_d, v_dest_d
|
||||
local v_sorce = ""
|
||||
local v_dest = ""
|
||||
for v_sorce_d in v2["Source"]:gmatch('[^/]+') do
|
||||
if v_sorce_d and #v_sorce_d > 12 then
|
||||
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,8) .. ".."
|
||||
else
|
||||
v_sorce = v_sorce .."/".. v_sorce_d
|
||||
end
|
||||
end
|
||||
for v_dest_d in v2["Destination"]:gmatch('[^/]+') do
|
||||
if v_dest_d and #v_dest_d > 12 then
|
||||
v_dest = v_dest .. "/" .. v_dest_d:sub(1,8) .. ".."
|
||||
else
|
||||
v_dest = v_dest .."/".. v_dest_d
|
||||
end
|
||||
end
|
||||
data[index]["_mounts"] = (data[index]["_mounts"] and (data[index]["_mounts"] .. "<br>") or "") .. '<span title="'.. v2.Source.. "→" .. v2.Destination .. '" ><a href="'..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..'/file?path='..v2["Destination"]..'">' .. v_sorce .. "→" .. v_dest..'</a></span>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
data[index]["_image_id"] = v.ImageID:sub(8,20)
|
||||
data[index]["_command"] = v.Command
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
local container_list = not lost_state and get_containers() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Containers"),
|
||||
translate("This page displays all containers that have been created on the connected docker host."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
m:append(Template("dockerman/containers_running_stats"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table, container_list, translate("Containers"))
|
||||
s.nodescr=true
|
||||
s.config="containers"
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.width = "1%"
|
||||
o.write=function(self, section, value)
|
||||
container_list[section]._selected = value
|
||||
end
|
||||
|
||||
-- o = s:option(DummyValue, "_id", translate("ID"))
|
||||
-- o.width="10%"
|
||||
|
||||
-- o = s:option(DummyValue, "_name", translate("Container Name"))
|
||||
-- o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_id_name", translate("Container Info"))
|
||||
o.rawhtml = true
|
||||
o.width="15%"
|
||||
|
||||
o = s:option(DummyValue, "_status", translate("Status"))
|
||||
o.width="15%"
|
||||
o.rawhtml=true
|
||||
|
||||
o = s:option(DummyValue, "_network", translate("Network"))
|
||||
o.width="10%"
|
||||
|
||||
o = s:option(DummyValue, "_ports", translate("Ports"))
|
||||
o.width="5%"
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_mounts", translate("Mounts"))
|
||||
o.width="25%"
|
||||
o.rawhtml = true
|
||||
|
||||
-- o = s:option(DummyValue, "_image", translate("Image"))
|
||||
-- o.width="8%"
|
||||
|
||||
o = s:option(DummyValue, "_command", translate("Command"))
|
||||
o.width="15%"
|
||||
|
||||
local start_stop_remove = function(m, cmd)
|
||||
local container_selected = {}
|
||||
-- 遍历table中sectionid
|
||||
for k in pairs(container_list) do
|
||||
-- 得到选中项的名字
|
||||
if container_list[k]._selected == 1 then
|
||||
container_selected[#container_selected + 1] = container_list[k]["_id"]
|
||||
end
|
||||
end
|
||||
if #container_selected > 0 then
|
||||
local success = true
|
||||
|
||||
docker:clear_status()
|
||||
for _, cont in ipairs(container_selected) do
|
||||
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
|
||||
local res = dk.containers[cmd](dk, {id = cont})
|
||||
if res and res.code and res.code >= 300 then
|
||||
success = false
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_new")
|
||||
o.inputtitle = translate("Add")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Start")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"start")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"restart")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_stop")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Stop")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"stop")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_kill")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Kill")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"kill")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Remove")
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m, "remove")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
return m
|
284
luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua
Normal file
|
@ -0,0 +1,284 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
local containers, images, res, lost_state
|
||||
local m, s, o
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.images:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
images = res.body
|
||||
end
|
||||
|
||||
res = dk.containers:list({ query = { all = true } })
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
function get_images()
|
||||
local data = {}
|
||||
|
||||
for i, v in ipairs(images) do
|
||||
local index = v.Created .. v.Id
|
||||
|
||||
data[index]={}
|
||||
data[index]["_selected"] = 0
|
||||
data[index]["id"] = v.Id:sub(8)
|
||||
data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
|
||||
|
||||
if v.RepoTags and next(v.RepoTags)~=nil then
|
||||
for i, v1 in ipairs(v.RepoTags) do
|
||||
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
|
||||
|
||||
if not data[index]["tag"] then
|
||||
data[index]["tag"] = v1
|
||||
end
|
||||
end
|
||||
else
|
||||
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
|
||||
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
|
||||
end
|
||||
|
||||
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","<none>")
|
||||
for ci,cv in ipairs(containers) do
|
||||
if v.Id == cv.ImageID then
|
||||
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
|
||||
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>"
|
||||
end
|
||||
end
|
||||
|
||||
data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB"
|
||||
data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created)
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local image_list = not lost_state and get_images() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Images"),
|
||||
translate("On this page all images are displayed that are available on the system and with which a container can be created."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
local pull_value={
|
||||
_image_tag_name="",
|
||||
_registry="index.docker.io"
|
||||
}
|
||||
|
||||
s = m:section(SimpleSection,
|
||||
translate("Pull Image"),
|
||||
translate("By entering a valid image name with the corresponding version, the docker image can be downloaded from the configured registry."))
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Value, "_image_tag_name")
|
||||
o.template = "dockerman/cbi/inlinevalue"
|
||||
o.placeholder="lisaac/luci:latest"
|
||||
o.write = function(self, section, value)
|
||||
local hastag = value:find(":")
|
||||
|
||||
if not hastag then
|
||||
value = value .. ":latest"
|
||||
end
|
||||
pull_value["_image_tag_name"] = value
|
||||
end
|
||||
|
||||
o = s:option(Button, "_pull")
|
||||
o.inputtitle= translate("Pull")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "add"
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local tag = pull_value["_image_tag_name"]
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
|
||||
if tag and tag ~= "" then
|
||||
docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
|
||||
local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
|
||||
|
||||
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
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
|
||||
end
|
||||
else
|
||||
docker:append_status("code: 400 please input the name of image name!")
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection,
|
||||
translate("Import Image"),
|
||||
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."))
|
||||
|
||||
o = s:option(DummyValue, "_image_import")
|
||||
o.template = "dockerman/images_import"
|
||||
o.disable = lost_state
|
||||
|
||||
s = m:section(Table, image_list, translate("Images overview"))
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.write = function(self, section, value)
|
||||
image_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_id", translate("ID"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_tags", translate("RepoTags"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_containers", translate("Containers"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_size", translate("Size"))
|
||||
|
||||
o = s:option(DummyValue, "_created", translate("Created"))
|
||||
|
||||
local remove_action = function(force)
|
||||
local image_selected = {}
|
||||
|
||||
for k in pairs(image_list) do
|
||||
if image_list[k]._selected == 1 then
|
||||
image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("<br>") or image_list[k]["_tags"]:match("<none>")) and image_list[k].id or image_list[k].tag
|
||||
end
|
||||
end
|
||||
|
||||
if next(image_selected) ~= nil then
|
||||
local success = true
|
||||
|
||||
docker:clear_status()
|
||||
for _, img in ipairs(image_selected) do
|
||||
local query
|
||||
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
|
||||
|
||||
if force then
|
||||
query = {force = true}
|
||||
end
|
||||
|
||||
local msg = dk.images:remove({
|
||||
id = img,
|
||||
query = query
|
||||
})
|
||||
if msg and msg.code ~= 200 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err = docker:read_status()
|
||||
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
remove_action()
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "forceremove")
|
||||
o.inputtitle= translate("Force Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
remove_action(true)
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "save")
|
||||
o.inputtitle= translate("Save")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "edit"
|
||||
o.disable = lost_state
|
||||
o.forcewrite = true
|
||||
o.write = function (self, section)
|
||||
local image_selected = {}
|
||||
|
||||
for k in pairs(image_list) do
|
||||
if image_list[k]._selected == 1 then
|
||||
image_selected[#image_selected + 1] = image_list[k].id
|
||||
end
|
||||
end
|
||||
|
||||
if next(image_selected) ~= nil then
|
||||
local names, first, show_name
|
||||
|
||||
for _, img in ipairs(image_selected) do
|
||||
names = names and (names .. "&names=".. img) or img
|
||||
end
|
||||
if #image_selected > 1 then
|
||||
show_name = "images"
|
||||
else
|
||||
show_name = image_selected[1]
|
||||
end
|
||||
local cb = function(res, chunk)
|
||||
if res and res.code and res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. show_name .. '.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
|
||||
local msg = dk.images:get({query = {names = names}}, cb)
|
||||
if msg and msg.code and msg.code ~= 200 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Button, "load")
|
||||
o.inputtitle= translate("Load")
|
||||
o.template = "dockerman/images_load"
|
||||
o.inputstyle = "add"
|
||||
o.disable = lost_state
|
||||
|
||||
return m
|
159
luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua
Normal file
|
@ -0,0 +1,159 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
local networks, dk, res, lost_state
|
||||
|
||||
dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.networks:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
networks = res.body
|
||||
end
|
||||
end
|
||||
|
||||
local get_networks = function ()
|
||||
local data = {}
|
||||
|
||||
if type(networks) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i, v in ipairs(networks) do
|
||||
local index = v.Created .. v.Id
|
||||
|
||||
data[index]={}
|
||||
data[index]["_selected"] = 0
|
||||
data[index]["_id"] = v.Id:sub(1,12)
|
||||
data[index]["_name"] = v.Name
|
||||
data[index]["_driver"] = v.Driver
|
||||
|
||||
if v.Driver == "bridge" then
|
||||
data[index]["_interface"] = v.Options["com.docker.network.bridge.name"]
|
||||
elseif v.Driver == "macvlan" then
|
||||
data[index]["_interface"] = v.Options.parent
|
||||
end
|
||||
|
||||
data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
|
||||
data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local network_list = not lost_state and get_networks() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Networks"),
|
||||
translate("This page displays all docker networks that have been created on the connected docker host."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
s = m:section(Table, network_list, translate("Networks overview"))
|
||||
s.nodescr=true
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.template = "dockerman/cbi/xfvalue"
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.render = function(self, section, scope)
|
||||
self.disable = 0
|
||||
if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then
|
||||
self.disable = 1
|
||||
end
|
||||
Flag.render(self, section, scope)
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
network_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_id", translate("ID"))
|
||||
|
||||
o = s:option(DummyValue, "_name", translate("Network Name"))
|
||||
|
||||
o = s:option(DummyValue, "_driver", translate("Driver"))
|
||||
|
||||
o = s:option(DummyValue, "_interface", translate("Parent Interface"))
|
||||
|
||||
o = s:option(DummyValue, "_subnet", translate("Subnet"))
|
||||
|
||||
o = s:option(DummyValue, "_gateway", translate("Gateway"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err = docker:read_status()
|
||||
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_new")
|
||||
o.inputtitle= translate("New")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.notitle=true
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local network_selected = {}
|
||||
local network_name_selected = {}
|
||||
local network_driver_selected = {}
|
||||
|
||||
for k in pairs(network_list) do
|
||||
if network_list[k]._selected == 1 then
|
||||
network_selected[#network_selected + 1] = network_list[k]._id
|
||||
network_name_selected[#network_name_selected + 1] = network_list[k]._name
|
||||
network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver
|
||||
end
|
||||
end
|
||||
|
||||
if next(network_selected) ~= nil then
|
||||
local success = true
|
||||
docker:clear_status()
|
||||
|
||||
for ii, net in ipairs(network_selected) do
|
||||
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
|
||||
local res = dk.networks["remove"](dk, {id = net})
|
||||
|
||||
if res and res.code and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
if network_driver_selected[ii] == "macvlan" then
|
||||
docker.remove_macvlan_interface(network_name_selected[ii])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
923
luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua
Normal file
|
@ -0,0 +1,923 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
|
||||
local dk = docker.new()
|
||||
|
||||
local cmd_line = table.concat(arg, '/')
|
||||
local images, networks
|
||||
local create_body = {}
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
images = {}
|
||||
networks = {}
|
||||
else
|
||||
images = dk.images:list().body
|
||||
networks = dk.networks:list().body
|
||||
end
|
||||
|
||||
local is_quot_complete = function(str)
|
||||
local num = 0, w
|
||||
require "math"
|
||||
|
||||
if not str then
|
||||
return true
|
||||
end
|
||||
|
||||
local num = 0, w
|
||||
for w in str:gmatch("\"") do
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
if math.fmod(num, 2) ~= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
num = 0
|
||||
for w in str:gmatch("\'") do
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
if math.fmod(num, 2) ~= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function contains(list, x)
|
||||
for _, v in pairs(list) do
|
||||
if v == x then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local resolve_cli = function(cmd_line)
|
||||
local config = {
|
||||
advance = 1
|
||||
}
|
||||
|
||||
local key_no_val = {
|
||||
't',
|
||||
'd',
|
||||
'i',
|
||||
'tty',
|
||||
'rm',
|
||||
'read_only',
|
||||
'interactive',
|
||||
'init',
|
||||
'help',
|
||||
'detach',
|
||||
'privileged',
|
||||
'P',
|
||||
'publish_all',
|
||||
}
|
||||
|
||||
local key_with_val = {
|
||||
'sysctl',
|
||||
'add_host',
|
||||
'a',
|
||||
'attach',
|
||||
'blkio_weight_device',
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'device',
|
||||
'device_cgroup_rule',
|
||||
'device_read_bps',
|
||||
'device_read_iops',
|
||||
'device_write_bps',
|
||||
'device_write_iops',
|
||||
'dns',
|
||||
'dns_option',
|
||||
'dns_search',
|
||||
'e',
|
||||
'env',
|
||||
'env_file',
|
||||
'expose',
|
||||
'group_add',
|
||||
'l',
|
||||
'label',
|
||||
'label_file',
|
||||
'link',
|
||||
'link_local_ip',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'network_alias',
|
||||
'p',
|
||||
'publish',
|
||||
'security_opt',
|
||||
'storage_opt',
|
||||
'tmpfs',
|
||||
'v',
|
||||
'volume',
|
||||
'volumes_from',
|
||||
'blkio_weight',
|
||||
'cgroup_parent',
|
||||
'cidfile',
|
||||
'cpu_period',
|
||||
'cpu_quota',
|
||||
'cpu_rt_period',
|
||||
'cpu_rt_runtime',
|
||||
'c',
|
||||
'cpu_shares',
|
||||
'cpus',
|
||||
'cpuset_cpus',
|
||||
'cpuset_mems',
|
||||
'detach_keys',
|
||||
'disable_content_trust',
|
||||
'domainname',
|
||||
'entrypoint',
|
||||
'gpus',
|
||||
'health_cmd',
|
||||
'health_interval',
|
||||
'health_retries',
|
||||
'health_start_period',
|
||||
'health_timeout',
|
||||
'h',
|
||||
'hostname',
|
||||
'ip',
|
||||
'ip6',
|
||||
'ipc',
|
||||
'isolation',
|
||||
'kernel_memory',
|
||||
'mac_address',
|
||||
'm',
|
||||
'memory',
|
||||
'memory_reservation',
|
||||
'memory_swap',
|
||||
'memory_swappiness',
|
||||
'mount',
|
||||
'name',
|
||||
'network',
|
||||
'no_healthcheck',
|
||||
'oom_kill_disable',
|
||||
'oom_score_adj',
|
||||
'pid',
|
||||
'pids_limit',
|
||||
'restart',
|
||||
'runtime',
|
||||
'shm_size',
|
||||
'sig_proxy',
|
||||
'stop_signal',
|
||||
'stop_timeout',
|
||||
'ulimit',
|
||||
'u',
|
||||
'user',
|
||||
'userns',
|
||||
'uts',
|
||||
'volume_driver',
|
||||
'w',
|
||||
'workdir'
|
||||
}
|
||||
|
||||
local key_abb = {
|
||||
net='network',
|
||||
a='attach',
|
||||
c='cpu-shares',
|
||||
d='detach',
|
||||
e='env',
|
||||
h='hostname',
|
||||
i='interactive',
|
||||
l='label',
|
||||
m='memory',
|
||||
p='publish',
|
||||
P='publish_all',
|
||||
t='tty',
|
||||
u='user',
|
||||
v='volume',
|
||||
w='workdir'
|
||||
}
|
||||
|
||||
local key_with_list = {
|
||||
'sysctl',
|
||||
'add_host',
|
||||
'a',
|
||||
'attach',
|
||||
'blkio_weight_device',
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'device',
|
||||
'device_cgroup_rule',
|
||||
'device_read_bps',
|
||||
'device_read_iops',
|
||||
'device_write_bps',
|
||||
'device_write_iops',
|
||||
'dns',
|
||||
'dns_optiondns_search',
|
||||
'e',
|
||||
'env',
|
||||
'env_file',
|
||||
'expose',
|
||||
'group_add',
|
||||
'l',
|
||||
'label',
|
||||
'label_file',
|
||||
'link',
|
||||
'link_local_ip',
|
||||
'log_opt',
|
||||
'network_alias',
|
||||
'p',
|
||||
'publish',
|
||||
'security_opt',
|
||||
'storage_opt',
|
||||
'tmpfs',
|
||||
'v',
|
||||
'volume',
|
||||
'volumes_from',
|
||||
}
|
||||
|
||||
local key = nil
|
||||
local _key = nil
|
||||
local val = nil
|
||||
local is_cmd = false
|
||||
|
||||
cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
|
||||
for w in cmd_line:gmatch("[^%s]+") do
|
||||
if w =='\\' then
|
||||
elseif not key and not _key and not is_cmd then
|
||||
--key=val
|
||||
key, val = w:match("^%-%-([%lP%-]-)=(.+)")
|
||||
if not key then
|
||||
--key val
|
||||
key = w:match("^%-%-([%lP%-]+)")
|
||||
if not key then
|
||||
-- -v val
|
||||
key = w:match("^%-([%lP%-]+)")
|
||||
if key then
|
||||
-- for -dit
|
||||
if key:match("i") or key:match("t") or key:match("d") then
|
||||
if key:match("i") then
|
||||
config[key_abb["i"]] = true
|
||||
key:gsub("i", "")
|
||||
end
|
||||
if key:match("t") then
|
||||
config[key_abb["t"]] = true
|
||||
key:gsub("t", "")
|
||||
end
|
||||
if key:match("d") then
|
||||
config[key_abb["d"]] = true
|
||||
key:gsub("d", "")
|
||||
end
|
||||
if key:match("P") then
|
||||
config[key_abb["P"]] = true
|
||||
key:gsub("P", "")
|
||||
end
|
||||
if key == "" then
|
||||
key = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if key then
|
||||
key = key:gsub("-","_")
|
||||
key = key_abb[key] or key
|
||||
if contains(key_no_val, key) then
|
||||
config[key] = true
|
||||
val = nil
|
||||
key = nil
|
||||
elseif contains(key_with_val, key) then
|
||||
-- if key == "cap_add" then config.privileged = true end
|
||||
else
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
else
|
||||
config.image = w
|
||||
key = nil
|
||||
val = nil
|
||||
is_cmd = true
|
||||
end
|
||||
elseif (key or _key) and not is_cmd then
|
||||
if key == "mount" then
|
||||
-- we need resolve mount options here
|
||||
-- type=bind,source=/source,target=/app
|
||||
local _type = w:match("^type=([^,]+),") or "bind"
|
||||
local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
|
||||
local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
|
||||
local ro = w:match(",readonly") and "ro" or nil
|
||||
|
||||
if source and target then
|
||||
if _type ~= "tmpfs" then
|
||||
local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
|
||||
val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
|
||||
else
|
||||
local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
|
||||
local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
|
||||
key = "tmpfs"
|
||||
val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
|
||||
if not config[key] then
|
||||
config[key] = {}
|
||||
end
|
||||
table.insert( config[key], val )
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
val = w
|
||||
end
|
||||
elseif is_cmd then
|
||||
config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w
|
||||
end
|
||||
if (key or _key) and val then
|
||||
key = _key or key
|
||||
if contains(key_with_list, key) then
|
||||
if not config[key] then
|
||||
config[key] = {}
|
||||
end
|
||||
if _key then
|
||||
config[key][#config[key]] = config[key][#config[key]] .. " " .. w
|
||||
else
|
||||
table.insert( config[key], val )
|
||||
end
|
||||
if is_quot_complete(config[key][#config[key]]) then
|
||||
config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
|
||||
_key = nil
|
||||
else
|
||||
_key = key
|
||||
end
|
||||
else
|
||||
config[key] = (config[key] and (config[key] .. " ") or "") .. val
|
||||
if is_quot_complete(config[key]) then
|
||||
config[key] = config[key]:gsub("[\"\']", "")
|
||||
_key = nil
|
||||
else
|
||||
_key = key
|
||||
end
|
||||
end
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
local default_config = {}
|
||||
|
||||
if cmd_line and cmd_line:match("^DOCKERCLI.+") then
|
||||
default_config = resolve_cli(cmd_line)
|
||||
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
|
||||
local container_id = cmd_line:match("^duplicate/(.+)")
|
||||
create_body = dk:containers_duplicate_config({id = container_id}) or {}
|
||||
if not create_body.HostConfig then
|
||||
create_body.HostConfig = {}
|
||||
end
|
||||
|
||||
if next(create_body) ~= nil then
|
||||
default_config.name = nil
|
||||
default_config.image = create_body.Image
|
||||
default_config.hostname = create_body.Hostname
|
||||
default_config.tty = create_body.Tty and true or false
|
||||
default_config.interactive = create_body.OpenStdin and true or false
|
||||
default_config.privileged = create_body.HostConfig.Privileged and true or false
|
||||
default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
|
||||
-- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
|
||||
-- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
|
||||
default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
|
||||
default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
|
||||
default_config.link = create_body.HostConfig.Links
|
||||
default_config.env = create_body.Env
|
||||
default_config.dns = create_body.HostConfig.Dns
|
||||
default_config.volume = create_body.HostConfig.Binds
|
||||
default_config.cap_add = create_body.HostConfig.CapAdd
|
||||
default_config.publish_all = create_body.HostConfig.PublishAllPorts
|
||||
|
||||
if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
|
||||
default_config.sysctl = {}
|
||||
for k, v in pairs(create_body.HostConfig.Sysctls) do
|
||||
table.insert( default_config.sysctl, k.."="..v )
|
||||
end
|
||||
end
|
||||
if create_body.HostConfig.LogConfig then
|
||||
if create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
|
||||
default_config.log_opt = {}
|
||||
for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
|
||||
table.insert( default_config.log_opt, k.."="..v )
|
||||
end
|
||||
end
|
||||
default_config.log_driver = create_body.HostConfig.LogConfig.Type or nil
|
||||
end
|
||||
|
||||
if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
|
||||
default_config.publish = {}
|
||||
for k, v in pairs(create_body.HostConfig.PortBindings) do
|
||||
for x, y in ipairs(v) do
|
||||
table.insert( default_config.publish, y.HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
default_config.user = create_body.User or nil
|
||||
default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
|
||||
default_config.advance = 1
|
||||
default_config.cpus = create_body.HostConfig.NanoCPUs
|
||||
default_config.cpu_shares = create_body.HostConfig.CpuShares
|
||||
default_config.memory = create_body.HostConfig.Memory
|
||||
default_config.blkio_weight = create_body.HostConfig.BlkioWeight
|
||||
|
||||
if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
|
||||
default_config.device = {}
|
||||
for _, v in ipairs(create_body.HostConfig.Devices) do
|
||||
table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
|
||||
end
|
||||
end
|
||||
|
||||
if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
|
||||
default_config.tmpfs = {}
|
||||
for k, v in pairs(create_body.HostConfig.Tmpfs) do
|
||||
table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Containers"))
|
||||
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
|
||||
if lost_state then
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection, translate("Create new docker container"))
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
|
||||
o.rawhtml = true
|
||||
o.template = "dockerman/newcontainer_resolve"
|
||||
|
||||
o = s:option(Value, "name", translate("Container Name"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.name or nil
|
||||
|
||||
o = s:option(Flag, "interactive", translate("Interactive (-i)"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.interactive and 1 or 0
|
||||
|
||||
o = s:option(Flag, "tty", translate("TTY (-t)"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.tty and 1 or 0
|
||||
|
||||
o = s:option(Value, "image", translate("Docker Image"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.image or nil
|
||||
for _, v in ipairs (images) do
|
||||
if v.RepoTags then
|
||||
o:value(v.RepoTags[1], v.RepoTags[1])
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "_force_pull", translate("Always pull image first"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, "privileged", translate("Privileged"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.privileged and 1 or 0
|
||||
|
||||
o = s:option(ListValue, "restart", translate("Restart Policy"))
|
||||
o.rmempty = true
|
||||
o:value("no", "No")
|
||||
o:value("unless-stopped", "Unless stopped")
|
||||
o:value("always", "Always")
|
||||
o:value("on-failure", "On failure")
|
||||
o.default = default_config.restart or "unless-stopped"
|
||||
|
||||
local d_network = s:option(ListValue, "network", translate("Networks"))
|
||||
d_network.rmempty = true
|
||||
d_network.default = default_config.network or "bridge"
|
||||
|
||||
local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
|
||||
d_ip.datatype="ip4addr"
|
||||
d_ip:depends("network", "nil")
|
||||
d_ip.default = default_config.ip or nil
|
||||
|
||||
o = s:option(DynamicList, "link", translate("Links with other containers"))
|
||||
o.placeholder = "container_name:alias"
|
||||
o.rmempty = true
|
||||
o:depends("network", "bridge")
|
||||
o.default = default_config.link or nil
|
||||
|
||||
o = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
|
||||
o.placeholder = "8.8.8.8"
|
||||
o.rmempty = true
|
||||
o.default = default_config.dns or nil
|
||||
|
||||
o = s:option(Value, "user",
|
||||
translate("User(-u)"),
|
||||
translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
|
||||
o.placeholder = "1000:1000"
|
||||
o.rmempty = true
|
||||
o.default = default_config.user or nil
|
||||
|
||||
o = s:option(DynamicList, "env",
|
||||
translate("Environmental Variable(-e)"),
|
||||
translate("Set environment variables to inside the container"))
|
||||
o.placeholder = "TZ=Asia/Shanghai"
|
||||
o.rmempty = true
|
||||
o.default = default_config.env or nil
|
||||
|
||||
o = s:option(DynamicList, "volume",
|
||||
translate("Bind Mount(-v)"),
|
||||
translate("Bind mount a volume"))
|
||||
o.placeholder = "/media:/media:slave"
|
||||
o.rmempty = true
|
||||
o.default = default_config.volume or nil
|
||||
|
||||
local d_publish = s:option(DynamicList, "publish",
|
||||
translate("Exposed Ports(-p)"),
|
||||
translate("Publish container's port(s) to the host"))
|
||||
d_publish.placeholder = "2200:22/tcp"
|
||||
d_publish.rmempty = true
|
||||
d_publish.default = default_config.publish or nil
|
||||
|
||||
o = s:option(Value, "command", translate("Run command"))
|
||||
o.placeholder = "/bin/sh init.sh"
|
||||
o.rmempty = true
|
||||
o.default = default_config.command or nil
|
||||
|
||||
o = s:option(Flag, "advance", translate("Advance"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.advance or 0
|
||||
|
||||
o = s:option(Value, "hostname",
|
||||
translate("Host Name"),
|
||||
translate("The hostname to use for the container"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.hostname or nil
|
||||
o:depends("advance", 1)
|
||||
|
||||
o = s:option(Flag, "publish_all",
|
||||
translate("Exposed All Ports(-P)"),
|
||||
translate("Allocates an ephemeral host port for all of a container's exposed ports"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.publish_all and 1 or 0
|
||||
o:depends("advance", 1)
|
||||
|
||||
o = s:option(DynamicList, "device",
|
||||
translate("Device(--device)"),
|
||||
translate("Add host device to the container"))
|
||||
o.placeholder = "/dev/sda:/dev/xvdc:rwm"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.device or nil
|
||||
|
||||
o = s:option(DynamicList, "tmpfs",
|
||||
translate("Tmpfs(--tmpfs)"),
|
||||
translate("Mount tmpfs directory"))
|
||||
o.placeholder = "/run:rw,noexec,nosuid,size=65536k"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.tmpfs or nil
|
||||
|
||||
o = s:option(DynamicList, "sysctl",
|
||||
translate("Sysctl(--sysctl)"),
|
||||
translate("Sysctls (kernel parameters) options"))
|
||||
o.placeholder = "net.ipv4.ip_forward=1"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.sysctl or nil
|
||||
|
||||
o = s:option(DynamicList, "cap_add",
|
||||
translate("CAP-ADD(--cap-add)"),
|
||||
translate("A list of kernel capabilities to add to the container"))
|
||||
o.placeholder = "NET_ADMIN"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.cap_add or nil
|
||||
|
||||
o = s:option(Value, "cpus",
|
||||
translate("CPUs"),
|
||||
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
|
||||
o.placeholder = "1.5"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="ufloat"
|
||||
o.default = default_config.cpus or nil
|
||||
|
||||
o = s:option(Value, "cpu_shares",
|
||||
translate("CPU Shares Weight"),
|
||||
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
|
||||
o.placeholder = "1024"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="uinteger"
|
||||
o.default = default_config.cpu_shares or nil
|
||||
|
||||
o = s:option(Value, "memory",
|
||||
translate("Memory"),
|
||||
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
|
||||
o.placeholder = "128m"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.memory or nil
|
||||
|
||||
o = s:option(Value, "blkio_weight",
|
||||
translate("Block IO Weight"),
|
||||
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
|
||||
o.placeholder = "500"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="uinteger"
|
||||
o.default = default_config.blkio_weight or nil
|
||||
|
||||
o = s:option(Value, "log_driver",
|
||||
translate("Logging driver"),
|
||||
translate("The logging driver for the container"))
|
||||
o.placeholder = "json-file"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.log_driver or nil
|
||||
|
||||
o = s:option(DynamicList, "log_opt",
|
||||
translate("Log driver options"),
|
||||
translate("The logging configuration for this container"))
|
||||
o.placeholder = "max-size=1m"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.log_opt or nil
|
||||
|
||||
for _, v in ipairs (networks) do
|
||||
if v.Name then
|
||||
local parent = v.Options and v.Options.parent or nil
|
||||
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
|
||||
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
|
||||
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
|
||||
d_network:value(v.Name, network_name)
|
||||
|
||||
if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
|
||||
d_ip:depends("network", v.Name)
|
||||
end
|
||||
|
||||
if v.Driver == "bridge" then
|
||||
d_publish:depends("network", v.Name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m.handle = function(self, state, data)
|
||||
if state ~= FORM_VALID then
|
||||
return
|
||||
end
|
||||
|
||||
local tmp
|
||||
local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
|
||||
local hostname = data.hostname
|
||||
local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false
|
||||
local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false
|
||||
local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
|
||||
local image = data.image
|
||||
local user = data.user
|
||||
|
||||
if image and not image:match(".-:.+") then
|
||||
image = image .. ":latest"
|
||||
end
|
||||
|
||||
local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
|
||||
local restart = data.restart
|
||||
local env = data.env
|
||||
local dns = data.dns
|
||||
local cap_add = data.cap_add
|
||||
local sysctl = {}
|
||||
local log_driver = data.log_driver
|
||||
|
||||
tmp = data.sysctl
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp) do
|
||||
local k,v1 = v:match("(.-)=(.+)")
|
||||
if k and v1 then
|
||||
sysctl[k]=v1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local log_opt = {}
|
||||
tmp = data.log_opt
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp) do
|
||||
local k,v1 = v:match("(.-)=(.+)")
|
||||
if k and v1 then
|
||||
log_opt[k]=v1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local network = data.network
|
||||
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
|
||||
local volume = data.volume
|
||||
local memory = data.memory or nil
|
||||
local cpu_shares = data.cpu_shares or nil
|
||||
local cpus = data.cpus or nil
|
||||
local blkio_weight = data.blkio_weight or nil
|
||||
|
||||
local portbindings = {}
|
||||
local exposedports = {}
|
||||
|
||||
local tmpfs = {}
|
||||
tmp = data.tmpfs
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp)do
|
||||
local k= v:match("([^:]+)")
|
||||
local v1 = v:match(".-:([^:]+)") or ""
|
||||
if k then
|
||||
tmpfs[k]=v1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local device = {}
|
||||
tmp = data.device
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp) do
|
||||
local t = {}
|
||||
local _,_, h, c, p = v:find("(.-):(.-):(.+)")
|
||||
if h and c then
|
||||
t['PathOnHost'] = h
|
||||
t['PathInContainer'] = c
|
||||
t['CgroupPermissions'] = p or "rwm"
|
||||
else
|
||||
local _,_, h, c = v:find("(.-):(.+)")
|
||||
if h and c then
|
||||
t['PathOnHost'] = h
|
||||
t['PathInContainer'] = c
|
||||
t['CgroupPermissions'] = "rwm"
|
||||
else
|
||||
t['PathOnHost'] = v
|
||||
t['PathInContainer'] = v
|
||||
t['CgroupPermissions'] = "rwm"
|
||||
end
|
||||
end
|
||||
|
||||
if next(t) ~= nil then
|
||||
table.insert( device, t )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tmp = data.publish or {}
|
||||
for i, v in ipairs(tmp) do
|
||||
for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
|
||||
local _,_,p= v2:find("^%d+/(%w+)")
|
||||
if p == nil then
|
||||
v2=v2..'/tcp'
|
||||
end
|
||||
portbindings[v2] = {{HostPort=v1}}
|
||||
exposedports[v2] = {HostPort=v1}
|
||||
end
|
||||
end
|
||||
|
||||
local link = data.link
|
||||
tmp = data.command
|
||||
local command = {}
|
||||
if tmp ~= nil then
|
||||
for v in string.gmatch(tmp, "[^%s]+") do
|
||||
command[#command+1] = v
|
||||
end
|
||||
end
|
||||
|
||||
if memory and memory ~= 0 then
|
||||
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
|
||||
if n then
|
||||
unit = unit and unit:sub(1,1):upper() or "B"
|
||||
if unit == "M" then
|
||||
memory = tonumber(n) * 1024 * 1024
|
||||
elseif unit == "G" then
|
||||
memory = tonumber(n) * 1024 * 1024 * 1024
|
||||
elseif unit == "K" then
|
||||
memory = tonumber(n) * 1024
|
||||
else
|
||||
memory = tonumber(n)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
create_body.Hostname = network ~= "host" and (hostname or name) or nil
|
||||
create_body.Tty = tty and true or false
|
||||
create_body.OpenStdin = interactive and true or false
|
||||
create_body.User = user
|
||||
create_body.Cmd = command
|
||||
create_body.Env = env
|
||||
create_body.Image = image
|
||||
create_body.ExposedPorts = exposedports
|
||||
create_body.HostConfig = create_body.HostConfig or {}
|
||||
create_body.HostConfig.Dns = dns
|
||||
create_body.HostConfig.Binds = volume
|
||||
create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
|
||||
create_body.HostConfig.Privileged = privileged and true or false
|
||||
create_body.HostConfig.PortBindings = portbindings
|
||||
create_body.HostConfig.Memory = memory and tonumber(memory)
|
||||
create_body.HostConfig.CpuShares = cpu_shares and tonumber(cpu_shares)
|
||||
create_body.HostConfig.NanoCPUs = cpus and tonumber(cpus) * 10 ^ 9
|
||||
create_body.HostConfig.BlkioWeight = blkio_weight and tonumber(blkio_weight)
|
||||
create_body.HostConfig.PublishAllPorts = publish_all
|
||||
|
||||
if create_body.HostConfig.NetworkMode ~= network then
|
||||
create_body.NetworkingConfig = nil
|
||||
end
|
||||
|
||||
create_body.HostConfig.NetworkMode = network
|
||||
|
||||
if ip then
|
||||
if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
|
||||
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
|
||||
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
|
||||
v.IPAMConfig.IPv4Address = ip
|
||||
else
|
||||
create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
|
||||
end
|
||||
break
|
||||
end
|
||||
else
|
||||
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
|
||||
end
|
||||
elseif not create_body.NetworkingConfig then
|
||||
create_body.NetworkingConfig = nil
|
||||
end
|
||||
|
||||
create_body["HostConfig"]["Tmpfs"] = tmpfs
|
||||
create_body["HostConfig"]["Devices"] = device
|
||||
create_body["HostConfig"]["Sysctls"] = sysctl
|
||||
create_body["HostConfig"]["CapAdd"] = cap_add
|
||||
create_body["HostConfig"]["LogConfig"] = {
|
||||
Config = log_opt,
|
||||
Type = log_driver
|
||||
}
|
||||
|
||||
if network == "bridge" then
|
||||
create_body["HostConfig"]["Links"] = link
|
||||
end
|
||||
|
||||
local pull_image = function(image)
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
|
||||
local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
|
||||
if res and res.code and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
|
||||
docker:append_status("done\n")
|
||||
else
|
||||
res.code = (res.code == 200) and 500 or res.code
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
|
||||
end
|
||||
end
|
||||
|
||||
docker:clear_status()
|
||||
local exist_image = false
|
||||
|
||||
if image then
|
||||
for _, v in ipairs (images) do
|
||||
if v.RepoTags and v.RepoTags[1] == image then
|
||||
exist_image = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not exist_image then
|
||||
pull_image(image)
|
||||
elseif data._force_pull == 1 then
|
||||
pull_image(image)
|
||||
end
|
||||
end
|
||||
|
||||
create_body = docker.clear_empty_tables(create_body)
|
||||
|
||||
docker:append_status("Container: " .. "create" .. " " .. name .. "...")
|
||||
local res = dk.containers:create({name = name, body = create_body})
|
||||
if res and res.code and res.code == 201 then
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
258
luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua
Normal file
|
@ -0,0 +1,258 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
|
||||
local dk = docker.new()
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
end
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Network"))
|
||||
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
|
||||
if lost_state then
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
end
|
||||
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection, translate("Create new docker network"))
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(Value, "name",
|
||||
translate("Network Name"),
|
||||
translate("Name of the network that can be selected during container creation"))
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(ListValue, "driver", translate("Driver"))
|
||||
o.rmempty = true
|
||||
o:value("bridge", translate("Bridge device"))
|
||||
o:value("macvlan", translate("MAC VLAN"))
|
||||
o:value("ipvlan", translate("IP VLAN"))
|
||||
o:value("overlay", translate("Overlay network"))
|
||||
|
||||
o = s:option(Value, "parent", translate("Base device"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "macvlan")
|
||||
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
|
||||
for _, v in ipairs(interfaces) do
|
||||
o:value(v, v)
|
||||
end
|
||||
o.default="br-lan"
|
||||
o.placeholder="br-lan"
|
||||
|
||||
o = s:option(ListValue, "macvlan_mode", translate("Mode"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "macvlan")
|
||||
o.default="bridge"
|
||||
o:value("bridge", translate("Bridge (Support direct communication between MAC VLANs)"))
|
||||
o:value("private", translate("Private (Prevent communication between MAC VLANs)"))
|
||||
o:value("vepa", translate("VEPA (Virtual Ethernet Port Aggregator)"))
|
||||
o:value("passthru", translate("Pass-through (Mirror physical device to single MAC VLAN)"))
|
||||
|
||||
o = s:option(ListValue, "ipvlan_mode", translate("Ipvlan Mode"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "ipvlan")
|
||||
o.default="l3"
|
||||
o:value("l2", translate("L2 bridge"))
|
||||
o:value("l3", translate("L3 bridge"))
|
||||
|
||||
o = s:option(Flag, "ingress",
|
||||
translate("Ingress"),
|
||||
translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o:depends("driver", "overlay")
|
||||
|
||||
o = s:option(DynamicList, "options", translate("Options"))
|
||||
o.rmempty = true
|
||||
o.placeholder="com.docker.network.driver.mtu=1500"
|
||||
|
||||
o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "overlay")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
|
||||
o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
|
||||
o:depends("driver", "macvlan")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 1
|
||||
end
|
||||
|
||||
o = s:option(Value, "subnet", translate("Subnet"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.0.0/16"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(Value, "gateway", translate("Gateway"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.1.1"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(Value, "ip_range", translate("IP range"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.1.0/24"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
|
||||
o.rmempty = true
|
||||
o.placeholder="my-route=10.1.1.1"
|
||||
|
||||
o = s:option(Flag, "ipv6", translate("Enable IPv6"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Value, "subnet6", translate("IPv6 Subnet"))
|
||||
o.rmempty = true
|
||||
o.placeholder="fe80::/10"
|
||||
o.datatype="ip6addr"
|
||||
o:depends("ipv6", 1)
|
||||
|
||||
o = s:option(Value, "gateway6", translate("IPv6 Gateway"))
|
||||
o.rmempty = true
|
||||
o.placeholder="fe80::1"
|
||||
o.datatype="ip6addr"
|
||||
o:depends("ipv6", 1)
|
||||
|
||||
m.handle = function(self, state, data)
|
||||
if state == FORM_VALID then
|
||||
local name = data.name
|
||||
local driver = data.driver
|
||||
|
||||
local internal = data.internal == 1 and true or false
|
||||
|
||||
local subnet = data.subnet
|
||||
local gateway = data.gateway
|
||||
local ip_range = data.ip_range
|
||||
|
||||
local aux_address = {}
|
||||
local tmp = data.aux_address or {}
|
||||
for i,v in ipairs(tmp) do
|
||||
_,_,k1,v1 = v:find("(.-)=(.+)")
|
||||
aux_address[k1] = v1
|
||||
end
|
||||
|
||||
local options = {}
|
||||
tmp = data.options or {}
|
||||
for i,v in ipairs(tmp) do
|
||||
_,_,k1,v1 = v:find("(.-)=(.+)")
|
||||
options[k1] = v1
|
||||
end
|
||||
|
||||
local ipv6 = data.ipv6 == 1 and true or false
|
||||
|
||||
local create_body = {
|
||||
Name = name,
|
||||
Driver = driver,
|
||||
EnableIPv6 = ipv6,
|
||||
IPAM = {
|
||||
Driver= "default"
|
||||
},
|
||||
Internal = internal
|
||||
}
|
||||
|
||||
if subnet or gateway or ip_range then
|
||||
create_body["IPAM"]["Config"] = {
|
||||
{
|
||||
Subnet = subnet,
|
||||
Gateway = gateway,
|
||||
IPRange = ip_range,
|
||||
AuxAddress = aux_address,
|
||||
AuxiliaryAddresses = aux_address
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
if driver == "macvlan" then
|
||||
create_body["Options"] = {
|
||||
macvlan_mode = data.macvlan_mode,
|
||||
parent = data.parent
|
||||
}
|
||||
elseif driver == "ipvlan" then
|
||||
create_body["Options"] = {
|
||||
ipvlan_mode = data.ipvlan_mode
|
||||
}
|
||||
elseif driver == "overlay" then
|
||||
create_body["Ingress"] = data.ingerss == 1 and true or false
|
||||
end
|
||||
|
||||
if ipv6 and data.subnet6 and data.subnet6 then
|
||||
if type(create_body["IPAM"]["Config"]) ~= "table" then
|
||||
create_body["IPAM"]["Config"] = {}
|
||||
end
|
||||
local index = #create_body["IPAM"]["Config"]
|
||||
create_body["IPAM"]["Config"][index+1] = {
|
||||
Subnet = data.subnet6,
|
||||
Gateway = data.gateway6
|
||||
}
|
||||
end
|
||||
|
||||
if next(options) ~= nil then
|
||||
create_body["Options"] = create_body["Options"] or {}
|
||||
for k, v in pairs(options) do
|
||||
create_body["Options"][k] = v
|
||||
end
|
||||
end
|
||||
|
||||
create_body = docker.clear_empty_tables(create_body)
|
||||
docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
|
||||
|
||||
local res = dk.networks:create({
|
||||
body = create_body
|
||||
})
|
||||
|
||||
if res and res.code == 201 then
|
||||
docker:write_status("Network: " .. "create macvlan interface...")
|
||||
res = dk.networks:inspect({
|
||||
name = create_body.Name
|
||||
})
|
||||
|
||||
if driver == "macvlan" and
|
||||
data.op_macvlan ~= 0 and
|
||||
res and
|
||||
res.code and
|
||||
res.code == 200 and
|
||||
res.body and
|
||||
res.body.IPAM and
|
||||
res.body.IPAM.Config and
|
||||
res.body.IPAM.Config[1] and
|
||||
res.body.IPAM.Config[1].Gateway and
|
||||
res.body.IPAM.Config[1].Subnet then
|
||||
|
||||
docker.create_macvlan_interface(data.name,
|
||||
data.parent,
|
||||
res.body.IPAM.Config[1].Gateway,
|
||||
res.body.IPAM.Config[1].Subnet)
|
||||
end
|
||||
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
151
luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua
Normal file
|
@ -0,0 +1,151 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local m, s, o, lost_state
|
||||
local dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
end
|
||||
|
||||
m = SimpleForm("dockerd",
|
||||
translate("Docker - Overview"),
|
||||
translate("An overview with the relevant data is displayed here with which the LuCI docker client is connected.")
|
||||
..
|
||||
" " ..
|
||||
[[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..
|
||||
translate("Github") ..
|
||||
[[</a>]])
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
local docker_info_table = {}
|
||||
-- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'}
|
||||
-- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'}
|
||||
-- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'}
|
||||
docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'}
|
||||
docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
|
||||
docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
|
||||
docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'}
|
||||
docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'}
|
||||
docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
|
||||
docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template = "cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = lost_state and translate("Start") or translate("Stop")
|
||||
o.inputstyle = lost_state and "add" or "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
docker:clear_status()
|
||||
|
||||
if lost_state then
|
||||
docker:append_status("Docker daemon: starting")
|
||||
luci.util.exec("/etc/init.d/dockerd start")
|
||||
luci.util.exec("sleep 5")
|
||||
luci.util.exec("/etc/init.d/dockerman start")
|
||||
|
||||
else
|
||||
docker:append_status("Docker daemon: stopping")
|
||||
luci.util.exec("/etc/init.d/dockerd stop")
|
||||
end
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
docker:clear_status()
|
||||
docker:append_status("Docker daemon: restarting")
|
||||
luci.util.exec("/etc/init.d/dockerd restart")
|
||||
luci.util.exec("sleep 5")
|
||||
luci.util.exec("/etc/init.d/dockerman start")
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(Table, docker_info_table)
|
||||
s:option(DummyValue, "_key", translate("Info"))
|
||||
s:option(DummyValue, "_value")
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/overview"
|
||||
|
||||
s.containers_running = '-'
|
||||
s.images_used = '-'
|
||||
s.containers_total = '-'
|
||||
s.images_total = '-'
|
||||
s.networks_total = '-'
|
||||
s.volumes_total = '-'
|
||||
|
||||
-- local socket = luci.model.uci.cursor():get("dockerd", "dockerman", "socket_path")
|
||||
if not lost_state then
|
||||
local containers_list = dk.containers:list({query = {all=true}}).body
|
||||
local images_list = dk.images:list().body
|
||||
local vol = dk.volumes:list()
|
||||
local volumes_list = vol and vol.body and vol.body.Volumes or {}
|
||||
local networks_list = dk.networks:list().body or {}
|
||||
local docker_info = dk:info()
|
||||
|
||||
-- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem
|
||||
-- docker_info_table['1Architecture']._value = docker_info.body.Architecture
|
||||
-- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion
|
||||
docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
|
||||
docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
|
||||
docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
|
||||
docker_info_table['6MemTotal']._value = docker.byte_format(docker_info.body.MemTotal)
|
||||
if docker_info.body.DockerRootDir then
|
||||
local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
|
||||
local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0
|
||||
docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(docker.byte_format(size)) .. " " .. translate("Available") .. ")"
|
||||
end
|
||||
|
||||
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
|
||||
for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do
|
||||
docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v)
|
||||
end
|
||||
|
||||
s.images_used = 0
|
||||
for i, v in ipairs(images_list) do
|
||||
for ci,cv in ipairs(containers_list) do
|
||||
if v.Id == cv.ImageID then
|
||||
s.images_used = s.images_used + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
s.containers_running = tostring(docker_info.body.ContainersRunning)
|
||||
s.images_used = tostring(s.images_used)
|
||||
s.containers_total = tostring(docker_info.body.Containers)
|
||||
s.images_total = tostring(#images_list)
|
||||
s.networks_total = tostring(#networks_list)
|
||||
s.volumes_total = tostring(#volumes_list)
|
||||
else
|
||||
docker_info_table['3ServerVersion']._value = translate("Can NOT connect to docker daemon, please check!!")
|
||||
end
|
||||
|
||||
return m
|
142
luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua
Normal file
|
@ -0,0 +1,142 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
local m, s, o
|
||||
|
||||
local res, containers, volumes, lost_state
|
||||
|
||||
function get_volumes()
|
||||
local data = {}
|
||||
for i, v in ipairs(volumes) do
|
||||
local index = v.Name
|
||||
data[index]={}
|
||||
data[index]["_selected"] = 0
|
||||
data[index]["_nameraw"] = v.Name
|
||||
data[index]["_name"] = v.Name:sub(1,12)
|
||||
|
||||
for ci,cv in ipairs(containers) do
|
||||
if cv.Mounts and type(cv.Mounts) ~= "table" then
|
||||
break
|
||||
end
|
||||
for vi, vv in ipairs(cv.Mounts) do
|
||||
if v.Name == vv.Name then
|
||||
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
|
||||
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2)..'</a>'
|
||||
end
|
||||
end
|
||||
end
|
||||
data[index]["_driver"] = v.Driver
|
||||
data[index]["_mountpoint"] = nil
|
||||
|
||||
for v1 in v.Mountpoint:gmatch('[^/]+') do
|
||||
if v1 == index then
|
||||
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
|
||||
else
|
||||
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
|
||||
end
|
||||
end
|
||||
data[index]["_created"] = v.CreatedAt
|
||||
data[index]["_size"] = "<font class='volume_size_" .. v.Name .. "'>-</font>"
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.volumes:list()
|
||||
if res and res.code and res.code <300 then
|
||||
volumes = res.body.Volumes
|
||||
end
|
||||
|
||||
res = dk.containers:list({
|
||||
query = {
|
||||
all=true
|
||||
}
|
||||
})
|
||||
if res and res.code and res.code <300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
local volume_list = not lost_state and get_volumes() or {}
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Volumes"))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
m:append(Template("dockerman/volume_size"))
|
||||
|
||||
s = m:section(Table, volume_list, translate("Volumes overview"))
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.write = function(self, section, value)
|
||||
volume_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_name", translate("Name"))
|
||||
o = s:option(DummyValue, "_driver", translate("Driver"))
|
||||
o = s:option(DummyValue, "_containers", translate("Containers"))
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_mountpoint", translate("Mount Point"))
|
||||
o = s:option(DummyValue, "_size", translate("Size"))
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_created", translate("Created"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local volume_selected = {}
|
||||
|
||||
for k in pairs(volume_list) do
|
||||
if volume_list[k]._selected == 1 then
|
||||
volume_selected[#volume_selected+1] = k
|
||||
end
|
||||
end
|
||||
|
||||
if next(volume_selected) ~= nil then
|
||||
local success = true
|
||||
docker:clear_status()
|
||||
for _,vol in ipairs(volume_selected) do
|
||||
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
|
||||
local msg = dk.volumes["remove"](dk, {id = vol})
|
||||
if msg and msg.code and msg.code ~= 204 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
507
luci-app-dockerman/luasrc/model/docker.lua
Normal file
|
@ -0,0 +1,507 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.docker"
|
||||
local fs = require "nixio.fs"
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local _docker = {}
|
||||
_docker.options = {}
|
||||
|
||||
--pull image and return iamge id
|
||||
local update_image = function(self, image_name)
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
|
||||
local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb)
|
||||
|
||||
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
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
|
||||
end
|
||||
|
||||
new_image_id = self.images:inspect({name = image_name}).body.Id
|
||||
return new_image_id, res
|
||||
end
|
||||
|
||||
local table_equal = function(t1, t2)
|
||||
if not t1 then
|
||||
return true
|
||||
end
|
||||
|
||||
if not t2 then
|
||||
return false
|
||||
end
|
||||
|
||||
if #t1 ~= #t2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for i, v in ipairs(t1) do
|
||||
if t1[i] ~= t2[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local table_subtract = function(t1, t2)
|
||||
if not t1 or next(t1) == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not t2 or next(t2) == nil then
|
||||
return t1
|
||||
end
|
||||
|
||||
local res = {}
|
||||
for _, v1 in ipairs(t1) do
|
||||
local found = false
|
||||
for _, v2 in ipairs(t2) do
|
||||
if v1 == v2 then
|
||||
found= true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
table.insert(res, v1)
|
||||
end
|
||||
end
|
||||
|
||||
return next(res) == nil and nil or res
|
||||
end
|
||||
|
||||
local map_subtract = function(t1, t2)
|
||||
if not t1 or next(t1) == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not t2 or next(t2) == nil then
|
||||
return t1
|
||||
end
|
||||
|
||||
local res = {}
|
||||
for k1, v1 in pairs(t1) do
|
||||
local found = false
|
||||
for k2, v2 in ipairs(t2) do
|
||||
if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
|
||||
found= true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not found then
|
||||
res[k1] = v1
|
||||
end
|
||||
end
|
||||
|
||||
return next(res) ~= nil and res or nil
|
||||
end
|
||||
|
||||
_docker.clear_empty_tables = function ( t )
|
||||
local k, v
|
||||
|
||||
if next(t) == nil then
|
||||
t = nil
|
||||
else
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == 'table' then
|
||||
t[k] = _docker.clear_empty_tables(v)
|
||||
if t[k] and next(t[k]) == nil then
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
local get_config = function(container_config, image_config)
|
||||
local config = container_config.Config
|
||||
local old_host_config = container_config.HostConfig
|
||||
local old_network_setting = container_config.NetworkSettings.Networks or {}
|
||||
|
||||
if config.WorkingDir == image_config.WorkingDir then
|
||||
config.WorkingDir = ""
|
||||
end
|
||||
|
||||
if config.User == image_config.User then
|
||||
config.User = ""
|
||||
end
|
||||
|
||||
if table_equal(config.Cmd, image_config.Cmd) then
|
||||
config.Cmd = nil
|
||||
end
|
||||
|
||||
if table_equal(config.Entrypoint, image_config.Entrypoint) then
|
||||
config.Entrypoint = nil
|
||||
end
|
||||
|
||||
if table_equal(config.ExposedPorts, image_config.ExposedPorts) then
|
||||
config.ExposedPorts = nil
|
||||
end
|
||||
|
||||
config.Env = table_subtract(config.Env, image_config.Env)
|
||||
config.Labels = table_subtract(config.Labels, image_config.Labels)
|
||||
config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
|
||||
|
||||
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
|
||||
config.ExposedPorts = {}
|
||||
for p, v in pairs(old_host_config.PortBindings) do
|
||||
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
|
||||
end
|
||||
end
|
||||
|
||||
local network_setting = {}
|
||||
local multi_network = false
|
||||
local extra_network = {}
|
||||
|
||||
for k, v in pairs(old_network_setting) do
|
||||
if multi_network then
|
||||
extra_network[k] = v
|
||||
else
|
||||
network_setting[k] = v
|
||||
end
|
||||
multi_network = true
|
||||
end
|
||||
|
||||
local host_config = old_host_config
|
||||
host_config.Mounts = {}
|
||||
for i, v in ipairs(container_config.Mounts) do
|
||||
if v.Type == "volume" then
|
||||
table.insert(host_config.Mounts, {
|
||||
Type = v.Type,
|
||||
Target = v.Destination,
|
||||
Source = v.Source:match("([^/]+)\/_data"),
|
||||
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
|
||||
ReadOnly = not v.RW
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local create_body = config
|
||||
create_body["HostConfig"] = host_config
|
||||
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
|
||||
create_body = _docker.clear_empty_tables(create_body) or {}
|
||||
extra_network = _docker.clear_empty_tables(extra_network) or {}
|
||||
|
||||
return create_body, extra_network
|
||||
end
|
||||
|
||||
local upgrade = function(self, request)
|
||||
_docker:clear_status()
|
||||
|
||||
local container_info = self.containers:inspect({id = request.id})
|
||||
|
||||
if container_info.code > 300 and type(container_info.body) == "table" then
|
||||
return container_info
|
||||
end
|
||||
|
||||
local image_name = container_info.body.Config.Image
|
||||
if not image_name:match(".-:.+") then
|
||||
image_name = image_name .. ":latest"
|
||||
end
|
||||
|
||||
local old_image_id = container_info.body.Image
|
||||
local container_name = container_info.body.Name:sub(2)
|
||||
|
||||
local image_id, res = update_image(self, image_name)
|
||||
if res and res.code and res.code ~= 200 then
|
||||
return res
|
||||
end
|
||||
|
||||
if image_id == old_image_id then
|
||||
return {code = 305, body = {message = "Already up to date"}}
|
||||
end
|
||||
|
||||
local t = os.date("%Y%m%d%H%M%S")
|
||||
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old_".. t .. "...")
|
||||
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old_" ..t }})
|
||||
if res and res.code and res.code < 300 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
local image_config = self.images:inspect({id = old_image_id}).body.Config
|
||||
local create_body, extra_network = get_config(container_info.body, image_config)
|
||||
|
||||
-- create new container
|
||||
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
|
||||
create_body = _docker.clear_empty_tables(create_body)
|
||||
res = self.containers:create({name = container_name, body = create_body})
|
||||
if res and res.code and res.code > 300 then
|
||||
return res
|
||||
end
|
||||
_docker:append_status("done\n")
|
||||
|
||||
-- extra networks need to network connect action
|
||||
for k, v in pairs(extra_network) do
|
||||
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
|
||||
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
|
||||
if res and res.code and res.code > 300 then
|
||||
return res
|
||||
end
|
||||
_docker:append_status("done\n")
|
||||
end
|
||||
|
||||
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "_old_".. t .. "...")
|
||||
res = self.containers:stop({name = container_name .. "_old_" ..t })
|
||||
if res and res.code and res.code < 305 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
_docker:append_status("Container: " .. "Start" .. " " .. container_name .. "...")
|
||||
res = self.containers:start({name = container_name})
|
||||
if res and res.code and res.code < 305 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
_docker:clear_status()
|
||||
return res
|
||||
end
|
||||
|
||||
local duplicate_config = function (self, request)
|
||||
local container_info = self.containers:inspect({id = request.id})
|
||||
if container_info.code > 300 and type(container_info.body) == "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local old_image_id = container_info.body.Image
|
||||
local image_config = self.images:inspect({id = old_image_id}).body.Config
|
||||
|
||||
return get_config(container_info.body, image_config)
|
||||
end
|
||||
|
||||
_docker.new = function()
|
||||
local host = nil
|
||||
local port = nil
|
||||
local socket_path = nil
|
||||
local debug_path = nil
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
host = uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
port = uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
else
|
||||
socket_path = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
end
|
||||
|
||||
local debug = uci:get_bool("dockerd", "dockerman", "debug")
|
||||
if debug then
|
||||
debug_path = uci:get("dockerd", "dockerman", "debug_path") or "/tmp/.docker_debug"
|
||||
end
|
||||
|
||||
local status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
|
||||
|
||||
_docker.options = {
|
||||
host = host,
|
||||
port = port,
|
||||
socket_path = socket_path,
|
||||
debug = debug,
|
||||
debug_path = debug_path,
|
||||
status_path = status_path
|
||||
}
|
||||
|
||||
local _new = docker.new(_docker.options)
|
||||
_new.containers_upgrade = upgrade
|
||||
_new.containers_duplicate_config = duplicate_config
|
||||
|
||||
return _new
|
||||
end
|
||||
|
||||
_docker.options.status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
|
||||
|
||||
_docker.append_status=function(self,val)
|
||||
if not val then
|
||||
return
|
||||
end
|
||||
local file_docker_action_status=io.open(self.options.status_path, "a+")
|
||||
file_docker_action_status:write(val)
|
||||
file_docker_action_status:close()
|
||||
end
|
||||
|
||||
_docker.write_status=function(self,val)
|
||||
if not val then
|
||||
return
|
||||
end
|
||||
local file_docker_action_status=io.open(self.options.status_path, "w+")
|
||||
file_docker_action_status:write(val)
|
||||
file_docker_action_status:close()
|
||||
end
|
||||
|
||||
_docker.read_status=function(self)
|
||||
return fs.readfile(self.options.status_path)
|
||||
end
|
||||
|
||||
_docker.clear_status=function(self)
|
||||
fs.remove(self.options.status_path)
|
||||
end
|
||||
|
||||
local status_cb = function(res, source, handler)
|
||||
res.body = res.body or {}
|
||||
while true do
|
||||
local chunk = source()
|
||||
if chunk then
|
||||
--standard output to res.body
|
||||
table.insert(res.body, chunk)
|
||||
handler(chunk)
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--{"status":"Pulling from library\/debian","id":"latest"}
|
||||
--{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"}
|
||||
--{"status":"Download complete","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"}
|
||||
--{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
|
||||
--{"status":"Status: Downloaded newer image for debian:latest"}
|
||||
_docker.pull_image_show_status_cb = function(res, source)
|
||||
return status_cb(res, source, function(chunk)
|
||||
local json_parse = luci.jsonc.parse
|
||||
local step = json_parse(chunk)
|
||||
if type(step) == "table" then
|
||||
local buf = _docker:read_status()
|
||||
local num = 0
|
||||
local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
|
||||
if step.id then
|
||||
buf, num = buf:gsub("\t"..step.id .. ": .-\n", str)
|
||||
end
|
||||
if num == 0 then
|
||||
buf = buf .. str
|
||||
end
|
||||
_docker:write_status(buf)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"}
|
||||
--{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
|
||||
--{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
|
||||
_docker.import_image_show_status_cb = function(res, source)
|
||||
return status_cb(res, source, function(chunk)
|
||||
local json_parse = luci.jsonc.parse
|
||||
local step = json_parse(chunk)
|
||||
if type(step) == "table" then
|
||||
local buf = _docker:read_status()
|
||||
local num = 0
|
||||
local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
|
||||
if step.status then
|
||||
buf, num = buf:gsub("\t"..step.status .. " .-\n", str)
|
||||
end
|
||||
if num == 0 then
|
||||
buf = buf .. str
|
||||
end
|
||||
_docker:write_status(buf)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
_docker.create_macvlan_interface = function(name, device, gateway, subnet)
|
||||
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
return
|
||||
end
|
||||
|
||||
local ip = require "luci.ip"
|
||||
local if_name = "docker_"..name
|
||||
local dev_name = "macvlan_"..name
|
||||
local net_mask = tostring(ip.new(subnet):mask())
|
||||
local lan_interfaces
|
||||
|
||||
-- add macvlan device
|
||||
uci:delete("network", dev_name)
|
||||
uci:set("network", dev_name, "device")
|
||||
uci:set("network", dev_name, "name", dev_name)
|
||||
uci:set("network", dev_name, "ifname", device)
|
||||
uci:set("network", dev_name, "type", "macvlan")
|
||||
uci:set("network", dev_name, "mode", "bridge")
|
||||
|
||||
-- add macvlan interface
|
||||
uci:delete("network", if_name)
|
||||
uci:set("network", if_name, "interface")
|
||||
uci:set("network", if_name, "proto", "static")
|
||||
uci:set("network", if_name, "ifname", dev_name)
|
||||
uci:set("network", if_name, "ipaddr", gateway)
|
||||
uci:set("network", if_name, "netmask", net_mask)
|
||||
uci:foreach("firewall", "zone", function(s)
|
||||
if s.name == "lan" then
|
||||
local interfaces
|
||||
if type(s.network) == "table" then
|
||||
interfaces = table.concat(s.network, " ")
|
||||
uci:delete("firewall", s[".name"], "network")
|
||||
else
|
||||
interfaces = s.network and s.network or ""
|
||||
end
|
||||
interfaces = interfaces .. " " .. if_name
|
||||
interfaces = interfaces:gsub("%s+", " ")
|
||||
uci:set("firewall", s[".name"], "network", interfaces)
|
||||
end
|
||||
end)
|
||||
|
||||
uci:commit("firewall")
|
||||
uci:commit("network")
|
||||
|
||||
os.execute("ifup " .. if_name)
|
||||
end
|
||||
|
||||
_docker.remove_macvlan_interface = function(name)
|
||||
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
return
|
||||
end
|
||||
|
||||
local if_name = "docker_"..name
|
||||
local dev_name = "macvlan_"..name
|
||||
uci:foreach("firewall", "zone", function(s)
|
||||
if s.name == "lan" then
|
||||
local interfaces
|
||||
if type(s.network) == "table" then
|
||||
interfaces = table.concat(s.network, " ")
|
||||
else
|
||||
interfaces = s.network and s.network or ""
|
||||
end
|
||||
interfaces = interfaces and interfaces:gsub(if_name, "")
|
||||
interfaces = interfaces and interfaces:gsub("%s+", " ")
|
||||
uci:set("firewall", s[".name"], "network", interfaces)
|
||||
end
|
||||
end)
|
||||
|
||||
uci:delete("network", dev_name)
|
||||
uci:delete("network", if_name)
|
||||
uci:commit("network")
|
||||
uci:commit("firewall")
|
||||
|
||||
os.execute("ip link del " .. if_name)
|
||||
end
|
||||
|
||||
_docker.byte_format = function (byte)
|
||||
if not byte then return 'NaN' end
|
||||
local suff = {"B", "KB", "MB", "GB", "TB"}
|
||||
for i=1, 5 do
|
||||
if byte > 1024 and i < 5 then
|
||||
byte = byte / 1024
|
||||
else
|
||||
return string.format("%.2f %s", byte, suff[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return _docker
|
147
luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm
Normal file
|
@ -0,0 +1,147 @@
|
|||
<style type="text/css">
|
||||
#docker_apply_overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message {
|
||||
position: relative;
|
||||
top: 10%;
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message > h4,
|
||||
#docker_apply_overlay .alert-message > p,
|
||||
#docker_apply_overlay .alert-message > div {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message > img {
|
||||
margin-right: 1em;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
body.apply-overlay-active {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.apply-overlay-active #docker_apply_overlay {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var xhr = new XHR(),
|
||||
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 90, 90)%>,
|
||||
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
|
||||
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
|
||||
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
|
||||
was_xhr_poll_running = false;
|
||||
|
||||
function docker_status_message(type, content) {
|
||||
document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>')
|
||||
var overlay = document.getElementById('docker_apply_overlay')
|
||||
message = overlay.querySelector('.alert-message');
|
||||
|
||||
if (message && type) {
|
||||
if (!message.classList.contains(type)) {
|
||||
message.classList.remove('notice');
|
||||
message.classList.remove('warning');
|
||||
message.classList.add(type);
|
||||
}
|
||||
|
||||
if (content)
|
||||
message.innerHTML = content;
|
||||
|
||||
document.body.classList.add('apply-overlay-active');
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0;
|
||||
if (!was_xhr_poll_running) {
|
||||
was_xhr_poll_running = XHR.running();
|
||||
XHR.halt();
|
||||
}
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('apply-overlay-active');
|
||||
|
||||
if (was_xhr_poll_running)
|
||||
XHR.run();
|
||||
}
|
||||
}
|
||||
|
||||
var loading_msg="Loading.."
|
||||
function uci_confirm_docker() {
|
||||
var tt;
|
||||
docker_status_message('notice');
|
||||
var call = function(r, resjson, duration) {
|
||||
if (r && r.status === 200 ) {
|
||||
var indicator = document.querySelector('.uci_change_indicator');
|
||||
if (indicator) indicator.style.display = 'none';
|
||||
docker_status_message('notice', '<%:Docker actions done.%>');
|
||||
document.body.classList.remove('apply-overlay-active');
|
||||
window.clearTimeout(tt);
|
||||
return;
|
||||
}
|
||||
loading_msg = resjson?resjson.info:loading_msg
|
||||
// var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
|
||||
var delay =1000
|
||||
window.setTimeout(function() {
|
||||
xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
var tick = function() {
|
||||
var now = Date.now();
|
||||
|
||||
docker_status_message('notice',
|
||||
'<img src="<%=resource%>/icons/loading.gif" alt="" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
loading_msg + '</span>');
|
||||
|
||||
tt = window.setTimeout(tick, 200);
|
||||
ts = now;
|
||||
};
|
||||
|
||||
tick();
|
||||
/* wait a few seconds for the settings to become effective */
|
||||
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
|
||||
}
|
||||
// document.getElementsByTagName("form")[0].addEventListener("submit", (e)=>{
|
||||
// uci_confirm_docker()
|
||||
// })
|
||||
|
||||
function fnSubmitForm(el){
|
||||
if (el.id != "cbid.table.1._new") {
|
||||
uci_confirm_docker()
|
||||
}
|
||||
}
|
||||
|
||||
<% if self.err then -%>
|
||||
docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>');
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{
|
||||
docker_status_message()
|
||||
})
|
||||
<%- end %>
|
||||
|
||||
window.onload= function (){
|
||||
var buttons = document.querySelectorAll('input[type="submit"]');
|
||||
[].slice.call(buttons).forEach(function (el) {
|
||||
el.onclick = fnSubmitForm.bind(this, el);
|
||||
});
|
||||
|
||||
if(typeof(fnWindowLoad) == "function"){
|
||||
fnWindowLoad()
|
||||
}
|
||||
}
|
||||
|
||||
//]]></script>
|
|
@ -0,0 +1,7 @@
|
|||
<div style="display: inline-block;">
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="btn cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
33
luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm
Normal file
|
@ -0,0 +1,33 @@
|
|||
<div style="display: inline-block;">
|
||||
<!-- <%- if self.title then -%>
|
||||
<label class="cbi-value-title"<%= attr("for", cbid) %>>
|
||||
<%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%>
|
||||
<%-=self.title-%>
|
||||
<%- if self.titleref then -%></a><%- end -%>
|
||||
</label>
|
||||
<%- end -%> -->
|
||||
<%- if self.password then -%>
|
||||
<input type="password" style="position:absolute; left:-100000px" aria-hidden="true"<%=
|
||||
attr("name", "password." .. cbid)
|
||||
%> />
|
||||
<%- end -%>
|
||||
<input data-update="change"<%=
|
||||
attr("id", cbid) ..
|
||||
attr("name", cbid) ..
|
||||
attr("type", self.password and "password" or "text") ..
|
||||
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
|
||||
attr("value", self:cfgvalue(section) or self.default) ..
|
||||
ifattr(self.password, "autocomplete", "new-password") ..
|
||||
ifattr(self.size, "size") ..
|
||||
ifattr(self.placeholder, "placeholder") ..
|
||||
ifattr(self.readonly, "readonly") ..
|
||||
ifattr(self.maxlength, "maxlength") ..
|
||||
ifattr(self.datatype, "data-type", self.datatype) ..
|
||||
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
|
||||
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
|
||||
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
|
||||
%> />
|
||||
<%- if self.password then -%>
|
||||
<div class="btn cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'">∗</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
<% if self:cfgvalue(self.section) then section = self.section %>
|
||||
<div class="cbi-section" id="cbi-<%=self.config%>-<%=section%>">
|
||||
<%+cbi/tabmenu%>
|
||||
<div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
|
||||
<%+cbi/ucisection%>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- /nsection -->
|
10
luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%+cbi/valueheader%>
|
||||
<input type="hidden" value="1"<%=
|
||||
attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
|
||||
%> />
|
||||
<input class="cbi-input-checkbox" data-update="click change" type="checkbox" <% if self.disable == 1 then %>disabled <% end %><%=
|
||||
attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) ..
|
||||
ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked")
|
||||
%> />
|
||||
<label<%= attr("for", cbid)%>></label>
|
||||
<%+cbi/valuefooter%>
|
28
luci-app-dockerman/luasrc/view/dockerman/container.htm
Normal file
|
@ -0,0 +1,28 @@
|
|||
<br>
|
||||
<ul class="cbi-tabmenu">
|
||||
<li id="cbi-tab-container_info"><a id="a-cbi-tab-container_info" href=""><%:Info%></a></li>
|
||||
<li id="cbi-tab-container_resources"><a id="a-cbi-tab-container_resources" href=""><%:Resources%></a></li>
|
||||
<li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></a></li>
|
||||
<li id="cbi-tab-container_file"><a id="a-cbi-tab-container_file" href=""><%:File%></a></li>
|
||||
<li id="cbi-tab-container_console"><a id="a-cbi-tab-container_console" href=""><%:Console%></a></li>
|
||||
<li id="cbi-tab-container_inspect"><a id="a-cbi-tab-container_inspect" href=""><%:Inspect%></a></li>
|
||||
<li id="cbi-tab-container_logs"><a id="a-cbi-tab-container_logs" href=""><%:Logs%></a></li>
|
||||
</ul>
|
||||
|
||||
<script type="text/javascript">
|
||||
let re = /\/admin\/docker\/container\//
|
||||
let p = window.location.href
|
||||
let path = p.split(re)
|
||||
let container_id = path[1].split('/')[0] || path[1]
|
||||
let action = path[1].split('/')[1] || "info"
|
||||
let actions=["info","resources","stats","file","console","logs","inspect"]
|
||||
actions.forEach(function(item) {
|
||||
document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item
|
||||
if (action === item) {
|
||||
document.getElementById("cbi-tab-container_" + item).className="cbi-tab"
|
||||
}
|
||||
else {
|
||||
document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled"
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="cbi-map">
|
||||
<iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
document.getElementById("terminal").src = window.location.protocol + "//" + window.location.hostname + ":7682";
|
||||
</script>
|
|
@ -0,0 +1,332 @@
|
|||
<link rel="stylesheet" href="/luci-static/resources/dockerman/file-manager.css?v=@ver">
|
||||
<fieldset class="cbi-section fb-container">
|
||||
<input id="current-path" type="text" class="current-path cbi-input-text" value="/" />
|
||||
<div class="panel-container">
|
||||
<input type="file" name="upload_archive" accept="*/*"
|
||||
style="visibility:hidden; position: absolute;top: 0px; left: 0px;" multiple="multiple" id="upload_archive" />
|
||||
<button id="upload-file" class="upload-toggle cbi-button cbi-button-edit"><%:Upload%></button>
|
||||
</div>
|
||||
<div id="list-content"></div>
|
||||
</fieldset>
|
||||
<script type="text/javascript" src="<%=resource%>/dockerman/tar.min.js"></script>
|
||||
<script>
|
||||
String.prototype.replaceAll = function (search, replacement) {
|
||||
var target = this;
|
||||
return target.replace(new RegExp(search, 'g'), replacement);
|
||||
};
|
||||
(function () {
|
||||
var iwxhr = new XHR();
|
||||
var listElem = document.getElementById("list-content");
|
||||
listElem.onclick = handleClick;
|
||||
var currentPath;
|
||||
var pathElem = document.getElementById("current-path");
|
||||
pathElem.onblur = function () {
|
||||
update_list(this.value.trim());
|
||||
};
|
||||
pathElem.onkeydown = function (evt) {
|
||||
if (evt.keyCode == 13) {
|
||||
this.blur()
|
||||
evt.preventDefault()
|
||||
}
|
||||
};
|
||||
function removePath(filename, isdir) {
|
||||
var c = confirm('!!!<%:DELETING%> ' + filename + ' ... <%:PLEASE CONFIRM%>!!!');
|
||||
if (c) {
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_remove_file")%>/<%=self.container%>',
|
||||
{
|
||||
path: concatPath(currentPath, filename),
|
||||
isdir: isdir
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renamePath(filename) {
|
||||
var newname = prompt('%:Please input new filename%>: ', filename);
|
||||
if (newname) {
|
||||
newname = newname.trim();
|
||||
if (newname != filename) {
|
||||
var newpath = concatPath(currentPath, newname);
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_rename_file")%>/<%=self.container%>',
|
||||
{
|
||||
filepath: concatPath(currentPath, filename),
|
||||
newpath: newpath
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openpath(filename, dirname) {
|
||||
dirname = dirname || currentPath;
|
||||
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path='
|
||||
+ encodeURIComponent(dirname + '/' + filename) + '&filename='
|
||||
+ encodeURIComponent(filename))
|
||||
}
|
||||
|
||||
function getFileElem(elem) {
|
||||
if (elem.className.indexOf('-icon') > -1) {
|
||||
return elem;
|
||||
}
|
||||
else if (elem.parentNode.className.indexOf('-icon') > -1) {
|
||||
return elem.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function concatPath(path, filename) {
|
||||
if (path === '/') {
|
||||
return path + filename;
|
||||
}
|
||||
else {
|
||||
return path.replace(/\/$/, '') + '/' + filename;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(evt) {
|
||||
// evt.preventDefault();
|
||||
var targetElem = evt.target;
|
||||
var infoElem;
|
||||
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
removePath(infoElem.dataset['filename'], infoElem.dataset['isdir'])
|
||||
evt.preventDefault();
|
||||
location.reload()
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-download') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
openpath(targetElem.parentNode.parentNode.dataset['filename']);
|
||||
evt.preventDefault();
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-rename') > -1) {
|
||||
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
|
||||
evt.preventDefault();
|
||||
location.reload()
|
||||
}
|
||||
else if (targetElem = getFileElem(targetElem)) {
|
||||
if (targetElem.className.indexOf('parent-icon') > -1) {
|
||||
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
|
||||
}
|
||||
else if (targetElem.className.indexOf('file-icon') > -1) {
|
||||
openpath(targetElem.parentNode.dataset['filename']);
|
||||
}
|
||||
else if (targetElem.className.indexOf('link-icon') > -1) {
|
||||
infoElem = targetElem.parentNode;
|
||||
var filepath = infoElem.dataset['linktarget'];
|
||||
if (filepath) {
|
||||
if (infoElem.dataset['isdir'] === "1") {
|
||||
update_list(filepath);
|
||||
}
|
||||
else {
|
||||
var lastSlash = filepath.lastIndexOf('/')
|
||||
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targetElem.className.indexOf('folder-icon') > -1) {
|
||||
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
|
||||
}
|
||||
}
|
||||
}
|
||||
function refresh_list(filenames, path) {
|
||||
var listHtml = '<table class="cbi-section-table"><tbody>';
|
||||
if (path !== '/') {
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..</strong></td></tr>';
|
||||
}
|
||||
if (filenames) {
|
||||
for (var i = 0; i < filenames.length; i++) {
|
||||
var line = filenames[i]
|
||||
if (line) {
|
||||
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
|
||||
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
|
||||
var o = {
|
||||
displayname: f[9],
|
||||
filename: isLink ? f[9].split(' -> ')[0] : f[9],
|
||||
perms: f[1],
|
||||
date: f[7] + ' ' + f[6] + ' ' + f[8],
|
||||
size: f[5],
|
||||
owner: f[3],
|
||||
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
|
||||
};
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i % 2)
|
||||
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
|
||||
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
|
||||
+ '">'
|
||||
+ '<td class="cbi-value-field ' + o.icon + '">'
|
||||
+ '<strong>' + o.displayname + '</strong>'
|
||||
+ '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-owner">' + o.owner + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-date">' + o.date + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-size">' + o.size + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-perm">' + o.perms + '</td>'
|
||||
+ '<td class="cbi-section-table-cell">\
|
||||
<button class="btn cbi-button cbi-button-rename cbi-button-edit">'+ "<%:Rename%>" + '</button>\
|
||||
<button class="btn cbi-button cbi-button-download cbi-button-add">'+ "<%:Download%>" + '</button>\
|
||||
<button class="btn cbi-button cbi-button-remove">'+ "<%:Remove%>" + '</button>\
|
||||
</td>'
|
||||
+ '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
listHtml += "</table>";
|
||||
listElem.innerHTML = listHtml;
|
||||
}
|
||||
function update_list(path, opt) {
|
||||
opt = opt || {};
|
||||
path = concatPath(path, '');
|
||||
if (currentPath != path) {
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_list_file")%>/<%=self.container%>',
|
||||
{ path: path },
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, path);
|
||||
}
|
||||
else {
|
||||
refresh_list([], path);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!opt.popState) {
|
||||
history.pushState({ path: path }, null, '?path=' + path);
|
||||
}
|
||||
currentPath = path;
|
||||
pathElem.value = currentPath;
|
||||
}
|
||||
};
|
||||
|
||||
async function file2Tar(tarFile, fileToLoad) {
|
||||
if (! fileToLoad) return
|
||||
function file2Byte(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onerror = () => {
|
||||
fileReader.abort();
|
||||
reject(new DOMException("Problem parsing input file."));
|
||||
};
|
||||
fileReader.onload = (fileLoadedEvent) => {
|
||||
resolve(ByteHelper.stringUTF8ToBytes(fileLoadedEvent.target.result));
|
||||
}
|
||||
fileReader.readAsBinaryString(file);
|
||||
})
|
||||
}
|
||||
const x = await file2Byte(fileToLoad)
|
||||
return fileByte2Tar(tarFile, fileToLoad.name, x).downloadAs(fileToLoad.name + ".tar")
|
||||
}
|
||||
|
||||
function fileByte2Tar(tarFile, fileName, fileBytes) {
|
||||
if (!tarFile) tarFile = TarFile.create(fileName)
|
||||
var tarHeader = TarFileEntryHeader.default();
|
||||
var tarFileEntryHeader = new TarFileEntryHeader
|
||||
(
|
||||
// ByteHelper.bytesToStringUTF8(fileName),
|
||||
fileName,
|
||||
tarHeader.fileMode,
|
||||
tarHeader.userIDOfOwner,
|
||||
tarHeader.userIDOfGroup,
|
||||
fileBytes.length, // fileSizeInBytes,
|
||||
tarHeader.timeModifiedInUnixFormat, // todo
|
||||
0, // checksum,
|
||||
TarFileTypeFlag.Instances().Normal,
|
||||
tarHeader.nameOfLinkedFile,
|
||||
tarHeader.uStarIndicator,
|
||||
tarHeader.uStarVersion,
|
||||
tarHeader.userNameOfOwner,
|
||||
tarHeader.groupNameOfOwner,
|
||||
tarHeader.deviceNumberMajor,
|
||||
tarHeader.deviceNumberMinor,
|
||||
tarHeader.filenamePrefix
|
||||
);
|
||||
|
||||
tarFileEntryHeader.checksumCalculate();
|
||||
var entryForFileToAdd = new TarFileEntry
|
||||
(
|
||||
tarFileEntryHeader,
|
||||
fileBytes
|
||||
);
|
||||
|
||||
tarFile.entries.push(entryForFileToAdd);
|
||||
return tarFile
|
||||
}
|
||||
|
||||
var btnUpload = document.getElementById('upload-file');
|
||||
|
||||
btnUpload.onclick = function (e) {
|
||||
document.getElementById("upload_archive").click()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
let fileLoad = document.getElementById('upload_archive')
|
||||
fileLoad.onchange = async function (e) {
|
||||
let uploadArchive = document.getElementById('upload_archive')
|
||||
// let uploadPath = document.getElementById('path').value
|
||||
if (!uploadArchive.value) {
|
||||
docker_status_message('warning', "<%:Please input the PATH and select the file !%>")
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
|
||||
docker_status_message()
|
||||
})
|
||||
return
|
||||
}
|
||||
docker_status_message('notice',
|
||||
'<img src="<%=resource%>/icons/loading.gif" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
'Uploading...' + '</span>');
|
||||
Globals.Instance.tarFile = TarFile.create("Archive.tar")
|
||||
let bytesToWriteAsBlob
|
||||
for (let i = 0; i < uploadArchive.files.length; i++) {
|
||||
let fileName = uploadArchive.files[i].name
|
||||
bytesToWriteAsBlob = await file2Tar(Globals.Instance.tarFile, uploadArchive.files[i])
|
||||
}
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', "Archive.tar")
|
||||
formData.append('upload-path', concatPath(currentPath, ''))
|
||||
formData.append('upload-archive', bytesToWriteAsBlob)
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/container_put_archive")%>/<%=self.container%>', true)
|
||||
xhr.onload = function () {
|
||||
if (xhr.status == 200) {
|
||||
uploadArchive.value = ''
|
||||
docker_status_message('notice', "<%:Upload Success%>")
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
}
|
||||
sleep(800).then(() => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
else {
|
||||
// docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
|
||||
docker_status_message('warning', "<%:Upload Error%>:" + '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
JSON.parse(xhr.response).message + '</span>')
|
||||
}
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
|
||||
docker_status_message()
|
||||
})
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function (evt) {
|
||||
var initPath = '/';
|
||||
if (/path=([/\w\.\-\_]+)/.test(location.search)) {
|
||||
initPath = RegExp.$1;
|
||||
}
|
||||
update_list(initPath, { popState: true });
|
||||
});
|
||||
window.addEventListener('popstate', function (evt) {
|
||||
var path = '/';
|
||||
if (evt.state && evt.state.path) {
|
||||
path = evt.state.path;
|
||||
}
|
||||
update_list(path, { popState: true });
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
</script>
|
81
luci-app-dockerman/luasrc/view/dockerman/container_stats.htm
Normal file
|
@ -0,0 +1,81 @@
|
|||
<script type="text/javascript">//<![CDATA[
|
||||
let last_bw_tx
|
||||
let last_bw_rx
|
||||
let interval = 3
|
||||
|
||||
function progressbar(v, m, pc, np, f) {
|
||||
m = m || 100
|
||||
|
||||
return String.format(
|
||||
'<div style="width:100%%; max-width:500px; position:relative; border:1px solid #999999">' +
|
||||
'<div style="background-color:#CCCCCC; width:%d%%; height:15px">' +
|
||||
'<div style="position:absolute; left:0; top:0; text-align:center; width:100%%; color:#000000">' +
|
||||
'<small>%s '+(f?f:'/')+' %s ' + (np ? "" : '(%d%%)') + '</small>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>', pc, v, m, pc, f
|
||||
);
|
||||
}
|
||||
|
||||
function niceBytes(bytes, decimals) {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
var k = 1000,
|
||||
dm = decimals + 1 || 3,
|
||||
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/<%=self.container_id%>', { status: 1 },
|
||||
function (x, info) {
|
||||
var e;
|
||||
|
||||
if (e = document.getElementById('cbi-table-cpu-value'))
|
||||
e.innerHTML = progressbar(
|
||||
(info.cpu_percent), 100, (info.cpu_percent ? info.cpu_percent : 0));
|
||||
if (e = document.getElementById('cbi-table-memory-value'))
|
||||
e.innerHTML = progressbar(
|
||||
niceBytes(info.memory.mem_useage),
|
||||
niceBytes(info.memory.mem_limit),
|
||||
((100 / (info.memory.mem_limit ? info.memory.mem_limit : 100)) * (info.memory.mem_useage ? info.memory.mem_useage : 0))
|
||||
);
|
||||
|
||||
for (var eth in info.bw_rxtx) {
|
||||
if (!document.getElementById("cbi-table-speed_" + eth + "-value")) {
|
||||
let tab = document.getElementById("cbi-table-cpu").parentNode
|
||||
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
|
||||
div.id = "cbi-table-speed_" + eth;
|
||||
div.children[0].innerHTML = "<%:Upload/Download%>: " + eth
|
||||
div.children[1].id = "cbi-table-speed_" + eth + "-value"
|
||||
tab.appendChild(div)
|
||||
}
|
||||
if (!document.getElementById("cbi-table-network_" + eth + "-value")) {
|
||||
let tab = document.getElementById("cbi-table-cpu").parentNode
|
||||
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
|
||||
div.id = "cbi-table-network_" + eth;
|
||||
div.children[0].innerHTML = "<%:TX/RX%>: " + eth
|
||||
div.children[1].id = "cbi-table-network_" + eth + "-value"
|
||||
tab.appendChild(div)
|
||||
}
|
||||
e = document.getElementById("cbi-table-network_" + eth + "-value")
|
||||
e.innerHTML = progressbar(
|
||||
'↑'+niceBytes(info.bw_rxtx[eth].bw_tx),
|
||||
'↓'+niceBytes(info.bw_rxtx[eth].bw_rx),
|
||||
null,
|
||||
true, " "
|
||||
);
|
||||
e = document.getElementById("cbi-table-speed_" + eth + "-value")
|
||||
if (! last_bw_tx) last_bw_tx = info.bw_rxtx[eth].bw_tx
|
||||
if (! last_bw_rx) last_bw_rx = info.bw_rxtx[eth].bw_rx
|
||||
e.innerHTML = progressbar(
|
||||
'↑'+niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx)/interval)+'/s',
|
||||
'↓'+niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx)/interval)+'/s',
|
||||
null,
|
||||
true, " "
|
||||
);
|
||||
last_bw_tx = info.bw_rxtx[eth].bw_tx
|
||||
last_bw_rx = info.bw_rxtx[eth].bw_rx
|
||||
}
|
||||
|
||||
});
|
||||
//]]></script>
|
|
@ -0,0 +1,91 @@
|
|||
<script type="text/javascript">
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function niceBytes(x) {
|
||||
let l = 0, n = parseInt(x, 10) || 0;
|
||||
while (n >= 1024 && ++l) {
|
||||
n = n / 1024;
|
||||
}
|
||||
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
|
||||
}
|
||||
|
||||
fnWindowLoad = function () {
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
|
||||
if(!info || !info.Containers || !info.Containers.forEach) return
|
||||
info.Containers.forEach(item=>{
|
||||
const size_c = document.getElementsByClassName("container_size_" + item.Id)
|
||||
size_c[0].title = "RW Size: " + niceBytes(item.SizeRw) + " / RootFS Size(Include Image): " + niceBytes(item.SizeRootFs)
|
||||
size_c[0].innerText = "Size: " + niceBytes(item.SizeRw) + "/" + niceBytes(item.SizeRootFs)
|
||||
})
|
||||
})
|
||||
let lines = document.querySelectorAll('[id^=cbi-containers-]')
|
||||
let last_bw_tx = {}
|
||||
let last_bw_rx = {}
|
||||
let interval = 30
|
||||
let containers = []
|
||||
lines.forEach((item) => {
|
||||
let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
|
||||
if (!containerId) { return }
|
||||
containerId = containerId[1]
|
||||
if (item.getElementsByClassName("container_not_running").length > 0) { return }
|
||||
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/' + containerId, null, (x, info) => {
|
||||
// handle stats info
|
||||
if (!info) { return }
|
||||
item.childNodes.forEach((cell) => {
|
||||
if (cell && cell.attributes) {
|
||||
if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
|
||||
let runningStats = cell.getElementsByClassName("container_cpu_status")
|
||||
runningStats[0].innerText = "CPU: " + info.cpu_percent + "%"
|
||||
runningStats = cell.getElementsByClassName("container_mem_status")
|
||||
runningStats[0].innerText = "MEM: " + niceBytes(info.memory.mem_useage)
|
||||
runningStats = cell.getElementsByClassName("container_network_status")
|
||||
for (var eth in info.bw_rxtx) {
|
||||
if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
|
||||
runningStats[0].innerText = '↑' + niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
|
||||
}
|
||||
last_bw_rx[containerId] = info.bw_rxtx[eth].bw_rx
|
||||
last_bw_tx[containerId] = info.bw_rxtx[eth].bw_tx
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// containers.push(containerId)
|
||||
})
|
||||
// XHR.post('<%=luci.dispatcher.build_url("admin/docker/containers_stats")%>', {
|
||||
// containers: JSON.stringify(containers)
|
||||
// }, (x, info) => {
|
||||
// lines.forEach((item) => {
|
||||
// if (!info) { return }
|
||||
|
||||
// let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
|
||||
// if (!containerId) { return }
|
||||
// containerId = containerId[1]
|
||||
// if (!info[containerId]) { return }
|
||||
// infoC = info[containerId]
|
||||
// if (item.getElementsByClassName("container_not_running").length > 0) { return }
|
||||
// item.childNodes.forEach((cell) => {
|
||||
// if (cell && cell.attributes) {
|
||||
// if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
|
||||
// let runningStats = cell.getElementsByClassName("container_cpu_status")
|
||||
// runningStats[0].innerText = "CPU: " + infoC.cpu_percent + "%"
|
||||
// runningStats = cell.getElementsByClassName("container_mem_status")
|
||||
// runningStats[0].innerText = "MEM: " + niceBytes(infoC.memory.mem_useage)
|
||||
// runningStats = cell.getElementsByClassName("container_network_status")
|
||||
// for (var eth in infoC.bw_rxtx) {
|
||||
// if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
|
||||
// runningStats[0].innerText = '↑' + niceBytes((infoC.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((infoC.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
|
||||
// }
|
||||
// last_bw_rx[containerId] = infoC.bw_rxtx[eth].bw_rx
|
||||
// last_bw_tx[containerId] = infoC.bw_rxtx[eth].bw_tx
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
|
||||
XHR.run()
|
||||
XHR.halt()
|
||||
}
|
||||
</script>
|
104
luci-app-dockerman/luasrc/view/dockerman/images_import.htm
Normal file
|
@ -0,0 +1,104 @@
|
|||
<input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" />
|
||||
<input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" />
|
||||
<div style="display: inline-block;">
|
||||
<input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" <% if self.disable then %>disabled <% end %>/>
|
||||
<input type="file" id="file_import" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" />
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
let btnImport = document.getElementById('btnimport')
|
||||
let valISrc = document.getElementById('isrc')
|
||||
let valITag = document.getElementById('itag')
|
||||
btnImport.onclick = function (e) {
|
||||
if (valISrc.value == "") {
|
||||
document.getElementById("file_import").click()
|
||||
return
|
||||
}
|
||||
else {
|
||||
let formData = new FormData()
|
||||
formData.append('src', valISrc.value)
|
||||
formData.append('tag', valITag.value)
|
||||
let xhr = new XMLHttpRequest()
|
||||
uci_confirm_docker()
|
||||
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
|
||||
xhr.onload = function () {
|
||||
location.reload()
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
}
|
||||
|
||||
let fileimport = document.getElementById('file_import')
|
||||
fileimport.onchange = function (e) {
|
||||
let fileimport = document.getElementById('file_import')
|
||||
if (!fileimport.value) {
|
||||
return
|
||||
}
|
||||
let valITag = document.getElementById('itag')
|
||||
let fileName = fileimport.files[0].name
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', fileName)
|
||||
formData.append('tag', valITag.value)
|
||||
formData.append('upload-archive', fileimport.files[0])
|
||||
let xhr = new XMLHttpRequest()
|
||||
uci_confirm_docker()
|
||||
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
|
||||
xhr.onload = function () {
|
||||
fileimport.value = ''
|
||||
location.reload()
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
|
||||
let new_tag = function (image_id) {
|
||||
let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
|
||||
if (new_tag) {
|
||||
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
|
||||
{
|
||||
id: image_id,
|
||||
tag: new_tag
|
||||
},
|
||||
function (r) {
|
||||
if (r.status == 201) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
|
||||
document.getElementById('docker_apply_overlay').addEventListener(
|
||||
"click",
|
||||
(e)=>{
|
||||
docker_status_message()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let un_tag = function (tag) {
|
||||
if (tag.match("<none>"))
|
||||
return
|
||||
if (confirm("<%:Remove tag%>: " + tag + " ?")) {
|
||||
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>",
|
||||
{
|
||||
tag: tag
|
||||
},
|
||||
function (r) {
|
||||
if (r.status == 200) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
|
||||
document.getElementById('docker_apply_overlay').addEventListener(
|
||||
"click",
|
||||
(e)=>{
|
||||
docker_status_message()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
40
luci-app-dockerman/luasrc/view/dockerman/images_load.htm
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div style="display: inline-block;">
|
||||
<input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" <% if self.disable then %>disabled <% end %>/>
|
||||
<input type="file" id="file_load" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" accept="application/x-tar" />
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
let btnLoad = document.getElementById('btnload')
|
||||
btnLoad.onclick = function (e) {
|
||||
document.getElementById("file_load").click()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
let fileLoad = document.getElementById('file_load')
|
||||
fileLoad.onchange = function(e){
|
||||
let fileLoad = document.getElementById('file_load')
|
||||
if (!fileLoad.value) {
|
||||
return
|
||||
}
|
||||
let fileName = fileLoad.files[0].name
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', fileName)
|
||||
formData.append('upload-archive', fileLoad.files[0])
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
|
||||
xhr.onload = function() {
|
||||
if (xhr.status == 200) {
|
||||
docker_status_message('notice', xhr.statusText)
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
}
|
||||
sleep(1500).then(() => {
|
||||
location.reload()
|
||||
})
|
||||
} else {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
uci_confirm_docker()
|
||||
xhr.send(formData)
|
||||
}
|
||||
</script>
|
13
luci-app-dockerman/luasrc/view/dockerman/logs.htm
Normal file
|
@ -0,0 +1,13 @@
|
|||
<% if self.title == "Events" then %>
|
||||
<%+header%>
|
||||
<h2 name="content"><%:Docker - Events%></h2>
|
||||
<div class="cbi-section">
|
||||
<h3><%:Events%></h3>
|
||||
<% end %>
|
||||
<div id="content_syslog">
|
||||
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
|
||||
</div>
|
||||
<% if self.title == "Events" then %>
|
||||
</div>
|
||||
<%+footer%>
|
||||
<% end %>
|
|
@ -0,0 +1,102 @@
|
|||
<style type="text/css">
|
||||
#dialog_reslov {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255);
|
||||
top: 10%;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height:auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_line {
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box>h4,
|
||||
#dialog_reslov .dialog_box>p,
|
||||
#dialog_reslov .dialog_box>div {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box>img {
|
||||
margin-right: 1em;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
body.dialog-reslov-active {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.dialog-reslov-active #dialog_reslov {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
function close_reslov_dialog() {
|
||||
document.body.classList.remove('dialog-reslov-active')
|
||||
document.documentElement.style.overflowY = 'scroll'
|
||||
}
|
||||
|
||||
function reslov_container() {
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
|
||||
if (!s)
|
||||
return
|
||||
|
||||
let cmd_line = document.getElementById("dialog_reslov_text").value;
|
||||
if (cmd_line == null || cmd_line == "") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd_line = cmd_line.replace(/(^\s*)/g,"")
|
||||
if (!cmd_line.match(/^docker\s+(run|create)/)) {
|
||||
s.innerHTML = "<font color='red'><%:Command line Error%></font>"
|
||||
return
|
||||
}
|
||||
|
||||
let reg_space = /\s+/g
|
||||
let reg_muti_line= /\\\s*\n/g
|
||||
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
|
||||
let reg_rem =/`#.+`/g// the command has `# `
|
||||
cmd_line = cmd_line.replace(/^docker\s+(run|create)/,"DOCKERCLI").replace(reg_rem, " ").replace(reg_muti_line, " ").replace(reg_space, " ")
|
||||
console.log(cmd_line)
|
||||
window.location.href = '<%=luci.dispatcher.build_url("admin/docker/newcontainer")%>/' + encodeURI(cmd_line)
|
||||
}
|
||||
|
||||
function clear_text(){
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
s.innerHTML = ""
|
||||
}
|
||||
|
||||
function show_reslov_dialog() {
|
||||
document.getElementById('dialog_reslov') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_reslov"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Plese input <docker create/run> command line:%></span><br><span id="cmd-line-status"></span></div><div class="dialog_line"><textarea class="cbi-input-textarea" id="dialog_reslov_text" style="width: 100%; height:100%;" rows="15" onkeyup="clear_text()" placeholder="docker run -d alpine sh"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()"/> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>')
|
||||
document.body.classList.add('dialog-reslov-active')
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
s.innerHTML = ""
|
||||
document.documentElement.style.overflowY = 'hidden'
|
||||
}
|
||||
</script>
|
||||
<%+cbi/valueheader%>
|
||||
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
|
||||
|
||||
<%+cbi/valuefooter%>
|
197
luci-app-dockerman/luasrc/view/dockerman/overview.htm
Normal file
|
@ -0,0 +1,197 @@
|
|||
<style>
|
||||
/*!
|
||||
Pure v1.0.1
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/pure-css/pure/blob/master/LICENSE.md
|
||||
*/
|
||||
.pure-g {
|
||||
letter-spacing: -.31em;
|
||||
text-rendering: optimizespeed;
|
||||
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-flow: row wrap;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
-webkit-align-content: flex-start;
|
||||
-ms-flex-line-pack: start;
|
||||
align-content: flex-start
|
||||
}
|
||||
|
||||
.pure-u {
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
vertical-align: top;
|
||||
text-rendering: auto
|
||||
}
|
||||
|
||||
.pure-g [class*=pure-u] {
|
||||
font-family: sans-serif
|
||||
}
|
||||
|
||||
.pure-u-1-4,
|
||||
.pure-u-2-5,
|
||||
.pure-u-3-5 {
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
vertical-align: top;
|
||||
text-rendering: auto
|
||||
}
|
||||
|
||||
.pure-u-1-4 {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
.pure-u-2-5 {
|
||||
width: 40%
|
||||
}
|
||||
|
||||
.pure-u-3-5 {
|
||||
width: 60%
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 1rem -0.5rem 1rem -0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
margin: 0.5rem 0.5rem;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
min-width: inherit;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, .05);
|
||||
border-radius: .375rem;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
|
||||
}
|
||||
|
||||
.img-con {
|
||||
margin: 1rem;
|
||||
min-width: 4rem;
|
||||
max-width: 4rem;
|
||||
min-height: 4rem;
|
||||
max-height: 4rem;
|
||||
}
|
||||
|
||||
.block h4 {
|
||||
font-size: .8125rem;
|
||||
font-weight: 600;
|
||||
margin: 1rem;
|
||||
color: #8898aa !important;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
.cbi-section-table-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.pure-u-1-4 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.cbi-button-add {
|
||||
position: fixed;
|
||||
padding: 0.3rem 0.5rem;
|
||||
z-index: 1000;
|
||||
width: 50px !important;
|
||||
height: 50px;
|
||||
bottom: 90px;
|
||||
right: 5px;
|
||||
font-size: 16px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
background-color: #fb6340 !important;
|
||||
border-color: #fb6340 !important;
|
||||
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pure-g status">
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/containers.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
|
||||
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
|
||||
<%- if self.containers_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/images.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span>
|
||||
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span>
|
||||
<%- if self.images_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/networks.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
|
||||
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
|
||||
<%- if self.networks_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/volumes.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.volumes_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/volumes")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
|
||||
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
|
||||
<%- if self.volumes_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
21
luci-app-dockerman/luasrc/view/dockerman/volume_size.htm
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script type="text/javascript">
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function niceBytes(x) {
|
||||
let l = 0, n = parseInt(x, 10) || 0;
|
||||
while (n >= 1024 && ++l) {
|
||||
n = n / 1024;
|
||||
}
|
||||
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
|
||||
}
|
||||
|
||||
fnWindowLoad = function () {
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
|
||||
if(!info || !info.Volumes || !info.Volumes.forEach) return
|
||||
info.Volumes.forEach(item=>{
|
||||
console.log(info)
|
||||
const size_c = document.getElementsByClassName("volume_size_" + item.Name)
|
||||
size_c[0].innerText = item.UsageData ? niceBytes(item.UsageData.Size) : '-'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
1002
luci-app-dockerman/po/templates/dockerman.pot
Normal file
1094
luci-app-dockerman/po/zh-cn/dockerman.po
Normal file
1
luci-app-dockerman/po/zh_Hans
Normal file
|
@ -0,0 +1 @@
|
|||
zh-cn
|
14
luci-app-dockerman/postinst
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
/init.sh env
|
||||
touch /etc/config/dockerd
|
||||
uci set dockerd.dockerman=dockerman
|
||||
uci set dockerd.dockerman.socket_path=`uci get dockerd.dockerman.socket_path 2&> /dev/null || echo '/var/run/docker.sock'`
|
||||
uci set dockerd.dockerman.status_path=`uci get dockerd.dockerman.status_path 2&> /dev/null || echo '/tmp/.docker_action_status'`
|
||||
uci set dockerd.dockerman.debug=`uci get dockerd.dockerman.debug 2&> /dev/null || echo 'false'`
|
||||
uci set dockerd.dockerman.debug_path=`uci get dockerd.dockerman.debug_path 2&> /dev/null || echo '/tmp/.docker_debug'`
|
||||
uci set dockerd.dockerman.remote_port=`uci get dockerd.dockerman.remote_port 2&> /dev/null || echo '2375'`
|
||||
uci set dockerd.dockerman.remote_endpoint=`uci get dockerd.dockerman.remote_endpoint 2&> /dev/null || echo '0'`
|
||||
uci del_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
uci add_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
uci commit dockerd
|
131
luci-app-dockerman/root/etc/init.d/dockerman
Normal file
|
@ -0,0 +1,131 @@
|
|||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
# PROCD_DEBUG=1
|
||||
config_load 'dockerd'
|
||||
# config_get daemon_ea "dockerman" daemon_ea
|
||||
_DOCKERD=/etc/init.d/dockerd
|
||||
|
||||
docker_running(){
|
||||
docker version > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
add_ports() {
|
||||
[ $# -eq 0 ] && return
|
||||
$($_DOCKERD running) && docker_running || return 1
|
||||
ids=$@
|
||||
for id in $ids; do
|
||||
id=$(docker ps --filter "ID=$id" --quiet)
|
||||
[ -z "$id" ] && {
|
||||
echo "Docker containner not running";
|
||||
return 1;
|
||||
}
|
||||
ports=$(docker ps --filter "ID=$id" --format "{{.Ports}}")
|
||||
# echo "$ports"
|
||||
for port in $ports; do
|
||||
echo "$port" | grep -qE "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:.*$" || continue;
|
||||
[ "${port: -1}" == "," ] && port="${port:0:-1}"
|
||||
local protocol=""
|
||||
[ "${port%tcp}" != "$port" ] && protocol="/tcp"
|
||||
[ "${port%udp}" != "$port" ] && protocol="/udp"
|
||||
[ "$protocol" == "" ] && continue
|
||||
port="${port%%->*}"
|
||||
port="${port##*:}"
|
||||
uci_add_list dockerd dockerman ac_allowed_ports "${port}${protocol}"
|
||||
done
|
||||
done
|
||||
uci_commit dockerd
|
||||
}
|
||||
|
||||
|
||||
convert() {
|
||||
_convert() {
|
||||
_id=$1
|
||||
_id=$(docker ps --all --filter "ID=$_id" --quiet)
|
||||
if [ -z "$_id" ]; then
|
||||
uci_remove_list dockerd dockerman ac_allowed_container "$1"
|
||||
return
|
||||
fi
|
||||
if /etc/init.d/dockerman add_ports "$_id"; then
|
||||
uci_remove_list dockerd dockerman ac_allowed_container "$_id"
|
||||
fi
|
||||
}
|
||||
config_list_foreach dockerman ac_allowed_container _convert
|
||||
uci_commit dockerd
|
||||
}
|
||||
|
||||
iptables_append(){
|
||||
# Wait for a maximum of 10 second per command, retrying every millisecond
|
||||
local iptables_wait_args="--wait 10 --wait-interval 1000"
|
||||
if ! iptables ${iptables_wait_args} --check $@ 2>/dev/null; then
|
||||
iptables ${iptables_wait_args} -A $@ 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
init_dockerman_chain(){
|
||||
iptables -N DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -F DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -I DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
delete_dockerman_chain(){
|
||||
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -F DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -X DOCKER-MAN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
add_allowed_interface(){
|
||||
iptables_append DOCKER-MAN -i $1 -o docker0 -j RETURN
|
||||
}
|
||||
|
||||
add_allowed_ports(){
|
||||
port=$1
|
||||
if [ "${port%/tcp}" != "$port" ]; then
|
||||
iptables_append DOCKER-MAN -p tcp -m conntrack --ctorigdstport ${port%/tcp} --ctdir ORIGINAL -j RETURN
|
||||
elif [ "${port%/udp}" != "$port" ]; then
|
||||
iptables_append DOCKER-MAN -p udp -m conntrack --ctorigdstport ${port%/udp} --ctdir ORIGINAL -j RETURN
|
||||
fi
|
||||
}
|
||||
|
||||
handle_allowed_ports(){
|
||||
config_list_foreach "dockerman" "ac_allowed_ports" add_allowed_ports
|
||||
}
|
||||
|
||||
handle_allowed_interface(){
|
||||
config_list_foreach "dockerman" "ac_allowed_interface" add_allowed_interface
|
||||
iptables_append DOCKER-MAN -m conntrack --ctstate ESTABLISHED,RELATED -o docker0 -j RETURN >/dev/null 2>&1
|
||||
iptables_append DOCKER-MAN -m conntrack --ctstate NEW,INVALID -o docker0 -j DROP >/dev/null 2>&1
|
||||
iptables_append DOCKER-MAN -j RETURN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_service(){
|
||||
[ -x "$_DOCKERD" ] && $($_DOCKERD enabled) || return 0
|
||||
delete_dockerman_chain
|
||||
$($_DOCKERD running) && docker_running || return 0
|
||||
init_dockerman_chain
|
||||
handle_allowed_ports
|
||||
handle_allowed_interface
|
||||
}
|
||||
|
||||
stop_service(){
|
||||
delete_dockerman_chain
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'dockerd'
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
start
|
||||
}
|
||||
|
||||
boot() {
|
||||
sleep 5s
|
||||
start
|
||||
}
|
||||
|
||||
extra_command "add_ports" "Add allowed ports based on the container ID(s)"
|
||||
extra_command "convert" "Convert Ac allowed container to AC allowed ports"
|
36
luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman
Normal file
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
|
||||
. $IPKG_INSTROOT/lib/functions.sh
|
||||
|
||||
[ -x "$(command -v dockerd)" ] && chmod +x /etc/init.d/dockerman && /etc/init.d/dockerman enable >/dev/null 2>&1
|
||||
sed -i 's/self:cfgvalue(section) or {}/self:cfgvalue(section) or self.default or {}/' /usr/lib/lua/luci/view/cbi/dynlist.htm
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1
|
||||
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache >/dev/null 2>&1
|
||||
touch /etc/config/dockerd
|
||||
ls /etc/rc.d/*dockerd &> /dev/null && uci -q set dockerd.globals.auto_start="1" || uci -q set dockerd.globals.auto_start="0"
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set uhttpd.main.script_timeout="3600"
|
||||
commit uhttpd
|
||||
set dockerd.dockerman=dockerman
|
||||
set dockerd.dockerman.socket_path='/var/run/docker.sock'
|
||||
set dockerd.dockerman.status_path='/tmp/.docker_action_status'
|
||||
set dockerd.dockerman.debug='false'
|
||||
set dockerd.dockerman.debug_path='/tmp/.docker_debug'
|
||||
set dockerd.dockerman.remote_endpoint='0'
|
||||
|
||||
del_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
add_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
|
||||
commit dockerd
|
||||
EOF
|
||||
# remove dockerd firewall
|
||||
config_load dockerd
|
||||
remove_firewall(){
|
||||
cfg=${1}
|
||||
uci_remove dockerd ${1}
|
||||
}
|
||||
config_foreach remove_firewall firewall
|
||||
# Convert ac_allowed_container to ac_allowed_ports
|
||||
(sleep 30s && /etc/init.d/dockerman convert;/etc/init.d/dockerman restart) &
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"luci-app-dockerman": {
|
||||
"description": "Grant UCI access for luci-app-dockerman",
|
||||
"read": {
|
||||
"uci": [ "dockerd" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "dockerd" ]
|
||||
}
|
||||
}
|
||||
}
|