luci-app-lxc: convert to JS
authorPaul Donald <newtwen+github@gmail.com>
Thu, 19 Feb 2026 01:00:14 +0000 (02:00 +0100)
committerPaul Donald <newtwen+github@gmail.com>
Thu, 19 Feb 2026 01:00:14 +0000 (02:00 +0100)
migrate away from the old luci-app lua control system.

See: https://github.com/openwrt/luci/issues/7310

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
50 files changed:
applications/luci-app-lxc/Makefile
applications/luci-app-lxc/htdocs/luci-static/resources/cbi/green.gif [deleted file]
applications/luci-app-lxc/htdocs/luci-static/resources/cbi/purple.gif [deleted file]
applications/luci-app-lxc/htdocs/luci-static/resources/cbi/red.gif [deleted file]
applications/luci-app-lxc/htdocs/luci-static/resources/view/lxc/overview.js [new file with mode: 0644]
applications/luci-app-lxc/luasrc/controller/lxc.lua [deleted file]
applications/luci-app-lxc/luasrc/model/cbi/lxc.lua [deleted file]
applications/luci-app-lxc/luasrc/view/lxc.htm [deleted file]
applications/luci-app-lxc/po/ar/lxc.po
applications/luci-app-lxc/po/bg/lxc.po
applications/luci-app-lxc/po/bn_BD/lxc.po
applications/luci-app-lxc/po/ca/lxc.po
applications/luci-app-lxc/po/cs/lxc.po
applications/luci-app-lxc/po/da/lxc.po
applications/luci-app-lxc/po/de/lxc.po
applications/luci-app-lxc/po/el/lxc.po
applications/luci-app-lxc/po/es/lxc.po
applications/luci-app-lxc/po/et/lxc.po
applications/luci-app-lxc/po/fa/lxc.po
applications/luci-app-lxc/po/fi/lxc.po
applications/luci-app-lxc/po/fr/lxc.po
applications/luci-app-lxc/po/ga/lxc.po
applications/luci-app-lxc/po/he/lxc.po
applications/luci-app-lxc/po/hi/lxc.po
applications/luci-app-lxc/po/hu/lxc.po
applications/luci-app-lxc/po/it/lxc.po
applications/luci-app-lxc/po/ja/lxc.po
applications/luci-app-lxc/po/ko/lxc.po
applications/luci-app-lxc/po/lt/lxc.po
applications/luci-app-lxc/po/mr/lxc.po
applications/luci-app-lxc/po/ms/lxc.po
applications/luci-app-lxc/po/nb_NO/lxc.po
applications/luci-app-lxc/po/nl/lxc.po
applications/luci-app-lxc/po/pl/lxc.po
applications/luci-app-lxc/po/pt/lxc.po
applications/luci-app-lxc/po/pt_BR/lxc.po
applications/luci-app-lxc/po/ro/lxc.po
applications/luci-app-lxc/po/ru/lxc.po
applications/luci-app-lxc/po/sk/lxc.po
applications/luci-app-lxc/po/sv/lxc.po
applications/luci-app-lxc/po/ta/lxc.po
applications/luci-app-lxc/po/templates/lxc.pot
applications/luci-app-lxc/po/tr/lxc.po
applications/luci-app-lxc/po/uk/lxc.po
applications/luci-app-lxc/po/vi/lxc.po
applications/luci-app-lxc/po/yua/lxc.po
applications/luci-app-lxc/po/zh_Hans/lxc.po
applications/luci-app-lxc/po/zh_Hant/lxc.po
applications/luci-app-lxc/root/usr/share/luci/menu.d/luci-app-lxc.json [new file with mode: 0644]
applications/luci-app-lxc/ucode/controller/lxc.uc [new file with mode: 0644]

index 0955e518baf33bf64fbdb5cfa4a7b9bb04194ca7..91745b596feb38ca95ecee785b0609632498d09d 100644 (file)
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2017 Dan Luedtke <mail@danrl.com>
+# Copyright (C) 2026 Paul Donald <newtwen+github@gmail.com>
 #
 # This is free software, licensed under the Apache License, Version 2.0 .
 #
@@ -7,15 +7,14 @@
 include $(TOPDIR)/rules.mk
 
 LUCI_TITLE:=LXC management Web UI
-LUCI_DEPENDS:=@!arc +luci-compat +luci-base +lxc +lxc-attach +lxc-console +lxc-create +liblxc +rpcd-mod-lxc +getopt +!LXC_BUSYBOX_OPTIONS:tar
+LUCI_DEPENDS:=@!arc +luci-base +lxc +lxc-attach +lxc-console +lxc-create +liblxc +rpcd-mod-lxc +getopt +!LXC_BUSYBOX_OPTIONS:tar
 
 define Package/luci-app-lxc/conffiles
 /etc/config/lxc
 endef
 
 PKG_LICENSE:=Apache-2.0
-PKG_MAINTAINER:=Petar Koretic <petar.koretic@sartura.hr>, \
-       Dirk Brenken <dev@brenken.org>
+PKG_MAINTAINER:=Paul Donald <newtwen+github@gmail.com>
 
 include ../../luci.mk
 
diff --git a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/green.gif b/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/green.gif
deleted file mode 100644 (file)
index d09febf..0000000
Binary files a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/green.gif and /dev/null differ
diff --git a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/purple.gif b/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/purple.gif
deleted file mode 100644 (file)
index f0d68cc..0000000
Binary files a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/purple.gif and /dev/null differ
diff --git a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/red.gif b/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/red.gif
deleted file mode 100644 (file)
index c1b39bb..0000000
Binary files a/applications/luci-app-lxc/htdocs/luci-static/resources/cbi/red.gif and /dev/null differ
diff --git a/applications/luci-app-lxc/htdocs/luci-static/resources/view/lxc/overview.js b/applications/luci-app-lxc/htdocs/luci-static/resources/view/lxc/overview.js
new file mode 100644 (file)
index 0000000..4ffbd4b
--- /dev/null
@@ -0,0 +1,518 @@
+/*
+ * Copyright (c) 2026. All Rights Reserved.
+ * Paul Donald <newtwen+github@gmail.com>
+ */
+
+'use strict';
+'require rpc';
+'require form';
+'require network';
+'require uci';
+'require tools.widgets as widgets';
+
+return L.view.extend({
+       load() {
+               return Promise.all([
+
+               ]);
+       },
+
+       imagepanel() {
+
+               window.img    = { 'red' : '🟥', 'green' : '🟩', 'blue' : '🟦' };
+               window.states = { 'STOPPED' : 'red', 'RUNNING' : 'green', 'FROZEN' : 'blue' };
+
+               const t_lxc_list  = document.getElementById('t_lxc_list');
+               const loader_html = `<img src='${L.resource('icons/loading.svg')}' alt='loading' width='16' height='16' style='vertical-align:middle' />`;
+               const output_list = document.getElementById('lxc-list-output');
+               const output_add  = document.getElementById('lxc-add-output');
+               const loader_add  = document.getElementById('lxc-add-loader');
+               const div_create  = document.getElementById('div_create');
+               const bt_create   = div_create.querySelector('#bt_create');
+
+               bt_create.disabled = true;
+               info_message(output_add, _('Template download in progress, please be patient!'));
+               bt_create.addEventListener('click', lxc_create);
+
+               function lxc_create() {
+                       const lxc_name     = div_create.querySelector('#tx_name').value.replace(/[\s!@#$%^&*()+=[\]{};':'\\|,<>/?]/g,'');
+                       const lxc_template = div_create.querySelector('#s_template').value;
+
+                       if (t_lxc_list.querySelector(`[data-id="${lxc_name}"]`) != null) {
+                               return info_message(output_add, _('Container with that name already exists!'), 2000);
+                       }
+
+                       bt_create.disabled = true;
+                       output_add.innerHTML = '';
+
+                       if (!lxc_template) {
+                               return set_no_template();
+                       }
+
+                       if (!lxc_name || !lxc_name.length) {
+                               bt_create.disabled = false;
+                               return info_message(output_add, _('The Name field must not be empty!'), 2000);
+                       }
+
+                       loading(loader_add);
+
+                       new window.XHR().get(`/cgi-bin/luci/admin/services/lxc/lxc_create/${lxc_name}/${lxc_template}`, null,
+                       function(x) {
+                               bt_create.disabled = false;
+                               loading(loader_add, 0);
+
+                               if (!x) {
+                                       info_message(output_add, _('Container creation failed!'), 2000);
+                               }
+                       })
+               }
+
+               function lxc_create_template(lxc_name, lxc_state) {
+                       if (document.getElementById(lxc_name)) {
+                               return;
+                       }
+
+                       info_message(output_list, '');
+                       let actions = '';
+                       actions += `<input type='button' onclick='action_handler(this)' data-action='start' value='${_('Start')}' class='cbi-button cbi-button-apply' />`;
+                       actions += `&#160;<input type='button' onclick='action_handler(this)' data-action='stop' value='${_('Stop')}' class='cbi-button cbi-button-reset' />`;
+                       actions += `&#160;<input type='button' onclick='action_handler(this)' data-action='destroy' value='${_('Delete')}' class='cbi-button cbi-button-remove' />`;
+                       actions += `&#160;<select class='cbi-input-select cbi-button' style='width:10em' onchange='action_more_handler(this)'>\
+                                                       <option selected='selected' disabled='disabled'>more</option>\
+                                                       <option>configure</option>\
+                                                       <option>freeze</option>\
+                                                       <option>unfreeze</option>\
+                                                       <option>reboot</option>\
+                                               </select>`;
+                       actions += `<span data-loader='' style='display:inline-block; width:16px; height:16px; margin:0 5px'></span>`;
+
+                       const div0 = document.createElement('div');
+                       div0.className = 'tr cbi-section-table-row';
+                       div0.id = lxc_name;
+                       div0.setAttribute('data-id', lxc_name);
+
+                       const div1 = document.createElement('div');
+                       div1.className = 'td';
+                       div1.style.width = "30%";
+                       div1.setAttribute('data-id', lxc_name);
+                       div1.innerHTML = `<strong>${lxc_name}</strong>`;
+
+                       const div2 = document.createElement('div');
+                       div2.className = 'td statusimg';
+                       div2.style.width = '20%';
+                       div2.innerHTML = window.img[lxc_state];
+
+                       const div3 = document.createElement('div');
+                       div3.className = 'td';
+                       div3.style.width = '50%';
+                       div3.innerHTML = actions;
+
+                       document.getElementById('t_lxc_list').appendChild(div0);
+                       div0.appendChild(div1);
+                       div0.appendChild(div2);
+                       div0.appendChild(div3);
+               }
+
+               function action_handler(self) {
+                       const bt_action  = self;
+                       const action     = self.dataset['action'];
+                       const lxc_name   = self.parentNode.parentNode.dataset['id'];
+                       const status_img = self.parentNode.parentNode.querySelector('.statusimg');
+                       const loader     = self.parentNode.querySelector('[data-loader]');
+
+                       bt_action.disabled = true;
+
+                       if (action == 'stop') {
+                               loading(loader);
+
+                               new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(action, lxc_name)), null,
+                               function(x, ec) {
+                                       loading(loader, 0);
+                                       bt_action.disabled = false;
+
+                                       if (!x || ec) {
+                                               return info_message(output_list, _('Action failed!'), 2000);
+                                       }
+                                       set_status(status_img, 'red');
+                               });
+                       }
+                       else if (action == 'start') {
+                               loading(loader);
+
+                               new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(action, lxc_name)), null,
+                               function(x, data) {
+                                       loading(loader, 0);
+                                       bt_action.disabled = false;
+
+                                       if (!x || data) {
+                                               return info_message(output_list, _('Action failed!'), 2000);
+                                       }
+                                       set_status(status_img, 'green');
+                               });
+                       }
+                       else if (action == 'destroy') {
+                               const div = self.parentNode.parentNode;
+                               const img = div.querySelector('.statusimg');;
+
+                               if (img.innerHTML != window.img['red']) {
+                                       bt_action.disabled = false;
+                                       return info_message(output_list, _('Container is still running!'), 2000);
+                               }
+
+                               if (!confirm(_('This will completely remove a stopped LXC container from disk. Are you sure?'))) {
+                                       bt_action.disabled = false;
+                                       return;
+                               }
+                               loading(loader);
+
+                               new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(action, lxc_name)), null,
+                               function(x, ec) {
+                                       loading(loader, 0);
+                                       bt_action.disabled = false;
+
+                                       if (!x || ec) {
+                                               return info_message(output_list, _('Action failed!'), 2000);
+                                       }
+                                       const div = self.parentNode.parentNode;
+                                       div.parentNode.removeChild(div);
+                               });
+                       }
+               }
+
+               function lxc_configure_handler(self) {
+                       const div      = self.parentNode;
+                       const textarea = div.querySelector('[data-id]');
+                       const lxc_name = textarea.dataset['id'];
+                       const lxc_conf = textarea.value;
+
+                       new window.XHR().post(L.url('admin/services/lxc/lxc_configuration_set/' + lxc_name), {'lxc_conf': encodeURIComponent(lxc_conf)},
+                       function(x) {
+                               if (!x || x.responseText != '0') {
+                                       return info_message(output_list, _('Action failed!'), 2000);
+                               }
+                               info_message(output_list, _('LXC configuration updated'), 2000);
+                               var rmdiv = div.parentNode;
+                               rmdiv.parentNode.removeChild(rmdiv);
+                       })
+               }
+
+               function lxc_configure_template(lxc_name, lxc_conf) {
+                       const h = 
+                               `<textarea data-id="${lxc_name}" rows="20" style="width:600px;font-family:monospace;white-space:pre;overflow-wrap:normal;overflow-x:scroll;">` +
+                               lxc_conf + `</textarea>` +
+                               `<input data-id="bt_confirm" onclick="lxc_configure_handler(this)" type="button" class="cbi-button" value="${_('Confirm')}" />`;
+                       return h;
+               }
+
+               function action_more_handler(self) {
+                       const lxc_name = self.parentNode.parentNode.dataset['id'];
+                       const loader   = self.parentNode.querySelector('[data-loader]');
+                       const option   = self.options[self.selectedIndex].text;
+                       self.value   = 'more';
+
+                       let img;
+
+                       const div0 = document.createElement('div');
+                       const div1 = self.parentNode.parentNode;
+                       const next_div = div1.nextSibling;
+
+                       switch(option) {
+                               case 'configure':
+                                       if (next_div && next_div.dataset['action'] !== null) {
+                                               div1.parentNode.removeChild(next_div);
+                                       }
+
+                                       new window.XHR().get(L.url('admin/services/lxc/lxc_configuration_get/' + lxc_name), null,
+                                       function(x) {
+                                               div0.innerHTML=`<div>${lxc_configure_template(lxc_name, x.responseText)}</div>`;
+                                               div0.setAttribute('data-action','');
+                                               div1.parentNode.insertBefore(div0, div1.nextSibling);
+                                       })
+                               break;
+
+                               case 'freeze':
+                                       img = self.parentNode.parentNode.querySelector('.statusimg');
+                                       if(img.innerHTML != window.img['green']) {
+                                               return info_message(output_list, _('Container is not running!'), 2000);
+                                       }
+
+                                       loading(loader);
+
+                                       new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(option, lxc_name)), null,
+                                       function(x, ec) {
+                                               loading(loader, 0)
+                                               if (!x || ec) {
+                                                       return info_message(output_list, _('Action failed!'), 2000);
+                                               }
+                                               set_status(img, 'blue');
+                                       })
+                               break;
+
+                               case 'unfreeze':
+                                       img = self.parentNode.parentNode.querySelector('.statusimg');
+                                       if(img.innerHTML != window.img['blue']) {
+                                               return info_message(output_list, _('Container is not frozen!'), 2000);
+                                       }
+
+                                       loading(loader);
+
+                                       new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(option, lxc_name)), null,
+                                       function(x, ec) {
+                                               loading(loader, 0);
+                                               if (!x || ec) {
+                                                       return info_message(output_list, _('Action failed!'), 2000);
+                                               }
+                                               set_status(img, 'green');
+                                       })
+                               break;
+
+                               case 'reboot':
+                                       img = self.parentNode.parentNode.querySelector('.statusimg');
+                                       if(img.innerHTML != window.img['green']) {
+                                               return info_message(output_list, _('Container is not running!'), 2000);
+                                       }
+
+                                       if (!confirm('Are you sure?')) {
+                                               return;
+                                       }
+
+                                       loading(loader);
+
+                                       new window.XHR().get(L.url('admin/services/lxc/lxc_action/%h/%h'.format(option, lxc_name)), null,
+                                       function(x, ec) {
+                                               loading(loader, 0)
+                                               if (!x || ec) {
+                                                       return info_message(output_list, _('Action failed!'), 2000);
+                                               }
+                                               info_message(output_list, _('LXC container rebooted'), 2000);
+                                       })
+                               break;
+                       }
+               }
+
+               function set_no_container() {
+                       info_message(output_list, _('There are no containers available yet.'));
+               }
+
+               function set_no_template() {
+                       bt_create.disabled = true;
+                       info_message(output_add, _('There are no templates for your architecture available.') + ' ' +
+                               _('Please select another containers URL.'));
+               }
+
+               function lxc_list_update() {
+                       window.XHR.poll(4, L.url('admin/services/lxc/lxc_action/list'), null,
+                       function(x, data) {
+                               if (!x || !data)
+                               {
+                                       return;
+                               }
+
+                               const lxc_count = Object.keys(data).length;
+                               if (!lxc_count) {
+                                       return set_no_container();
+                               }
+
+                               const lxcs = t_lxc_list.querySelectorAll('.td[data-id]');
+                               const lxc_name_div = {};
+                               for (let i = 0, len = lxcs.length; i < len; i++) {
+                                       const lxc_name = lxcs[i].dataset['id'];
+                                       if (!(lxc_name in data)) {
+                                               const div = t_lxc_list.querySelector(`[data-id="${lxc_name}"]`).parentNode;
+                                               div.parentNode.removeChild(div);
+                                               continue;
+                                       }
+                                       lxc_name_div[lxc_name] = lxcs[i].parentNode.querySelector('.statusimg');
+                               }
+
+                               for(let key in data) {
+                                       const lxc_name = key;
+                                       const state = window.states[data[key]];
+
+                                       if (!(lxc_name in lxc_name_div))
+                                       {
+                                               lxc_create_template(lxc_name, state);
+                                       }
+                                       else if (state != get_status(lxc_name_div[lxc_name]))
+                                       {
+                                               set_status(lxc_name_div[lxc_name], state);
+                                       }
+                               }
+                       })
+               }
+
+               function loading(elem, state) {
+                       state = (typeof state === 'undefined') ? 1 : state;
+                       if (state === 1) {
+                               elem.innerHTML = loader_html;
+                       }
+                       else {
+                               setTimeout(function() { elem.innerHTML = ''}, 2000);
+                       }
+               }
+
+               function set_status(elem, state) {
+                       if (!elem || typeof elem.setAttribute !== 'function') {
+                               console.warn('set_status: invalid element for', state, elem);
+                               return;
+                       }
+
+                       state = (typeof state === 'undefined') ? 1 : state;
+                       setTimeout(function() { elem.innerHTML = window.img[state] }, 300);
+               }
+
+               function get_status(elem) {
+                       if (!elem || typeof elem.getAttribute !== 'function') {
+                               console.warn('get_status: invalid element', elem);
+                               return undefined;
+                       }
+                       const src = elem.innerHTML;
+                       for (let i in window.img) {
+                               if (window.img[i] == src) {
+                                       return i;
+                               }
+                       }
+                       return undefined;
+               }
+
+               function info_message(output, msg, timeout) {
+                       timeout = timeout || 0;
+                       output.innerHTML = '<em>' + msg + '</em>';
+                       if (timeout > 0) {
+                               setTimeout(function(){ output.innerHTML=''}, timeout);
+                       }
+               }
+
+               // expose handlers for generated inline attributes (keeps parity with legacy Lua view)
+               window.action_handler = action_handler;
+               window.action_more_handler = action_more_handler;
+               window.lxc_configure_handler = lxc_configure_handler;
+               window.lxc_create = lxc_create;
+
+               new window.XHR().get(L.url('admin/services/lxc/lxc_get_downloadable'), null,
+               function(x, data) {
+                       if (!x) return;
+
+                       if (!data) return set_no_template();
+
+
+                       var lxc_count = Object.keys(data).length;
+                       if (!lxc_count) return set_no_template();
+
+                       var select = document.getElementById('s_template');
+                       for(var key in data) {
+                               var option = document.createElement('option');
+                               option.value = data[key];
+                               option.text = data[key].replace(/[_:]/g, ' ');
+                               select.add(option, -1);
+                       }
+
+                       info_message(output_add, '');
+                       bt_create.disabled = false;
+               })
+
+               lxc_list_update();
+
+       },
+
+       /** @private */
+       populateBasicOptions(s, tab) {
+               let o;
+
+               o = s.taboption(tab, form.Value, 'url', _('Containers URL'))
+               o.value('images.linuxcontainers.org')
+               o.value('repo.turris.cz/lxc', 'repo.turris.cz/lxc (SSL req.)')
+               o.default = 'images.linuxcontainers.org'
+               o.rmempty = false
+
+               o = s.taboption(tab, form.Value, 'min_space', _('Free Space Threshold'),
+                       _('Minimum required free space for LXC Container creation in KB'))
+               o.default = '100000'
+               o.datatype = 'min(50000)'
+               o.rmempty = false
+
+               o = s.taboption(tab, form.Value, 'min_temp', _('Free Temp Threshold'),
+                       _('Minimum required free temp space for LXC Container creation in KB'))
+               o.default = '100000'
+               o.datatype = 'min(50000)'
+               o.rmempty = false
+
+       },
+
+       /** @private */
+       populateOptions(s) {
+
+               s.tab('basic', _('Basic Settings'));
+               this.populateBasicOptions(s, 'basic');
+
+       },
+
+       render() {
+               let m, s;
+
+               m = new form.Map('lxc', _('LXC Containers'),
+                       _('<b>Please note:</b> LXC Containers require features not available on OpenWrt images for devices with small flash.') + '<br /> ' +
+                       _("Also you may want to install 'kmod-veth' for optional network support."));
+
+               s = m.section(form.TypedSection, 'lxc');
+               s.addremove = false;
+               s.anonymous = true;
+
+               let lxc_list = E('div', {'class': 'cbi-section'}, [
+                       E('h3', {}, [_('Available Containers')]),
+                       E('div', {'class': 'cbi-section-node'}, [
+                               E('div', { 'class': 'table cbi-section-table', id: 't_lxc_list' }, [
+                                       E('div', { 'class': 'tr cbi-section-table-titles' }, [
+                                               E('div', { 'class': 'th cbi-section-table-cell' }, [ _('Name') ]),
+                                               E('div', { 'class': 'th cbi-section-table-cell' }, [ _('Status') ]),
+                                               E('div', { 'class': 'th cbi-section-table-cell' }, [ _('Actions') ]),
+                                       ])
+                               ])
+                       ])
+               ]);
+
+               let lxc_output= E('div', { 'class': 'cbi-section' }, [
+                       E('span', { 'id': 'lxc-list-output' }, [
+
+                       ])
+               ]);
+
+               let create_new = E('div', {'class': 'cbi-section'}, [
+                       E('h3', {}, [_('Create New Container')]),
+                       E('div', {'class': 'cbi-section-node'}, [
+                               E('table', { 'class': 'table cbi-section-table', id: 't_lxc_create' }, [
+                                       E('tr', { 'class': 'tr cbi-section-table-titles' }, [
+                                               E('th', { 'class': 'th cbi-section-table-cell' }, [ _('Name') ]),
+                                               E('th', { 'class': 'th cbi-section-table-cell' }, [ _('Status') ]),
+                                               E('th', { 'class': 'th cbi-section-table-cell' }, [ _('Actions') ]),
+                                       ]),
+                                       E('tr', { 'class': 'tr cbi-section-table-row', id: 'div_create' }, [
+                                               E('td', { 'class': 'td cbi-section-table-titles' }, [ 
+                                                       E('input', { id: 'tx_name', 'class': 'cbi-input-text', type: 'text', placeholder: _('Enter new name') }),
+                                               ]),
+                                               E('td', { }, [ 
+                                                       E('select', { id: 's_template', 'class': 'cbi-input-select cbi-button' }),
+                                               ]),
+                                               E('td', { }, [ 
+                                                       E('button', { id: 'bt_create', 'class': 'cbi-button cbi-button-add', disabled: false }, [ _('Create') ]),
+                                                       E('span', { 'id': 'lxc-add-loader' }, [  ]),
+                                               ]),
+                                       ]),
+                               ]),                             
+                       ])
+               ]);
+
+               let create_output = E('span', { id: 'lxc-add-output' }, []);
+
+               this.populateOptions(s);
+
+               return m.render().then(node => {
+                       document.getElementById('tabmenu').append(node);
+                       document.getElementById('tabmenu').append(lxc_list);
+                       document.getElementById('tabmenu').append(lxc_output);
+                       document.getElementById('tabmenu').append(create_new);
+                       document.getElementById('tabmenu').append(create_output);
+                       this.imagepanel();
+               });
+       },
+});
diff --git a/applications/luci-app-lxc/luasrc/controller/lxc.lua b/applications/luci-app-lxc/luasrc/controller/lxc.lua
deleted file mode 100644 (file)
index c1777a0..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
---[[
-
-LuCI LXC module
-
-Copyright (C) 2014, Cisco Systems, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-Author: Petar Koretic <petar.koretic@sartura.hr>
-
-]]--
-
-module("luci.controller.lxc", package.seeall)
-
-local uci  = require "luci.model.uci".cursor()
-local util = require "luci.util"
-local nx   = require "nixio"
-local url  = util.shellquote(uci:get("lxc", "lxc", "url"))
-
-function index()
-       if not nixio.fs.access("/etc/config/lxc") then
-               return
-       end
-
-       page = node("admin", "services", "lxc")
-       page.target = cbi("lxc")
-       page.title = _("LXC Containers")
-       page.order = 70
-       page.acl_depends = { "luci-app-lxc" }
-
-       page = entry({"admin", "services", "lxc_create"}, call("lxc_create"), nil)
-       page.acl_depends = { "luci-app-lxc" }
-       page.leaf = true
-
-       page = entry({"admin", "services", "lxc_action"}, call("lxc_action"), nil)
-       page.acl_depends = { "luci-app-lxc" }
-       page.leaf = true
-
-       page = entry({"admin", "services", "lxc_get_downloadable"}, call("lxc_get_downloadable"), nil)
-       page.acl_depends = { "luci-app-lxc" }
-       page.leaf = true
-
-       page = entry({"admin", "services", "lxc_configuration_get"}, call("lxc_configuration_get"), nil)
-       page.acl_depends = { "luci-app-lxc" }
-       page.leaf = true
-
-       page = entry({"admin", "services", "lxc_configuration_set"}, call("lxc_configuration_set"), nil)
-       page.acl_depends = { "luci-app-lxc" }
-       page.leaf = true
-end
-
-function lxc_get_downloadable()
-       local target = lxc_get_arch_target(url)
-       local templates = {}
-
-       local f = io.popen('sh /usr/share/lxc/templates/lxc-download --list --server %s 2>/dev/null'
-               %{ url }, 'r')
-       local line
-       for line in f:lines() do
-               local dist, version, dist_target = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+default%s+(%S+)%s*$")
-               if dist and version and dist_target and dist_target == target then
-                       templates[#templates+1] = "%s:%s" %{ dist, version }
-               end
-       end
-       f:close()
-
-       luci.http.prepare_content("application/json")
-       luci.http.write_json(templates)
-end
-
-function lxc_create(lxc_name, lxc_template)
-       luci.http.prepare_content("text/plain")
-       local path = lxc_get_config_path()
-       if not path then
-               return
-       end
-
-       local lxc_dist, lxc_release = lxc_template:match("^(.+):(.+)$")
-       luci.sys.call('/usr/bin/lxc-create --quiet --name %s --bdev best --template download -- --dist %s --release %s --arch %s --server %s'
-               %{ lxc_name, lxc_dist, lxc_release, lxc_get_arch_target(url), url })
-
-       while (nx.fs.access(path .. lxc_name .. "/partial")) do
-               nx.nanosleep(1)
-       end
-
-       luci.http.write("0")
-end
-
-function lxc_action(lxc_action, lxc_name)
-       local data, ec = util.ubus("lxc", lxc_action, lxc_name and { name = lxc_name } or {})
-
-       luci.http.prepare_content("application/json")
-       luci.http.write_json(ec and {} or data)
-end
-
-function lxc_get_config_path()
-       local f = io.open("/etc/lxc/lxc.conf", "r")
-       local content = f:read("*all")
-       f:close()
-
-       local ret = content:match('^%s*lxc.lxcpath%s*=%s*([^%s]*)')
-       if ret then
-               if nx.fs.access(ret) then
-                       local min_space = tonumber(uci:get("lxc", "lxc", "min_space")) or 100000
-                       local free_space = tonumber(util.exec("df " ..ret.. " | awk '{if(NR==2)print $4}'"))
-                       if free_space and free_space >= min_space then
-                               local min_temp = tonumber(uci:get("lxc", "lxc", "min_temp")) or 100000
-                               local free_temp = tonumber(util.exec("df /tmp | awk '{if(NR==2)print $4}'"))
-                               if free_temp and free_temp >= min_temp then
-                                       return ret .. "/"
-                               else
-                                       util.perror("lxc error: not enough temporary space (< " ..min_temp.. " KB)")
-                               end
-                       else
-                               util.perror("lxc error: not enough space (< " ..min_space.. " KB)")
-                       end
-               else
-                       util.perror("lxc error: directory not found")
-               end
-       else
-               util.perror("lxc error: config path is empty")
-       end
-end
-
-function lxc_configuration_get(lxc_name)
-       luci.http.prepare_content("text/plain")
-
-       local f = io.open(lxc_get_config_path() .. lxc_name .. "/config", "r")
-       local content = f:read("*all")
-       f:close()
-
-       luci.http.write(content)
-end
-
-function lxc_configuration_set(lxc_name)
-       luci.http.prepare_content("text/plain")
-
-       local lxc_configuration = luci.http.formvalue("lxc_conf")
-       lxc_configuration = luci.http.urldecode(lxc_configuration, true)
-       if lxc_configuration == nil then
-               util.perror("lxc error: config formvalue is empty")
-               return
-       end
-
-       local f, err = io.open(lxc_get_config_path() .. lxc_name .. "/config","w+")
-       if not f then
-               util.perror("lxc error: config file not found")
-               return
-       end
-
-       f:write(lxc_configuration)
-       f:close()
-
-       luci.http.write("0")
-end
-
-function lxc_get_arch_target(url)
-       local target = nx.uname().machine
-       if url and url:match("images.linuxcontainers.org") then
-               local target_map = {
-                       armv5  = "armel",
-                       armv6  = "armel",
-                       armv7  = "armhf",
-                       armv8  = "arm64",
-                       aarch64  = "arm64",
-                       i686   = "i386",
-                       x86_64 = "amd64"
-               }
-               local k, v
-               for k, v in pairs(target_map) do
-                       if target:find(k) then
-                               return v
-                       end
-               end
-       end
-       return target
-end
diff --git a/applications/luci-app-lxc/luasrc/model/cbi/lxc.lua b/applications/luci-app-lxc/luasrc/model/cbi/lxc.lua
deleted file mode 100644 (file)
index df3f79e..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
---[[
-
-LuCI LXC module
-
-Copyright (C) 2014, Cisco Systems, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-Author: Petar Koretic <petar.koretic@sartura.hr>
-
-]]--
-
-m = Map("lxc", translate("LXC Containers"),
-       translate("<b>Please note:</b> LXC Containers require features not available on OpenWrt images for devices with small flash.<br />")
-       .. translate("Also you may want to install 'kmod-veth' for optional network support."))
-m:section(SimpleSection).template = "lxc"
-
-s = m:section(TypedSection, "lxc", translate("Options"))
-s.anonymous = true
-
-o1 = s:option(Value, "url", translate("Containers URL"))
-o1:value("images.linuxcontainers.org")
-o1:value("repo.turris.cz/lxc", "repo.turris.cz/lxc (SSL req.)")
-o1.default = "images.linuxcontainers.org"
-o1.rmempty = false
-
-o3 = s:option(Value, "min_space", translate("Free Space Threshold"),
-       translate("Minimum required free space for LXC Container creation in KB"))
-o3.default = "100000"
-o3.datatype = "min(50000)"
-o3.rmempty = false
-
-o4 = s:option(Value, "min_temp", translate("Free Temp Threshold"),
-       translate("Minimum required free temp space for LXC Container creation in KB"))
-o4.default = "100000"
-o4.datatype = "min(50000)"
-o4.rmempty = false
-
-return m
diff --git a/applications/luci-app-lxc/luasrc/view/lxc.htm b/applications/luci-app-lxc/luasrc/view/lxc.htm
deleted file mode 100644 (file)
index 390631e..0000000
+++ /dev/null
@@ -1,504 +0,0 @@
-<%#
-
-LuCI LXC module
-
-Copyright (C) 2014, Cisco Systems, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-Author: Petar Koretic <petar.koretic@sartura.hr>
-
--%>
-
-<%-
-local nx     = require "nixio"
-local target = nx.uname().machine
--%>
-
-<div class="cbi-section">
-       <h3><%:Available Containers%></h3>
-       <div class="cbi-section-node">
-               <div class="table cbi-section-table" id="t_lxc_list">
-                       <div class="tr cbi-section-table-titles">
-                               <div class="th cbi-section-table-cell"><%:Name%></div>
-                               <div class="th cbi-section-table-cell"><%:Status%></div>
-                               <div class="th cbi-section-table-cell"><%:Actions%></div>
-                       </div>
-               </div>
-       </div>
-</div>
-
-<div class="cbi-section">
-       <span id="lxc-list-output"></span>
-</div>
-
-<hr />
-<div class="cbi-section">
-       <h3><%:Create New Container%></h3>
-       <div class="cbi-section-node">
-               <div class="table cbi-section-table" id="t_lxc_create">
-                       <div class="tr cbi-section-table-titles">
-                               <div class="th cbi-section-table-cell"><%:Name%></div>
-                               <div class="th cbi-section-table-cell"><%:Template%></div>
-                               <div class="th cbi-section-table-cell"><%:Actions%></div>
-                       </div>
-                       <div class="tr cbi-section-table-row" id="div_create">
-                               <div class="td"><input class="cbi-input-text" type="text" id="tx_name" placeholder="<%:Enter new name%>" value='' /></div>
-                               <div class="td"><select id="s_template" class="cbi-input-select cbi-button"></select></div>
-                               <div class="td">
-                                       <input type="button" id="bt_create" value="<%:Create%>" onclick="lxc_create()" class="cbi-button cbi-button-add" />
-                                       <span id="lxc-add-loader" style="display:inline-block; width:16px; height:16px; margin:0 5px"></span>
-                               </div>
-                       </div>
-               </div>
-       </div>
-</div>
-
-<div class="cbi-section">
-       <span id="lxc-add-output"></span>
-</div>
-
-<hr />
-
-<script>
-       window.img    = { "red" : "<%=resource%>/cbi/red.gif", "green" : "<%=resource%>/cbi/green.gif", "purple" : "<%=resource%>/cbi/purple.gif" };
-       window.states = { "STOPPED" : "red", "RUNNING" : "green", "FROZEN" : "purple" };
-
-       var t_lxc_list  = document.getElementById('t_lxc_list');
-       var loader_html = '<img src="<%=resource%>/icons/loading.svg" alt="<%:Loading%>" width="16" height="16" style="vertical-align:middle" />';
-       var output_list = document.getElementById("lxc-list-output");
-       var output_add  = document.getElementById("lxc-add-output");
-       var loader_add  = document.getElementById("lxc-add-loader");
-       var div_create  = document.getElementById("div_create");
-       var bt_create   = div_create.querySelector("#bt_create");
-
-       bt_create.disabled = true;
-       info_message(output_add, "Template download in progress, please be patient!");
-
-       function lxc_create()
-       {
-               var lxc_name     = div_create.querySelector("#tx_name").value.replace(/[\s!@#$%^&*()+=\[\]{};':"\\|,<>\/?]/g,'');
-               var lxc_template = div_create.querySelector("#s_template").value;
-
-               if (t_lxc_list.querySelector("[data-id='" + lxc_name + "']") != null)
-               {
-                       return info_message(output_add, "Container with that name already exists!", 2000);
-               }
-
-               bt_create.disabled = true;
-               output_add.innerHTML = '';
-
-               if (!lxc_template)
-               {
-                       return set_no_template();
-               }
-
-               if (!lxc_name || !lxc_name.length)
-               {
-                       bt_create.disabled = false;
-                       return info_message(output_add, "The 'Name' field must not be empty!", 2000);
-               }
-
-               loading(loader_add);
-
-               new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_create/' + '%h/%h'.format(lxc_name, lxc_template) , null,
-               function(x)
-               {
-                       bt_create.disabled = false;
-                       loading(loader_add, 0);
-
-                       if (!x)
-                       {
-                               info_message(output_add, "Container creation failed!", 2000);
-                       }
-               })
-       }
-
-       function lxc_create_template(lxc_name, lxc_state)
-       {
-               if (document.getElementById(lxc_name))
-               {
-                       return;
-               }
-
-               info_message(output_list, "");
-               var actions = '';
-               actions += '<input type="button" onclick="action_handler(this)" data-action="start" value="<%:Start%>" class="cbi-button cbi-button-apply" />';
-               actions += '&#160;<input type="button" onclick="action_handler(this)" data-action="stop" value="<%:Stop%>" class="cbi-button cbi-button-reset" />';
-               actions += '&#160;<input type="button" onclick="action_handler(this)" data-action="destroy" value="<%:Delete%>" class="cbi-button cbi-button-remove" />';
-               actions += '&#160;<select class="cbi-input-select cbi-button" style="width:10em" onchange="action_more_handler(this)">\
-                                               <option selected="selected" disabled="disabled">more</option>\
-                                               <option>configure</option>\
-                                               <option>freeze</option>\
-                                               <option>unfreeze</option>\
-                                               <option>reboot</option>\
-                                       </select>';
-               actions += '<span data-loader="" style="display:inline-block; width:16px; height:16px; margin:0 5px"></span>';
-
-               var div0 = document.createElement("div");
-               div0.className = "tr cbi-section-table-row";
-               div0.id = lxc_name;
-               div0.setAttribute("data-id", lxc_name);
-
-               var div1 = document.createElement("div");
-               div1.className = "td";
-               div1.style.width = "30%";
-               div1.innerHTML = '%q%h%q'.format("<strong>", lxc_name, "</strong>");
-
-               var div2 = document.createElement("div");
-               div2.className = "td";
-               div2.style.width = "20%";
-               div2.innerHTML = "<img src='"+window.img[lxc_state]+"'/>";
-
-               var div3 = document.createElement("div");
-               div3.className = "td";
-               div3.style.width = "50%";
-               div3.innerHTML = actions;
-
-               document.getElementById("t_lxc_list").appendChild(div0);
-               div0.appendChild(div1);
-               div0.appendChild(div2);
-               div0.appendChild(div3);
-       }
-
-       function action_handler(self)
-       {
-               var bt_action  = self;
-               var action     = self.dataset['action'];
-               var lxc_name   = self.parentNode.parentNode.dataset['id'];
-               var status_img = self.parentNode.parentNode.querySelector('img');
-               var loader     = self.parentNode.querySelector('[data-loader]');
-
-               bt_action.disabled = true;
-
-               if (action == "stop")
-               {
-                       loading(loader);
-
-                       new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(action, lxc_name), null,
-                       function(x, ec)
-                       {
-                               loading(loader, 0);
-                               bt_action.disabled = false;
-
-                               if (!x || ec)
-                               {
-                                       return info_message(output_list,"Action failed!", 2000);
-                               }
-                               set_status(status_img, "red");
-                       });
-               }
-               else if (action == "start")
-               {
-                       loading(loader);
-
-                       new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(action, lxc_name), null,
-                       function(x, data)
-                       {
-                               loading(loader, 0);
-                               bt_action.disabled = false;
-
-                               if (!x || data)
-                               {
-                                       return info_message(output_list,"Action failed!", 2000);
-                               }
-                               set_status(status_img, "green");
-                       });
-               }
-               else if (action == "destroy")
-               {
-                       var div = self.parentNode.parentNode;
-                       var img = div.querySelector('img');
-
-                       if (img.getAttribute('src') != window.img["red"])
-                       {
-                               bt_action.disabled = false;
-                               return info_message(output_list,"Container is still running!", 2000);
-                       }
-
-                       if (!confirm("This will completely remove a stopped LXC container from disk. Are you sure?"))
-                       {
-                               bt_action.disabled = false;
-                               return;
-                       }
-                       loading(loader);
-
-                       new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(action, lxc_name), null,
-                       function(x, ec)
-                       {
-                               loading(loader, 0);
-                               bt_action.disabled = false;
-
-                               if (!x || ec)
-                               {
-                                       return info_message(output_list,"Action failed!", 2000);
-                               }
-                               var div = self.parentNode.parentNode;
-                               div.parentNode.removeChild(div);
-                       });
-               }
-       }
-
-       function lxc_configure_handler(self)
-       {
-               var div      = self.parentNode;
-               var textarea = div.querySelector('[data-id]');
-               var lxc_name = textarea.dataset['id'];
-               var lxc_conf = textarea.value;
-
-       new XHR().post('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_configuration_set/' + lxc_name, {"lxc_conf": encodeURIComponent(lxc_conf)},
-               function(x)
-               {
-                       if (!x || x.responseText != "0")
-                       {
-                               return info_message(output_list,"Action failed!", 2000);
-                       }
-                       info_message(output_list,"LXC configuration updated", 2000);
-                       var rmdiv = div.parentNode;
-                       rmdiv.parentNode.removeChild(rmdiv);
-               })
-       }
-
-       function lxc_configure_template(lxc_name, lxc_conf)
-       {
-               var h = '\
-                       <textarea data-id="' + lxc_name + '" rows="20" style="width:600px;font-family:monospace;white-space:pre;overflow-wrap:normal;overflow-x:scroll;">'+ lxc_conf +'</textarea> \
-                       <input data-id="bt_confirm" onclick="lxc_configure_handler(this)" type="button" class="cbi-button" value="Confirm" />';
-               return h;
-       }
-
-       function action_more_handler(self)
-       {
-               var lxc_name = self.parentNode.parentNode.dataset['id'];
-               var loader   = self.parentNode.querySelector('[data-loader]');
-               var option   = self.options[self.selectedIndex].text;
-               self.value   = "more";
-
-               switch(option)
-               {
-                       case "configure":
-                               var div0 = document.createElement('div');
-                               var div1 = self.parentNode.parentNode;
-                               var next_div = div1.nextSibling;
-
-                               if (next_div && next_div.dataset['action'] !== null)
-                               {
-                                       div1.parentNode.removeChild(next_div);
-                               }
-
-                               new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_configuration_get/' + lxc_name, null,
-                               function(x)
-                               {
-                                       div0.innerHTML="<div>" + lxc_configure_template(lxc_name, x.responseText) + "</div>";
-                                       div0.setAttribute('data-action','');
-                                       div1.parentNode.insertBefore(div0, div1.nextSibling);
-                               })
-                       break;
-
-                       case "freeze":
-                               var img = self.parentNode.parentNode.querySelector('img');
-                               if(img.getAttribute('src') != window.img["green"])
-                               {
-                                       return info_message(output_list,"Container is not running!", 2000);
-                               }
-
-                               loading(loader);
-
-                               new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(option, lxc_name), null,
-                               function(x, ec)
-                               {
-                                       loading(loader, 0)
-                                       if (!x || ec)
-                                       {
-                                               return info_message(output_list,"Action failed!", 2000);
-                                       }
-                                       set_status(img, "purple");
-                               })
-                       break;
-
-                       case "unfreeze":
-                               var img = self.parentNode.parentNode.querySelector('img');
-                               if(img.getAttribute('src') != window.img["purple"])
-                               {
-                                       return info_message(output_list,"Container is not frozen!", 2000);
-                               }
-
-                               loading(loader);
-
-                               new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(option, lxc_name), null,
-                               function(x, ec)
-                               {
-                                       loading(loader, 0);
-                                       if (!x || ec)
-                                       {
-                                               return info_message(output_list,"Action failed!", 2000);
-                                       }
-                                       set_status(img, "green");
-                               })
-                       break;
-
-                       case "reboot":
-                               var img = self.parentNode.parentNode.querySelector('img');
-                               if(img.getAttribute('src') != window.img["green"])
-                               {
-                                       return info_message(output_list,"Container is not running!", 2000);
-                               }
-
-                               if (!confirm("Are you sure?"))
-                               {
-                                       return;
-                               }
-
-                               loading(loader);
-
-                               new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/' + '%h/%h'.format(option, lxc_name), null,
-                               function(x, ec)
-                               {
-                                       loading(loader, 0)
-                                       if (!x || ec)
-                                       {
-                                               return info_message(output_list,"Action failed!", 2000);
-                                       }
-                                       info_message(output_list,"LXC container rebooted", 2000);
-                               })
-                       break;
-               }
-       }
-
-       function set_no_container()
-       {
-               info_message(output_list, "There are no containers available yet.");
-       }
-
-       function set_no_template()
-       {
-               bt_create.disabled = true;
-               info_message(output_add, "There are no templates for your architecture (<%=target%>) available, please select another containers URL.");
-       }
-
-       function lxc_list_update()
-       {
-               XHR.poll(4, '<%=luci.dispatcher.build_url("admin", "services")%>/lxc_action/list', null,
-               function(x, data)
-               {
-                       if (!x || !data)
-                       {
-                               return;
-                       }
-
-                       var lxc_count = Object.keys(data).length;
-                       if (!lxc_count)
-                       {
-                               return set_no_container();
-                       }
-
-                       var lxcs = t_lxc_list.querySelectorAll('td[data-id]');
-                       var lxc_name_div = {};
-                       for (var i = 0, len = lxcs.length; i < len; i++)
-                       {
-                               var lxc_name = lxcs[i].dataset['id'];
-                               if (!(lxc_name in data))
-                               {
-                                       var div = t_lxc_list.querySelector("[data-id='" + lxc_name + "']").parentNode;
-                                       div.parentNode.removeChild(div);
-                                       continue;
-                               }
-                               lxc_name_div[lxc_name] = lxcs[i].parentNode.querySelector('img');
-                       }
-
-                       for(var key in data)
-                       {
-                               var lxc_name = key;
-                               var state = window.states[data[key]];
-
-                               if (!(lxc_name in lxc_name_div))
-                               {
-                                       lxc_create_template(lxc_name, state);
-                               }
-                               else if (state != get_status(lxc_name_div[lxc_name]))
-                               {
-                                       set_status(lxc_name_div[lxc_name], state);
-                               }
-                       }
-               })
-       }
-
-       function loading(elem, state)
-       {
-               state = (typeof state === 'undefined') ? 1 : state;
-               if (state === 1)
-               {
-                       elem.innerHTML = loader_html;
-               }
-               else
-               {
-                       setTimeout(function() { elem.innerHTML = ''}, 2000);
-               }
-       }
-
-       function set_status(elem, state)
-       {
-               state = (typeof state === 'undefined') ? 1 : state;
-               setTimeout(function() { elem.setAttribute('src', window.img[state])}, 300);
-       }
-
-       function get_status(elem)
-       {
-               var src = elem.getAttribute('src');
-               for (var i in img)
-               {
-                       if (img[i] == src)
-                       {
-                               return i;
-                       }
-               }
-       }
-
-       function info_message(output, msg, timeout)
-       {
-               timeout = timeout || 0;
-               output.innerHTML = '<em>' + msg + '</em>';
-               if (timeout > 0)
-               {
-                       setTimeout(function(){ output.innerHTML=""}, timeout);
-               }
-       }
-
-       new XHR().get('<%=luci.dispatcher.build_url("admin", "services")%>/lxc_get_downloadable', null,
-       function(x, data)
-       {
-               if (!x)
-               {
-                       return;
-               }
-
-               if (!data)
-               {
-                       return set_no_template();
-               }
-
-               var lxc_count = Object.keys(data).length;
-               if (!lxc_count)
-               {
-                       return set_no_template();
-               }
-
-               var select = document.getElementById("s_template");
-               for(var key in data)
-               {
-                       var option = document.createElement('option');
-                       option.value = data[key];
-                       option.text = data[key].replace(/[_:]/g, ' ');
-                       select.add(option, -1);
-               }
-
-               info_message(output_add, "");
-               bt_create.disabled = false;
-       })
-
-       lxc_list_update();
-</script>
index 7e6f8b51fe4ccae669985541999e8a510fdf6e5c..eaaee36f4ec58ef2c55f10a3a2ed06b686a00806 100644 (file)
@@ -14,7 +14,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index a587b9140547671b2be15c3544537fa54777b8b5..40e32af384bc2c3d9d4243acc71c91a89e3e8484 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Обърнете внимание:</b> LXC контейнерите изискват функции, които не са "
-"налични в изображенията на OpenWrt за устройства с малка флаш памет.<br />"
+"налични в изображенията на OpenWrt за устройства с малка флаш памет."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index bd04da45506b008bc931db73a374c961615e34c9..3d947e3090d38666b4d229bb795a1251f082f81e 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 44bd1209f97f3120dc02793c0180de6cb618d888..d82f77d99f05f6f09b9e755b2b28caa766b70d1d 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 1ab2abd71bb0d6cc15c9ed69c73789657325ede1..b1de8660be25167c0c172bc0fd7c84509e7f5b2f 100644 (file)
@@ -13,11 +13,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Uvědomte si prosím:</b> LXC kontejnery vyžadují funkce, které nejsou k "
 "dispozici v obrazech OpenWrt, určených pro zařízení s malým úložištěm (flash "
-"paměť).<br />"
+"paměť)."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 71ef422d26d6fb673649e38f695d8844e4b5bca2..53672db4de757674bd2216985d19562a2e8dfcd6 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Bemærk venligst:</b> LXC Containers kræver funktioner, der ikke er "
-"tilgængelige på OpenWrt-aftryk for enheder med lille flash.<br />"
+"tilgængelige på OpenWrt-aftryk for enheder med lille flash."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index c4df21010b95d50cd8a947632453352d97e7b97e..bfa110042cd0c8b6fd7b50a63b5bb78f544d5926 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Bitte beachten:</b> LXC-Container benötigen Funktionen, die bei OpenWrt-"
-"Images für Geräte mit kleinem Flashspeicher nicht verfügbar sind.<br />"
+"Images für Geräte mit kleinem Flashspeicher nicht verfügbar sind."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 18499938bb649776c5d21822793937647598b715..546f2e367388f112ad545d2542967beb76c6ef8f 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index de7813ee4baff3ff3d37e04452f55711c35159dc..b0be758848dfb66caa3e719a0371c222fa779190 100644 (file)
@@ -16,11 +16,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Tenga en cuenta:</b> los contenedores LXC requieren funciones que no "
 "están disponibles en las imágenes OpenWrt para dispositivos con poco "
-"almacenamiento.<br />"
+"almacenamiento."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 5285c0f453577bc9c483f33cf3e2c1cc40962295..a3f6b281d132a598f88d3134d723677a6074e818 100644 (file)
@@ -10,7 +10,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index c428ea108b10120370d589dd96946fc64a147a1b..fc928f9520b6c4605fb85cfc82b6668962067d24 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>لطفاً توجه کنید:</b> کانتینرهای LXC به ویژگی‌هایی نیاز دارند که در ایمیج "
-"OpenWrt برای دستگاه‌های با حافظه فلش کوچک در دسترس نیستند.<br />"
+"OpenWrt برای دستگاه‌های با حافظه فلش کوچک در دسترس نیستند."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 310b99b9421578722cb0fdfb3b92ad2cabc5cd1f..037595d15a8904e81c146363e23bd22962bf28b9 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Huomaa:</b> LXC-kontit vaativat ominaisuuksia, jotka eivät ole "
-"käytettävissä OpenWrt-kuvissa laitteissa, joissa on pieni flash-muisti.<br />"
+"käytettävissä OpenWrt-kuvissa laitteissa, joissa on pieni flash-muisti."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 7f81fd31ef0952d01d5f4c3ef7304243851d71f6..4bb4650ef228f39b160354b66584e9ec49058f8d 100644 (file)
@@ -13,11 +13,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Note:</b> Les conteneurs LXC nécessitent des fonctionnalités non "
 "disponibles dans les images OpenWrt pour les matériels avec une mémoire "
-"flash limitée.<br />"
+"flash limitée."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 24794291e1a91e79ce895389e52146f5b487aa13..c16e99220293dfda755b555f193f88f392b276e1 100644 (file)
@@ -14,11 +14,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Tabhair faoi deara le do thoil:</b> Teastaíonn gnéithe nach bhfuil ar "
 "fáil ar íomhánna OpenWrt le haghaidh gléasanna le splanc bheag ó choimeádáin "
-"LXC.<br />"
+"LXC."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index e4851c4d4d91d6331bf27e1308731cbf4e53a5f5..15ea961c1e39bc63997cbb703035e442d3105b66 100644 (file)
@@ -14,7 +14,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 49de57930d9928afbf503e586c2d3f2fc531da3b..f031b68e3ffdf48ec2d2ae77aa97a736e8299042 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 388747eb5c325519d1fd6b5dfc5050632e3a114f..7194dcd040cf8ff23f65fb2ea3abc6f9caa24e13 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Ne feledje:</b> az LXC-konténerek által igényelt funkciók nem érhetők el "
-"az OpenWrt lemezképeken a kis tárhellyel rendelkező eszközöknél.<br />"
+"az OpenWrt lemezképeken a kis tárhellyel rendelkező eszközöknél."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index b1da30a6cbd8b9abb33b575be5ef5d0ab7a3be33..7b5b04ae419d5f63895640cabbd652392ec05691 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Nota:</b> I contenitori LXC richiedono funzionalità non disponibili nelle "
-"immagini OpenWrt per apparati con memoria limitata.<br />"
+"immagini OpenWrt per apparati con memoria limitata."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 552f885e240a1262afa07c5245ceea2fca98fc41..7f502122279d7d9874bbee6d7a6a291da1268adf 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>注 :</b> LXC コンテナには、小さなフラッシュを搭載したデバイスの OpenWrt イ"
-"メージでは利用できない機能が必要です。<br />"
+"メージでは利用できない機能が必要です。"
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 24b7b5881f257afa4880e50dae1f6f34df429cd0..bbebd0e88d04b97e8f741968af5152b098d8a8c1 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>참고:</b> 플래시 메모리 용량이 부족한 장치용 OpenWrt 이미지에서는 LXC "
-"컨테이너 기능을 사용할 수 없습니다.<br />"
+"컨테이너 기능을 사용할 수 없습니다."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 3ad7741934db61e9c09c2acd1bac14755082e858..64567cc4a027c7973488da07a8a8d6c8bef99aac 100644 (file)
@@ -16,11 +16,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Pastaba:</b> „LXC“ konteineriai reikalauja funkcijų, kurie nėra "
 "pasiekiami ant „OpenWrt“ laikmenų, kurie skirti įrenginiams su mažos talpos "
-"apribojimu.<br />"
+"apribojimu."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 3fb72208fabb18ea058231747893639bd977c2f0..5277ae95722609052535aeab0986aa76d55cbda9 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index e651c394797339ce56f1ec64ec759b338ddd2400..5c150dd6c3a8608169fa39ae5fa7e3b4d1153333 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 3d266e3959c5709cfc5c6e5c1d73a8d8aa263a23..9649d4871f948df8c0a7a81949ef2f5a28b3c561 100644 (file)
@@ -13,7 +13,7 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 794c0d745c62700ae0e0417bf5d2c337b89a446d..d0ad0a2a21334bbe89f3f621929f3f9798f6d9ca 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Let op:</b> LXC-containers vereisen functies die niet beschikbaar zijn op "
-"OpenWrt-afbeeldingen voor apparaten met een kleine flitser.<br />"
+"OpenWrt-afbeeldingen voor apparaten met een kleine flitser."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 9df6a1983e7c38fd6a164cf9a21384623447274f..01352adb7078093ce608deb1c4e22637d3f2921f 100644 (file)
@@ -14,10 +14,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Zwróć uwagę:</b>Kontenery LXC wymagają funkcji niedostępnych w OpenWrt "
-"dla urządzeń z małą pamięcią flash.<br />"
+"dla urządzeń z małą pamięcią flash."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 993d37bc96cdf0c4e570a9f5aa9306efbea36bdd..4f1f312293b53e5d99cb69458fe3d4bfc0e5b9e9 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Por favor note:</b> LXC Containers requerem recursos não disponíveis nas "
-"imagens do OpenWrt para aparelhos com flash pequeno.<br />"
+"imagens do OpenWrt para aparelhos com flash pequeno."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 192d365649b3e710a8988cb13845d06f348f1021..8a5faea25392c012def174d97d2da939a51cd068 100644 (file)
@@ -13,11 +13,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Atenção:</b> Os contêineres LXC requerem recursos não estão disponíveis "
 "nas imagens do OpenWrt para os dispositivos que tenham uma memória flash "
-"pequena.<br />"
+"pequena."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 7c2416af9b0ab074fa13de9ce18f515f8f64e1b4..19f2adb9adf733b90c10988333a4995b358b43aa 100644 (file)
@@ -14,10 +14,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Te rugăm să reții:</b> Containerele LXC necesită caracteristici care nu "
-"sunt disponibile pe imaginile OpenWrt pentru dispozitive cu flash mic.<br />"
+"sunt disponibile pe imaginile OpenWrt pentru dispozitive cu flash mic."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index e0e97f4e78107cbbd52eebdfa809e666a6e1eca5..2d9be9ae12afb31ea05b3cb8472773ed1c2e86e6 100644 (file)
@@ -14,10 +14,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Пожалуйста, обратите внимание:</b> LXC-контейнеры требуют функций, "
-"недоступных в образах OpenWrt для устройств с небольшой флэш-памятью.<br />"
+"недоступных в образах OpenWrt для устройств с небольшой флэш-памятью."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 32a272f42409cf65b7b0bd7b04dfa86c6e148a12..6ea3d097c46fdb09e68976c50c97a289fe13d1c0 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Uvedomte si prosím:</b> Kontajnery LXC vyžadujú funkcie, ktoré nie sú k "
-"dispozícii na obrazoch OpenWrt pre zariadenia s malým flash priestorom.<br />"
+"dispozícii na obrazoch OpenWrt pre zariadenia s malým flash priestorom."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 613e28edc2390dabe52bac209d6e78d91471573c..2334a1d7f3d07b7feca0b752e7044e3e2fa2e847 100644 (file)
@@ -14,10 +14,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b> Obs! : </b> LXC-behållare kräver funktioner som inte är tillgängliga på "
-"OpenWrt-bilder för enheter med liten flash-minne. <br />"
+"OpenWrt-bilder för enheter med liten flash-minne. "
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index a6dbe5a3e04f482ebaeb1a8e4546d5998648c04e..49729ac2e193417fb68fc8fc022336dcf8ddf542 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b> தயவுசெய்து கவனிக்கவும்: </b> எல்எக்ச்சி கொள்கலன்களுக்கு சிறிய ஃபிளாச் கொண்ட "
-"சாதனங்களுக்கான ஓபன்ஆர்டி படங்களில் நற்பொருத்தங்கள் கிடைக்கவில்லை. <br />"
+"சாதனங்களுக்கான ஓபன்ஆர்டி படங்களில் நற்பொருத்தங்கள் கிடைக்கவில்லை. "
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index a660af54628159a122f60a4f8d7289223cfdab7c..19f03f65d431ad0dbe7ca3d0cd688acfee91a143 100644 (file)
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=UTF-8"
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
index 4ed5c8ea03449c6ea7fb7e3c269709d709ba8114..de1e949a63fb695d0ceb3bc168db1534780f08b5 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b> Lütfen unutmayın: </b> LXC Kapsayıcıları, küçük flaşlı aygıtlar için "
-"OpenWrt görüntülerinde bulunmayan özellikler gerektirir. <br />"
+"OpenWrt görüntülerinde bulunmayan özellikler gerektirir. "
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 36f144892f16c32d6145a526134af8483dbbc816..2d23649639c92c30496e93c8257758b05dc99629 100644 (file)
@@ -14,10 +14,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b> Завбачте: </b> Контейнери LXC вимагають функцій, недоступних в образах "
-"OpenWrt для пристроїв з малою флеш-пам'ятю. <br />"
+"OpenWrt для пристроїв з малою флеш-пам'ятю. "
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index d66cd297eab407cd9cd825df9c3209d907d9795d..33ad384f2d002de6d04de319ccc7b716ffcc4cec 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Xin lưu ý:</b> Bộ chứa LXC yêu cầu các tính năng không khả dụng trên ảnh "
-"OpenWrt dành cho thiết bị có đèn flash nhỏ.<br />"
+"OpenWrt dành cho thiết bị có đèn flash nhỏ."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 2b1cfbbe7b3e68253d579ac7431435130ec63730..52509a4808c26a7cbb33f5abb49a9a0d18bdf4e0 100644 (file)
@@ -13,11 +13,11 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>Tenga en cuenta:</b> los contenedores LXC requieren funciones que no "
 "están disponibles en las imágenes OpenWrt para dispositivos con poco "
-"almacenamiento.<br />"
+"almacenamiento."
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 468261ceee2d0549114e22ef50ad92b573ed402c..ffc8a30e68bce31431f5f67feebf11eb8a61c96e 100644 (file)
@@ -14,8 +14,8 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
-msgstr "<b>请注意: </b>LXC容器需要针对小闪存设备的OpenWrt镜像所不具备的功能。<br />"
+"images for devices with small flash."
+msgstr "<b>请注意: </b>LXC容器需要针对小闪存设备的OpenWrt镜像所不具备的功能。"
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
index 1ea475f58a77ad1ac70fa56efd15333e7114fe29..05442ca0b22f018596858ced0e89586399d2617e 100644 (file)
@@ -13,10 +13,10 @@ msgstr ""
 #: applications/luci-app-lxc/luasrc/model/cbi/lxc.lua:18
 msgid ""
 "<b>Please note:</b> LXC Containers require features not available on OpenWrt "
-"images for devices with small flash.<br />"
+"images for devices with small flash."
 msgstr ""
 "<b>請注意:</b> 對於具有較小快閃記憶體的裝置,LXC 容器需要 OpenWrt 映像檔上不"
-"提供的功能。<br />"
+"提供的功能。"
 
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:29
 #: applications/luci-app-lxc/luasrc/view/lxc.htm:47
diff --git a/applications/luci-app-lxc/root/usr/share/luci/menu.d/luci-app-lxc.json b/applications/luci-app-lxc/root/usr/share/luci/menu.d/luci-app-lxc.json
new file mode 100644 (file)
index 0000000..0c1f951
--- /dev/null
@@ -0,0 +1,86 @@
+{
+       "admin/services/lxccm": {
+               "title": "LXC Container mgr",
+               "order": 80,
+               "action": {
+                       "type": "firstchild"
+               },
+               "depends": {
+                       "uci": {
+                               "lxc": true
+                       }
+               }
+       },
+
+       "admin/services/lxccm/overview": {
+               "title": "Overview",
+               "order": 10,
+               "action": {
+                       "type": "view",
+                       "path": "lxc/overview"
+               },
+               "depends": {
+                       "acl": [ "luci-app-lxc" ]
+               }
+       },
+
+       "admin/services/lxc/lxc_create/*": {
+               "action": {
+                       "type": "function",
+                       "module": "luci.controller.lxc",
+                       "function": "lxc_create"
+               },
+               "auth": {
+                       "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
+                       "login": true
+               }
+       },
+
+       "admin/services/lxc/lxc_action/*": {
+               "action": {
+                       "type": "function",
+                       "module": "luci.controller.lxc",
+                       "function": "lxc_action"
+               },
+               "auth": {
+                       "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
+                       "login": true
+               }
+       },
+
+       "admin/services/lxc/lxc_get_downloadable/*": {
+               "action": {
+                       "type": "function",
+                       "module": "luci.controller.lxc",
+                       "function": "lxc_get_downloadable"
+               },
+               "auth": {
+                       "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
+                       "login": true
+               }
+       },
+
+       "admin/services/lxc/lxc_configuration_get/*": {
+               "action": {
+                       "type": "function",
+                       "module": "luci.controller.lxc",
+                       "function": "lxc_configuration_get"
+               },
+               "auth": {
+                       "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
+                       "login": true
+               }
+       },
+
+       "admin/services/lxc/lxc_configuration_set/*": {
+               "action": {
+                       "type": "function",
+                       "module": "luci.controller.lxc",
+                       "function": "lxc_configuration_set"
+               },
+               "auth": {
+                       "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ],
+                       "login": true
+               }
+       }
+}
diff --git a/applications/luci-app-lxc/ucode/controller/lxc.uc b/applications/luci-app-lxc/ucode/controller/lxc.uc
new file mode 100644 (file)
index 0000000..879ee42
--- /dev/null
@@ -0,0 +1,154 @@
+// LXC management endpoint
+// Copyright 2026 Paul Donald <newtwen+github@gmail.com>
+// Licensed to the public under the Apache License 2.0.
+// Built against the lxc API v6
+'use strict';
+
+import * as fs from 'fs';
+import { cursor } from 'uci';
+import { connect } from 'ubus';
+const ctx = cursor();
+const LXC_URL  = ctx.get('lxc', 'lxc', 'url');
+
+function statfs(path) {
+       let p = fs.popen('df -kP ' + path);
+       p.read('line');               // header
+       let line = p.read('line');    // data line
+       p.close();
+
+       if (!line) return null;
+       let cols = split(trim(line), /\s+/);
+       // return {
+       //      filesystem: cols[0],
+       //      blocks_kb: int(cols[1],10),
+       //      used_kb:    int(cols[2],10),
+       //      avail_kb:   int(cols[3],10),
+       //      used_pct:   cols[4],
+       //      mount:      cols.slice(5).join(' ')
+       // };
+       return int(cols[3],10);
+}
+
+const LXCController = {
+
+       lxc_get_downloadable: function() {
+               let target = this.lxc_get_arch_target(LXC_URL);
+               let templates = [];
+               let content = fs.popen(`sh /usr/share/lxc/templates/lxc-download --list --server ${LXC_URL} 2>/dev/null`, 'r').read('all');
+               content = split(content, '\n');
+               for (let line in content) {
+                       let arr = match(line, /^(\S+)\s+(\S+)\s+(\S+)\s+default\s+(\S+)\s*$/);
+                       if(length(arr) < 3) continue;
+                       let dist = trim(arr[1]);
+                       let version = trim(arr[2]);
+                       let dist_target = trim(arr[3]);
+                       if (dist && version && dist_target && dist_target == target) 
+                               push(templates, `${ dist }:${ version }`);
+               }
+               // content.close();
+
+               http.prepare_content('application/json');
+               http.write_json(templates);
+       },
+
+       lxc_create: function(lxc_name, lxc_template) {
+               http.prepare_content('text/plain');
+               let path = this.lxc_get_config_path();
+               if (!path) return;
+               let arr = match(lxc_template, /^(.+):(.+)$/);
+               let lxc_dist = arr[1], lxc_release = arr[2];
+
+               system(`/usr/bin/lxc-create --quiet --name ${lxc_name} --bdev best --template download -- --dist ${lxc_dist} --release ${lxc_release} --arch ${this.lxc_get_arch_target(LXC_URL)} --server ${LXC_URL}`);
+
+               while (fs.access(path + lxc_name + '/partial')) {
+                       sleep(1000);
+               }
+
+               http.write('0');
+       },
+
+       lxc_action: function(lxc_action, lxc_name) {
+               let ubus = connect();
+               let data = ubus.call('lxc', lxc_action, { name: lxc_name });
+
+               http.prepare_content('application/json');
+               http.write_json(data ? data : '');
+       },
+
+       lxc_get_config_path: function() {
+               let content = fs.readfile('/etc/lxc/lxc.conf');
+               let ret = match(content, /^\s*lxc.lxcpath\s*=\s*(\S*)/);
+               if (ret && length(ret) == 2) {
+                       if (fs.access(ret[1])) {
+                               let min_space = int(ctx.get('lxc', 'lxc', 'min_space')) || 100000;
+                               let free_space = statfs(ret[1]);
+                               if (free_space && free_space >= min_space) {
+                                       let min_temp = int(ctx.get('lxc', 'lxc', 'min_temp')) || 100000;
+                                       let free_temp = statfs('/tmp');
+                                       if (free_temp && free_temp >= min_temp)
+                                               return ret[1] + '/';
+                                       else
+                                               return 'lxc error: not enough temporary space (< ' + min_temp + ' KB)';
+                               }
+                               else
+                                       return 'lxc error: not enough space (< ' + min_space + ' KB)';
+                       }
+                       else
+                               return 'lxc error: directory not found';
+               }
+               else
+                       return 'lxc error: config path is empty';
+       },
+
+       lxc_configuration_get: function(lxc_name) {
+               let content = fs.readfile(this.lxc_get_config_path() + lxc_name + '/config');
+
+               http.prepare_content('text/plain');
+               http.write(content);
+       },
+
+       lxc_configuration_set: function(lxc_name) {
+               http.prepare_content('text/plain');
+
+               let lxc_configuration = http.formvalue('lxc_conf');
+               lxc_configuration = http.urldecode(lxc_configuration, true);
+               if (!lxc_configuration) {
+                       return 'lxc error: config formvalue is empty';
+               }
+
+               fs.writefile(this.lxc_get_config_path() + lxc_name + '/config', lxc_configuration);
+
+               http.write('0');
+       },
+
+       lxc_get_arch_target: function(url) {
+               let target = split(fs.popen('uname -m', 'r').read('line'), '\n');
+               if (url && match(url, /images.linuxcontainers.org/)) {
+                       let target_map = {
+                               armv5:  'armel',
+                               armv6:  'armel',
+                               armv7:  'armhf',
+                               armv8:  'arm64',
+                               aarch64:'arm64',
+                               i686 :  'i386',
+                               x86_64: 'amd64',
+                       };
+                       for (let k, v in target_map) {
+                               if (target[0] == k) {
+                                       return v;
+                               }
+                       }
+               }
+               return target[0];
+       },
+};
+
+// Export all handlers with automatic error wrapping
+let controller = LXCController;
+let exports = {};
+for (let k, v in controller) {
+       if (type(v) == 'function')
+               exports[k] = v;
+}
+
+return exports;